Skip to content

Intent (data sharing)

Jacques Bonet edited this page Jan 2, 2020 · 13 revisions

Introduction

React-native provide mechanism that permits to share a document into the desired application (ent).

In Android, we can achieve this with the help of Intent Filters.

Intent Filter

Intent filters are basically filters that define what an app can expect as its input.

Intent filter are specified in androidmanifest.xml file.

Here an extract for ent app containing the intent-filter sections:

         <activity
                android:name=".MainActivity"
                android:label="@string/app_name"
                android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
                android:windowSoftInputMode="adjustResize"
                android:launchMode="singleTask"
                android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.SEND"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <data android:mimeType="application/*"/>
                <data android:mimeType="audio/*"/>
                <data android:mimeType="image/*"/>
                <data android:mimeType="text/*"/>
                <data android:mimeType="video/*"/>
            </intent-filter>
        </activity>

The first intent filter permits to:

  • create an icon on the desktop of the application
  • launch the ent application from the desktop icon

The second intent filter permits to:

  • associate document (intent) to the ent application
  • launch the ent application from the share menu of the document

Another thing interesting to note: intents are embedded inside android activities

Architecture

How the ent application can receive these documents?

There is differents possibilities to do it. We have privilegied API with loose coupling between software layers.

react-native provide 3 api which both permits that:

  • ActivityEventListener,
  • LifecycleEventListener,
  • RCTDeviceEventEmitter

ActivityEventListener

/**
 * Listener for receiving activity events.
**/
public interface ActivityEventListener {

  ...

  /** Called when a new intent is passed to the activity, 
  *   Application could be inactive or in background
  **/
  void onNewIntent(Intent intent);
}

LifecycleEventListener

/**
 * Listener for receiving activity lifecycle events.
 */
public interface LifecycleEventListener {

  /**
   * Called when application resume (background ===> foreground)
   */
  void onHostResume();

  ...
}

RCTDeviceEventEmitter

Signal events to JavaScript code from android native code (Java/Kotlin language) Will be call by onHostResume()

Implementation

We forked the project react-native-file-intent which provided interested functionnalities like filepath calculation.

We added the following enhancments:

  • return the content uri
  • have a "publish subscribe" API

Native code

Here the corresponding native code permitting to handle intent notifications

public RNFileShareIntentModule(ReactApplicationContext reactContext) {
    ...
    reactContext.addActivityEventListener(this);
    reactContext.addLifecycleEventListener(this);
  }
  @Override
  public void onNewIntent(Intent aIntent) {
    /* if call to js code is made on onNewIntent, that not works on the second call
       when application in background. So this is done on onHostResume() function.
    */

    if (mIntent == null) {
      mIntent = aIntent;
    }
  }
  @Override
  public void onHostResume() {
    Intent aIntent =  mIntent;

    if (mIntent != null) {
      mIntent = null;
      shareFile(aIntent);
    }
  }
  /**
   * Share the intent to js application part
   * @param intent
   */
   private void shareFile(Intent intent) {
    String action = intent.getAction();
    String type = intent.getType();
    Activity currentActivity = getCurrentActivity();

    WritableMap res = new WritableNativeMap();
    if (Intent.ACTION_SEND.equals(action)) {
      if (type.startsWith("application/") || type.startsWith("audio/") || type.startsWith("image/") ||
              type.startsWith("video/")) {
          sendEvent( fileHelper.getFileData(intent.getParcelableExtra(Intent.EXTRA_STREAM), currentActivity));
      }
    }
  /**
   * send event to js code
   * "FileShareIntent" is the name space subscription
   * @param params
   */
  private void sendEvent(WritableMap params) {
    reactContext
      .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
      .emit("FileShareIntent", params);
  }

js code

MainNavigator instance is mandatory because on receipt of an intent, we navigate to the workspace. So We use an HOC above the MainNavigator to do the navigation. That permits not to have to test a MainNavigator instance has been created.

export default function withLinkingAppWrapper(WrappedComponent: React.Component): React.Component {
  class HOC extends React.Component {
    eventEmitter: NativeEventEmitter | null = null;

    public componentDidMount() {
      if (Platform.OS === "android") {
        RNFileShareIntent.getFilePath((contentUri: ContentUri) => {
          this.navigate(contentUri);
        });

        this.eventEmitter = new NativeEventEmitter(NativeModules.RNFileShareIntent);

        this.eventEmitter.addListener("FileShareIntent", (contentUri: ContentUri) => {
          this.navigate(contentUri);
        });
      }
    }

    private navigate(contentUri: ContentUri) {
      nainNavNavigate("Workspace", {
        contentUri: null,
        filter: FilterId.root,
        parentId: FilterId.root,
        title: I18n.t("workspace"),
        childRoute: "Workspace",
        childParams: {
          parentId: "owner",
          filter: FilterId.owner,
          title: I18n.t("owner"),
          contentUri: [contentUri],
        },
      });
    }

    public componentWillUnmount(): void {
      if (Platform.OS === "android") this.eventEmitter?.removeListener("FileShareIntent", this.navigate);
    }

    public render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return HOC;
}

On the function componentDidMount, an explicit call to native module

RNFileShareIntent.getFilePath((contentUri: ContentUri) 

is done.

getFilePath is just a call to shareFile function

  @ReactMethod
  public void getFilePath(Callback successCallback) {
    Activity mActivity = getCurrentActivity();

    if(mActivity == null) { return; }

    Intent intent = mActivity.getIntent();
    shareFile(intent);
  }

This code is neccessary because ActivityEvent and LifecycleEvent are not call when ent application is open explicitly, so an explicit call to shareFile must be done.

Clone this wiki locally