diff --git a/packages/app-mobile/android/app/src/main/AndroidManifest.xml b/packages/app-mobile/android/app/src/main/AndroidManifest.xml index 453811060..27e2d9506 100644 --- a/packages/app-mobile/android/app/src/main/AndroidManifest.xml +++ b/packages/app-mobile/android/app/src/main/AndroidManifest.xml @@ -77,18 +77,8 @@ - - - + @@ -99,8 +89,8 @@ + - diff --git a/packages/app-mobile/android/app/src/main/java/net/cozic/joplin/ShareActivity.java b/packages/app-mobile/android/app/src/main/java/net/cozic/joplin/ShareActivity.java deleted file mode 100644 index 492f1dda5..000000000 --- a/packages/app-mobile/android/app/src/main/java/net/cozic/joplin/ShareActivity.java +++ /dev/null @@ -1,12 +0,0 @@ -package net.cozic.joplin; - -import com.facebook.react.ReactActivity; - -public class ShareActivity extends ReactActivity { - @Override - protected String getMainComponentName() { - // this is the name AppRegistry will use to launch the Share View - return "Joplin"; - } -} - diff --git a/packages/app-mobile/android/app/src/main/java/net/cozic/joplin/share/SharePackage.java b/packages/app-mobile/android/app/src/main/java/net/cozic/joplin/share/SharePackage.java index f85aa2604..981fd37b2 100644 --- a/packages/app-mobile/android/app/src/main/java/net/cozic/joplin/share/SharePackage.java +++ b/packages/app-mobile/android/app/src/main/java/net/cozic/joplin/share/SharePackage.java @@ -14,6 +14,7 @@ import androidx.annotation.NonNull; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; @@ -21,6 +22,7 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.uimanager.ViewManager; import java.io.File; @@ -42,11 +44,14 @@ public class SharePackage implements ReactPackage { return Collections.emptyList(); } - public static class ShareModule extends ReactContextBaseJavaModule implements ActivityEventListener { + public static class ShareModule extends ReactContextBaseJavaModule implements ActivityEventListener, LifecycleEventListener { + private boolean handledStartIntent = false; + private Intent receivedShareIntent = null; ShareModule(@NonNull ReactApplicationContext reactContext) { super(reactContext); reactContext.addActivityEventListener(this); + reactContext.addLifecycleEventListener(this); } @Override @@ -55,6 +60,14 @@ public class SharePackage implements ReactPackage { @Override public void onNewIntent(Intent intent) { + if (intent == null || !(Intent.ACTION_SEND.equals(intent.getAction()) + || Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction()))) { + return; + } + receivedShareIntent = intent; + this.getReactApplicationContext() + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("new_share_intent", null); } @NonNull @@ -77,37 +90,29 @@ public class SharePackage implements ReactPackage { } private WritableMap processIntent() { - Activity currentActivity = getCurrentActivity(); WritableMap map = Arguments.createMap(); - if (currentActivity == null) { + if (receivedShareIntent == null) { return null; } - Intent intent = currentActivity.getIntent(); - - if (intent == null || !(Intent.ACTION_SEND.equals(intent.getAction()) - || Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction()))) { + if (receivedShareIntent.getBooleanExtra("is_processed", false)) { return null; } - if (intent.getBooleanExtra("is_processed", false)) { - return null; - } - - String type = intent.getType() == null ? "" : intent.getType(); + String type = receivedShareIntent.getType() == null ? "" : receivedShareIntent.getType(); map.putString("type", type); - map.putString("title", getTitle(intent)); - map.putString("text", intent.getStringExtra(Intent.EXTRA_TEXT)); + map.putString("title", getTitle(receivedShareIntent)); + map.putString("text", receivedShareIntent.getStringExtra(Intent.EXTRA_TEXT)); WritableArray resources = Arguments.createArray(); - if (Intent.ACTION_SEND.equals(intent.getAction())) { - if (intent.hasExtra(Intent.EXTRA_STREAM)) { - resources.pushMap(getFileData(intent.getParcelableExtra(Intent.EXTRA_STREAM))); + if (Intent.ACTION_SEND.equals(receivedShareIntent.getAction())) { + if (receivedShareIntent.hasExtra(Intent.EXTRA_STREAM)) { + resources.pushMap(getFileData(receivedShareIntent.getParcelableExtra(Intent.EXTRA_STREAM))); } - } else if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) { - ArrayList imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + } else if (Intent.ACTION_SEND_MULTIPLE.equals(receivedShareIntent.getAction())) { + ArrayList imageUris = receivedShareIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); if (imageUris != null) { for (Uri uri : imageUris) { resources.pushMap(getFileData(uri)); @@ -116,7 +121,7 @@ public class SharePackage implements ReactPackage { } map.putArray("resources", resources); - intent.putExtra("is_processed", true); + receivedShareIntent.putExtra("is_processed", true); return map; } @@ -185,5 +190,36 @@ public class SharePackage implements ReactPackage { } return ext; } + + @ReactMethod + public void addListener(String eventName) { + // Set up any upstream listeners or background tasks as necessary + } + + @ReactMethod + public void removeListeners(Integer count) { + // Remove upstream listeners, stop unnecessary background tasks + } + + @Override + public void onHostResume() { + if (this.getCurrentActivity() != null) { + Intent intent = this.getCurrentActivity().getIntent(); + if (this.handledStartIntent) { + // sometimes onHostResume is fired after onNewIntent + // and we only care about the activity intent when the first time app opens + return; + } + this.handledStartIntent = true; + this.onNewIntent(intent); + } + } + + @Override + public void onHostPause() {} + + @Override + public void onHostDestroy() { + } } } diff --git a/packages/app-mobile/components/screens/Note.tsx b/packages/app-mobile/components/screens/Note.tsx index 42363688a..e41492773 100644 --- a/packages/app-mobile/components/screens/Note.tsx +++ b/packages/app-mobile/components/screens/Note.tsx @@ -139,15 +139,7 @@ class NoteScreenComponent extends BaseScreenComponent { } if (this.state.fromShare) { - // effectively the same as NAV_BACK but NAV_BACK causes undesired behaviour in this case: - // - share to Joplin from some other app - // - open Joplin and open any note - // - go back -- with NAV_BACK this causes the app to exit rather than just showing notes - this.props.dispatch({ - type: 'NAV_GO', - routeName: 'Notes', - folderId: this.state.note.parent_id, - }); + ShareExtension.close(); return true; } @@ -458,10 +450,6 @@ class NoteScreenComponent extends BaseScreenComponent { shared.uninstallResourceHandling(this.refreshResource); - if (this.state.fromShare) { - ShareExtension.close(); - } - this.saveActionQueue(this.state.note.id).processAllNow(); // It cannot theoretically be undefined, since componentDidMount should always be called before diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx index 6bbd2d978..349205d65 100644 --- a/packages/app-mobile/root.tsx +++ b/packages/app-mobile/root.tsx @@ -735,6 +735,15 @@ class AppComponent extends React.Component { } }; + this.handleNewShare_ = () => { + // look at this.handleOpenURL_ comment + if (this.props.biometricsDone) { + void this.handleShareData(); + } + }; + + this.unsubscribeNewShareListener_ = ShareExtension.addShareListener(this.handleNewShare_); + this.handleScreenWidthChange_ = this.handleScreenWidthChange_.bind(this); } @@ -840,6 +849,11 @@ class AppComponent extends React.Component { } if (this.unsubscribeNetInfoHandler_) this.unsubscribeNetInfoHandler_(); + + if (this.unsubscribeNewShareListener_) { + this.unsubscribeNewShareListener_(); + this.unsubscribeNewShareListener_ = undefined; + } } public componentDidUpdate(prevProps: any) { diff --git a/packages/app-mobile/utils/ShareExtension.ts b/packages/app-mobile/utils/ShareExtension.ts index 7f457400c..3d26d5661 100644 --- a/packages/app-mobile/utils/ShareExtension.ts +++ b/packages/app-mobile/utils/ShareExtension.ts @@ -1,3 +1,5 @@ +import { NativeEventEmitter } from 'react-native'; + const { NativeModules, Platform } = require('react-native'); export interface SharedData { @@ -6,16 +8,25 @@ export interface SharedData { resources?: string[]; } +let eventEmitter: NativeEventEmitter | undefined; + const ShareExtension = (NativeModules.ShareExtension) ? { data: () => NativeModules.ShareExtension.data(), close: () => NativeModules.ShareExtension.close(), shareURL: (Platform.OS === 'ios') ? NativeModules.ShareExtension.getConstants().SHARE_EXTENSION_SHARE_URL : '', + addShareListener: (Platform.OS === 'android') ? ((handler: (event: any)=> void) => { + if (!eventEmitter) { + eventEmitter = new NativeEventEmitter(NativeModules.ShareExtension); + } + return eventEmitter.addListener('new_share_intent', handler).remove; + }) : (() => {}), } : { data: () => {}, close: () => {}, shareURL: '', + addShareListener: () => {}, }; export default ShareExtension;