3
0
Fork 0

Fix share on ipad, improve share file names

This commit is contained in:
Nate Brown 2020-08-17 19:12:28 -05:00
parent 2bc97856af
commit 646550575d
15 changed files with 325 additions and 30 deletions

View File

@ -65,6 +65,9 @@ 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

@ -0,0 +1,115 @@
package net.defined.mobile_nebula
import android.content.Intent
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, 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, 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 = "*/*"
intent.putExtra(Intent.EXTRA_SUBJECT, title)
intent.putExtra(Intent.EXTRA_STREAM, fileUri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val chooserIntent = Intent.createChooser(intent, title)
chooserIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
chooserIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
val resInfoList: List<ResolveInfo> = context.packageManager.queryIntentActivities(chooserIntent, PackageManager.MATCH_DEFAULT_ONLY)
for (resolveInfo in resInfoList) {
print(fileUri)
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, null, null)
}
result.success(true)
}
}
}

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="sites" path="."/>
<external-path name="external-sites" path="." />
<cache-path name="tmp cache" path="share/" />
</paths>

View File

@ -41,8 +41,6 @@ PODS:
- Flutter (1.0.0)
- flutter_plugin_android_lifecycle (0.0.1):
- Flutter
- flutter_share (0.0.1):
- Flutter
- MMWormhole (2.0.0):
- MMWormhole/Core (= 2.0.0)
- MMWormhole/Core (2.0.0)
@ -74,7 +72,6 @@ DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_plugin_android_lifecycle (from `.symlinks/plugins/flutter_plugin_android_lifecycle/ios`)
- flutter_share (from `.symlinks/plugins/flutter_share/ios`)
- MMWormhole (~> 2.0.0)
- package_info (from `.symlinks/plugins/package_info/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`)
@ -104,8 +101,6 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_plugin_android_lifecycle:
:path: ".symlinks/plugins/flutter_plugin_android_lifecycle/ios"
flutter_share:
:path: ".symlinks/plugins/flutter_share/ios"
package_info:
:path: ".symlinks/plugins/package_info/ios"
path_provider:
@ -129,7 +124,6 @@ SPEC CHECKSUMS:
FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
flutter_plugin_android_lifecycle: dc0b544e129eebb77a6bfb1239d4d1c673a60a35
flutter_share: 4be0208963c60b537e6255ed2ce1faae61cd9ac2
MMWormhole: 0cd3fd35a9118b2e2d762b499f54eeaace0be791
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62

View File

@ -20,6 +20,7 @@
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 */; };
4CF2F06A02A63B862C9F6F03 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384887B4785D38431E800D3A /* Pods_Runner.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
@ -76,6 +77,7 @@
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; };
43B828DA249C08DC00CA229C /* MMWormhole.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MMWormhole.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -188,6 +190,7 @@
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
43871C9C2444E2EC004F9075 /* Sites.swift */,
43AD63F324EB3802000FB47E /* Share.swift */,
);
path = Runner;
sourceTree = "<group>";
@ -418,6 +421,7 @@
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
43AD63F424EB3802000FB47E /* Share.swift in Sources */,
43871C9D2444E2EC004F9075 /* Sites.swift in Sources */,
437F725F2469B4B000A0C4B9 /* Site.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,

View File

@ -51,6 +51,9 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
case "active.setRemoteForTunnel": self.activeSetRemoteForTunnel(call: call, result: result)
case "active.closeTunnel": self.activeCloseTunnel(call: call, result: result)
case "share": Share.share(call: call, result: result)
case "shareFile": Share.shareFile(call: call, result: result)
default:
result(FlutterMethodNotImplemented)
}
@ -86,7 +89,6 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
guard let config = call.arguments as? String else { return result(NoArgumentsError()) }
var err: NSError?
print(config)
let yaml = MobileNebulaRenderConfig(config, "<hidden>", &err)
if (err != nil) {
return result(CallFailedError(message: "Error while rendering config", details: err!.localizedDescription))

119
ios/Runner/Share.swift Normal file
View File

@ -0,0 +1,119 @@
// 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: [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
}
}

View File

@ -160,10 +160,10 @@ class _MainScreenState extends State<MainScreen> {
var uuid = Uuid();
var cert = '''-----BEGIN NEBULA CERTIFICATE-----
CmMKBnBpeGVsNBIJiYCEUID+//8PKLqMivcFMKTzjoYGOiB4iANINzCjLdlQJSj/
vJDd080yggfLgW9hT4a/bhGZekog+W+YEJiV36evX4MueQ+npDzJd3zGg5gialu4
UNGYBP0SQL5bjEyafC0YtETEbrraSfwuFHMvUoi1Kc4XRzTPPvHsEaq3hNNTZtD7
Pt3sjH83zTMZfnD/Du3ahsvV0rAXUgc=
CmIKBHRlc3QSCoKUoIUMgP7//w8ourrS+QUwjre3iAY6IDbmIX5cwd+UYVhLADLa
A5PwucZPVrNtP0P9NJE0boM2SiBSGzy8bcuFWWK5aVArJGA9VDtLg1HuujBu8lOp
VTgklxJAgbI1Xb1C9JC3a1Cnc6NPqWhnw+3VLoDXE9poBav09+zhw5DPDtgvQmxU
Sbw6cAF4gPS4e/tZ5Kjc8QEvjk3HDQ==
-----END NEBULA CERTIFICATE-----''';
var ca = '''-----BEGIN NEBULA CERTIFICATE-----
@ -183,7 +183,9 @@ mUOcsdFcCZiXrj7ryQIG1+WfqA46w71A/lV4nAc=
unsafeRoutes: [UnsafeRoute(route: '10.3.3.3/32', via: '10.1.0.1')]
);
s.key = "-----BEGIN NEBULA X25519 PRIVATE KEY-----\ndYgPb04Bb1xzfgdCfVsKGZrCYe+u5tDWNXKipQBVZ44=\n-----END NEBULA X25519 PRIVATE KEY-----";
s.key = '''-----BEGIN NEBULA X25519 PRIVATE KEY-----
rmXnR1yvDZi1VPVmnNVY8NMsQpEpbbYlq7rul+ByQvg=
-----END NEBULA X25519 PRIVATE KEY-----''';
var err = await s.save();
if (err != null) {

View File

@ -1,13 +1,12 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:flutter_share/flutter_share.dart';
import 'package:mobile_nebula/components/SimplePage.dart';
import 'package:mobile_nebula/models/Site.dart';
import 'package:mobile_nebula/services/share.dart';
import 'package:mobile_nebula/services/utils.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
@ -79,7 +78,7 @@ class _SiteLogsScreenState extends State<SiteLogsScreen> {
padding: padding,
icon: Icon(context.platformIcons.share, size: 30),
onPressed: () {
FlutterShare.shareFile(title: '${widget.site.name} logs', filePath: widget.site.logFile);
Share.shareFile(title: '${widget.site.name} logs', filePath: widget.site.logFile, filename: '${widget.site.name}.log');
},
)),
Expanded(

View File

@ -172,7 +172,7 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
try {
var config = await widget.site.renderConfig();
Utils.openPage(context, (context) {
return RenderedConfigScreen(config: config);
return RenderedConfigScreen(config: config, name: widget.site.name);
});
} catch (err) {
Utils.popError(context, 'Failed to render the site config', err);

View File

@ -6,7 +6,6 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_share/flutter_share.dart';
import 'package:mobile_nebula/components/FormPage.dart';
import 'package:mobile_nebula/components/config/ConfigButtonItem.dart';
import 'package:mobile_nebula/components/config/ConfigItem.dart';
@ -15,6 +14,7 @@ import 'package:mobile_nebula/components/config/ConfigSection.dart';
import 'package:mobile_nebula/components/config/ConfigTextItem.dart';
import 'package:mobile_nebula/models/Certificate.dart';
import 'package:mobile_nebula/screens/siteConfig/CertificateDetailsScreen.dart';
import 'package:mobile_nebula/services/share.dart';
import 'package:mobile_nebula/services/utils.dart';
class CertificateResult {
@ -140,7 +140,7 @@ class _CertificateScreenState extends State<CertificateScreen> {
ConfigButtonItem(
content: Text('Share Public Key'),
onPressed: () async {
await FlutterShare.share(title: 'Please sign and return a certificate', text: pubKey);
await Share.share(title: 'Please sign and return a certificate', text: pubKey, filename: 'device.pub');
setState(() {
shared = true;
});

View File

@ -1,17 +1,27 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:mobile_nebula/components/SimplePage.dart';
import 'package:mobile_nebula/services/share.dart';
class RenderedConfigScreen extends StatelessWidget {
final String config;
final String name;
RenderedConfigScreen({Key key, this.config}) : super(key: key);
RenderedConfigScreen({Key key, this.config, this.name}) : super(key: key);
@override
Widget build(BuildContext context) {
return SimplePage(
title: 'Rendered Site Config',
scrollable: SimpleScrollable.both,
trailingActions: <Widget>[
PlatformIconButton(
padding: EdgeInsets.zero,
icon: Icon(context.platformIcons.share, size: 28.0),
onPressed: () => Share.share(title: '$name.yaml', text: config, filename: '$name.yaml'),
)
],
child: Container(
padding: EdgeInsets.all(5),
constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width),

56
lib/services/share.dart Normal file
View File

@ -0,0 +1,56 @@
// This code comes from https://github.com/lubritto/flutter_share with bugfixes for ipad
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class Share {
static const _channel = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
/// Shares a string of text
/// - 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
static Future<bool> share({@required String title, @required String text, @required String filename}) async {
assert(title != null && title.isNotEmpty);
assert(text != null && text.isNotEmpty);
assert(filename != null && filename.isNotEmpty);
if (title == null || title.isEmpty) {
throw FlutterError('Title cannot be null');
}
final bool success = await _channel.invokeMethod('share', <String, dynamic>{
'title': title,
'text': text,
'filename': filename,
});
return success;
}
/// Shares a local file
/// - title: Title of message or subject if sending an email
/// - filePath: Path to the file to share
/// - filename: An optional filename to override the existing file
static Future<bool> shareFile({@required String title, @required String filePath, String filename}) async {
assert(title != null && title.isNotEmpty);
assert(filePath != null && filePath.isNotEmpty);
if (title == null || title.isEmpty) {
throw FlutterError('Title cannot be null');
} else if (filePath == null || filePath.isEmpty) {
throw FlutterError('FilePath cannot be null');
}
final bool success =
await _channel.invokeMethod('shareFile', <String, dynamic>{
'title': title,
'filePath': filePath,
'filename': filename,
});
return success;
}
}

View File

@ -111,13 +111,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.8"
flutter_share:
dependency: "direct main"
description:
name: flutter_share
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2+1"
flutter_test:
dependency: "direct dev"
description: flutter

View File

@ -11,7 +11,7 @@ description: Mobile Nebula Client
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+31
version: 0.0.32+4
environment:
sdk: ">=2.1.0 <3.0.0"
@ -27,7 +27,6 @@ dependencies:
path_provider: ^1.6.0
file_picker: ^1.9.0
barcode_scan: ^3.0.1
flutter_share: ^1.0.2
uuid: ^2.0.4
package_info: '>=0.4.1 <2.0.0'
url_launcher: ^5.4.10