From d43c373c964262193575bb06ed921f8ef74991e7 Mon Sep 17 00:00:00 2001 From: Komposten Date: Sat, 23 May 2020 18:46:42 +0200 Subject: [PATCH] :construction: Migrate share plugin to new Flutter plugin API (and a new class) Also doing some minor refactoring of the share code --- .../com/example/openbook/MainActivity.java | 263 ++--------------- .../example/openbook/plugins/SharePlugin.java | 274 ++++++++++++++++++ 2 files changed, 293 insertions(+), 244 deletions(-) create mode 100644 android/app/src/main/java/com/example/openbook/plugins/SharePlugin.java diff --git a/android/app/src/main/java/com/example/openbook/MainActivity.java b/android/app/src/main/java/com/example/openbook/MainActivity.java index 38a52714e..9936e534e 100644 --- a/android/app/src/main/java/com/example/openbook/MainActivity.java +++ b/android/app/src/main/java/com/example/openbook/MainActivity.java @@ -1,260 +1,35 @@ package social.openbook.app; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; -import android.util.Log; -import android.webkit.MimeTypeMap; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import com.example.openbook.ImageConverter; import com.example.openbook.plugins.ImageConverterPlugin; -import com.example.openbook.util.InputStreamSupplier; - +import com.example.openbook.plugins.SharePlugin; import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.plugins.PluginRegistry; -import io.flutter.plugin.common.EventChannel; public class MainActivity extends FlutterActivity { + private SharePlugin sharePlugin; - public static final String SHARE_STREAM = "openbook.social/receive_share"; - - private EventChannel.EventSink eventSink = null; - private List intentBacklog = new ArrayList<>(); - private boolean streamCanceled = false; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Register the ImageConverterPlugin manually. It won't register automatically since it isn't added as a plugin via - // our pubspec.yaml. - // Note: getFlutterEngine() should not be null here since it is created in super.onCreate(). - PluginRegistry pluginRegistry = getFlutterEngine().getPlugins(); - pluginRegistry.add(new ImageConverterPlugin()); - - sendIntent(getIntent()); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - sendIntent(intent); - } - - private void sendIntent(Intent intent) { - if (intent.getAction().equals(Intent.ACTION_SEND)) { - if (eventSink == null) { - if (!streamCanceled && !intentBacklog.contains(intent)) { - intentBacklog.add(intent); - } - return; - } - - Map args = new HashMap<>(); - - try { - if (intent.getType().startsWith("image/")) { - Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); - if (!getExtensionFromUri(uri).equalsIgnoreCase("gif")) { - args.put("image", copyImageToTempFile(uri).toString()); - } else { - args.put("video", copyVideoToTempFile(uri).toString()); - } - } else if (intent.getType().startsWith("video/")) { - Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); - args.put("video", copyVideoToTempFile(uri).toString()); - } else if (intent.getType().startsWith("text/")) { - args.put("text", intent.getStringExtra(Intent.EXTRA_TEXT)); - } else { - Log.w(getClass().getSimpleName(), "unknown intent type \"" + intent.getType() + "\" received, ignoring"); - return; - } - } catch (KeyedException e) { - String msg; - if (e.getCause() != null) { - msg = String.format("an exception occurred while receiving share of type %s" + - "%n %s%n caused by %s", intent.getType(), e.toString(), e.getCause().toString()); - } else { - msg = String.format("an exception occurred while receiving share of type %s" + - "%n %s", intent.getType(), e.toString()); - } - String errorTextKey = getLocalizationKey(e); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); - args.put("error", errorTextKey); - Log.w(getClass().getSimpleName(), msg); - } + // Register the ImageConverterPlugin manually. It won't register automatically since it isn't added as a plugin via + // our pubspec.yaml. + // Note: getFlutterEngine() should not be null here since it is created in super.onCreate(). + PluginRegistry pluginRegistry = getFlutterEngine().getPlugins(); + pluginRegistry.add(new ImageConverterPlugin()); + sharePlugin = new SharePlugin(); + pluginRegistry.add(sharePlugin); - Log.i(getClass().getSimpleName(), "sending intent to flutter"); - eventSink.success(args); + // Pass the intent that created this activity to the share plugin, + // just in case it is an ACTION_SEND intent with share data. + sharePlugin.handleIntent(getIntent()); } - } - - private Uri copyImageToTempFile(Uri imageUri) throws KeyedException { - byte[] data = convertImage(imageUri); - File imageFile = createTemporaryFile(".jpeg"); - copyResourceToFile(() -> new ByteArrayInputStream(data), imageFile); - - return Uri.fromFile(imageFile); - } - - private byte[] convertImage(Uri imageUri) throws KeyedException { - try { - InputStream imageStream; - - if (imageUri.getScheme().equals("content")) { - imageStream = this.getContentResolver().openInputStream(imageUri); - } else if (imageUri.getScheme().equals("file")) { - imageStream = new FileInputStream(imageUri.getPath()); - } else { - throw new KeyedException(KeyedException.Key.UriSchemeNotSupported, imageUri.getScheme(), null); - } - - return ImageConverter.convertImageData(imageStream, ImageConverter.TargetFormat.JPEG); - } catch (FileNotFoundException e) { - throw new KeyedException(KeyedException.Key.ReadFileMissing, e); - } - } - - private Uri copyVideoToTempFile(Uri videoUri) throws KeyedException { - Uri result = null; - - if (videoUri.getScheme().equals("content") || videoUri.getScheme().equals("file")) { - String extension = getExtensionFromUri(videoUri); - File tempFile = createTemporaryFile("." + extension); - copyResourceToFile(() -> getContentResolver().openInputStream(videoUri), tempFile); - result = Uri.fromFile(tempFile); - } else { - throw new KeyedException(KeyedException.Key.UriSchemeNotSupported, videoUri.getScheme(), null); - } - - return result; - } - private String getExtensionFromUri(Uri uri) throws KeyedException { - if (uri.getScheme().equals("content")) { - String mime = this.getContentResolver().getType(uri); - return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); - } else if (uri.getScheme().equals("file")) { - return MimeTypeMap.getFileExtensionFromUrl(uri.toString()); - } else { - throw new KeyedException(KeyedException.Key.UriSchemeNotSupported, uri.getScheme(), null); + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + sharePlugin.handleIntent(intent); } - } - - private File createTemporaryFile(String extension) throws KeyedException { - String name = UUID.randomUUID().toString(); - - try { - File directory = new File(getCacheDir(), "mediaCache"); - if (!directory.exists()) { - directory.mkdirs(); - } - - return File.createTempFile(name, extension, directory); - } catch (IOException e) { - throw new KeyedException(KeyedException.Key.TempCreationFailed, e); - } catch (SecurityException e) { - throw new KeyedException(KeyedException.Key.TempCreationDenied, e); - } - } - - private void copyResourceToFile(InputStreamSupplier inputSupplier, File target) throws KeyedException { - try (InputStream input = inputSupplier.get()) { - try (OutputStream output = new FileOutputStream(target)) { - byte[] data = new byte[1024]; - int length; - while ((length = input.read(data)) > 0) { - output.write(data, 0, length); - } - } - catch (FileNotFoundException e) { - throw new KeyedException(KeyedException.Key.WriteTempMissing, e); - } catch (IOException e) { - throw new KeyedException(KeyedException.Key.WriteTempFailed, e); - } catch (SecurityException e) { - throw new KeyedException(KeyedException.Key.WriteTempDenied, e); - } - } - catch (FileNotFoundException e) { - throw new KeyedException(KeyedException.Key.ReadFileMissing, e); - } catch (IOException e) { - //Exception when closing the streams. Ignore. - } - } - - private String getLocalizationKey(KeyedException e) { - String errorTextKey = ""; - - switch (e.getKey()) { - case TempCreationFailed: - case WriteTempFailed: - case WriteTempMissing: - errorTextKey = "error__receive_share_temp_write_failed"; - break; - case WriteTempDenied: - case TempCreationDenied: - errorTextKey = "error__receive_share_temp_write_denied"; - break; - case UriSchemeNotSupported: - errorTextKey = "error__receive_share_invalid_uri_scheme"; - break; - case ReadFileMissing: - errorTextKey = "error__receive_share_file_not_found"; - break; - } - - return errorTextKey; - } -} - -class KeyedException extends Exception { - public enum Key { - TempCreationFailed("Failed to create temporary file."), - TempCreationDenied(TempCreationFailed.message), - WriteTempDenied("Failed to write to temporary file."), - WriteTempFailed(WriteTempDenied.message), - WriteTempMissing(WriteTempDenied.message), - ReadFileMissing("Failed to read the shared file."), - UriSchemeNotSupported("Unsupported URI scheme: "); - - private String message; - - private Key(String msg) { - message = msg; - } - } - private final Key key; - - public KeyedException(Key key, Throwable cause) { - super("", cause); - this.key = key; - } - - public KeyedException(Key key, String message, Throwable cause) { - super(message, cause); - this.key = key; - } - - public Key getKey() { - return key; - } - - @Override - public String getMessage() { - return key.message + super.getMessage(); - } } diff --git a/android/app/src/main/java/com/example/openbook/plugins/SharePlugin.java b/android/app/src/main/java/com/example/openbook/plugins/SharePlugin.java new file mode 100644 index 000000000..40b4c42bd --- /dev/null +++ b/android/app/src/main/java/com/example/openbook/plugins/SharePlugin.java @@ -0,0 +1,274 @@ +package com.example.openbook.plugins; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; +import android.webkit.MimeTypeMap; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.example.openbook.ImageConverter; +import com.example.openbook.util.InputStreamSupplier; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.EventChannel; + +import java.io.*; +import java.util.*; + +public class SharePlugin implements FlutterPlugin, EventChannel.StreamHandler { + private final Queue intentBacklog = new LinkedList<>(); + private Context applicationContext; + private EventChannel eventChannel; + private EventChannel.EventSink eventSink; + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + applicationContext = binding.getApplicationContext(); + eventChannel = new EventChannel(binding.getBinaryMessenger(), "openbook.social/receive_share"); + eventChannel.setStreamHandler(this); + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + applicationContext = null; + eventChannel.setStreamHandler(null); + eventChannel = null; + } + + @Override + public void onListen(Object arguments, EventChannel.EventSink eventSink) { + this.eventSink = eventSink; + while (!intentBacklog.isEmpty()) { + handleIntent(intentBacklog.poll()); + } + } + + @Override + public void onCancel(Object arguments) { + eventSink = null; + } + + public void handleIntent(@Nullable Intent intent) { + // Only handle ACTION_SEND intents. + if (intent == null || !Objects.equals(intent.getAction(), Intent.ACTION_SEND)) { + return; + } + + // Backlog the intent if we don't yet have an event sink to send it to. + if (eventSink == null) { + if (!intentBacklog.contains(intent)) { + intentBacklog.add(intent); + } + return; + } + + Map args = new HashMap<>(); + + String intentType = Objects.toString(intent.getType()); + try { + if (intentType.startsWith("image/")) { + handleImageIntent(intent, args); + } else if (intentType.startsWith("video/")) { + handleVideoIntent(intent, args); + } else if (intentType.startsWith("text/")) { + args.put("text", intent.getStringExtra(Intent.EXTRA_TEXT)); + } else { + Log.w(getClass().getSimpleName(), "unknown intent type \"" + intentType + "\" received, ignoring"); + } + } catch (KeyedException e) { + String msg; + if (e.getCause() != null) { + msg = String.format("an exception occurred while receiving share of type %s" + + "%n %s%n caused by %s", intentType, e.toString(), e.getCause().toString()); + } else { + msg = String.format("an exception occurred while receiving share of type %s" + + "%n %s", intentType, e.toString()); + } + String errorTextKey = getLocalizationKey(e); + + args.put("error", errorTextKey); + Log.w(getClass().getSimpleName(), msg); + } + + if (!args.isEmpty()) { + Log.i(getClass().getSimpleName(), "sending intent to flutter"); + eventSink.success(args); + } + } + + private void handleVideoIntent(@NonNull Intent intent, Map args) throws KeyedException { + Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); + if (uri != null) { + args.put("video", copyVideoToTempFile(uri).toString()); + } else { + Log.w(getClass().getSimpleName(), "video intent without video URI received, ignoring"); + } + } + + private void handleImageIntent(@NonNull Intent intent, Map args) throws KeyedException { + Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); + if (uri != null) { + if (!getExtensionFromUri(uri).equalsIgnoreCase("gif")) { + args.put("image", copyImageToTempFile(uri).toString()); + } else { + args.put("video", copyVideoToTempFile(uri).toString()); + } + } else { + Log.w(getClass().getSimpleName(), "image intent without image URI received, ignoring"); + } + } + + private Uri copyImageToTempFile(Uri imageUri) throws KeyedException { + byte[] data = convertImage(imageUri); + File imageFile = createTemporaryFile(".jpeg"); + copyResourceToFile(() -> new ByteArrayInputStream(data), imageFile); + + return Uri.fromFile(imageFile); + } + + private byte[] convertImage(Uri imageUri) throws KeyedException { + try { + InputStream imageStream; + + if (imageUri.getScheme().equals("content")) { + imageStream = applicationContext.getContentResolver().openInputStream(imageUri); + } else if (imageUri.getScheme().equals("file")) { + imageStream = new FileInputStream(imageUri.getPath()); + } else { + throw new KeyedException(KeyedException.Key.UriSchemeNotSupported, imageUri.getScheme(), null); + } + + return ImageConverter.convertImageData(imageStream, ImageConverter.TargetFormat.JPEG); + } catch (FileNotFoundException e) { + throw new KeyedException(KeyedException.Key.ReadFileMissing, e); + } + } + + private Uri copyVideoToTempFile(Uri videoUri) throws KeyedException { + Uri result; + + if (videoUri.getScheme().equals("content") || videoUri.getScheme().equals("file")) { + String extension = getExtensionFromUri(videoUri); + File tempFile = createTemporaryFile("." + extension); + copyResourceToFile(() -> applicationContext.getContentResolver().openInputStream(videoUri), tempFile); + result = Uri.fromFile(tempFile); + } else { + throw new KeyedException(KeyedException.Key.UriSchemeNotSupported, videoUri.getScheme(), null); + } + + return result; + } + + private String getExtensionFromUri(Uri uri) throws KeyedException { + if (uri.getScheme().equals("content")) { + String mime = applicationContext.getContentResolver().getType(uri); + return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); + } else if (uri.getScheme().equals("file")) { + return MimeTypeMap.getFileExtensionFromUrl(uri.toString()); + } else { + throw new KeyedException(KeyedException.Key.UriSchemeNotSupported, uri.getScheme(), null); + } + } + + private File createTemporaryFile(String extension) throws KeyedException { + String name = UUID.randomUUID().toString(); + + try { + File directory = new File(applicationContext.getCacheDir(), "mediaCache"); + if (!directory.exists()) { + directory.mkdirs(); + } + + return File.createTempFile(name, extension, directory); + } catch (IOException e) { + throw new KeyedException(KeyedException.Key.TempCreationFailed, e); + } catch (SecurityException e) { + throw new KeyedException(KeyedException.Key.TempCreationDenied, e); + } + } + + private void copyResourceToFile(InputStreamSupplier inputSupplier, File target) throws KeyedException { + try (InputStream input = inputSupplier.get()) { + try (OutputStream output = new FileOutputStream(target)) { + byte[] data = new byte[1024]; + int length; + while ((length = input.read(data)) > 0) { + output.write(data, 0, length); + } + } catch (FileNotFoundException e) { + throw new KeyedException(KeyedException.Key.WriteTempMissing, e); + } catch (IOException e) { + throw new KeyedException(KeyedException.Key.WriteTempFailed, e); + } catch (SecurityException e) { + throw new KeyedException(KeyedException.Key.WriteTempDenied, e); + } + } catch (FileNotFoundException e) { + throw new KeyedException(KeyedException.Key.ReadFileMissing, e); + } catch (IOException e) { + //Exception when closing the streams. Ignore. + } + } + + private String getLocalizationKey(KeyedException e) { + String errorTextKey = ""; + + switch (e.getKey()) { + case TempCreationFailed: + case WriteTempFailed: + case WriteTempMissing: + errorTextKey = "error__receive_share_temp_write_failed"; + break; + case WriteTempDenied: + case TempCreationDenied: + errorTextKey = "error__receive_share_temp_write_denied"; + break; + case UriSchemeNotSupported: + errorTextKey = "error__receive_share_invalid_uri_scheme"; + break; + case ReadFileMissing: + errorTextKey = "error__receive_share_file_not_found"; + break; + } + + return errorTextKey; + } +} + +class KeyedException extends Exception { + public enum Key { + TempCreationFailed("Failed to create temporary file."), + TempCreationDenied(TempCreationFailed.message), + WriteTempDenied("Failed to write to temporary file."), + WriteTempFailed(WriteTempDenied.message), + WriteTempMissing(WriteTempDenied.message), + ReadFileMissing("Failed to read the shared file."), + UriSchemeNotSupported("Unsupported URI scheme: "); + + private final String message; + + Key(String msg) { + message = msg; + } + } + + private final KeyedException.Key key; + + public KeyedException(KeyedException.Key key, Throwable cause) { + super("", cause); + this.key = key; + } + + public KeyedException(KeyedException.Key key, String message, Throwable cause) { + super(message, cause); + this.key = key; + } + + public KeyedException.Key getKey() { + return key; + } + + @Override + public String getMessage() { + return key.message + super.getMessage(); + } +}