Skip to content

Commit

Permalink
Fix Android not using Intent on startup (keybase#19047)
Browse files Browse the repository at this point in the history
* Fix Android not using intent for startup routing

* Use engine to pass initial intent through

* Add docs
  • Loading branch information
MarcoPolo authored Aug 22, 2019
1 parent 3e339b6 commit cd4a4c7
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 35 deletions.
65 changes: 35 additions & 30 deletions shared/actions/platform-specific/push.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import * as PushGen from '../push-gen'
import * as PushNotifications from 'react-native-push-notification'
import * as RPCChatTypes from '../../constants/types/rpc-chat-gen'
import * as RPCTypes from '../../constants/types/rpc-gen'
import * as ChatTypes from '../../constants/types/chat2'
import * as Saga from '../../util/saga'
import * as WaitingGen from '../waiting-gen'
import * as RouteTreeGen from '../route-tree-gen'
Expand Down Expand Up @@ -48,6 +47,10 @@ const updateAppBadge = (_: Container.TypedState, action: NotificationsGen.Receiv
// 2. in `onResume` we check if we have an intent, if we do call `emitIntent`
// 3. `emitIntent` eventually calls `RCTDeviceEventEmitter` with a couple different event names for various events
// 4. We subscribe to those events below (e.g. `RNEmitter.addListener('initialIntentFromNotification', evt => {`)

// At startup the flow above can be racy, since we may not have registered the
// event listener before the event is emitted. In that case you can always use
// `getInitialPushAndroid`.
const listenForNativeAndroidIntentNotifications = emitter => {
const RNEmitter = new NativeEventEmitter(NativeModules.KeybaseEngine)
RNEmitter.addListener('initialIntentFromNotification', evt => {
Expand Down Expand Up @@ -344,43 +347,45 @@ function* _checkPermissions(action: ConfigGen.MobileAppStatePayload | null) {
}
}

const getStartupDetailsFromInitialPush = (): Promise<
| null
| {
startupFollowUser: string
function* getStartupDetailsFromInitialPush() {
const {push, pushTimeout}: {push: PushGen.NotificationPayload; pushTimeout: boolean} = yield Saga.race({
push: Saga.callPromise(isAndroid ? getInitialPushAndroid : getInitialPushiOS),
pushTimeout: Saga.delay(10),
})
if (pushTimeout || !push) {
return null
}

const notification = push.payload.notification
if (notification.type === 'follow') {
if (notification.username) {
return {startupFollowUser: notification.username}
}
| {
startupConversation: ChatTypes.ConversationIDKey
}
> =>
new Promise(resolve => {
if (isAndroid) {
// For android, we won't rely on the initial notification.
// We'll do all routing based of the intent
resolve(null)
return
} else if (notification.type === 'chat.newmessage') {
if (notification.conversationIDKey) {
return {startupConversation: notification.conversationIDKey}
}
}

return null
}

const getInitialPushAndroid = (): Promise<PushGen.NotificationPayload | null> =>
NativeModules.KeybaseEngine.getInitialIntent().then(n => {
let notification = n && Constants.normalizePush(n)
return notification && PushGen.createNotification({notification})
})

const getInitialPushiOS = (): Promise<PushGen.NotificationPayload | null> =>
new Promise(resolve =>
PushNotifications.popInitialNotification(n => {
const notification = Constants.normalizePush(n)
if (!notification) {
resolve(null)
return
}
if (notification.type === 'follow') {
if (notification.username) {
resolve({startupFollowUser: notification.username})
return
}
} else if (notification.type === 'chat.newmessage') {
if (notification.conversationIDKey) {
resolve({startupConversation: notification.conversationIDKey})
return
}
if (notification !== null) {
resolve(PushGen.createNotification({notification}))
}
resolve(null)
})
})
)

function* pushSaga(): Saga.SagaGenerator<any, any> {
// Permissions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import java.security.cert.CertificateException;
import java.util.UUID;

import io.keybase.ossifrage.modules.KeybaseEngine;
import io.keybase.ossifrage.modules.NativeLogger;
import io.keybase.ossifrage.util.ContactsPermissionsWrapper;
import io.keybase.ossifrage.util.DNSNSFetcher;
Expand Down Expand Up @@ -230,15 +231,19 @@ public void emit() {
}

// Closure like class so we can keep our emit logic together
class Emit implements Runnable {
class Emit {
private final ReactContext context;
private DeviceEventManagerModule.RCTDeviceEventEmitter emitter;

Emit(DeviceEventManagerModule.RCTDeviceEventEmitter emitter) {
Emit(DeviceEventManagerModule.RCTDeviceEventEmitter emitter, ReactContext context) {
this.emitter = emitter;
this.context = context;
}

@Override
public void run() {
KeybaseEngine engine = context.getNativeModule(KeybaseEngine.class);
engine.setInitialIntent(Arguments.fromBundle(bundleFromNotification));

assert emitter != null;
// If there are any other bundle sources we care about, emit them here
if (bundleFromNotification != null) {
Expand Down Expand Up @@ -276,14 +281,14 @@ public void run() {
DeviceEventManagerModule.RCTDeviceEventEmitter emitter = context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class);

(new Emit(emitter)).run();
(new Emit(emitter, context)).run();

} else {
// Otherwise wait for construction, then send the notification
reactInstanceManager.addReactInstanceEventListener(rctContext -> {
DeviceEventManagerModule.RCTDeviceEventEmitter emitter = rctContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
(new Emit(emitter)).run();
(new Emit(emitter, rctContext)).run();
});
if (!reactInstanceManager.hasStartedCreatingInitialContext()) {
// Construct it in the background
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@

import com.facebook.react.TurboReactPackage;
import com.facebook.react.bridge.LifecycleEventListener;
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.WritableMap;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.modules.core.DeviceEventManagerModule;

import java.io.BufferedReader;
Expand All @@ -31,6 +34,7 @@
import static keybase.Keybase.writeB64;
import static keybase.Keybase.version;

@ReactModule(name = "KeybaseEngine")
public class KeybaseEngine extends ReactContextBaseJavaModule implements KillableModule {

private static final String NAME = "KeybaseEngine";
Expand All @@ -40,6 +44,7 @@ public class KeybaseEngine extends ReactContextBaseJavaModule implements Killabl
private ExecutorService executor;
private Boolean started = false;
private ReactApplicationContext reactContext;
private WritableMap initialIntent;

private static void relayReset(ReactApplicationContext reactContext) {
if (!reactContext.hasActiveCatalystInstance()) {
Expand Down Expand Up @@ -219,4 +224,16 @@ public void start() {
NativeLogger.error("Exception in KeybaseEngine.start", e);
}
}

// This isn't related to the Go Engine, but it's a small thing that wouldn't be worth putting in
// its own react module. That's because starting up a react module is a bit expensive and we
// wouldn't be able to lazy load this because we need it on startup.
@ReactMethod
public void getInitialIntent(Promise promise) {
promise.resolve(initialIntent);
}

public void setInitialIntent(WritableMap initialIntent) {
this.initialIntent = initialIntent;
}
}

0 comments on commit cd4a4c7

Please sign in to comment.