Skip to content

Commit

Permalink
[notifications] Add support for android actions
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbianca committed Mar 9, 2018
1 parent 983bbbc commit e537955
Show file tree
Hide file tree
Showing 12 changed files with 539 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -110,23 +110,28 @@ public void setOnJSON(JSONObject json, String key, Object value) throws JSONExce
SETTERS.put(JSONArray.class, new Setter() {
public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException {
JSONArray jsonArray = (JSONArray) value;
ArrayList<String> stringArrayList = new ArrayList<String>();
// Empty list, can't even figure out the type, assume an ArrayList<String>
if (jsonArray.length() == 0) {
bundle.putStringArrayList(key, stringArrayList);
bundle.putStringArrayList(key, new ArrayList<String>());
return;
}

// Only strings are supported for now
for (int i = 0; i < jsonArray.length(); i++) {
Object current = jsonArray.get(i);
if (current instanceof String) {
stringArrayList.add((String) current);
} else {
throw new IllegalArgumentException("Unexpected type in an array: " + current.getClass());
if (jsonArray.get(0) instanceof String) {
ArrayList<String> stringArrayList = new ArrayList<String>();
for (int i = 0; i < jsonArray.length(); i++) {
stringArrayList.add((String) jsonArray.get(i));
}
bundle.putStringArrayList(key, stringArrayList);
} else if (jsonArray.get(0) instanceof JSONObject) {
ArrayList<Bundle> bundleArrayList = new ArrayList<>();
for (int i =0; i < jsonArray.length(); i++) {
bundleArrayList.add(convertToBundle((JSONObject) jsonArray.get(i)));
}
bundle.putSerializable(key, bundleArrayList);
} else {
throw new IllegalArgumentException("Unexpected type in an array: " + jsonArray.get(0).getClass());
}
bundle.putStringArrayList(key, stringArrayList);
}

@Override
Expand All @@ -152,13 +157,18 @@ public static JSONObject convertToJSON(Bundle bundle) throws JSONException {
continue;
}

// Special case List<String> as getClass would not work, since List is an interface
// Special case List<?> as getClass would not work, since List is an interface
if (value instanceof List<?>) {
JSONArray jsonArray = new JSONArray();
@SuppressWarnings("unchecked")
List<String> listValue = (List<String>) value;
for (String stringValue : listValue) {
jsonArray.put(stringValue);
List<Object> listValue = (List<Object>) value;
for (Object objValue : listValue) {
if (objValue instanceof String) {
jsonArray.put(objValue);
} else if (objValue instanceof Bundle) {
jsonArray.put(convertToJSON((Bundle) objValue));
} else {
throw new IllegalArgumentException("Unsupported type: " + objValue.getClass());
}
}
json.put(key, jsonArray);
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.RemoteInput;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

Expand Down Expand Up @@ -325,10 +327,8 @@ private void displayNotification(Bundle notification, Promise promise) {
}
if (android.containsKey("smallIcon")) {
Bundle smallIcon = android.getBundle("smallIcon");
int smallIconResourceId = getResourceId("mipmap", smallIcon.getString("icon"));
if (smallIconResourceId == 0) {
smallIconResourceId = getResourceId("drawable", smallIcon.getString("icon"));
}
int smallIconResourceId = getIcon(smallIcon.getString("icon"));

if (smallIconResourceId != 0) {
if (smallIcon.containsKey("level")) {
Double level = smallIcon.getDouble("level");
Expand Down Expand Up @@ -386,17 +386,17 @@ private void displayNotification(Bundle notification, Promise promise) {
notification.setStyle(bigPicture);
} */

// Create the notification intent
Intent intent = new Intent(context, intentClass);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtras(notification);
if (android.containsKey("clickAction")) {
intent.setAction(android.getString("clickAction"));
// Build any actions
if (android.containsKey("actions")) {
List<Bundle> actions = (List) android.getSerializable("actions");
for (Bundle a : actions) {
NotificationCompat.Action action = createAction(a, intentClass, notification);
nb = nb.addAction(action);
}
}

PendingIntent contentIntent = PendingIntent.getActivity(context, notificationId.hashCode(), intent,
PendingIntent.FLAG_UPDATE_CURRENT);
// Create the notification intent
PendingIntent contentIntent = createIntent(intentClass, notification, android.getString("clickAction"));
nb = nb.setContentIntent(contentIntent);

// Build the notification and send it
Expand All @@ -415,6 +415,75 @@ private void displayNotification(Bundle notification, Promise promise) {
}
}

private NotificationCompat.Action createAction(Bundle action, Class intentClass, Bundle notification) {
String actionKey = action.getString("action");
PendingIntent actionIntent = createIntent(intentClass, notification, actionKey);

int icon = getIcon(action.getString("icon"));
String title = action.getString("title");

NotificationCompat.Action.Builder ab = new NotificationCompat.Action.Builder(icon, title, actionIntent);

if (action.containsKey("allowGeneratedReplies")) {
ab = ab.setAllowGeneratedReplies(action.getBoolean("allowGeneratedReplies"));
}
if (action.containsKey("remoteInputs")) {
List<Bundle> remoteInputs = (List) action.getSerializable("remoteInputs");
for (Bundle ri : remoteInputs) {
RemoteInput remoteInput = createRemoteInput(ri);
ab = ab.addRemoteInput(remoteInput);
}
}
// TODO: SemanticAction and ShowsUserInterface only available on v28?
// if (action.containsKey("semanticAction")) {
// Double semanticAction = action.getDouble("semanticAction");
// ab = ab.setSemanticAction(semanticAction.intValue());
// }
// if (action.containsKey("showsUserInterface")) {
// ab = ab.setShowsUserInterface(action.getBoolean("showsUserInterface"));
// }

return ab.build();
}

private PendingIntent createIntent(Class intentClass, Bundle notification, String action) {
Intent intent = new Intent(context, intentClass);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtras(notification);

if (action != null) {
intent.setAction(action);
}

String notificationId = notification.getString("notificationId");

return PendingIntent.getActivity(context, notificationId.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
}

private RemoteInput createRemoteInput(Bundle remoteInput) {
String resultKey = remoteInput.getString("resultKey");

RemoteInput.Builder rb = new RemoteInput.Builder(resultKey);

if (remoteInput.containsKey("allowedDataTypes")) {
List<Bundle> allowedDataTypes = (List) remoteInput.getSerializable("allowedDataTypes");
for (Bundle adt : allowedDataTypes) {
rb.setAllowDataType(adt.getString("mimeType"), adt.getBoolean("allow"));
}
}
if (remoteInput.containsKey("allowFreeFormInput")) {
rb.setAllowFreeFormInput(remoteInput.getBoolean("allowFreeFormInput"));
}
if (remoteInput.containsKey("choices")) {
rb.setChoices(remoteInput.getStringArray("choices"));
}
if (remoteInput.containsKey("label")) {
rb.setLabel(remoteInput.getString("label"));
}

return rb.build();
}

private Bitmap getBitmap(String image) {
if (image.startsWith("http://") || image.startsWith("https://")) {
return getBitmapFromUrl(image);
Expand All @@ -436,6 +505,14 @@ private Bitmap getBitmapFromUrl(String imageUrl) {
}
}

private int getIcon(String icon) {
int smallIconResourceId = getResourceId("mipmap", icon);
if (smallIconResourceId == 0) {
smallIconResourceId = getResourceId("drawable", icon);
}
return smallIconResourceId;
}

private Class getMainActivityClass() {
String packageName = context.getPackageName();
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
Expand Down Expand Up @@ -555,9 +632,9 @@ private void scheduleNotification(Bundle notification, Promise promise) {
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationId.hashCode(),
notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);

if (schedule.containsKey("interval")) {
if (schedule.containsKey("repeatInterval")) {
Long interval = null;
switch (schedule.getString("interval")) {
switch (schedule.getString("repeatInterval")) {
case "minute":
interval = 60000L;
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v4.app.RemoteInput;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
Expand Down Expand Up @@ -198,6 +198,12 @@ private WritableMap parseIntentForLocalNotification(Intent intent) {
notificationOpenMap.putString("action", intent.getAction());
notificationOpenMap.putMap("notification", notificationMap);

// Check for remote input results
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
notificationOpenMap.putMap("results", Arguments.makeNativeMap(remoteInput));
}

return notificationOpenMap;
}

Expand Down
150 changes: 150 additions & 0 deletions lib/modules/notifications/AndroidAction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* @flow
* AndroidAction representation wrapper
*/
import RemoteInput, {
fromNativeAndroidRemoteInput,
} from './AndroidRemoteInput';
import { SemanticAction } from './types';
import type { NativeAndroidAction, SemanticActionType } from './types';

export default class AndroidAction {
_action: string;
_allowGeneratedReplies: boolean | void;
_icon: string;
_remoteInputs: RemoteInput[];
_semanticAction: SemanticActionType | void;
_showUserInterface: boolean | void;
_title: string;

constructor(action: string, icon: string, title: string) {
this._action = action;
this._icon = icon;
this._remoteInputs = [];
this._title = title;
}

get action(): string {
return this._action;
}

get allowGeneratedReplies(): ?boolean {
return this._allowGeneratedReplies;
}

get icon(): string {
return this._icon;
}

get remoteInputs(): RemoteInput[] {
return this._remoteInputs;
}

get semanticAction(): ?SemanticActionType {
return this._semanticAction;
}

get showUserInterface(): ?boolean {
return this._showUserInterface;
}

get title(): string {
return this._title;
}

/**
*
* @param remoteInput
* @returns {AndroidAction}
*/
addRemoteInput(remoteInput: RemoteInput): AndroidAction {
if (!(remoteInput instanceof RemoteInput)) {
throw new Error(
`AndroidAction:addRemoteInput expects an 'RemoteInput' but got type ${typeof remoteInput}`
);
}
this._remoteInputs.push(remoteInput);
return this;
}

/**
*
* @param allowGeneratedReplies
* @returns {AndroidAction}
*/
setAllowGenerateReplies(allowGeneratedReplies: boolean): AndroidAction {
this._allowGeneratedReplies = allowGeneratedReplies;
return this;
}

/**
*
* @param semanticAction
* @returns {AndroidAction}
*/
setSemanticAction(semanticAction: SemanticActionType): AndroidAction {
if (!Object.values(SemanticAction).includes(semanticAction)) {
throw new Error(
`AndroidAction:setSemanticAction Invalid Semantic Action: ${semanticAction}`
);
}
this._semanticAction = semanticAction;
return this;
}

/**
*
* @param showUserInterface
* @returns {AndroidAction}
*/
setShowUserInterface(showUserInterface: boolean): AndroidAction {
this._showUserInterface = showUserInterface;
return this;
}

build(): NativeAndroidAction {
if (!this._action) {
throw new Error('AndroidAction: Missing required `action` property');
} else if (!this._icon) {
throw new Error('AndroidAction: Missing required `icon` property');
} else if (!this._title) {
throw new Error('AndroidAction: Missing required `title` property');
}

return {
action: this._action,
allowGeneratedReplies: this._allowGeneratedReplies,
icon: this._icon,
remoteInputs: this._remoteInputs.map(remoteInput => remoteInput.build()),
semanticAction: this._semanticAction,
showUserInterface: this._showUserInterface,
title: this._title,
};
}
}

export const fromNativeAndroidAction = (
nativeAction: NativeAndroidAction
): AndroidAction => {
const action = new AndroidAction(
nativeAction.action,
nativeAction.icon,
nativeAction.title
);
if (nativeAction.allowGeneratedReplies) {
action.setAllowGenerateReplies(nativeAction.allowGeneratedReplies);
}
if (nativeAction.remoteInputs) {
nativeAction.remoteInputs.forEach(remoteInput => {
action.addRemoteInput(fromNativeAndroidRemoteInput(remoteInput));
});
}
if (nativeAction.semanticAction) {
action.setSemanticAction(nativeAction.semanticAction);
}
if (nativeAction.showUserInterface) {
action.setShowUserInterface(nativeAction.showUserInterface);
}

return action;
};
Loading

0 comments on commit e537955

Please sign in to comment.