Skip to content

Commit

Permalink
android: Kotlin-side code for receiving data from other apps.
Browse files Browse the repository at this point in the history
This is part of the implementation of zulip#117, to show up in the
"share" UI from other apps so the user can send a message with
the shared data.

This commit consists of the needed Android-native code, which uses
the relevant Android APIs to get the data and then the relevant
RN-on-Android APIs to send the data over to our main JS codebase.

On its own, this code doesn't yet do anything useful because we
don't have the JS code to listen for the data.  That's still in
development as zulip#4124, and coming soon.

Because the feature doesn't yet work in this version, the manifest
elements to advertise it are commented out, so that it doesn't
actually get presented to users.

Also include the new dummy root React component, `SharingRoot`,
because the Kotlin-side code refers to it by name.  It exists in the
first place because 'Sharing' is linked with launching an activity
in the Android ecosystem.  But we don't always want to launch
`MainActivity` when receiving a share because it may cause two
instances of it to be opened simultaneously.  We can't check whether
the app is running before an activity launches.  So, we launch this
dummy component, then process the intent, identify whether the app
is running or not, and handle that as mentioned in the paragraph
above, and then quickly kill this dummy Activity.  All of this
happens fast enough that the component does not even get time to
render, so it's seamless.

[greg: split out Android-native portion as its own commit;
 adjusted commit message accordingly]
  • Loading branch information
agrawal-d authored and gnprice committed May 29, 2020
1 parent 7676977 commit 0b84717
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 5 deletions.
25 changes: 25 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,31 @@
</intent-filter>
</activity>

<!-- Disabled while the feature is experimental. See #117 and #4124.
<activity
android:name=".sharing.ReceiveShareActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>
-->

<!-- When `react-native run-android` learns from the decoy `package`
attribute in our comment above that the application ID is
`com.zulipmobile.debug`, it then tries to start an activity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.zulipmobile.notifications.ConversationMap;
import com.zulipmobile.notifications.FCMPushNotifications;
import com.zulipmobile.notifications.NotificationsPackage;
import com.zulipmobile.sharing.SharingPackage;

public class MainApplication extends Application implements ReactApplication {
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(new BasePackageList().getPackageList(), null);
Expand Down Expand Up @@ -58,6 +59,7 @@ protected List<ReactPackage> getPackages() {
new RNDeviceInfo(),
new ZulipNativePackage(),
new NotificationsPackage(),
new SharingPackage(),
new ModuleRegistryAdapter(mModuleRegistryProvider)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ import com.zulipmobile.MainActivity
* getReactInstanceManager it'll try to create one... which asserts we're
* on the UI thread, which isn't true if e.g. we got here from a Service.
*/
private fun ReactNativeHost.tryGetReactInstanceManager(): ReactInstanceManager? =
fun ReactNativeHost.tryGetReactInstanceManager(): ReactInstanceManager? =
if (this.hasInstance()) this.reactInstanceManager else null

/**
* A distillation of ReactContext.getLifecycleState() and related information.
*
* See ReactContext.getAppStatus().
*/
private enum class ReactAppStatus {
enum class ReactAppStatus {
/**
* The main activity has either never yet been in the foreground,
* or never will again. There might not be an active JS instance.
Expand All @@ -55,7 +55,7 @@ private enum class ReactAppStatus {
FOREGROUND
}

private val ReactContext.appStatus: ReactAppStatus
val ReactContext.appStatus: ReactAppStatus
get() {
if (!hasActiveCatalystInstance())
return ReactAppStatus.NOT_RUNNING
Expand Down Expand Up @@ -98,13 +98,13 @@ internal fun notifyReact(application: ReactApplication, data: Bundle) {
}
}

internal fun emit(reactContext: ReactContext, eventName: String, data: Any?) {
fun emit(reactContext: ReactContext, eventName: String, data: Any?) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(eventName, data)
}

private fun launchMainActivity(context: Context) {
fun launchMainActivity(context: Context) {
Log.d(TAG, "NotifyReact: launching main activity")
val intent = Intent(context, MainActivity::class.java)
// See these sections in the Android docs:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.zulipmobile.sharing

import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.webkit.WebView
import androidx.annotation.Nullable
import com.facebook.react.ReactActivity
import com.facebook.react.ReactApplication
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.Arguments
import com.zulipmobile.notifications.*

const val TAG = "ZulipReceiveShare"

class ReceiveShareActivity : ReactActivity() {

/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
override fun getMainComponentName(): String? {
return "SharingRoot"
}

private fun sendEvent(reactContext: ReactContext,
eventName: String,
@Nullable params: WritableMap) {
Log.d(TAG, "Sending event with shared data")
emit(reactContext, eventName, params)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WebView.setWebContentsDebuggingEnabled(true)
if (intent?.action == Intent.ACTION_SEND) {
handleSend(intent)
}
finish()
}

private fun handleSend(intent: Intent) {
val application = application as ReactApplication
val host = application.reactNativeHost
val reactContext = host.tryGetReactInstanceManager()?.currentReactContext
val params: WritableMap
try {
params = getParamsFromIntent(intent)
} catch (e: ShareParamsParseException) {
Log.w(TAG, "Ignoring malformed share Intent: ${e.message}")
return
}

val appStatus = reactContext?.appStatus
when (appStatus) {
null, ReactAppStatus.NOT_RUNNING ->
// Either there's no JS environment running, or we haven't yet
// reached foreground. Expect the app to check
// initialSharedData on launch.
SharingModule.initialSharedData = params
ReactAppStatus.BACKGROUND, ReactAppStatus.FOREGROUND ->
// JS is running and has already reached foreground. It won't
// check initialSharedData again, but it will see a
// shareReceived event.
sendEvent(reactContext, "shareReceived", params)
}
when (appStatus) {
null, ReactAppStatus.NOT_RUNNING, ReactAppStatus.BACKGROUND ->
launchMainActivity(application as Context)
ReactAppStatus.FOREGROUND -> Unit
}
}

private fun getParamsFromIntent(intent: Intent): WritableMap {
val params = Arguments.createMap()
when {
"text/plain" == intent.type -> {
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
params.putString("type", "text")
params.putString("sharedText", sharedText)
}
intent.type?.startsWith("image/") == true -> {
val url = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
?: throw ShareParamsParseException("Could not extract URL from Image Intent")
params.putString("type", "image")
params.putString("sharedImageUrl", url.toString())
}
else -> {
val url = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
?: throw ShareParamsParseException("Could not extract URL from File Intent")
params.putString("type", "file")
params.putString("sharedFileUrl", url.toString())
}
}
return params
}

override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val intent = Intent("onConfigurationChanged")
intent.putExtra("newConfig", newConfig)
this.sendBroadcast(intent)
}
}

class ShareParamsParseException(errorMessage: String) : RuntimeException(errorMessage)
24 changes: 24 additions & 0 deletions android/app/src/main/java/com/zulipmobile/sharing/SharingModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.zulipmobile.sharing

import com.facebook.react.bridge.*

internal class SharingModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {

override fun getName(): String {
return "Sharing"
}

@ReactMethod
fun getInitialSharedContent(promise: Promise) {
if (null == initialSharedData) {
promise.resolve(null)
} else {
promise.resolve(initialSharedData)
}

}

companion object {
var initialSharedData: WritableMap? = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.zulipmobile.sharing


import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager

import java.util.ArrayList

class SharingPackage : ReactPackage {

override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}

override fun createNativeModules(
reactContext: ReactApplicationContext): List<NativeModule> {
val modules = ArrayList<NativeModule>()

modules.add(SharingModule(reactContext))

return modules
}

}
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* @flow strict-local */
import { AppRegistry } from 'react-native';
import ZulipMobile from './src/ZulipMobile';
import SharingRoot from './src/sharing/SharingRoot';

AppRegistry.registerComponent('ZulipMobile', () => ZulipMobile);
AppRegistry.registerComponent('SharingRoot', () => SharingRoot);
18 changes: 18 additions & 0 deletions src/sharing/SharingRoot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* @flow strict-local */
import React from 'react';
import { View } from 'react-native';

/**
* This is a dummy component to by-pass some weird quirks of Android Activity
* launches in a React Native context. The native code in
* `ReceiveShareActivity.kt` finishes this activity quickly, after either
* i) Sending events to an already open app in the background
* ii) Launching `MainActivity` with some initial share data.
*/
class SharingRoot extends React.Component<{||}> {
render() {
return <View />;
}
}

export default SharingRoot;

0 comments on commit 0b84717

Please sign in to comment.