Commit 1fb887d9 authored by Yogev Ben David's avatar Yogev Ben David Committed by GitHub

Merge branch 'master' into 1.1.24

parents c0936868 731cc93b
...@@ -20,8 +20,8 @@ android { ...@@ -20,8 +20,8 @@ android {
dependencies { dependencies {
// Google's GCM. // Google's GCM.
compile 'com.google.android.gms:play-services-gcm:15.0.1' // compile 'com.google.android.gms:play-services-gcm:15.0.1'
compile "com.google.firebase:firebase-messaging:17.3.0"
compile 'com.facebook.react:react-native:+' compile 'com.facebook.react:react-native:+'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
......
...@@ -21,39 +21,16 @@ ...@@ -21,39 +21,16 @@
--> -->
<service android:name=".core.ProxyService"/> <service android:name=".core.ProxyService"/>
<!--
Google's ready-to-use GcmReceiver.
1. Awaits actual GCM messages (e.g. push notifications) and invokes the GCM service with the concrete content.
2. Awaits instance-ID/token refresh requests from the GCM and invokes the Instance-ID listener service.
-->
<receiver
android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="${applicationId}" />
</intent-filter>
</receiver>
<!-- Dispatched by the GcmReceiver when messages are received. -->
<service <service
android:name="com.wix.reactnativenotifications.gcm.GcmMessageHandlerService" android:name=".gcm.FcmInstanceIdListenerService">
android:exported="false">
<intent-filter> <intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" /> <action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter> <action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</service>
<!-- Dispatched by the GcmReceiver. Starts the designated refresh-handling service. -->
<service
android:name=".gcm.GcmInstanceIdListenerService"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.gms.iid.InstanceID" />
</intent-filter> </intent-filter>
</service> </service>
<service <service
android:name=".gcm.GcmInstanceIdRefreshHandlerService" android:name=".gcm.FcmInstanceIdRefreshHandlerService"
android:exported="false" /> android:exported="false" />
</application> </application>
......
...@@ -8,6 +8,7 @@ import android.os.Bundle; ...@@ -8,6 +8,7 @@ import android.os.Bundle;
import android.support.v4.app.NotificationManagerCompat; import android.support.v4.app.NotificationManagerCompat;
import android.util.Log; import android.util.Log;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
...@@ -23,20 +24,21 @@ import com.wix.reactnativenotifications.core.notification.PushNotification; ...@@ -23,20 +24,21 @@ import com.wix.reactnativenotifications.core.notification.PushNotification;
import com.wix.reactnativenotifications.core.notification.PushNotificationProps; import com.wix.reactnativenotifications.core.notification.PushNotificationProps;
import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer; import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer;
import com.wix.reactnativenotifications.core.notificationdrawer.PushNotificationsDrawer; import com.wix.reactnativenotifications.core.notificationdrawer.PushNotificationsDrawer;
import com.wix.reactnativenotifications.gcm.GcmInstanceIdRefreshHandlerService; import com.wix.reactnativenotifications.gcm.FcmInstanceIdRefreshHandlerService;
import com.google.firebase.FirebaseApp;
import static com.wix.reactnativenotifications.Defs.LOGTAG; import static com.wix.reactnativenotifications.Defs.LOGTAG;
public class RNNotificationsModule extends ReactContextBaseJavaModule implements AppLifecycleFacade.AppVisibilityListener, Application.ActivityLifecycleCallbacks { public class RNNotificationsModule extends ReactContextBaseJavaModule implements ActivityEventListener {
public RNNotificationsModule(Application application, ReactApplicationContext reactContext) { public RNNotificationsModule(Application application, ReactApplicationContext reactContext) {
super(reactContext); super(reactContext);
if (AppLifecycleFacadeHolder.get() instanceof ReactAppLifecycleFacade) { if (AppLifecycleFacadeHolder.get() instanceof ReactAppLifecycleFacade) {
((ReactAppLifecycleFacade) AppLifecycleFacadeHolder.get()).init(reactContext); ((ReactAppLifecycleFacade) AppLifecycleFacadeHolder.get()).init(reactContext);
} }
AppLifecycleFacadeHolder.get().addVisibilityListener(this);
application.registerActivityLifecycleCallbacks(this); reactContext.addActivityEventListener(this);
} }
@Override @Override
...@@ -47,16 +49,32 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule implements ...@@ -47,16 +49,32 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule implements
@Override @Override
public void initialize() { public void initialize() {
Log.d(LOGTAG, "Native module init"); Log.d(LOGTAG, "Native module init");
startGcmIntentService(GcmInstanceIdRefreshHandlerService.EXTRA_IS_APP_INIT); startGcmIntentService(FcmInstanceIdRefreshHandlerService.EXTRA_IS_APP_INIT);
final IPushNotificationsDrawer notificationsDrawer = PushNotificationsDrawer.get(getReactApplicationContext().getApplicationContext()); final IPushNotificationsDrawer notificationsDrawer = PushNotificationsDrawer.get(getReactApplicationContext().getApplicationContext());
notificationsDrawer.onAppInit(); notificationsDrawer.onAppInit();
} }
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
}
@Override
public void onNewIntent(Intent intent) {
Bundle notificationData = intent.getExtras();
if (notificationData != null) {
final IPushNotification notification = PushNotification.get(getReactApplicationContext().getApplicationContext(), notificationData);
if (notification != null) {
notification.onOpened();
}
}
}
@ReactMethod @ReactMethod
public void refreshToken() { public void refreshToken() {
Log.d(LOGTAG, "Native method invocation: refreshToken()"); Log.d(LOGTAG, "Native method invocation: refreshToken()");
startGcmIntentService(GcmInstanceIdRefreshHandlerService.EXTRA_MANUAL_REFRESH); startGcmIntentService(FcmInstanceIdRefreshHandlerService.EXTRA_MANUAL_REFRESH);
} }
@ReactMethod @ReactMethod
...@@ -96,49 +114,9 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule implements ...@@ -96,49 +114,9 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule implements
promise.resolve(new Boolean(hasPermission)); promise.resolve(new Boolean(hasPermission));
} }
@Override
public void onAppVisible() {
final IPushNotificationsDrawer notificationsDrawer = PushNotificationsDrawer.get(getReactApplicationContext().getApplicationContext());
notificationsDrawer.onAppVisible();
}
@Override
public void onAppNotVisible() {
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
final IPushNotificationsDrawer notificationsDrawer = PushNotificationsDrawer.get(getReactApplicationContext().getApplicationContext());
notificationsDrawer.onNewActivity(activity);
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
protected void startGcmIntentService(String extraFlag) { protected void startGcmIntentService(String extraFlag) {
final Context appContext = getReactApplicationContext().getApplicationContext(); final Context appContext = getReactApplicationContext().getApplicationContext();
final Intent tokenFetchIntent = new Intent(appContext, GcmInstanceIdRefreshHandlerService.class); final Intent tokenFetchIntent = new Intent(appContext, FcmInstanceIdRefreshHandlerService.class);
tokenFetchIntent.putExtra(extraFlag, true); tokenFetchIntent.putExtra(extraFlag, true);
appContext.startService(tokenFetchIntent); appContext.startService(tokenFetchIntent);
} }
......
package com.wix.reactnativenotifications; package com.wix.reactnativenotifications;
import android.app.Activity;
import android.app.Application; import android.app.Application;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import com.facebook.react.ReactPackage; import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager; import com.facebook.react.uimanager.ViewManager;
import com.google.firebase.FirebaseApp;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.AppLifecycleFacadeHolder;
import com.wix.reactnativenotifications.core.notification.IPushNotification;
import com.wix.reactnativenotifications.core.notification.PushNotification;
import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer;
import com.wix.reactnativenotifications.core.notificationdrawer.PushNotificationsDrawer;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
public class RNNotificationsPackage implements ReactPackage { import static com.wix.reactnativenotifications.Defs.LOGTAG;
public class RNNotificationsPackage implements ReactPackage, AppLifecycleFacade.AppVisibilityListener, Application.ActivityLifecycleCallbacks {
private final Application mApplication; private final Application mApplication;
public RNNotificationsPackage(Application application) { public RNNotificationsPackage(Application application) {
mApplication = application; mApplication = application;
FirebaseApp.initializeApp(application.getApplicationContext());
AppLifecycleFacadeHolder.get().addVisibilityListener(this);
application.registerActivityLifecycleCallbacks(this);
} }
@Override @Override
...@@ -28,4 +47,55 @@ public class RNNotificationsPackage implements ReactPackage { ...@@ -28,4 +47,55 @@ public class RNNotificationsPackage implements ReactPackage {
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList(); return Collections.emptyList();
} }
@Override
public void onAppVisible() {
final IPushNotificationsDrawer notificationsDrawer = PushNotificationsDrawer.get(mApplication.getApplicationContext());
notificationsDrawer.onAppVisible();
}
@Override
public void onAppNotVisible() {
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
final IPushNotificationsDrawer notificationsDrawer = PushNotificationsDrawer.get(mApplication.getApplicationContext());
notificationsDrawer.onNewActivity(activity);
Intent intent = activity.getIntent();
if (intent != null) {
Bundle notificationData = intent.getExtras();
if (notificationData != null) {
final IPushNotification pushNotification = PushNotification.get(mApplication.getApplicationContext(), notificationData);
if (pushNotification != null) {
pushNotification.onOpened();
}
}
}
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
} }
...@@ -43,6 +43,10 @@ public class PushNotification implements IPushNotification { ...@@ -43,6 +43,10 @@ public class PushNotification implements IPushNotification {
}; };
public static IPushNotification get(Context context, Bundle bundle) { public static IPushNotification get(Context context, Bundle bundle) {
if (verifyNotificationBundle(bundle) == false) {
return null;
}
Context appContext = context.getApplicationContext(); Context appContext = context.getApplicationContext();
if (appContext instanceof INotificationsApplication) { if (appContext instanceof INotificationsApplication) {
return ((INotificationsApplication) appContext).getPushNotification(context, bundle, AppLifecycleFacadeHolder.get(), new AppLaunchHelper()); return ((INotificationsApplication) appContext).getPushNotification(context, bundle, AppLifecycleFacadeHolder.get(), new AppLaunchHelper());
...@@ -58,6 +62,14 @@ public class PushNotification implements IPushNotification { ...@@ -58,6 +62,14 @@ public class PushNotification implements IPushNotification {
mNotificationProps = createProps(bundle); mNotificationProps = createProps(bundle);
} }
private static boolean verifyNotificationBundle(Bundle bundle) {
if (bundle.getString("google.message_id") != null) {
return true;
}
return false;
}
@Override @Override
public void onReceived() throws InvalidNotificationException { public void onReceived() throws InvalidNotificationException {
postNotification(null); postNotification(null);
...@@ -92,7 +104,6 @@ public class PushNotification implements IPushNotification { ...@@ -92,7 +104,6 @@ public class PushNotification implements IPushNotification {
protected void digestNotification() { protected void digestNotification() {
if (!mAppLifecycleFacade.isReactInitialized()) { if (!mAppLifecycleFacade.isReactInitialized()) {
setAsInitialNotification(); setAsInitialNotification();
launchOrResumeApp();
return; return;
} }
...@@ -122,10 +133,6 @@ public class PushNotification implements IPushNotification { ...@@ -122,10 +133,6 @@ public class PushNotification implements IPushNotification {
protected void dispatchUponVisibility() { protected void dispatchUponVisibility() {
mAppLifecycleFacade.addVisibilityListener(getIntermediateAppVisibilityListener()); mAppLifecycleFacade.addVisibilityListener(getIntermediateAppVisibilityListener());
// Make the app visible so that we'll dispatch the notification opening when visibility changes to 'true' (see
// above listener registration).
launchOrResumeApp();
} }
protected AppVisibilityListener getIntermediateAppVisibilityListener() { protected AppVisibilityListener getIntermediateAppVisibilityListener() {
...@@ -197,9 +204,4 @@ public class PushNotification implements IPushNotification { ...@@ -197,9 +204,4 @@ public class PushNotification implements IPushNotification {
private void notifyOpenedToJS() { private void notifyOpenedToJS() {
mJsIOHelper.sendEventToJS(NOTIFICATION_OPENED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext()); mJsIOHelper.sendEventToJS(NOTIFICATION_OPENED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
} }
protected void launchOrResumeApp() {
final Intent intent = mAppLaunchHelper.getLaunchIntent(mContext);
mContext.startActivity(intent);
}
} }
...@@ -3,16 +3,25 @@ package com.wix.reactnativenotifications.gcm; ...@@ -3,16 +3,25 @@ package com.wix.reactnativenotifications.gcm;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import com.google.android.gms.gcm.GcmListenerService; import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import com.wix.reactnativenotifications.core.notification.IPushNotification; import com.wix.reactnativenotifications.core.notification.IPushNotification;
import com.wix.reactnativenotifications.core.notification.PushNotification; import com.wix.reactnativenotifications.core.notification.PushNotification;
import java.util.Map;
import static com.wix.reactnativenotifications.Defs.LOGTAG; import static com.wix.reactnativenotifications.Defs.LOGTAG;
public class GcmMessageHandlerService extends GcmListenerService { /**
* Instance-ID + token refreshing handling service. Contacts the GCM to fetch the updated token.
*
* @author amitd
*/
public class FcmInstanceIdListenerService extends FirebaseMessagingService {
@Override @Override
public void onMessageReceived(String s, Bundle bundle) { public void onMessageReceived(RemoteMessage message){
Bundle bundle = message.toIntent().getExtras();
Log.d(LOGTAG, "New message from GCM: " + bundle); Log.d(LOGTAG, "New message from GCM: " + bundle);
try { try {
......
...@@ -3,18 +3,18 @@ package com.wix.reactnativenotifications.gcm; ...@@ -3,18 +3,18 @@ package com.wix.reactnativenotifications.gcm;
import android.app.IntentService; import android.app.IntentService;
import android.content.Intent; import android.content.Intent;
public class GcmInstanceIdRefreshHandlerService extends IntentService { public class FcmInstanceIdRefreshHandlerService extends IntentService {
public static String EXTRA_IS_APP_INIT = "isAppInit"; public static String EXTRA_IS_APP_INIT = "isAppInit";
public static String EXTRA_MANUAL_REFRESH = "doManualRefresh"; public static String EXTRA_MANUAL_REFRESH = "doManualRefresh";
public GcmInstanceIdRefreshHandlerService() { public FcmInstanceIdRefreshHandlerService() {
super(GcmInstanceIdRefreshHandlerService.class.getSimpleName()); super(FcmInstanceIdRefreshHandlerService.class.getSimpleName());
} }
@Override @Override
protected void onHandleIntent(Intent intent) { protected void onHandleIntent(Intent intent) {
IGcmToken gcmToken = GcmToken.get(this); IFcmToken gcmToken = FcmToken.get(this);
if (gcmToken == null) { if (gcmToken == null) {
return; return;
} }
......
package com.wix.reactnativenotifications.gcm; package com.wix.reactnativenotifications.gcm;
import android.content.Context; import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.support.annotation.NonNull;
import android.util.Log; import android.util.Log;
import com.facebook.react.ReactApplication; import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager; import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.google.android.gms.gcm.GoogleCloudMessaging; import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.iid.InstanceID; import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.InstanceIdResult;
import static com.wix.reactnativenotifications.Defs.GCM_SENDER_ID_ATTR_NAME;
import static com.wix.reactnativenotifications.Defs.LOGTAG; import static com.wix.reactnativenotifications.Defs.LOGTAG;
import static com.wix.reactnativenotifications.Defs.TOKEN_RECEIVED_EVENT_NAME; import static com.wix.reactnativenotifications.Defs.TOKEN_RECEIVED_EVENT_NAME;
public class GcmToken implements IGcmToken { public class FcmToken implements IFcmToken {
final protected Context mAppContext; final protected Context mAppContext;
protected static String sToken; protected static String sToken;
protected GcmToken(Context appContext) { protected FcmToken(Context appContext) {
if (!(appContext instanceof ReactApplication)) { if (!(appContext instanceof ReactApplication)) {
throw new IllegalStateException("Application instance isn't a react-application"); throw new IllegalStateException("Application instance isn't a react-application");
} }
mAppContext = appContext; mAppContext = appContext;
} }
public static IGcmToken get(Context context) { public static IFcmToken get(Context context) {
Context appContext = context.getApplicationContext(); Context appContext = context.getApplicationContext();
if (appContext instanceof INotificationsGcmApplication) { if (appContext instanceof INotificationsGcmApplication) {
return ((INotificationsGcmApplication) appContext).getGcmToken(context); return ((INotificationsGcmApplication) appContext).getFcmToken(context);
} }
return new GcmToken(appContext); return new FcmToken(appContext);
} }
@Override @Override
...@@ -73,51 +70,14 @@ public class GcmToken implements IGcmToken { ...@@ -73,51 +70,14 @@ public class GcmToken implements IGcmToken {
} }
protected void refreshToken() { protected void refreshToken() {
try { FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener( new OnSuccessListener<InstanceIdResult>() {
sToken = getNewToken(); @Override
} catch (Exception e) { public void onSuccess(InstanceIdResult instanceIdResult) {
Log.e(LOGTAG, "Failed to retrieve new token", e); sToken = instanceIdResult.getToken();
return; Log.i(LOGTAG, "FCM has a new token" + "=" + sToken);
} sendTokenToJS();
}
sendTokenToJS(); });
}
@NonNull
protected String getNewToken() throws Exception {
final InstanceID instanceId = InstanceID.getInstance(mAppContext);
Log.d(LOGTAG, "GCM is refreshing token... instanceId=" + instanceId.getId());
// TODO why is this needed?
GoogleCloudMessaging.getInstance(mAppContext).close();
try {
final String registrationToken = instanceId.getToken(getSenderId(), GoogleCloudMessaging.INSTANCE_ID_SCOPE);
Log.i(LOGTAG, "GCM has a new token: instanceId=" + instanceId.getId() + ", token=" + registrationToken);
return registrationToken;
} catch (Exception e) {
throw new Exception("FATAL: Failed to fetch a fresh new token, instanceId=" + instanceId.getId(), e);
}
}
protected String getSenderId() {
final String senderId = getSenderIdFromManifest();
if (senderId == null) {
throw new IllegalStateException("Sender ID not found in manifest. Did you forget to add it as the value of a '"+GCM_SENDER_ID_ATTR_NAME+"' meta-data field?");
}
return senderId;
}
protected String getSenderIdFromManifest() {
final ApplicationInfo appInfo;
try {
appInfo = mAppContext.getPackageManager().getApplicationInfo(mAppContext.getPackageName(), PackageManager.GET_META_DATA);
return appInfo.metaData.getString(GCM_SENDER_ID_ATTR_NAME);
} catch (PackageManager.NameNotFoundException e) {
// Should REALLY never happen cause we're querying for our own package.
Log.e(LOGTAG, "Failed to resolve sender ID from manifest", e);
return null;
}
} }
protected void sendTokenToJS() { protected void sendTokenToJS() {
......
package com.wix.reactnativenotifications.gcm;
import android.content.Intent;
import com.google.android.gms.iid.InstanceIDListenerService;
/**
* Instance-ID + token refreshing handling service. Contacts the GCM to fetch the updated token.
*
* @author amitd
*/
public class GcmInstanceIdListenerService extends InstanceIDListenerService {
@Override
public void onTokenRefresh() {
// Fetch updated Instance ID token and notify our app's server of any changes (if applicable).
// Google recommends running this from an intent service.
Intent intent = new Intent(this, GcmInstanceIdRefreshHandlerService.class);
startService(intent);
}
}
package com.wix.reactnativenotifications.gcm; package com.wix.reactnativenotifications.gcm;
public interface IGcmToken { public interface IFcmToken {
/** /**
* Handle an event where we've been notified of a that a fresh token is now available from Google. * Handle an event where we've been notified of a that a fresh token is now available from Google.
......
...@@ -3,5 +3,5 @@ package com.wix.reactnativenotifications.gcm; ...@@ -3,5 +3,5 @@ package com.wix.reactnativenotifications.gcm;
import android.content.Context; import android.content.Context;
public interface INotificationsGcmApplication { public interface INotificationsGcmApplication {
IGcmToken getGcmToken(Context context); IFcmToken getFcmToken(Context context);
} }
{ {
"name": "react-native-notifications", "name": "react-native-notifications",
"version": "1.1.24", "version": "1.2.52",
"description": "Advanced Push Notifications (Silent, interactive notifications) for iOS & Android", "description": "Advanced Push Notifications (Silent, interactive notifications) for iOS & Android",
"author": "Lidan Hifi <lidan.hifi@gmail.com>", "author": "Lidan Hifi <lidan.hifi@gmail.com>",
"license": "MIT", "license": "MIT",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment