diff --git a/.eslintignore b/.eslintignore
index 86a551e09..f69757cda 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -89,6 +89,7 @@ ElectronClient/gui/NoteToolbar/NoteToolbar.js
ElectronClient/gui/ResourceScreen.js
ElectronClient/gui/ShareNoteDialog.js
ReactNativeClient/lib/AsyncActionQueue.js
+ReactNativeClient/lib/checkPermissions.js
ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.js
ReactNativeClient/lib/hooks/usePrevious.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.js
@@ -96,6 +97,8 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/JoplinServerApi.js
+ReactNativeClient/lib/ShareExtension.js
+ReactNativeClient/lib/shareHandler.js
ReactNativeClient/lib/services/keychain/KeychainService.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js
diff --git a/.gitignore b/.gitignore
index 2297c54d0..aeb34dde4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -79,6 +79,7 @@ ElectronClient/gui/NoteToolbar/NoteToolbar.js
ElectronClient/gui/ResourceScreen.js
ElectronClient/gui/ShareNoteDialog.js
ReactNativeClient/lib/AsyncActionQueue.js
+ReactNativeClient/lib/checkPermissions.js
ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.js
ReactNativeClient/lib/hooks/usePrevious.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.js
@@ -86,6 +87,8 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/JoplinServerApi.js
+ReactNativeClient/lib/ShareExtension.js
+ReactNativeClient/lib/shareHandler.js
ReactNativeClient/lib/services/keychain/KeychainService.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js
diff --git a/ReactNativeClient/android/app/src/main/AndroidManifest.xml b/ReactNativeClient/android/app/src/main/AndroidManifest.xml
index 8b00160c8..5851ab218 100644
--- a/ReactNativeClient/android/app/src/main/AndroidManifest.xml
+++ b/ReactNativeClient/android/app/src/main/AndroidManifest.xml
@@ -101,11 +101,17 @@
android:configChanges="orientation"
android:label="@string/app_name"
android:screenOrientation="portrait"
- android:theme="@style/AppTheme" >
+ android:excludeFromRecents="true"
+ android:theme="@style/AppTheme">
-
+
+
+
+
+
+
diff --git a/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/MainApplication.java b/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/MainApplication.java
index fe20e550f..c267a78d6 100644
--- a/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/MainApplication.java
+++ b/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/MainApplication.java
@@ -2,23 +2,21 @@ package net.cozic.joplin;
import android.app.Application;
import android.content.Context;
+import android.database.CursorWindow;
import android.os.Build;
import android.webkit.WebView;
+
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
-import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage;
-import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
-import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
+
+import net.cozic.joplin.share.SharePackage;
+
import java.lang.reflect.Field;
-// import com.alinz.parkerdan.shareextension.SharePackage;
-import java.util.Arrays;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
-import android.database.CursorWindow;
-import com.reactNativeQuickActions.AppShortcutsPackage;
public class MainApplication extends Application implements ReactApplication {
@@ -31,10 +29,9 @@ public class MainApplication extends Application implements ReactApplication {
@Override
protected List getPackages() {
- @SuppressWarnings("UnnecessaryLocalVariable")
List packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
- // packages.add(new MyReactNativePackage());
+ packages.add(new SharePackage());
return packages;
}
diff --git a/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/share/ShareActivity.java b/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/share/ShareActivity.java
index eb402a360..4baf7b9f4 100644
--- a/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/share/ShareActivity.java
+++ b/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/share/ShareActivity.java
@@ -1,10 +1,7 @@
package net.cozic.joplin.share;
-
-// import ReactActivity
import com.facebook.react.ReactActivity;
-
public class ShareActivity extends ReactActivity {
@Override
protected String getMainComponentName() {
diff --git a/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/share/ShareApplication.java b/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/share/ShareApplication.java
deleted file mode 100644
index 04e8a5ce8..000000000
--- a/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/share/ShareApplication.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package net.cozic.joplin.share;
-// import build config
-import net.cozic.joplin.BuildConfig;
-
-// import com.alinz.parkerdan.shareextension.SharePackage;
-
-import android.app.Application;
-
-import com.facebook.react.shell.MainReactPackage;
-import com.facebook.react.ReactNativeHost;
-import com.facebook.react.ReactApplication;
-import com.facebook.react.ReactPackage;
-
-import java.util.Arrays;
-import java.util.List;
-
-
-public class ShareApplication extends Application implements ReactApplication {
- private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
- @Override
- public boolean getUseDeveloperSupport() {
- return BuildConfig.DEBUG;
- }
-
- @Override
- protected List getPackages() {
- return Arrays.asList(
- new MainReactPackage()
- // new SharePackage()
- );
- }
- };
-
- @Override
- public ReactNativeHost getReactNativeHost() {
- return mReactNativeHost;
- }
-}
diff --git a/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/share/SharePackage.java b/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/share/SharePackage.java
new file mode 100644
index 000000000..ebb61102e
--- /dev/null
+++ b/ReactNativeClient/android/app/src/main/java/net/cozic/joplin/share/SharePackage.java
@@ -0,0 +1,184 @@
+package net.cozic.joplin.share;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.OpenableColumns;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+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.NativeModule;
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+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.uimanager.ViewManager;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class SharePackage implements ReactPackage {
+
+ @NonNull
+ @Override
+ public List createNativeModules(@NonNull ReactApplicationContext reactContext) {
+ return Collections.singletonList(new ShareModule(reactContext));
+ }
+
+ @NonNull
+ @Override
+ public List createViewManagers(@NonNull ReactApplicationContext reactContext) {
+ return Collections.emptyList();
+ }
+
+ public static class ShareModule extends ReactContextBaseJavaModule implements ActivityEventListener {
+
+ ShareModule(@NonNull ReactApplicationContext reactContext) {
+ super(reactContext);
+ reactContext.addActivityEventListener(this);
+ }
+
+ @Override
+ public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ }
+
+ @NonNull
+ @Override
+ public String getName() {
+ return "ShareExtension";
+ }
+
+ @ReactMethod
+ public void close() {
+ Activity currentActivity = getCurrentActivity();
+ if (currentActivity != null) {
+ currentActivity.finish();
+ }
+ }
+
+ @ReactMethod
+ public void data(Promise promise) {
+ promise.resolve(processIntent());
+ }
+
+ private WritableMap processIntent() {
+ Activity currentActivity = getCurrentActivity();
+ WritableMap map = Arguments.createMap();
+
+ if (currentActivity == null) {
+ return null;
+ }
+
+ Intent intent = currentActivity.getIntent();
+
+ if (intent == null || !(Intent.ACTION_SEND.equals(intent.getAction())
+ || Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction()))) {
+ return null;
+ }
+
+ String type = intent.getType() == null ? "" : intent.getType();
+ map.putString("type", type);
+ map.putString("title", getTitle(intent));
+ map.putString("text", intent.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)));
+ }
+ } else if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {
+ ArrayList imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ if (imageUris != null) {
+ for (Uri uri : imageUris) {
+ resources.pushMap(getFileData(uri));
+ }
+ }
+ }
+
+ map.putArray("resources", resources);
+ return map;
+ }
+
+ private String getTitle(Intent intent) {
+ if (intent.hasExtra(Intent.EXTRA_SUBJECT)) {
+ return intent.getStringExtra(Intent.EXTRA_SUBJECT);
+ } else if (intent.hasExtra(Intent.EXTRA_TITLE)) {
+ return intent.getStringExtra(Intent.EXTRA_TITLE);
+ } else {
+ return null;
+ }
+ }
+
+ private WritableMap getFileData(Uri uri) {
+ Log.d("joplin", "getFileData: " + uri);
+
+ WritableMap imageData = Arguments.createMap();
+
+ ContentResolver contentResolver = getCurrentActivity().getContentResolver();
+ String mimeType = contentResolver.getType(uri);
+ String name = getFileName(uri, contentResolver);
+
+ if (mimeType == null || mimeType.equals("image/*")) {
+ String extension = getFileExtension(name);
+ mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+ }
+
+ imageData.putString("uri", uri.toString());
+ imageData.putString("name", name);
+ imageData.putString("mimeType", mimeType);
+ return imageData;
+ }
+
+ private String getFileName(Uri uri, ContentResolver contentResolver) {
+ if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+ File file = new File(uri.getPath());
+ return file.getName();
+ } else if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
+ String name = null;
+ Cursor cursor = contentResolver.query(uri, null, null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+ name = cursor.getString(nameIndex);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ return name;
+ } else {
+ Log.w("joplin", "Unknown URI scheme: " + uri.getScheme());
+ return null;
+ }
+ }
+
+ private String getFileExtension(String file) {
+ if (file == null) {
+ return null;
+ }
+ String ext = null;
+ int i = file.lastIndexOf('.');
+ if (i > 0) {
+ ext = file.substring(i + 1);
+ }
+ return ext;
+ }
+ }
+}
diff --git a/ReactNativeClient/lib/ShareExtension.ts b/ReactNativeClient/lib/ShareExtension.ts
new file mode 100644
index 000000000..84681d28e
--- /dev/null
+++ b/ReactNativeClient/lib/ShareExtension.ts
@@ -0,0 +1,19 @@
+const { NativeModules, Platform } = require('react-native');
+
+export interface SharedData {
+ title?: string,
+ text?: string,
+ resources?: string[]
+}
+
+const ShareExtension = (Platform.OS === 'android' && NativeModules.ShareExtension) ?
+ {
+ data: () => NativeModules.ShareExtension.data(),
+ close: () => NativeModules.ShareExtension.close(),
+ } :
+ {
+ data: () => {},
+ close: () => {},
+ };
+
+export default ShareExtension;
diff --git a/ReactNativeClient/lib/checkPermissions.ts b/ReactNativeClient/lib/checkPermissions.ts
new file mode 100644
index 000000000..23d26c37d
--- /dev/null
+++ b/ReactNativeClient/lib/checkPermissions.ts
@@ -0,0 +1,9 @@
+const { PermissionsAndroid } = require('react-native');
+
+export default async (permissions: string) => {
+ let result = await PermissionsAndroid.check(permissions);
+ if (result !== PermissionsAndroid.RESULTS.GRANTED) {
+ result = await PermissionsAndroid.request(permissions);
+ }
+ return result === PermissionsAndroid.RESULTS.GRANTED;
+};
diff --git a/ReactNativeClient/lib/components/screens/note.js b/ReactNativeClient/lib/components/screens/note.js
index 19e7f44a2..a7f7ca9b4 100644
--- a/ReactNativeClient/lib/components/screens/note.js
+++ b/ReactNativeClient/lib/components/screens/note.js
@@ -36,7 +36,7 @@ const ImageResizer = require('react-native-image-resizer').default;
const shared = require('lib/components/shared/note-screen-shared.js');
const ImagePicker = require('react-native-image-picker');
const { SelectDateTimeDialog } = require('lib/components/select-date-time-dialog.js');
-// const ShareExtension = require('react-native-share-extension').default;
+const ShareExtension = require('lib/ShareExtension.js').default;
const CameraView = require('lib/components/CameraView');
const SearchEngine = require('lib/services/SearchEngine');
const urlUtils = require('lib/urlUtils');
@@ -123,6 +123,19 @@ class NoteScreenComponent extends BaseScreenComponent {
return true;
}
+ 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,
+ });
+ return true;
+ }
+
return false;
};
@@ -333,9 +346,9 @@ class NoteScreenComponent extends BaseScreenComponent {
shared.uninstallResourceHandling(this.refreshResource);
- // if (Platform.OS !== 'ios' && this.state.fromShare) {
- // ShareExtension.close();
- // }
+ if (this.state.fromShare) {
+ ShareExtension.close();
+ }
}
title_changeText(text) {
@@ -527,7 +540,7 @@ class NoteScreenComponent extends BaseScreenComponent {
try {
if (mimeType == 'image/jpeg' || mimeType == 'image/jpg' || mimeType == 'image/png') {
- const done = await this.resizeImage(localFilePath, targetPath, pickerResponse.mime);
+ const done = await this.resizeImage(localFilePath, targetPath, mimeType);
if (!done) return;
} else {
if (fileType === 'image') {
diff --git a/ReactNativeClient/lib/components/shared/note-screen-shared.js b/ReactNativeClient/lib/components/shared/note-screen-shared.js
index 31129cd84..7824fa46d 100644
--- a/ReactNativeClient/lib/components/shared/note-screen-shared.js
+++ b/ReactNativeClient/lib/components/shared/note-screen-shared.js
@@ -200,8 +200,7 @@ shared.initState = async function(comp) {
const note = await Note.load(comp.props.noteId);
let mode = 'view';
- if (isProvisionalNote) {
- // note = comp.props.itemType == 'todo' ? Note.newTodo(comp.props.folderId) : Note.new(comp.props.folderId);
+ if (isProvisionalNote && !comp.props.sharedData) {
mode = 'edit';
comp.scheduleFocusUpdate();
}
@@ -218,8 +217,25 @@ shared.initState = async function(comp) {
noteResources: await shared.attachedResources(note ? note.body : ''),
});
+
if (comp.props.sharedData) {
- this.noteComponent_change(comp, 'body', comp.props.sharedData.value);
+ if (comp.props.sharedData.title) {
+ this.noteComponent_change(comp, 'title', comp.props.sharedData.title);
+ }
+ if (comp.props.sharedData.text) {
+ this.noteComponent_change(comp, 'body', comp.props.sharedData.text);
+ }
+ if (comp.props.sharedData.resources) {
+ for (let i = 0; i < comp.props.sharedData.resources.length; i++) {
+ const resource = comp.props.sharedData.resources[i];
+ reg.logger().info(`about to attach resource ${JSON.stringify(resource)}`);
+ await comp.attachFile({
+ uri: resource.uri,
+ type: resource.mimeType,
+ fileName: resource.name,
+ }, null);
+ }
+ }
}
// eslint-disable-next-line require-atomic-updates
diff --git a/ReactNativeClient/lib/shareHandler.ts b/ReactNativeClient/lib/shareHandler.ts
new file mode 100644
index 000000000..cfe6281b6
--- /dev/null
+++ b/ReactNativeClient/lib/shareHandler.ts
@@ -0,0 +1,42 @@
+const Note = require('lib/models/Note.js');
+const checkPermissions = require('lib/checkPermissions.js').default;
+const { ToastAndroid } = require('react-native');
+const { PermissionsAndroid } = require('react-native');
+
+import ShareExtension, { SharedData } from './ShareExtension';
+
+export default async (sharedData: SharedData, folderId: string, dispatch: Function) => {
+
+ if (!!sharedData.resources && sharedData.resources.length > 0) {
+ const hasPermissions = await checkPermissions(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE);
+
+ if (!hasPermissions) {
+ ToastAndroid.show('Cannot receive shared data - permission denied', ToastAndroid.SHORT);
+ ShareExtension.close();
+ return;
+ }
+ }
+
+ // This is a bit hacky, but the surest way to go to
+ // the needed note. We go back one screen in case there's
+ // already a note open - if we don't do this, the dispatch
+ // below will do nothing (because routeName wouldn't change)
+ // Then we wait a bit for the state to be set correctly, and
+ // finally we go to the new note.
+ await dispatch({ type: 'NAV_BACK' });
+
+ await dispatch({ type: 'SIDE_MENU_CLOSE' });
+
+ const newNote = await Note.save({
+ parent_id: folderId,
+ }, { provisional: true });
+
+ setTimeout(() => {
+ dispatch({
+ type: 'NAV_GO',
+ routeName: 'Note',
+ noteId: newNote.id,
+ sharedData: sharedData,
+ });
+ }, 5);
+};
diff --git a/ReactNativeClient/root.js b/ReactNativeClient/root.js
index db4c76fa1..d14a25515 100644
--- a/ReactNativeClient/root.js
+++ b/ReactNativeClient/root.js
@@ -2,7 +2,7 @@ import setUpQuickActions from './setUpQuickActions';
import PluginAssetsLoader from './PluginAssetsLoader';
const React = require('react');
-const { AppState, Keyboard, NativeModules, BackHandler, Platform, Animated, View, StatusBar } = require('react-native');
+const { AppState, Keyboard, NativeModules, BackHandler, Animated, View, StatusBar } = require('react-native');
const SafeAreaView = require('lib/components/SafeAreaView');
const { connect, Provider } = require('react-redux');
const { BackButtonService } = require('lib/services/back-button.js');
@@ -57,7 +57,8 @@ const { PoorManIntervals } = require('lib/poor-man-intervals.js');
const { reducer, defaultState } = require('lib/reducer.js');
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const DropdownAlert = require('react-native-dropdownalert').default;
-// const ShareExtension = require('react-native-share-extension').default;
+const ShareExtension = require('lib/ShareExtension.js').default;
+const handleShared = require('lib/shareHandler').default;
const ResourceFetcher = require('lib/services/ResourceFetcher');
const SearchEngine = require('lib/services/SearchEngine');
const WelcomeUtils = require('lib/WelcomeUtils');
@@ -614,43 +615,6 @@ class AppComponent extends React.Component {
});
}
- if (Platform.OS !== 'ios') {
- // try {
- // const { type, value } = await ShareExtension.data();
-
- // // reg.logger().info('Got share data:', type, value);
-
- // if (type != '' && this.props.selectedFolderId) {
- // const newNote = await Note.save({
- // title: Note.defaultTitleFromBody(value),
- // body: value,
- // parent_id: this.props.selectedFolderId,
- // });
-
- // // This is a bit hacky, but the surest way to go to
- // // the needed note. We go back one screen in case there's
- // // already a note open - if we don't do this, the dispatch
- // // below will do nothing (because routeName wouldn't change)
- // // Then we wait a bit for the state to be set correctly, and
- // // finally we go to the new note.
- // this.props.dispatch({
- // type: 'NAV_BACK',
- // });
-
- // setTimeout(() => {
- // this.props.dispatch({
- // type: 'NAV_GO',
- // routeName: 'Note',
- // noteId: newNote.id,
- // });
- // }, 5);
- // }
-
- // } catch (e) {
- // reg.logger().error('Error in ShareExtension.data', e);
- // }
- }
-
BackButtonService.initialize(this.backButtonHandler_);
AlarmService.setInAppNotificationHandler(async (alarmId) => {
@@ -660,6 +624,16 @@ class AppComponent extends React.Component {
});
AppState.addEventListener('change', this.onAppStateChange_);
+
+ const sharedData = await ShareExtension.data();
+ if (sharedData) {
+ reg.logger().info('Received shared data');
+ if (this.props.selectedFolderId) {
+ handleShared(sharedData, this.props.selectedFolderId, this.props.dispatch);
+ } else {
+ reg.logger.info('Cannot handle share - default folder id is not set');
+ }
+ }
}
componentWillUnmount() {