3
0
Fork 0

Fix share on Android by moving to flutter share lib (#87)

Co-authored-by: Nate Brown <nbrown.us@gmail.com>
This commit is contained in:
John Maguire 2022-11-17 16:46:06 -05:00 committed by GitHub
parent c7a53c3905
commit a5684e1978
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 67 additions and 331 deletions

View File

@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion flutter.compileSdkVersion
compileSdkVersion 33
ndkVersion flutter.ndkVersion
compileOptions {

View File

@ -84,9 +84,6 @@ class MainActivity: FlutterActivity() {
"active.setRemoteForTunnel" -> activeSetRemoteForTunnel(call, result)
"active.closeTunnel" -> activeCloseTunnel(call, result)
"share" -> Share.share(call, result)
"shareFile" -> Share.shareFile(call, result)
else -> result.notImplemented()
}
}

View File

@ -1,134 +0,0 @@
package net.defined.mobile_nebula
import android.app.PendingIntent
import android.content.*
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.util.Log
import androidx.core.content.FileProvider
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.io.File
class Share {
companion object {
fun share(call: MethodCall, result: MethodChannel.Result) {
val title = call.argument<String>("title")
val text = call.argument<String>("text")
val filename = call.argument<String>("filename")
if (filename == null || filename.isEmpty()) {
return result.error("filename was not provided", null, null)
}
try {
val context = MainActivity!!.getContext()!!
val cacheDir = context.cacheDir.resolve("share")
cacheDir.deleteRecursively()
cacheDir.mkdir()
val newFile = cacheDir.resolve(filename!!)
newFile.delete()
newFile.writeText(text ?: "")
pop(title, newFile, result)
} catch (err: Exception) {
Log.println(Log.ERROR, "", "Share: Error")
result.error(err.message ?: "Unknown error", null, null)
}
}
fun shareFile(call: MethodCall, result: MethodChannel.Result) {
val title = call.argument<String>("title")
val filename = call.argument<String>("filename")
val filePath = call.argument<String>("filePath")
if (filename == null || filename.isEmpty()) {
result.error("filename was not provided", null, null)
return
}
if (filePath == null || filePath.isEmpty()) {
result.error("filePath was not provided", null, null)
return
}
val file = File(filePath)
try {
val context = MainActivity!!.getContext()!!
val cacheDir = context.cacheDir.resolve("share")
cacheDir.deleteRecursively()
cacheDir.mkdir()
val newFile = cacheDir.resolve(filename!!)
newFile.delete()
file.copyTo(newFile)
pop(title, newFile, result)
} catch (err: Exception) {
Log.println(Log.ERROR, "", "Share: Error")
result.error(err.message ?: "Unknown error", null, null)
}
}
private fun pop(title: String?, file: File, result: MethodChannel.Result) {
if (title == null || title.isEmpty()) {
result.error("title was not provided", null, null)
return
}
try {
val context = MainActivity!!.getContext()!!
val fileUri = FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", file)
val intent = Intent()
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.action = Intent.ACTION_SEND
intent.type = "text/*"
intent.putExtra(Intent.EXTRA_SUBJECT, title)
intent.putExtra(Intent.EXTRA_STREAM, fileUri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val receiver = Intent(context, ShareReceiver::class.java)
receiver.putExtra(Intent.EXTRA_TEXT, file)
val pendingIntent = PendingIntent.getBroadcast(context, 0, receiver, PendingIntent.FLAG_UPDATE_CURRENT)
val chooserIntent = Intent.createChooser(intent, title, pendingIntent.intentSender)
val resInfoList: List<ResolveInfo> = context.packageManager.queryIntentActivities(chooserIntent, PackageManager.MATCH_DEFAULT_ONLY)
for (resolveInfo in resInfoList) {
val packageName: String = resolveInfo.activityInfo.packageName
context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(chooserIntent)
} catch (err: Exception) {
Log.println(Log.ERROR, "", "Share: Error")
return result.error(err.message ?: "Unknown error", null, null)
}
result.success(true)
}
}
}
class ShareReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent == null) {
return
}
val res = intent.extras!!.get(Intent.EXTRA_CHOSEN_COMPONENT) as? ComponentName ?: return
when (res.className) {
"org.chromium.arc.intent_helper.SendTextToClipboardActivity" -> {
val file = intent.extras!![Intent.EXTRA_TEXT] as? File ?: return
val clipboard = context?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("", file.readText()))
}
}
}
}

View File

@ -43,6 +43,8 @@ PODS:
- SDWebImage (5.13.3):
- SDWebImage/Core (= 5.13.3)
- SDWebImage/Core (5.13.3)
- share_plus (0.0.1):
- Flutter
- SwiftyGif (5.4.3)
- SwiftyJSON (5.0.1)
- url_launcher_ios (0.0.1):
@ -54,6 +56,7 @@ DEPENDENCIES:
- flutter_barcode_scanner (from `.symlinks/plugins/flutter_barcode_scanner/ios`)
- package_info (from `.symlinks/plugins/package_info/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- SwiftyJSON (~> 5.0)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@ -76,6 +79,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info/ios"
path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
@ -88,6 +93,7 @@ SPEC CHECKSUMS:
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
SDWebImage: af5bbffef2cde09f148d826f9733dcde1a9414cd
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
SwiftyJSON: 2f33a42c6fbc52764d96f13368585094bfd8aa5e
url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af

View File

@ -22,7 +22,6 @@
43AA89572444DA6500EDC39C /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AA89562444DA6500EDC39C /* PacketTunnelProvider.swift */; };
43AA895C2444DA6500EDC39C /* NebulaNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 43AA89542444DA6500EDC39C /* NebulaNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
43AA89622444DAA500EDC39C /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43AA894E2444D8BC00EDC39C /* NetworkExtension.framework */; };
43AD63F424EB3802000FB47E /* Share.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AD63F324EB3802000FB47E /* Share.swift */; };
43ED87842912D0DD004DAFC5 /* DNUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43ED87832912D0DD004DAFC5 /* DNUpdate.swift */; };
43ED87852912D0DD004DAFC5 /* DNUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43ED87832912D0DD004DAFC5 /* DNUpdate.swift */; };
4CF2F06A02A63B862C9F6F03 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384887B4785D38431E800D3A /* Pods_Runner.framework */; };
@ -89,7 +88,6 @@
43AA89562444DA6500EDC39C /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
43AA89582444DA6500EDC39C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
43AA89592444DA6500EDC39C /* NebulaNetworkExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NebulaNetworkExtension.entitlements; sourceTree = "<group>"; };
43AD63F324EB3802000FB47E /* Share.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Share.swift; sourceTree = "<group>"; };
43B66ECA245A0C8400B18C36 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
43B66ECC245A146300B18C36 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
43ED87832912D0DD004DAFC5 /* DNUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNUpdate.swift; sourceTree = "<group>"; };
@ -210,7 +208,6 @@
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
43871C9C2444E2EC004F9075 /* Sites.swift */,
43AD63F324EB3802000FB47E /* Share.swift */,
43ED87832912D0DD004DAFC5 /* DNUpdate.swift */,
BE45F625291AEAB300902884 /* PackageInfo.swift */,
BE5BC105291C41E600B6FE5B /* APIClient.swift */,
@ -362,6 +359,7 @@
"${BUILT_PRODUCTS_DIR}/flutter_barcode_scanner/flutter_barcode_scanner.framework",
"${BUILT_PRODUCTS_DIR}/package_info/package_info.framework",
"${BUILT_PRODUCTS_DIR}/path_provider_ios/path_provider_ios.framework",
"${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework",
"${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework",
);
name = "[CP] Embed Pods Frameworks";
@ -375,6 +373,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_barcode_scanner.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework",
);
runOnlyForDeploymentPostprocessing = 0;
@ -476,7 +475,6 @@
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
43AD63F424EB3802000FB47E /* Share.swift in Sources */,
432D0E3E291C562200752563 /* SiteList.swift in Sources */,
43871C9D2444E2EC004F9075 /* Sites.swift in Sources */,
BE5BC106291C41E600B6FE5B /* APIClient.swift in Sources */,

View File

@ -68,9 +68,6 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
case "active.setRemoteForTunnel": self.vpnRequest(command: "setRemoteForTunnel", arguments: call.arguments, result: result)
case "active.closeTunnel": self.vpnRequest(command: "closeTunnel", arguments: call.arguments, result: result)
case "share": Share.share(call: call, result: result)
case "shareFile": Share.shareFile(call: call, result: result)
default:
result(FlutterMethodNotImplemented)
}

View File

@ -1,150 +0,0 @@
// Basis of this code comes from https://github.com/lubritto/flutter_share
import Flutter
import UIKit
public class Share {
public static func share(call: FlutterMethodCall, result: @escaping FlutterResult) {
let args = call.arguments as? [String: Any?]
let title = args!["title"] as? String
let text = args!["text"] as? String
let filename = args!["filename"] as? String
let tmpDirURL = FileManager.default.temporaryDirectory
if (filename == nil || filename!.isEmpty) {
return result(false)
}
let tmpFile = tmpDirURL.appendingPathComponent(filename!)
do {
try text?.write(to: tmpFile, atomically: true, encoding: .utf8)
} catch {
//TODO: return error
return result(false)
}
pop(title: title, file: tmpFile) { pass in
let fm = FileManager()
do {
try fm.removeItem(at: tmpFile)
} catch {}
return result(pass)
}
}
public static func shareFile(call: FlutterMethodCall, result: @escaping FlutterResult) {
let args = call.arguments as? [String: Any?]
let title = args!["title"] as? String
let filePath = args!["filePath"] as? String
let filename = args!["filename"] as? String
if (filePath == nil || filePath!.isEmpty) {
return result(false)
}
var tmpFile: URL?
let fm = FileManager()
var realPath = URL(fileURLWithPath: filePath!)
if (filename != nil && !filename!.isEmpty) {
tmpFile = FileManager.default.temporaryDirectory.appendingPathComponent(filename!)
do {
try fm.linkItem(at: URL(fileURLWithPath: filePath!), to: tmpFile!)
} catch {
//TODO: return error
return result(false)
}
realPath = tmpFile!
}
pop(title: title, file: realPath) { pass in
if (tmpFile != nil) {
do {
try fm.removeItem(at: tmpFile!)
} catch {}
}
result(pass)
}
}
private static func pop(title: String?, file: URL, completion: @escaping ((Bool) -> Void)) {
if (title == nil || title!.isEmpty) {
return completion(false)
}
let activityViewController = UIActivityViewController(activityItems: [ShareCopy(file: file)], applicationActivities: nil)
activityViewController.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, returnedItems: [Any]?, error: Error?) in
completion(true)
}
// Subject
activityViewController.setValue(title, forKeyPath: "subject")
// For iPads, fix issue where Exception is thrown by using a popup instead
if UIDevice.current.userInterfaceIdiom == .pad {
activityViewController.popoverPresentationController?.sourceView = UIApplication.topViewController()?.view
if let view = UIApplication.topViewController()?.view {
activityViewController.popoverPresentationController?.permittedArrowDirections = []
activityViewController.popoverPresentationController?.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
}
}
DispatchQueue.main.async {
UIApplication.topViewController()?.present(activityViewController, animated: true)
}
}
}
extension UIApplication {
class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}
class ShareCopy: UIActivityItemProvider {
private let file: URL
private let content: String
init(file: URL) {
self.file = file
do {
self.content = try String.init(contentsOf: file)
} catch {
self.content = "Error"
}
// the type of the placeholder item is used to
// display correct activity types by UIActivityControler
super.init(placeholderItem: self.content)
}
override var item: Any {
get {
guard let activityType = activityType else {
return file
}
switch activityType {
case .copyToPasteboard: return content
default: return file
}
}
}
}

View File

@ -1,14 +1,12 @@
// This code comes from https://github.com/lubritto/flutter_share with bugfixes for ipad
import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart' as sp;
import 'package:path/path.dart' as p;
class Share {
static const _channel = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
/// Shares a string of text
/// Transforms a string of text into a file and shares that file
/// - title: Title of message or subject if sending an email
/// - text: The text to share
/// - filename: The filename to use if sending over airdrop for example
@ -21,17 +19,19 @@ class Share {
assert(text.isNotEmpty);
assert(filename.isNotEmpty);
if (title.isEmpty) {
throw FlutterError('Title cannot be empty');
final tmpDir = await getTemporaryDirectory();
final file = File(p.join(tmpDir.path, filename));
var res = false;
try {
file.writeAsStringSync(text, flush: true);
res = await Share.shareFile(title: title, filePath: file.path);
} catch (err) {
// Ignoring file write errors
}
final bool success = await _channel.invokeMethod('share', <String, dynamic>{
'title': title,
'text': text,
'filename': filename,
});
return success;
file.delete();
return res;
}
/// Shares a local file
@ -41,23 +41,16 @@ class Share {
static Future<bool> shareFile({
required String title,
required String filePath,
String? filename,
String? filename
}) async {
assert(title.isNotEmpty);
assert(filePath.isNotEmpty);
if (title.isEmpty) {
throw FlutterError('Title cannot be empty');
} else if (filePath.isEmpty) {
throw FlutterError('FilePath cannot be empty');
}
final bool success = await _channel.invokeMethod('shareFile', <String, dynamic>{
'title': title,
'filePath': filePath,
'filename': filename,
});
return success;
//NOTE: the filename used to specify the name of the file in gmail/slack/etc but no longer works that way
// If we want to support that again we will need to save the file to a temporary directory, share that,
// and then delete it
final xFile = sp.XFile(filePath, name: filename);
final result = await sp.Share.shareXFiles([xFile], subject: title);
return result.status == sp.ShareResultStatus.success;
}
}

View File

@ -36,6 +36,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.16.0"
cross_file:
dependency: transitive
description:
name: cross_file
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3+2"
crypto:
dependency: transitive
description:
@ -77,7 +84,7 @@ packages:
name: file_picker
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.1"
version: "5.2.2"
flutter:
dependency: "direct main"
description: flutter
@ -156,6 +163,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
package_info:
dependency: "direct main"
description:
@ -268,6 +282,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
share_plus:
dependency: "direct main"
description:
name: share_plus
url: "https://pub.dartlang.org"
source: hosted
version: "6.3.0"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.0"
sky_engine:
dependency: transitive
description: flutter
@ -349,7 +377,7 @@ packages:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "3.0.1"
url_launcher_macos:
dependency: transitive
description:
@ -370,21 +398,21 @@ packages:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
version: "2.0.13"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "3.0.1"
uuid:
dependency: "direct main"
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.5"
version: "3.0.7"
vector_math:
dependency: transitive
description:
@ -398,7 +426,7 @@ packages:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.7.0"
version: "3.1.1"
xdg_directories:
dependency: transitive
description:

View File

@ -33,6 +33,7 @@ dependencies:
flutter_barcode_scanner: ^2.0.0
flutter_svg: ^1.1.5
intl: ^0.17.0
share_plus: ^6.3.0
dev_dependencies:
flutter_test: