All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.amazonaws.mobileconnectors.pinpoint.targeting.notification.NotificationClientBase Maven / Gradle / Ivy

Go to download

The AWS Android SDK for Amazon Pinpoint module holds the client classes that are used for communicating with Amazon Pinpoint Service

There is a newer version: 2.77.0
Show newest version
/**
 * Copyright 2016-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 * http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.amazonaws.mobileconnectors.pinpoint.targeting.notification;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;

import com.amazonaws.mobileconnectors.pinpoint.analytics.AnalyticsEvent;
import com.amazonaws.mobileconnectors.pinpoint.internal.core.PinpointContext;
import com.amazonaws.mobileconnectors.pinpoint.internal.core.system.AndroidPreferences;
import com.amazonaws.services.pinpoint.model.ChannelType;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;

/**
 * NotificationClientBase is the entry point into the Amazon Mobile Analytics SDK to
 * handle Pinpoint notifications.
 */
abstract class NotificationClientBase {
    /**
     * Intent Key for GCM bundle.
     */
    public static final String INTENT_SNS_NOTIFICATION_FROM = "from";

    /**
     * Intent Key for GCM bundle data.
     */
    public static final String INTENT_SNS_NOTIFICATION_DATA = "data";
    // Pinpoint
    protected static final String PINPOINT_PUSH_KEY_PREFIX = "pinpoint.";
    // Campaign

    protected static final String CAMPAIGN_PUSH_KEY_PREFIX = PINPOINT_PUSH_KEY_PREFIX + "campaign.";
    protected static final String CAMPAIGN_ID_ATTRIBUTE_KEY = "campaign_id";
    protected static final String CAMPAIGN_ID_PUSH_KEY = CAMPAIGN_PUSH_KEY_PREFIX + CAMPAIGN_ID_ATTRIBUTE_KEY;
    protected static final String CAMPAIGN_ACTIVITY_ID_ATTRIBUTE_KEY = "campaign_activity_id";
    protected static final String CAMPAIGN_ACTIVITY_ID_PUSH_KEY =
        CAMPAIGN_PUSH_KEY_PREFIX + CAMPAIGN_ACTIVITY_ID_ATTRIBUTE_KEY;
    protected static final String CAMPAIGN_TREATMENT_ID_ATTRIBUTE_KEY = "treatment_id";
    protected static final String CAMPAIGN_TREATMENT_ID_PUSH_KEY =
        CAMPAIGN_PUSH_KEY_PREFIX + CAMPAIGN_TREATMENT_ID_ATTRIBUTE_KEY;
    protected static final Log log = LogFactory.getLog(NotificationClientBase.class);
    private static final String DEVICE_TOKEN_PREF_KEY = "AWSPINPOINT.GCMTOKEN";
    // Notification
    private static final String GCM_NOTIFICATION_PUSH_KEY_PREFIX = PINPOINT_PUSH_KEY_PREFIX + "notification.";
    private static final String NOTIFICATION_SILENT_PUSH_KEY = GCM_NOTIFICATION_PUSH_KEY_PREFIX + "silentPush";
    private static final String NOTIFICATION_TITLE_PUSH_KEY = GCM_NOTIFICATION_PUSH_KEY_PREFIX + "title";
    private static final String NOTIFICATION_BODY_PUSH_KEY = GCM_NOTIFICATION_PUSH_KEY_PREFIX + "body";
    private static final String NOTIFICATION_COLOR_PUSH_KEY = GCM_NOTIFICATION_PUSH_KEY_PREFIX + "color";
    private static final String NOTIFICATION_ICON_PUSH_KEY = GCM_NOTIFICATION_PUSH_KEY_PREFIX + "icon";
    private static final String CAMPAIGN_IMAGE_PUSH_KEY = GCM_NOTIFICATION_PUSH_KEY_PREFIX + "imageUrl";
    private static final String CAMPAIGN_IMAGE_ICON_PUSH_KEY = GCM_NOTIFICATION_PUSH_KEY_PREFIX + "imageIconUrl";
    private static final String CAMPAIGN_IMAGE_SMALL_ICON_PUSH_KEY =
        GCM_NOTIFICATION_PUSH_KEY_PREFIX + "imageSmallIconUrl";
    // Engage Attributes
    private static final String CAMPAIGN_URL_PUSH_KEY = PINPOINT_PUSH_KEY_PREFIX + "url";
    private static final String CAMPAIGN_DEEP_LINK_PUSH_KEY = PINPOINT_PUSH_KEY_PREFIX + "deeplink";
    private static final String CAMPAIGN_OPEN_APP_PUSH_KEY = PINPOINT_PUSH_KEY_PREFIX + "openApp";
    private static final String REQUEST_ID = "requestId";
    private static final int INVALID_RESOURCE = 0;
    private static final int ANDROID_JELLYBEAN = 16;
    private static final int ANDROID_KITKAT = 19;
    private static final int ANDROID_LOLLIPOP = 21;
    private static final int ANDROID_MARSHMALLOW = 23;
    private static final int ANDROID_NOUGAT = 24;
    private static final int ANDROID_OREO = 26;
    private static final int NOTIFICATION_CHANNEL_IMPORTANCE = 4; //IMPORTANCE_HIGH = 4. This corresponds to PRIORITY_HIGH (value 1) in NotificationBuilder. setPriority is deprecated in API 26

    private static final String AWS_EVENT_TYPE_OPENED = "_campaign.opened_notification";
    private static final String AWS_EVENT_TYPE_RECEIVED_FOREGROUND = "_campaign.received_foreground";
    private static final String AWS_EVENT_TYPE_RECEIVED_BACKGROUND = "_campaign.received_background";
    private static final String CHECK_OP_NO_THROW = "checkOpNoThrow";
    private static final String OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION";
    private static final String APP_OPS_MODE_ALLOWED = "MODE_ALLOWED";
    private static final String APP_OPS_SERVICE = "APP_OPS_SERVICE";
    private static final String DEFAULT_NOTIFICATION_CHANNEL_ID = "PINPOINT.NOTIFICATION";
    private static final CharSequence DEFAULT_NOTIFICATION_CHANNEL_NAME = "Notifications";
    protected final PinpointContext pinpointContext;
    private final AppUtil appUtil;
    private final List deviceRegisteredHandlers;
    private volatile String theDeviceToken;
    private Constructor notificationBuilderConstructor = null;
    private Class notificationBuilderClass = null;
    private Class notificationChannelClass = null;
    private Class notificationBigTextStyleClass = null;
    private Class notificationBigPictureStyleClass = null;
    private Class notificationStyleClass = null;
    private Class iconClass = null;
    private Method setContentTitleMethod;
    private Method setContentTextMethod;
    private Method setSmallIconMethod;
    private Method setSmallIconResIdMethod;
    private Method setLargeIconMethod;
    private Method setContentIntent;
    private Method setStyleMethod;
    private Method buildMethod;
    private Method bigTextMethod;
    private Method bigPictureMethod;
    private Method setSummaryMethod;
    private Method setPriorityMethod;
    private Method setSoundMethod;
    private Method createWithBitmapMethod;
    private Bitmap notificationImage;
    private Class appOpsClass = null;
    private Method checkOpNoThrowMethod = null;
    private Field opPostNotificationField = null;
    private Field modeAllowedField = null;
    private String notificationChannelId = null;

    /**
     * Constructor.
     *
     * @param pinpointContext the pinpoint context. {@link PinpointContext}
     */
    protected NotificationClientBase(final PinpointContext pinpointContext) {
        this.pinpointContext = pinpointContext;
        this.appUtil = new AppUtil(pinpointContext.getApplicationContext());
        this.deviceRegisteredHandlers = new ArrayList();
        this.loadDeviceToken();
    }

    /**
     * Add a device token register handler.
     *
     * @param handler the devices register handler.
     */
    public final void addDeviceTokenRegisteredHandler(final DeviceTokenRegisteredHandler handler) {
        if (handler == null) {
            throw new IllegalArgumentException("DeviceTokenRegisteredHandler cannot be null.");
        }
        deviceRegisteredHandlers.add(handler);
    }

    /**
     * Remove the device register handler.
     *
     * @param handler the device register handler.
     */
    public final void removeDeviceTokenRegisteredHandler(final DeviceTokenRegisteredHandler handler) {
        deviceRegisteredHandlers.remove(handler);
    }

    /**
     * This method should be called once the device token has been received from
     * the device's messaging api in order to enable being targeted for campaign push
     * notifications.
     *
     * @param deviceToken the device token.
     */
    public final void registerDeviceToken(final String deviceToken) {
        setDeviceToken(deviceToken);
    }

    /**
     * This method should be called once the user id and channel id has been received rom
     * the device's messaging api in order to enable being targeted for campaign push
     * notifications.
     *
     * @param userId the user id
     * @param channelId the channel id
     */
    public final void registerDeviceToken(final String userId, final String channelId) {
        setDeviceToken(userId + ":" + channelId);
    }

    private void setDeviceToken(String deviceToken) {
        this.theDeviceToken = deviceToken;
        // Persist the device token to shared preferences.
        final AndroidPreferences prefs = pinpointContext.getSystem().getPreferences();
        prefs.putString(DEVICE_TOKEN_PREF_KEY, deviceToken);

        for (final DeviceTokenRegisteredHandler handler : deviceRegisteredHandlers) {
            handler.tokenRegistered(deviceToken);
        }
    }

    private void loadDeviceToken() {
        final AndroidPreferences prefs = pinpointContext.getSystem().getPreferences();
        // Load the device token from shared preferences.
        theDeviceToken = prefs.getString(DEVICE_TOKEN_PREF_KEY, null);
    }

    /**
     * get the device token.
     *
     * @return the device token.
     */
    public final String getDeviceToken() {
        this.loadDeviceToken();
        return theDeviceToken;
    }

    /**
     * The Pinpoint channel type for the device associated with
     * this notification client.
     *
     * @return Channel type for the device.
     */
    public abstract String getChannelType();

    private void addGlobalCampaignAttributes(
        final Map campaignAttribs) {
        for (final Map.Entry entry : campaignAttribs.entrySet()) {
            if (entry.getValue() != null) {
                this.pinpointContext.getAnalyticsClient().addGlobalAttribute(entry.getKey(), entry.getValue());
            }
        }
    }

    private void addCampaignAttributesToEvent(final AnalyticsEvent pushEvent, final Map campaignAttribs) {
        for (final Map.Entry entry : campaignAttribs.entrySet()) {
            if (entry.getValue() != null) {
                pushEvent.addAttribute(entry.getKey(), entry.getValue());
            }
        }
    }


    private Resources getPackageResources() {
        final PackageManager packageManager = pinpointContext.getApplicationContext().getPackageManager();
        try {
            final String packageName = pinpointContext.getApplicationContext().getPackageName();
            final ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
            return packageManager.getResourcesForApplication(applicationInfo);
        } catch (final PackageManager.NameNotFoundException ex) {
            log.error("Can't find resources for our application package.", ex);
            return null;
        }
    }


    private int getNotificationIconResourceId(
        final String drawableResourceName) {
        final PackageManager packageManager = pinpointContext.getApplicationContext().getPackageManager();
        try {
            final String packageName = pinpointContext.getApplicationContext().getPackageName();
            final ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
            final Resources resources = packageManager.getResourcesForApplication(applicationInfo);

            if (drawableResourceName != null) {
                final int resId = resources.getIdentifier(drawableResourceName, "drawable", packageName);
                if (resId != INVALID_RESOURCE) {
                    return resId;
                }
            }
            return applicationInfo.icon;

        } catch (final PackageManager.NameNotFoundException ex) {
            log.error("Can't find icon for our application package.", ex);
            // 0 is an invalid resource id, so use it to indicate failure to
            // retrieve the resource.
            return INVALID_RESOURCE;
        }
    }

    private Notification createLegacyNotification(final int iconResId, final String title, final String contentText,
                                                  final PendingIntent contentIntent) {
        final Notification notification = new Notification();
        notification.icon = iconResId;
        notification.setLatestEventInfo(this.pinpointContext.getApplicationContext(), title, contentText, contentIntent);
        notification.contentIntent = contentIntent;
        return notification;
    }

    private boolean initClassesAndMethodsByReflection() {
        if (notificationBuilderClass != null) {
            return true;
        }
        try {
            notificationBuilderClass = Class.forName("android.app.Notification$Builder"); //API Level 11
            notificationBigTextStyleClass = Class.forName("android.app.Notification$BigTextStyle"); //API Level 16
            notificationStyleClass = Class.forName("android.app.Notification$Style"); //API Level 16
            notificationBigPictureStyleClass = Class.forName("android.app.Notification$BigPictureStyle"); //API Level 16
            if (android.os.Build.VERSION.SDK_INT >= ANDROID_NOUGAT) {
                iconClass = Class.forName("android.graphics.drawable.Icon"); //API Level 24
            }
            if (android.os.Build.VERSION.SDK_INT >= ANDROID_OREO) {
                notificationChannelClass = Class.forName("android.app.NotificationChannel"); //API Level 26
            }
            if (!buildMethodsByReflection()) {
                // fall back to creating the legacy notification.
                return false;
            }
            return true;
        } catch (final ClassNotFoundException ex) {
            log.debug("Failed to get notification builder classes by reflection : " + ex.getMessage(), ex);
            return false;
        }
    }

    private boolean buildMethodsByReflection() {
        try {
            if (android.os.Build.VERSION.SDK_INT >= ANDROID_OREO) {
                notificationBuilderConstructor = notificationBuilderClass.getDeclaredConstructor(Context.class, String.class);
            } else {
                notificationBuilderConstructor = notificationBuilderClass.getDeclaredConstructor(Context.class);
                setPriorityMethod = notificationBuilderClass.getDeclaredMethod("setPriority", int.class);
                setSoundMethod = notificationBuilderClass.getDeclaredMethod("setSound", Uri.class);
            }
            setContentTitleMethod = notificationBuilderClass.getDeclaredMethod("setContentTitle", CharSequence.class);
            setContentTextMethod = notificationBuilderClass.getDeclaredMethod("setContentText", CharSequence.class);
            setContentIntent = notificationBuilderClass.getDeclaredMethod("setContentIntent", PendingIntent.class);
            setStyleMethod = notificationBuilderClass.getDeclaredMethod("setStyle", notificationStyleClass);
            setSmallIconResIdMethod = notificationBuilderClass.getDeclaredMethod("setSmallIcon", int.class);
            buildMethod = notificationBuilderClass.getDeclaredMethod("build");
            bigTextMethod = notificationBigTextStyleClass.getDeclaredMethod("bigText", CharSequence.class);
            bigPictureMethod = notificationBigPictureStyleClass.getDeclaredMethod("bigPicture", Bitmap.class);
            setSummaryMethod = notificationBigPictureStyleClass.getDeclaredMethod("setSummaryText", CharSequence.class);
            setLargeIconMethod = notificationBuilderClass.getDeclaredMethod("setLargeIcon", Bitmap.class);

            if (android.os.Build.VERSION.SDK_INT >= ANDROID_NOUGAT) {
                setSmallIconMethod = notificationBuilderClass.getDeclaredMethod("setSmallIcon", iconClass);
                createWithBitmapMethod = iconClass.getDeclaredMethod("createWithBitmap", Bitmap.class);
            }
            return true;
        } catch (final NoSuchMethodException ex) {
            log.debug("Failed to get notification builder methods by reflection. : " + ex.getMessage(), ex);
            return false;
        }
    }

    private Object retrieveNotificationChannel(String channelId) {
        if (channelId == null) {
            return null;
        }
        try {
            log.info("Notification channel is needed");
            NotificationManager notificationManager = (NotificationManager) pinpointContext.getApplicationContext()
                    .getSystemService(
                            Context.NOTIFICATION_SERVICE);
            Method getNotificationChannelMethod = notificationManager.getClass().getDeclaredMethod("getNotificationChannel", String.class);
            return getNotificationChannelMethod.invoke(notificationManager, channelId);

        } catch (final NoSuchMethodException ex) {
            log.debug("Failed to get notification channel by reflection. : " + ex.getMessage(), ex);
            return null;
        } catch (final IllegalAccessException ex) {
            log.debug("Failed to get notification channel by reflection. : " + ex.getMessage(), ex);
            return null;
        } catch (final InvocationTargetException ex) {
            log.debug("Failed to get notification channel by reflection. : " + ex.getMessage(), ex);
            return null;
        }
    }

    private boolean registerDefaultNotificationChannel() {
        try {
            Object notificationChannel = retrieveNotificationChannel(DEFAULT_NOTIFICATION_CHANNEL_ID);
            if (notificationChannel != null) {
                return true;
            }
            /* Registering the default notification channel for the first time */
            Constructor notificationChannelConstructor = notificationChannelClass.getDeclaredConstructor(String.class, CharSequence.class, int.class);
            notificationChannel = notificationChannelConstructor.newInstance(DEFAULT_NOTIFICATION_CHANNEL_ID, DEFAULT_NOTIFICATION_CHANNEL_NAME, NOTIFICATION_CHANNEL_IMPORTANCE);
            final NotificationManager notificationManager = (NotificationManager) pinpointContext.getApplicationContext()
                    .getSystemService(
                            Context.NOTIFICATION_SERVICE);
            Method createNotificationChannelMethod =  notificationManager.getClass().getDeclaredMethod("createNotificationChannel", notificationChannelClass);
            createNotificationChannelMethod.invoke(notificationManager, notificationChannel);
            return true;
        } catch (final InvocationTargetException ex) {
            log.debug("Can't invoke notification channel constructor. : " + ex.getMessage(), ex);
            return false;
        } catch (final IllegalAccessException ex) {
            log.debug("Can't access notification channel  " + ex.getMessage(), ex);
            return false;
        } catch (final InstantiationException ex) {
            log.debug("Exception while instantiating notification channel . : " + ex.getMessage(),
                    ex);
            return false;
        } catch (final NoSuchMethodException ex) {
            log.debug("Failed to get notification channel method getId by reflection. : " + ex.getMessage(), ex);
            return false;
        }
    }

    private static final float RED_MULTIPLIER = 0.299f;
    private static final float GREEN_MULTIPLIER = 0.587f;
    private static final float BLUE_MULTIPLIER =  0.114f;
    private static final int TRANSPARENT_WHITE_COLOR = 0x00FFFFFF;
    private static final int BITS_TO_SHIFT_FOR_ALPHA = 24;
    private static final int MAX_ALPHA = Color.alpha(Color.WHITE);

    /**
     * Convert a bitmap to gray-scale and store the gray-scale value in the bitmap alpha channel.
     * If the bitmap to be converted is already a solid color, the input bitmap will be copied
     * as is to the output.
     *
     * @param input the input bitmap.
     * @return the output bitmap.
     */
    /*package*/ static Bitmap convertBitmapToAlphaGreyscale(final Bitmap input) {
        final int pixelCount = input.getWidth() * input.getHeight();
        int[] inPixels = new int[pixelCount];
        input.getPixels(inPixels, 0, input.getWidth(), 0, 0, input.getWidth(), input.getHeight());
        int[] outPixels = new int[pixelCount];
        boolean hasMoreThanOneNonTransparentColor = false;
        Integer firstColor = null;

        for (int i = 0; i < pixelCount; i++) {
            final int inputArgb = inPixels[i];
            final int greyScale;
            final int inputAlpha = Color.alpha(inputArgb);

            // Color is changed to gray-scale in the alpha channel.
            final int calculatedColor = MAX_ALPHA - (Math.round(Color.red(inputArgb) * RED_MULTIPLIER)
                + Math.round(Color.green(inputArgb) * GREEN_MULTIPLIER)
                + Math.round(Color.blue(inputArgb) * BLUE_MULTIPLIER));
            if (inputAlpha != 0) {
                if (firstColor == null) {
                    firstColor = inputArgb & TRANSPARENT_WHITE_COLOR;
                } else if ((inputArgb & TRANSPARENT_WHITE_COLOR) != firstColor) {
                    hasMoreThanOneNonTransparentColor = true;
                }
            }
            greyScale = calculatedColor * inputAlpha / MAX_ALPHA;
            outPixels[i] = (greyScale << BITS_TO_SHIFT_FOR_ALPHA) | TRANSPARENT_WHITE_COLOR;
        }

        if (!hasMoreThanOneNonTransparentColor) {
            // Images that have only one non-transparent color, are already in the expected format
            // for a small icon, and so can be used directly.
            return Bitmap.createBitmap(inPixels, input.getWidth(), input.getHeight(), Bitmap.Config.ARGB_8888);
        }

        return Bitmap.createBitmap(outPixels, input.getWidth(), input.getHeight(), Bitmap.Config.ARGB_8888);
    }

    private Bitmap obtainBitmapFromResId(final int iconResId) {
        final Resources resources = getPackageResources();
        if (resources == null) {
            return null;
        }
        return BitmapFactory.decodeResource(resources, iconResId);
    }

    private boolean buildNotificationIcons(final int iconResId, final String imageIconUrl,
                                           final String imageSmallIconUrl,
                                           final Object notificationBuilder) {
        try {
            Bitmap largeIconBitmap = null;
            if (imageIconUrl != null) {
                try {
                    largeIconBitmap = new DownloadImageTask().execute(imageIconUrl).get();
                } catch (final InterruptedException e) {
                    log.error("Interrupted when downloading image : " + e.getMessage(), e);
                } catch (final ExecutionException e) {
                    log.error("Failed to execute download image thread : " + e.getMessage(), e);
                }
            }

            // When no large icon is set or the large icon can't be loaded.
            if (largeIconBitmap == null &&
                    // For API level 21 and 22, a small icon will be set as the app icon in greyscale,
                    // this could result in a grey box if the app icon was fully opaque, it improve this situation,
                    // we can set the large icon as the app icon, so that the small icon will be shown in the corner
                    // of the large icon, where it doesn't look as bad that it may be a grey box.
                    ((android.os.Build.VERSION.SDK_INT >= ANDROID_LOLLIPOP
                        && android.os.Build.VERSION.SDK_INT < ANDROID_NOUGAT)
                    // For API level 23 and above when the small icon isn't set it makes sense to show the large icon
                    // also for the user experience (to make it easiest for the customer to quickly recognize the app
                    // that caused the notification) also if the large icon fails to load we can fall back to app icon.
                    // We were experiencing crashes using this behavior on API level 23 so only use on API level 24.
                     || (android.os.Build.VERSION.SDK_INT >= ANDROID_NOUGAT
                        && (imageIconUrl != null || imageSmallIconUrl == null)))) {
                largeIconBitmap = obtainBitmapFromResId(iconResId);
            }
            // else before SDK version 21, setting the small icon from the res id will render correctly in color.
            // and if after 23, we will convert the app icon to grey scale when setting the small icon so it will be
            // rendered in a way that looks reasonable.

            if (largeIconBitmap != null) {
                setLargeIconMethod.invoke(notificationBuilder, largeIconBitmap);
            }

            // If we are able to use a bitmap to set the small icon.
            if (iconClass != null && android.os.Build.VERSION.SDK_INT >= ANDROID_NOUGAT) {
                // Small icon cannot be set from a bitmap unless on API level 23 or above.
                // We were experiencing crashes using this behavior on API level 23 so only use on API level 24.
                Bitmap smallIconBitmap = null;
                if (imageSmallIconUrl != null) {
                    try {
                        smallIconBitmap = new DownloadImageTask().execute(imageSmallIconUrl).get();
                    } catch (final InterruptedException e) {
                        log.error("Interrupted when downloading small icon : " + e.getMessage(), e);
                    } catch (final ExecutionException e) {
                        log.error("Failed to execute download image small icon thread : " + e.getMessage(), e);
                    }
                }

                if (smallIconBitmap == null) {
                    // Fall back to using the app icon bitmap as the small icon if no icon was provided.
                    smallIconBitmap = obtainBitmapFromResId(iconResId);
                }

                if (smallIconBitmap != null) {
                    // Set the small icon from the obtained bitmap and always ensure it is properly formatted in
                    // greyscale to avoid ever erroneously showing a grey box.
                    setSmallIconMethod.invoke(notificationBuilder,
                        createWithBitmapMethod.invoke(iconClass, convertBitmapToAlphaGreyscale(smallIconBitmap)));
                    return true;
                }
                // fall through if we can't set the small icon from the bitmap and fall back to setting by icon res id.
            }

            setSmallIconResIdMethod.invoke(notificationBuilder, iconResId);
            return true;
        } catch (final InvocationTargetException ex) {
            log.debug("Can't invoke notification builder methods. : " + ex.getMessage(), ex);
            return false;
        } catch (final IllegalAccessException ex) {
            log.debug("Can't access notification builder methods. : " + ex.getMessage(), ex);
            return false;
        }
    }

    private Notification createNotification(final int iconResId, final String title, final String contentText, final String imageUrl,
                                            final String imageIconUrl, final String imageSmallIconUrl, final PendingIntent contentIntent) {
        log.info("Create Notification:" + title + ", Content:" + contentText);
        if (android.os.Build.VERSION.SDK_INT < ANDROID_JELLYBEAN) {
            return createLegacyNotification(iconResId, title, contentText, contentIntent);
        }

        if (!initClassesAndMethodsByReflection()) {
            // fall back to creating the legacy notification.
            return createLegacyNotification(iconResId, title, contentText, contentIntent);
        }

        final Object notificationBuilder;
        final Object bigTextStyle;
        final Object bigPictureStyle;
        if (android.os.Build.VERSION.SDK_INT >= ANDROID_OREO) {
            if (notificationChannelId == null || retrieveNotificationChannel(notificationChannelId) == null) {
                /* Need to Check if the Default Pinpoint notification channel is registered. The app developer can delete any channel */
                notificationChannelId = DEFAULT_NOTIFICATION_CHANNEL_ID;
                if (!registerDefaultNotificationChannel()) {
                    notificationChannelId = null;
                }
            }
        }

        try {
            if (android.os.Build.VERSION.SDK_INT < ANDROID_OREO || notificationChannelId == null) {
                notificationBuilder = notificationBuilderConstructor.newInstance(pinpointContext.getApplicationContext());
                setPriorityMethod.invoke(notificationBuilder, 1);
            } else {
                notificationBuilder = notificationBuilderConstructor.newInstance(pinpointContext.getApplicationContext(), notificationChannelId);
            }

            bigTextStyle = notificationBigTextStyleClass.newInstance();
            bigPictureStyle = notificationBigPictureStyleClass.newInstance();
        } catch (final InvocationTargetException ex) {
            log.debug("Can't invoke notification builder constructor. : " + ex.getMessage(), ex);
            return createLegacyNotification(iconResId, title, contentText, contentIntent);
        } catch (final IllegalAccessException ex) {
            log.debug("Can't access notification builder or bigTextStyle or bigPictureStyle classes. : " + ex.getMessage(), ex);
            return createLegacyNotification(iconResId, title, contentText, contentIntent);
        } catch (final InstantiationException ex) {
            log.debug("Exception while instantiating notification builder or bigTextStyle or bigPictureStyle classes. : " + ex.getMessage(),
                      ex);
            return createLegacyNotification(iconResId, title, contentText, contentIntent);
        }

        try {
            setContentTitleMethod.invoke(notificationBuilder, title);
            setContentTextMethod.invoke(notificationBuilder, contentText);
            setContentIntent.invoke(notificationBuilder, contentIntent);
            if (android.os.Build.VERSION.SDK_INT < ANDROID_OREO) {
                final Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
                setSoundMethod.invoke(notificationBuilder, defaultSoundUri);
            }

            if (!buildNotificationIcons(iconResId, imageIconUrl, imageSmallIconUrl, notificationBuilder)) {
                return createLegacyNotification(iconResId, title, contentText, contentIntent);
            }

            if (imageUrl != null) {
                try {
                    notificationImage = new DownloadImageTask().execute(imageUrl).get();
                    if (notificationImage != null) {
                        bigPictureMethod.invoke(bigPictureStyle, notificationImage);
                        setSummaryMethod.invoke(bigPictureStyle, contentText);
                        setStyleMethod.invoke(notificationBuilder, bigPictureStyle);
                    } else {
                        bigTextMethod.invoke(bigTextStyle, contentText);
                        setStyleMethod.invoke(notificationBuilder, bigTextStyle);
                    }
                } catch (final InterruptedException e) {
                    log.error("Interrupted when downloading image : " + e.getMessage(), e);
                } catch (final ExecutionException e) {
                    log.error("Failed execute download image thread : " + e.getMessage(), e);
                }
            }

            return (Notification) buildMethod.invoke(notificationBuilder);
        } catch (final InvocationTargetException ex) {
            log.debug("Can't invoke notification builder methods. : " + ex.getMessage(), ex);
            return createLegacyNotification(iconResId, title, contentText, contentIntent);
        } catch (final IllegalAccessException ex) {
            log.debug("Can't access notification builder methods. : " + ex.getMessage(), ex);
            return createLegacyNotification(iconResId, title, contentText, contentIntent);
        }
    }


    /**
     * Handles creation of the open app PendingIntent. Implementors of this method will need to
     * decide on how to create the intent based on the provided action.
     *
     * @param pushBundle    the data to push
     * @param targetClass   the class that handles receiving messages.
     * @param campaignId    pinpoint campaign id
     * @param requestId     request id
     * @param intentAction  intent action
     * @return {@link PendingIntent}
     */
    protected abstract PendingIntent createOpenAppPendingIntent(final Bundle pushBundle, final Class targetClass, final String campaignId,
                                                                final int requestId, final String intentAction);


    protected final Intent notificationIntent(final Bundle pushBundle, final String campaignId, final int requestId, final String intentAction,
                                      final Class targetClass) {
        final Intent notificationIntent = new Intent(pinpointContext.getApplicationContext(), targetClass);
        notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
        notificationIntent.setAction(intentAction);
        notificationIntent.putExtras(pushBundle);
        notificationIntent.putExtra(INTENT_SNS_NOTIFICATION_FROM, AWS_EVENT_TYPE_OPENED);
        notificationIntent.putExtra(CAMPAIGN_ID_PUSH_KEY, campaignId);
        notificationIntent.putExtra(REQUEST_ID, requestId);
        notificationIntent.setPackage(pinpointContext.getApplicationContext().getPackageName());
        return notificationIntent;
    }

    /* package */ int getNotificationRequestId(final String campaignId, final String activityId) {
        return (campaignId + ":" + activityId).hashCode();
    }

    private boolean displayNotification(final Bundle pushBundle, final Class targetClass, final String imageUrl,
                                        final String iconImageUrl, final String iconSmallImageUrl,
                                        final Map campaignAttributes, final String intentAction) {
        log.info("Display Notification: " + pushBundle.toString());

        final int iconResId = getNotificationIconResourceId(pushBundle.getString(NOTIFICATION_ICON_PUSH_KEY));
        if (iconResId == 0) {
            return false;
        }

        final String title = pushBundle.getString(NOTIFICATION_TITLE_PUSH_KEY);
        final String message = pushBundle.getString(NOTIFICATION_BODY_PUSH_KEY);

        final String campaignId = campaignAttributes.get(CAMPAIGN_ID_ATTRIBUTE_KEY);
        final String activityId = campaignAttributes.get(CAMPAIGN_ACTIVITY_ID_ATTRIBUTE_KEY);

        final int requestID = getNotificationRequestId(campaignId, activityId);

        new Thread(new Runnable() {
            @Override
            public void run() {
                final Notification notification = createNotification(iconResId, title, message, imageUrl, iconImageUrl,
                                                                     iconSmallImageUrl,
                                                                     NotificationClientBase.this.createOpenAppPendingIntent(pushBundle, targetClass,
                                                                                                     campaignId, requestID,
                                                                                                     intentAction));

                notification.flags |= Notification.FLAG_AUTO_CANCEL;
                notification.defaults |= Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE;

                if (android.os.Build.VERSION.SDK_INT >= ANDROID_LOLLIPOP) {
                    log.info("SDK greater than 21 detected: " + android.os.Build.VERSION.SDK_INT);

                    final String colorString = pushBundle.getString(NOTIFICATION_COLOR_PUSH_KEY);
                    if (colorString != null) {
                        int color;
                        try {
                            color = Color.parseColor(colorString);
                        } catch (final IllegalArgumentException ex) {
                            log.warn("Couldn't parse campaign notification color.", ex);
                            color = 0;
                        }
                        Exception exception = null;
                        try {
                            final Field colorField = notification.getClass().getDeclaredField("color");
                            colorField.setAccessible(true);
                            colorField.set(notification, color);
                        } catch (final IllegalAccessException ex) {
                            exception = ex;
                        } catch (final NoSuchFieldException ex) {
                            exception = ex;
                        }
                        if (exception != null) {
                            log.error("Couldn't set campaign notification color : " + exception.getMessage(), exception);
                        }
                    }
                }

                final NotificationManager notificationManager = (NotificationManager) pinpointContext.getApplicationContext()
                                                                                                     .getSystemService(
                                                                                                         Context.NOTIFICATION_SERVICE);

                notificationManager.notify(requestID, notification);
            }
        }).start();

        return true;
    }

    private boolean openApp() {
        final Intent launchIntent = pinpointContext.getApplicationContext().getPackageManager()
                                                   .getLaunchIntentForPackage(
                                                       pinpointContext.getApplicationContext().getPackageName());

        if (launchIntent == null) {
            log.error("Couldn't get app launch intent for campaign notification.");
            return false;
        }
        launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
        launchIntent.setPackage(null);
        pinpointContext.getApplicationContext().startActivity(launchIntent);
        return true;
    }

    private void openURL(final String url, final boolean noSchemeValidation) {
        final String validatedUrl;
        if (url.startsWith("http://") || url.startsWith("https://") || noSchemeValidation) {
            validatedUrl = url;
        } else {
            validatedUrl = "http://" + url;
        }

        final Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setData(Uri.parse(validatedUrl));
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (intent.resolveActivity(pinpointContext.getApplicationContext().getPackageManager()) != null) {
            pinpointContext.getApplicationContext().startActivity(intent);
        }
    }

    /* pkg */
    NotificationClient.CampaignPushResult handleNotificationOpen(
        Map campaignAttributes,
        final Bundle data) {
        // Add any campaign global attributes
        if (campaignAttributes != null) {
            // Stop Session
            if (this.pinpointContext.getSessionClient() != null) {
                this.pinpointContext.getSessionClient().stopSession();
            }
            addGlobalCampaignAttributes(campaignAttributes);

            final AnalyticsEvent pushEvent = this.pinpointContext.getAnalyticsClient().createEvent(AWS_EVENT_TYPE_OPENED);
            this.pinpointContext.getAnalyticsClient().recordEvent(pushEvent);
            this.pinpointContext.getAnalyticsClient().submitEvents();

            final String url = data.getString(CAMPAIGN_URL_PUSH_KEY);
            if (url != null) {
                openURL(url, false);
                return NotificationClient.CampaignPushResult.NOTIFICATION_OPENED;
            }
            final String deepLink = data.getString(CAMPAIGN_DEEP_LINK_PUSH_KEY);
            if (deepLink != null) {
                openURL(deepLink, true);
                return NotificationClient.CampaignPushResult.NOTIFICATION_OPENED;
            }
            final String openApp = data.getString(CAMPAIGN_OPEN_APP_PUSH_KEY);
            if (openApp == null) {
                log.warn("No key/value present to determine action for campaign notification, default to open app.");
            }
            openApp();
        }
        return NotificationClient.CampaignPushResult.NOTIFICATION_OPENED;
    }

    /**
     * Handles pinpoint push messages by posting a local notification when
     * the app is in the background, or sending a local broadcast if the app is
     * in the foreground. Also on Api level 19 devices and above, if local
     * notifications have been disabled and the app is in the background, a
     * local broadcast is sent.
     *
     * @param notificationDetails the notification message received by the device's messaging service
     * @return {@link NotificationClient.CampaignPushResult}.
     */
    public final NotificationClient.CampaignPushResult handleCampaignPush(NotificationDetails notificationDetails) {
        final String from = notificationDetails.getFrom();
        final Bundle data = notificationDetails.getBundle();
        final Class targetClass = notificationDetails.getTargetClass();
        String intentAction = notificationDetails.getIntentAction();
        notificationChannelId = notificationDetails.getNotificationChannelId();

        // Check if push data contains a Campaign Id
        if (data == null || !data.containsKey(CAMPAIGN_ID_PUSH_KEY)) {
            return NotificationClient.CampaignPushResult.NOT_HANDLED;
        }

        final boolean isAppInForeground = appUtil.isAppInForeground();

        final String imageUrl = data.getString(CAMPAIGN_IMAGE_PUSH_KEY);
        final String imageIconUrl = data.getString(CAMPAIGN_IMAGE_ICON_PUSH_KEY);
        final String imageSmallIconUrl = data.getString(CAMPAIGN_IMAGE_SMALL_ICON_PUSH_KEY);
        final Map campaignAttributes = new HashMap();

        campaignAttributes.put(CAMPAIGN_ID_ATTRIBUTE_KEY, data.getString(CAMPAIGN_ID_PUSH_KEY));
        campaignAttributes.put(CAMPAIGN_TREATMENT_ID_ATTRIBUTE_KEY, data.getString(CAMPAIGN_TREATMENT_ID_PUSH_KEY));
        campaignAttributes.put(CAMPAIGN_ACTIVITY_ID_ATTRIBUTE_KEY, data.getString(CAMPAIGN_ACTIVITY_ID_PUSH_KEY));

        this.pinpointContext.getAnalyticsClient().setCampaignAttributes(campaignAttributes);
        log.info("Campaign Attributes are:" + campaignAttributes);

        if (AWS_EVENT_TYPE_OPENED.equals(from)) {
            return this.handleNotificationOpen(campaignAttributes, data);
        }

        if (campaignAttributes != null) {
            // Create the push event.
            String eventType = null;
            if (isAppInForeground) {
                eventType = AWS_EVENT_TYPE_RECEIVED_FOREGROUND;
            } else {
                eventType = AWS_EVENT_TYPE_RECEIVED_BACKGROUND;
            }
            final AnalyticsEvent pushEvent = this.pinpointContext.getAnalyticsClient().createEvent(eventType);

            // Add the campaign attributes.
            addCampaignAttributesToEvent(pushEvent, campaignAttributes);
            pushEvent.addAttribute("isAppInForeground", Boolean.toString(isAppInForeground));
            try {
                // Ignore whether the app is in the foreground if the configuration indicates it should post
                // notifications in the foreground.
                if (
                    !pinpointContext.getPinpointConfiguration().getShouldPostNotificationsInForeground() && isAppInForeground) {
                    // Notify the caller that the app was in the foreground.
                    return NotificationClient.CampaignPushResult.APP_IN_FOREGROUND;
                } else {
                    // Display a notification with an icon, title, message,
                    // image, and default sound.
                    if ("1".equalsIgnoreCase(data.getString(NOTIFICATION_SILENT_PUSH_KEY))) {
                        return NotificationClient.CampaignPushResult.SILENT;
                    }

                    // App is in the background; attempt to display a
                    // notification in the notification center.
                    if (!areAppNotificationsEnabled() ||
                        !displayNotification(data, targetClass, imageUrl, imageIconUrl, imageSmallIconUrl, campaignAttributes,
                                             intentAction)) {
                        // Local app notifications have been disabled by the
                        // user from Settings -> App Info
                        // or we couldn't display the notification for some
                        // reason.

                        pushEvent.addAttribute("isOptedOut", "true");
                        // We can't post a notification, so delegate to the
                        // passed in handler.
                        return NotificationClient.CampaignPushResult.OPTED_OUT;
                    }
                }
            } finally {
                this.pinpointContext.getAnalyticsClient().recordEvent(pushEvent);
                this.pinpointContext.getAnalyticsClient().submitEvents();
            }
        }
        return NotificationClient.CampaignPushResult.POSTED_NOTIFICATION;
    }

    /**
     * If app-level opt-out is enabled, this method always returns false. Otherwise,
     * the following logic applies: On devices using Android API level 19 and above,
     * this method properly returns whether local notifications are enabled for the app.
     * For devices before API level 19, this method always returns true. Disabling
     * notifications was a feature added on devices supporting API Level 16 and
     * above, so devices from API level 16 to 18 will return true from this
     * method even when local notifications have been disabled for the app.
     *
     * @return true if local notifications are enabled for this app, otherwise
     * false.
     */
    public final boolean areAppNotificationsEnabled() {
        final AppLevelOptOutProvider provider = pinpointContext.getPinpointConfiguration().getAppLevelOptOutProvider();
        if (provider != null && provider.isOptedOut()) {
            return false;
        }

        return areAppNotificationsEnabledOnPlatform();
    }

    boolean areAppNotificationsEnabledOnPlatform() {
        if (android.os.Build.VERSION.SDK_INT < ANDROID_KITKAT) {
            return true;
        }

        final String appOpsServiceName;
        try {
            final Field appOpsServiceNameField = Context.class.getDeclaredField(APP_OPS_SERVICE);
            appOpsServiceName = (String) appOpsServiceNameField.get(String.class);
        } catch (final NoSuchFieldException e) {
            log.error(e.getMessage(), e);
            return true;
        } catch (final IllegalAccessException e) {
            log.error(e.getMessage(), e);
            return true;
        }

        final Object mAppOps = pinpointContext.getApplicationContext().getSystemService(appOpsServiceName);
        if (mAppOps == null) {
            return true;
        }

        final ApplicationInfo appInfo = pinpointContext.getApplicationContext().getApplicationInfo();
        final String pkg = pinpointContext.getApplicationContext().getPackageName();

        final int uid = appInfo.uid;

        try {
            if (appOpsClass == null || checkOpNoThrowMethod == null || opPostNotificationField == null || modeAllowedField == null) {
                appOpsClass = Class.forName(mAppOps.getClass().getName());
                checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE, String.class);
                opPostNotificationField = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);
                modeAllowedField = appOpsClass.getDeclaredField(APP_OPS_MODE_ALLOWED);
            }

            final int postNotificationValue = opPostNotificationField.getInt(null);
            final int opPostNotificationMode = (Integer) checkOpNoThrowMethod.invoke(mAppOps, postNotificationValue, uid, pkg);
            final int modeAllowed = modeAllowedField.getInt(null);
            return (modeAllowed == opPostNotificationMode);

        } catch (final ClassNotFoundException e) {
            log.error(e.getMessage(), e);
        } catch (final NoSuchMethodException e) {
            log.error(e.getMessage(), e);
        } catch (final NoSuchFieldException e) {
            log.error(e.getMessage(), e);
        } catch (final InvocationTargetException e) {
            log.error(e.getMessage(), e);
        } catch (final IllegalAccessException e) {
            log.error(e.getMessage(), e);
        }
        return true;
    }

    /**
     * Creates a new NotificationClientBase based on the supplied context and channel type. This method
     * returns an instance based on NotificationClientBase interface.
     *
     * @param pinpointContext Context to associate with the client.
     * @param channelType The channel type the client will support.
     * @return A new instance of the {@link NotificationClientBase}.
     */
    public static NotificationClientBase createClient(PinpointContext pinpointContext, ChannelType channelType) {
        NotificationClientBase client = null;
        switch (channelType) {
            case ADM:
                client = new ADMNotificationClient(pinpointContext);
                break;
            case GCM:
                client = new GCMNotificationClient(pinpointContext);
                break;
            case BAIDU:
                client = new BaiduNotificationClient(pinpointContext);
                break;
            default:
                client = new GCMNotificationClient(pinpointContext);
                break;
        }
        return client;
    }

    private class DownloadImageTask extends AsyncTask {

        @Override
        protected Bitmap doInBackground(String... urls) {
            try {
                return BitmapFactory.decodeStream((new URL(urls[0])).openConnection().getInputStream());
            } catch (final IOException ex) {
                log.error("Cannot download or find image for rich notification.", ex);
                return null;
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy