Java tutorial
// Copyright (c) 2016-present, Facebook, Inc. All rights reserved. // // You are hereby granted a non-exclusive, worldwide, royalty-free license to use, // copy, modify, and distribute this software in source code or binary form for use // in connection with the web services and APIs provided by Facebook. // // As with any software that integrates with the Facebook platform, your use of // this software is subject to the Facebook Developer Principles and Policies // [http://developers.facebook.com/policy/]. This copyright notice shall be // included in all copies or substantial portions of the software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package com.facebook.notifications; import android.app.Activity; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import com.facebook.notifications.internal.activity.CardActivity; import com.facebook.notifications.internal.appevents.AppEventsLogger; import com.facebook.notifications.internal.asset.Asset; import com.facebook.notifications.internal.asset.AssetManager; import com.facebook.notifications.internal.asset.handlers.BitmapAssetHandler; import com.facebook.notifications.internal.asset.handlers.ColorAssetHandler; import com.facebook.notifications.internal.asset.handlers.GifAssetHandler; import com.facebook.notifications.internal.configuration.CardConfiguration; import com.facebook.notifications.internal.content.ContentManager; import com.facebook.notifications.internal.utilities.Version; import org.json.JSONException; import org.json.JSONObject; /** * Manages incoming remote notifications for push card presentation. */ public final class NotificationsManager { /** * Represents handlers to be invoked when preparation of a card is completed. */ public interface PrepareCallback { void onPrepared(@NonNull Intent presentationIntent); void onError(@NonNull Exception exception); } /** * Allows for customizing of notifications before they are displayed. */ public interface NotificationExtender { Notification.Builder extendNotification(@NonNull Notification.Builder notification); } /** * The request code to use for intents returned by {@link PrepareCallback} */ public static final int REQUEST_CODE = 0xCA4D; // CARD /** * The intent extra key to be used for pending intents used in notifications created by * `presentNotification()` */ public static final String EXTRA_PAYLOAD_INTENT = "notification_push_payload_intent"; /** * The highest supported payload version by this version of the Notifications SDK */ public static final String PAYLOAD_VERSION = "1.0"; /** * The version of the In-App Notifications Library */ public static final String LIBRARY_VERSION = "1.0.2"; private static final @NonNull Version PAYLOAD_VERSION_OBJECT = new Version(1, 0, 0); private static final String LOG_TAG = NotificationsManager.class.getCanonicalName(); private static final String NOTIFICATION_TAG = "fb_notification_tag"; private static final String PUSH_PAYLOAD_KEY = "fb_push_payload"; private static final String CARD_PAYLOAD_KEY = "fb_push_card"; private static final AssetManager ASSET_MANAGER = new AssetManager(); private static final ContentManager CONTENT_MANAGER = new ContentManager(); static { ASSET_MANAGER.registerHandler(BitmapAssetHandler.TYPE, new BitmapAssetHandler()); ASSET_MANAGER.registerHandler(ColorAssetHandler.TYPE, new ColorAssetHandler()); ASSET_MANAGER.registerHandler(GifAssetHandler.TYPE, new GifAssetHandler()); } private NotificationsManager() { } @Nullable private static JSONObject getPushJSON(@NonNull Bundle bundle) throws JSONException { String pushPayload = bundle.getString(PUSH_PAYLOAD_KEY); if (pushPayload == null) { return null; } return new JSONObject(pushPayload); } @Nullable private static JSONObject getCardJSON(@NonNull Bundle bundle) throws JSONException { String cardPayload = bundle.getString(CARD_PAYLOAD_KEY); if (cardPayload == null) { return null; } return new JSONObject(cardPayload); } @Nullable private static Intent intentForBundle(@NonNull Context context, @Nullable JSONObject pushJSON, @NonNull JSONObject cardJSON, @NonNull AssetManager assetManager, @NonNull ContentManager contentManager) throws JSONException { Version cardVersion = Version.parse(cardJSON.optString("version")); if (cardVersion == null || cardVersion.compareTo(PAYLOAD_VERSION_OBJECT) > 0) { return null; } Intent intent = new Intent(context, CardActivity.class); if (pushJSON != null) { String campaignIdentifier = AppEventsLogger.getCampaignIdentifier(pushJSON); if (campaignIdentifier != null) { intent.putExtra(CardActivity.EXTRA_CAMPAIGN_IDENTIFIER, campaignIdentifier); } } intent.putExtra(CardActivity.EXTRA_ASSET_MANAGER, assetManager); intent.putExtra(CardActivity.EXTRA_CONTENT_MANAGER, contentManager); intent.putExtra(CardActivity.EXTRA_CARD_PAYLOAD, cardJSON.toString()); return intent; } @NonNull private static AssetManager getAssetManager(@NonNull Context context) { AssetManager manager = new AssetManager(ASSET_MANAGER); manager.setContext(context); return manager; } @NonNull private static ContentManager getContentManager(@NonNull Context context) { ContentManager manager = new ContentManager(CONTENT_MANAGER); manager.setContext(context); return manager; } /** * Returns whether or not a notification bundle has a valid push payload. * * @param notificationBundle The bundle to check for a push payload */ public static boolean canPresentCard(@NonNull Bundle notificationBundle) { return notificationBundle.containsKey(CARD_PAYLOAD_KEY); } /** * Present an intent from a given activity to show the notification bundle contained in notificationBundle. * * @param activity The activity to present from * @param notificationBundle The bundle containing the notification payload to present * @return whether or not the activity could successfully be presented */ public static boolean presentCard(@NonNull Activity activity, @NonNull Bundle notificationBundle) { try { JSONObject cardJSON = getCardJSON(notificationBundle); if (cardJSON == null) { return false; } JSONObject pushJSON = getPushJSON(notificationBundle); Intent presentationIntent = intentForBundle(activity, pushJSON, cardJSON, getAssetManager(activity), getContentManager(activity)); if (presentationIntent == null) { return false; } activity.startActivityForResult(presentationIntent, REQUEST_CODE); return true; } catch (JSONException ex) { Log.e(LOG_TAG, "Error while parsing JSON", ex); return false; } } /** * Prepare and pre-load a notification bundle into memory. * * @param context The current context of your program. Usually an activity, application, or * service context. * @param notificationBundle The bundle containing the notification payload to present * @param callback The callback to invoke once preparation is complete. This is guaranteed to be * invoked on the same thread as this method is invoked from. */ public static void prepareCard(@NonNull final Context context, @NonNull final Bundle notificationBundle, @NonNull final PrepareCallback callback) { final Handler handler = new Handler(); final AssetManager assetManager = getAssetManager(context); final ContentManager contentManager = getContentManager(context); // Cache and prepare in background. new Thread() { @Override public void run() { try { JSONObject cardJSON = getCardJSON(notificationBundle); if (cardJSON == null) { throw new NullPointerException("No content present in the notification bundle."); } Version cardVersion = Version.parse(cardJSON.optString("version")); if (cardVersion == null || cardVersion.compareTo(PAYLOAD_VERSION_OBJECT) > 0) { throw new Exception("Payload version " + cardVersion + " not supported by this version of the notifications SDK."); } assetManager.cachePayload(cardJSON, new AssetManager.CacheCompletionCallback() { @Override public void onCacheCompleted(@NonNull JSONObject payload) { assetManager.stopCaching(); try { JSONObject cardJSON = getCardJSON(notificationBundle); final Intent presentIntent = intentForBundle(context, getPushJSON(notificationBundle), cardJSON, assetManager, contentManager); if (presentIntent == null) { throw new NullPointerException( "presentIntent was null, this should never happen!"); } CardConfiguration configuration = new CardConfiguration(cardJSON, assetManager, contentManager); presentIntent.putExtra(CardActivity.EXTRA_CONFIGURATION, configuration); handler.post(new Runnable() { @Override public void run() { callback.onPrepared(presentIntent); } }); } catch (final Exception ex) { handler.post(new Runnable() { @Override public void run() { callback.onError(ex); } }); } } }); } catch (final Exception ex) { handler.post(new Runnable() { @Override public void run() { callback.onError(ex); } }); } } }.start(); } /** * Handle the result of an activity started using * {@code prepare(Context, Bundle, PrepareCallback)} or {@code present(Activity, Bundle)}. * * @param requestCode The request code used to start the activity * @param resultCode The result code returned by the activity * @param data The data returned by the activity * @return The notification card result of the activity if it exists, or null if it does not. */ public static NotificationCardResult handleActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (requestCode != REQUEST_CODE) { return null; } if (resultCode != Activity.RESULT_OK || data == null) { return null; } return data.getParcelableExtra(CardActivity.EXTRA_NOTIFICATION_CARD_RESULT); } /** * Present a {@link Notification} to be presented from a GCM push bundle. * <p/> * This does not present a notification immediately, instead it caches the assets from the * notification bundle, and then presents a notification to the user. This allows for a smoother * interaction without loading indicators for the user. * <p/> * Note that only one notification can be created for a specific push bundle, should you attempt * to present a new notification with the same payload bundle as an existing notification, it will * replace and update the old notification. * * @param context The context to send the notification from * @param notificationBundle The content of the push notification * @param launcherIntent The launcher intent that contains your Application's activity. * This will be modified with the FLAG_ACTIVITY_CLEAR_TOP and * FLAG_ACTIVITY_SINGLE_TOP flags, in order to properly show the * notification in an already running application. * <p/> * Should you not want this behavior, you may use the notificationExtender * parameter to customize the contentIntent of the notification before * presenting it. */ public static boolean presentNotification(@NonNull Context context, @NonNull Bundle notificationBundle, @NonNull Intent launcherIntent) { return presentNotification(context, notificationBundle, launcherIntent, null); } /** * Present a {@link Notification} to be presented from a GCM push bundle. * <p/> * This does not present a notification immediately, instead it caches the assets from the * notification bundle, and then presents a notification to the user. This allows for a smoother * interaction without loading indicators for the user. * <p/> * Note that only one notification can be created for a specific push bundle, should you attempt * to present a new notification with the same payload bundle as an existing notification, it will * replace and update the old notification. * * @param context The context to send the notification from * @param notificationBundle The content of the push notification * @param launcherIntent The launcher intent that contains your Application's activity. * This will be modified with the FLAG_ACTIVITY_CLEAR_TOP and * FLAG_ACTIVITY_SINGLE_TOP flags, in order to properly show the * notification in an already running application. * <p/> * Should you not want this behavior, you may use the notificationExtender * parameter to customize the contentIntent of the notification before * presenting it. * @param notificationExtender A nullable argument that allows you to customize the notification * before displaying it. Use this to configure Icons, text, sounds, * etc. before we pass the notification off to the OS. */ public static boolean presentNotification(@NonNull final Context context, @NonNull final Bundle notificationBundle, @NonNull final Intent launcherIntent, @Nullable final NotificationExtender notificationExtender) { final JSONObject alert; final int payloadHash; try { String payload = notificationBundle.getString(CARD_PAYLOAD_KEY); if (payload == null) { return false; } payloadHash = payload.hashCode(); JSONObject payloadObject = new JSONObject(payload); alert = payloadObject.optJSONObject("alert") != null ? payloadObject.optJSONObject("alert") : new JSONObject(); } catch (JSONException ex) { Log.e(LOG_TAG, "Error while parsing notification bundle JSON", ex); return false; } final boolean[] success = new boolean[1]; final Thread backgroundThread = new Thread(new Runnable() { @Override public void run() { Looper.prepare(); prepareCard(context, notificationBundle, new PrepareCallback() { @Override public void onPrepared(@NonNull Intent presentationIntent) { Intent contentIntent = new Intent(launcherIntent); contentIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); contentIntent.putExtra(EXTRA_PAYLOAD_INTENT, presentationIntent); NotificationManager manager = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); Notification.Builder builder = new Notification.Builder(context) .setSmallIcon(android.R.drawable.ic_dialog_alert) .setContentTitle(alert.optString("title")).setContentText(alert.optString("body")) .setAutoCancel(true) .setContentIntent(PendingIntent.getActivity(context.getApplicationContext(), payloadHash, contentIntent, PendingIntent.FLAG_ONE_SHOT)); if (notificationExtender != null) { builder = notificationExtender.extendNotification(builder); } manager.notify(NOTIFICATION_TAG, payloadHash, builder.getNotification()); success[0] = true; Looper.myLooper().quit(); } @Override public void onError(@NonNull Exception exception) { Log.e(LOG_TAG, "Error while preparing card", exception); Looper.myLooper().quit(); } }); Looper.loop(); } }); backgroundThread.start(); try { backgroundThread.join(); } catch (InterruptedException ex) { Log.e(LOG_TAG, "Failed to wait for background thread", ex); return false; } return success[0]; } /** * Present a card from the notification this activity * was created from, if the notification exists. * * @param activity The activity to present from. * @return Whether or not a card was presented. */ public static boolean presentCardFromNotification(@NonNull Activity activity) { return presentCardFromNotification(activity, activity.getIntent()); } /** * Present a card from the notification this activity * was relaunched from, if the notification exists. * * @param activity The activity to present from. * @param intent Intent that was used to re-launch the activity. * @return Whether or not a card was presented. */ public static boolean presentCardFromNotification(@NonNull Activity activity, @NonNull Intent intent) { Intent notificationIntent = intent.getParcelableExtra(EXTRA_PAYLOAD_INTENT); if (notificationIntent == null) { return false; } activity.startActivityForResult(notificationIntent, REQUEST_CODE); return true; } /** * Registers an Asset Handler for use. * * @param assetType The type of the asset to register for. * @param assetHandler The asset handler to register. */ private static void registerAssetHandler(@NonNull String assetType, @NonNull AssetManager.AssetHandler<? extends Asset> assetHandler) { ASSET_MANAGER.registerHandler(assetType, assetHandler); } }