com.ruesga.rview.misc.NotificationsHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.ruesga.rview.misc.NotificationsHelper.java

Source

/*
 * Copyright (C) 2016 Jorge Ruesga
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License 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.ruesga.rview.misc;

import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.app.RemoteInput;
import android.support.v4.content.ContextCompat;
import android.text.Html;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;

import com.google.gson.reflect.TypeToken;
import com.ruesga.rview.ChangeDetailsActivity;
import com.ruesga.rview.NotificationsActivity;
import com.ruesga.rview.R;
import com.ruesga.rview.gerrit.model.AccountInfo;
import com.ruesga.rview.gerrit.model.AssigneeInfo;
import com.ruesga.rview.gerrit.model.CloudNotificationEvents;
import com.ruesga.rview.model.Account;
import com.ruesga.rview.preferences.Constants;
import com.ruesga.rview.preferences.Preferences;
import com.ruesga.rview.providers.NotificationEntity;
import com.ruesga.rview.receivers.NotificationReceiver;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

public class NotificationsHelper {

    private static final String TAG = "Notifications";

    private static final String NOTIFICATION_KEY_GROUP = "rview-";

    public static int generateGroupId(com.ruesga.rview.model.Notification notification) {
        String hash = notification.token + "/" + notification.change;
        return FowlerNollVo.fnv1_32(hash.getBytes()).intValue();
    }

    public static int generateGroupId(Account account, String changeId) {
        String hash = account.getAccountHash() + "/" + changeId;
        return FowlerNollVo.fnv1_32(hash.getBytes()).intValue();
    }

    public static void dismissNotification(Context ctx, int groupId) {
        NotificationManagerCompat notificationManager = NotificationManagerCompat.from(ctx);
        notificationManager.cancel(groupId);
    }

    @SuppressWarnings("Convert2streamapi")
    public static void recreateNotifications(Context ctx) {
        List<NotificationEntity> entities = NotificationEntity.getAllNotifications(ctx, true, true);
        SparseArray<Account> notifications = new SparseArray<>();
        for (NotificationEntity entity : entities) {
            if (notifications.indexOfKey(entity.mGroupId) < 0) {
                notifications.put(entity.mGroupId, ModelHelper.getAccountFromHash(ctx, entity.mAccountId));
            }
        }

        int count = notifications.size();
        for (int i = 0; i < count; i++) {
            int groupId = notifications.keyAt(i);
            Account account = notifications.valueAt(i);
            dismissNotification(ctx, groupId);
            createNotification(ctx, account, groupId, false);
        }
    }

    public static boolean canHandleNotification(Context ctx, com.ruesga.rview.model.Notification notification) {
        switch (notification.event) {
        case CloudNotificationEvents.CHANGE_ABANDONED_EVENT:
        case CloudNotificationEvents.CHANGE_MERGED_EVENT:
        case CloudNotificationEvents.CHANGE_RESTORED_EVENT:
        case CloudNotificationEvents.CHANGE_REVERTED_EVENT:
        case CloudNotificationEvents.COMMENT_ADDED_EVENT:
        case CloudNotificationEvents.DRAFT_PUBLISHED_EVENT:
        case CloudNotificationEvents.HASHTAG_CHANGED_EVENT:
        case CloudNotificationEvents.PATCHSET_CREATED_EVENT:
        case CloudNotificationEvents.TOPIC_CHANGED_EVENT:
        case CloudNotificationEvents.ASSIGNEE_CHANGED_EVENT:
        case CloudNotificationEvents.VOTE_DELETED_EVENT:
            return true;

        case CloudNotificationEvents.REVIEWER_ADDED_EVENT:
            // Made this event looks like like an advise about when others added the
            // current user to the change (like email notifications). Ignore the
            // rest of the add or delete reviewer events
            Account me = ModelHelper.getAccountFromHash(ctx, notification.token);
            List<AccountInfo> reviewers = getReviewers(notification.extra);
            for (AccountInfo reviewer : reviewers) {
                if (me != null && isSameAccount(me.mAccount, reviewer)) {
                    return true;
                }
            }
            return false;
        }
        return false;
    }

    @SuppressWarnings("Convert2streamapi")
    public static void createNotification(Context ctx, Account account, long groupId, boolean feedback) {
        List<NotificationEntity> entities = NotificationEntity.getAllGroupNotifications(ctx, groupId, true, true);
        if (entities.isEmpty()) {
            Log.w(TAG, "There isn't notification to display for group " + groupId);
            return;
        }

        // Determine the best suitable way to display notifications
        if (entities.size() == 1) {
            // Single notification
            createSingleNotification(ctx, account, entities.get(0), feedback);
        } else {
            // Group notification
            createGroupNotifications(ctx, account, entities, feedback);
        }

        // Create an account group summary notification
        if (AndroidHelper.isNougatOrGreater()) {
            List<NotificationEntity> accountEntities = NotificationEntity.getAllAccountNotifications(ctx,
                    account.getAccountHash(), true, true);
            Set<String> notifications = new LinkedHashSet<>();
            for (NotificationEntity entity : accountEntities) {
                notifications.add(entity.mNotification.subject);
            }

            if (notifications.size() > 1) {
                createSummaryGroupNotification(ctx, account, notifications, feedback);
            }
        }
    }

    private static void createSingleNotification(Context ctx, Account account, NotificationEntity entity,
            boolean feedback) {
        if (AndroidHelper.isNougatOrGreater()) {
            List<NotificationEntity> entities = new ArrayList<>();
            entities.add(entity);
            createBundledNotifications(ctx, account, entities, feedback);
        } else {
            NotificationCompat.Builder builder = createNotificationBuilder(ctx, account, entity, feedback);
            builder.setStyle(
                    new NotificationCompat.BigTextStyle().bigText(getContentMessage(ctx, entity, false, false)))
                    .setGroup(NOTIFICATION_KEY_GROUP + account.getAccountHash());
            publishNotification(ctx, builder.build(), entity.mGroupId);
        }
    }

    private static void createGroupNotifications(Context ctx, Account account, List<NotificationEntity> entities,
            boolean feedback) {
        if (AndroidHelper.isNougatOrGreater()) {
            createBundledNotifications(ctx, account, entities, feedback);
        } else {
            createInboxNotification(ctx, account, entities, feedback);
        }
    }

    private static void createInboxNotification(Context ctx, Account account, List<NotificationEntity> entities,
            boolean feedback) {
        NotificationEntity lastEntity = entities.get(entities.size() - 1);
        NotificationCompat.Builder builder = createNotificationBuilder(ctx, account, lastEntity, feedback);
        NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle()
                .setBigContentTitle(lastEntity.mNotification.subject)
                .setSummaryText(getNotificationSubText(account));
        for (NotificationEntity entity : entities) {
            style.addLine(getContentMessage(ctx, entity, false, true));
        }
        builder.setStyle(style).setNumber(entities.size())
                .setGroup(NOTIFICATION_KEY_GROUP + account.getAccountHash());

        publishNotification(ctx, builder.build(), lastEntity.mGroupId);
    }

    @TargetApi(Build.VERSION_CODES.N)
    private static void createBundledNotifications(Context ctx, Account account, List<NotificationEntity> entities,
            boolean feedback) {

        NotificationEntity lastEntity = entities.get(entities.size() - 1);
        NotificationCompat.Builder builder = createNotificationBuilder(ctx, account, lastEntity, feedback);
        NotificationCompat.MessagingStyle style = new NotificationCompat.MessagingStyle("")
                .setConversationTitle(lastEntity.mNotification.subject);
        for (NotificationEntity entity : entities) {
            String author = getEventAuthor(ctx, entity);
            style.addMessage(getContentMessage(ctx, entity, true, false), entity.mWhen, author);
        }
        builder.setStyle(style).setNumber(entities.size())
                .setGroup(NOTIFICATION_KEY_GROUP + account.getAccountHash());

        createInlineReply(ctx, builder, lastEntity);

        publishNotification(ctx, builder.build(), lastEntity.mGroupId);
    }

    @TargetApi(Build.VERSION_CODES.N)
    private static void createSummaryGroupNotification(Context ctx, Account account, Set<String> notifications,
            boolean feedback) {
        int notificationId = FowlerNollVo.fnv1_32(account.getAccountHash().getBytes()).intValue();
        NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx, account.getAccountHash())
                .setContentTitle(getNotificationSubText(account)).setSubText(getNotificationSubText(account))
                .setSmallIcon(R.drawable.ic_stat_notify).setAutoCancel(true)
                .setCategory(NotificationCompat.CATEGORY_SOCIAL).setPriority(NotificationCompat.PRIORITY_DEFAULT)
                .setDefaults(feedback ? NotificationCompat.DEFAULT_ALL : 0)
                .setColor(ContextCompat.getColor(ctx, R.color.primaryDark))
                .setContentIntent(getViewAccountChangesPendingIntent(ctx, account, notificationId))
                .setDeleteIntent(getDeleteAccountNotificationPendingIntent(ctx, account, notificationId))
                .setOnlyAlertOnce(true).setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
        NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle()
                .setSummaryText(getNotificationSubText(account));
        builder.setStyle(style).setNumber(notifications.size()).setGroupSummary(true)
                .setGroup(NOTIFICATION_KEY_GROUP + account.getAccountHash());

        publishNotification(ctx, builder.build(), notificationId);
    }

    private static NotificationCompat.Builder createNotificationBuilder(Context ctx, Account account,
            NotificationEntity entity, boolean feedback) {
        return new NotificationCompat.Builder(ctx, account.getAccountHash())
                .setContentTitle(entity.mNotification.subject).setContentText(getContentTitle(ctx, entity, true))
                .setSubText(getNotificationSubText(account)).setSmallIcon(R.drawable.ic_stat_notify)
                .setAutoCancel(true).setCategory(NotificationCompat.CATEGORY_SOCIAL)
                .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                .setDefaults(feedback ? NotificationCompat.DEFAULT_ALL : 0).setWhen(entity.mWhen)
                .setColor(ContextCompat.getColor(ctx, R.color.primaryDark))
                .setContentIntent(getViewChangePendingIntent(ctx, entity))
                .setDeleteIntent(getDeleteGroupNotificationPendingIntent(ctx, entity.mGroupId))
                .setOnlyAlertOnce(true).setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
    }

    private static PendingIntent getViewChangePendingIntent(Context ctx, NotificationEntity entity) {
        Intent intent = new Intent(ctx, ChangeDetailsActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        intent.putExtra(Constants.EXTRA_ACCOUNT_HASH, entity.mAccountId);
        intent.putExtra(Constants.EXTRA_NOTIFICATION_GROUP_ID, entity.mGroupId);
        intent.putExtra(Constants.EXTRA_CHANGE_ID, entity.mNotification.change);
        intent.putExtra(Constants.EXTRA_LEGACY_CHANGE_ID, entity.mNotification.legacyChangeId);
        intent.putExtra(Constants.EXTRA_HAS_PARENT, false);
        intent.putExtra(Constants.EXTRA_FORCE_SINGLE_PANEL, true);

        return PendingIntent.getActivity(ctx, entity.mGroupId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    }

    private static PendingIntent getViewAccountChangesPendingIntent(Context ctx, Account account,
            int notificationId) {
        Intent intent = new Intent(ctx, NotificationsActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        intent.putExtra(Constants.EXTRA_ACCOUNT_HASH, account.getAccountHash());
        intent.putExtra(Constants.EXTRA_HAS_PARENT, false);

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

    private static PendingIntent getDeleteGroupNotificationPendingIntent(Context ctx, int groupId) {
        Intent intent = new Intent(NotificationReceiver.ACTION_NOTIFICATION_DISMISSED);
        intent.putExtra(Constants.EXTRA_NOTIFICATION_GROUP_ID, groupId);

        return PendingIntent.getBroadcast(ctx, groupId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    }

    private static PendingIntent getDeleteAccountNotificationPendingIntent(Context ctx, Account account,
            int notificationId) {
        Intent intent = new Intent(ctx, NotificationReceiver.class);
        intent.setAction(NotificationReceiver.ACTION_NOTIFICATION_DISMISSED);
        intent.putExtra(Constants.EXTRA_ACCOUNT_HASH, account.getAccountHash());

        return PendingIntent.getBroadcast(ctx, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    }

    private static PendingIntent getReplyPendingIntent(Context ctx, NotificationEntity entity) {
        Intent intent = new Intent(ctx, NotificationReceiver.class);
        intent.setAction(NotificationReceiver.ACTION_NOTIFICATION_REPLY);
        intent.putExtra(Constants.EXTRA_LEGACY_CHANGE_ID, entity.mNotification.legacyChangeId);
        intent.putExtra(Constants.EXTRA_ACCOUNT_HASH, entity.mAccountId);
        intent.putExtra(Constants.EXTRA_NOTIFICATION_GROUP_ID, entity.mGroupId);

        return PendingIntent.getBroadcast(ctx, entity.mGroupId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    }

    private static String getNotificationSubText(Account account) {
        return account.getRepositoryDisplayName() + " / " + account.getAccountDisplayName();
    }

    private static void publishNotification(Context ctx, Notification notification, int groupId) {
        NotificationManagerCompat notificationManager = NotificationManagerCompat.from(ctx);
        notificationManager.notify(groupId, notification);
    }

    @TargetApi(Build.VERSION_CODES.N)
    private static void createInlineReply(Context ctx, NotificationCompat.Builder builder,
            NotificationEntity entity) {
        RemoteInput remoteInput = new RemoteInput.Builder(Constants.EXTRA_COMMENT)
                .setLabel(ctx.getString(R.string.change_details_review_hint)).setAllowFreeFormInput(true).build();
        NotificationCompat.Action action = new NotificationCompat.Action.Builder(R.drawable.ic_send,
                ctx.getString(R.string.action_reply), getReplyPendingIntent(ctx, entity))
                        .addRemoteInput(remoteInput).setAllowGeneratedReplies(false).build();
        builder.addAction(action);
    }

    private static CharSequence getContentTitle(Context ctx, NotificationEntity entity, boolean withEventAuthor) {
        String event = getNotificationTitle(ctx, entity);
        if (event == null) {
            return null;
        }
        if (!withEventAuthor) {
            return event;
        }

        String author = getEventAuthor(ctx, entity);
        return String.format("%s %s", author, event);
    }

    @SuppressWarnings("deprecation")
    public static CharSequence getContentMessage(Context ctx, NotificationEntity entity, boolean bundled,
            boolean inbox) {
        final String event = getNotificationTitle(ctx, entity);
        if (event == null) {
            return null;
        }

        final String message = getNotificationMessage(entity);

        if (bundled) {
            return Html.fromHtml(String.format("%s%s", event, message));
        }
        String author = getEventAuthor(ctx, entity);
        return Html.fromHtml(String.format("<b>%s</b> %s%s", author, event,
                (inbox || message.trim().isEmpty()) ? "" : "<br/>" + message.trim().replaceAll("\n", "<br/>")));
    }

    private static String getEventAuthor(Context ctx, NotificationEntity entity) {
        String author = null;
        if (entity.mNotification.who != null) {
            author = ModelHelper.getAccountDisplayName(entity.mNotification.who);
        }
        if (author == null) {
            author = ctx.getString(R.string.account_anonymous_coward);
        }
        return author;
    }

    private static String getNotificationTitle(Context ctx, NotificationEntity entity) {
        switch (entity.mNotification.event) {
        case CloudNotificationEvents.CHANGE_ABANDONED_EVENT:
            return ctx.getString(R.string.notification_content_title_1);
        case CloudNotificationEvents.CHANGE_MERGED_EVENT:
            return ctx.getString(R.string.notification_content_title_2);
        case CloudNotificationEvents.CHANGE_RESTORED_EVENT:
            return ctx.getString(R.string.notification_content_title_4);
        case CloudNotificationEvents.CHANGE_REVERTED_EVENT:
            return ctx.getString(R.string.notification_content_title_8);
        case CloudNotificationEvents.COMMENT_ADDED_EVENT:
            return ctx.getString(R.string.notification_content_title_16);
        case CloudNotificationEvents.DRAFT_PUBLISHED_EVENT:
            return ctx.getString(R.string.notification_content_title_32);
        case CloudNotificationEvents.HASHTAG_CHANGED_EVENT:
            return ctx.getString(R.string.notification_content_title_64);
        case CloudNotificationEvents.REVIEWER_ADDED_EVENT:
            // Made this event looks like like an advise about when others added the
            // current user to the change (like email notifications). Ignore the
            // rest of the add or delete reviewer events
            Account me = ModelHelper.getAccountFromHash(ctx, entity.mAccountId);
            List<AccountInfo> reviewers = getReviewers(entity.mNotification.extra);
            for (AccountInfo reviewer : reviewers) {
                if (me != null && isSameAccount(me.mAccount, reviewer)) {
                    return ctx.getString(R.string.notification_content_title_128);
                }
            }
        case CloudNotificationEvents.PATCHSET_CREATED_EVENT:
            return ctx.getString(R.string.notification_content_title_512);
        case CloudNotificationEvents.TOPIC_CHANGED_EVENT:
            return ctx.getString(R.string.notification_content_title_1024);
        case CloudNotificationEvents.ASSIGNEE_CHANGED_EVENT:
            AssigneeInfo assignee = SerializationManager.getInstance().fromJson(entity.mNotification.extra,
                    AssigneeInfo.class);
            if (assignee._new == null) {
                return ctx.getString(R.string.notification_content_title_2048_unassign,
                        ModelHelper.getAccountDisplayName(assignee.old));
            }
            return ctx.getString(R.string.notification_content_title_2048_assign,
                    ModelHelper.getAccountDisplayName(assignee._new));
        case CloudNotificationEvents.VOTE_DELETED_EVENT:
            return ctx.getString(R.string.notification_content_title_4096);
        }
        return null;
    }

    private static String getNotificationMessage(NotificationEntity entity) {
        String msg = " ";
        switch (entity.mNotification.event) {
        case CloudNotificationEvents.COMMENT_ADDED_EVENT:
            msg += entity.mNotification.extra;
        }
        return msg;
    }

    @SuppressWarnings("RedundantIfStatement")
    private static boolean isSameAccount(AccountInfo o1, AccountInfo o2) {
        // Since not always account return
        if (o1.accountId == o2.accountId) {
            return true;
        }
        if (o1.username != null && o2.username != null && o1.username.equals(o2.username)) {
            return true;
        }
        if (o1.email != null && o2.email != null && o1.email.equals(o2.email)) {
            return true;
        }
        return false;
    }

    private static List<AccountInfo> getReviewers(String serialized) {
        if (TextUtils.isEmpty(serialized)) {
            return new ArrayList<>();
        }

        List<AccountInfo> reviewers = new ArrayList<>();
        if (serialized.startsWith("[")) {
            // Version 2.14. Returns an array of added reviewers
            Type type = new TypeToken<List<AccountInfo>>() {
            }.getType();
            reviewers.addAll(SerializationManager.getInstance().fromJson(serialized, type));
        } else {
            // Version 2.13. Returns the added reviewer
            reviewers.add(SerializationManager.getInstance().fromJson(serialized, AccountInfo.class));
        }

        return reviewers;
    }

    public static void createNotificationChannels(Context context) {
        List<Account> accounts = Preferences.getAccounts(context);
        for (Account account : accounts) {
            createNotificationChannel(context, account);
        }
    }

    @TargetApi(Build.VERSION_CODES.O)
    public static void createNotificationChannel(Context context, Account account) {
        if (AndroidHelper.isApi26OrGreater()) {
            final String defaultChannelName = context.getString(R.string.notifications_default_channel_name,
                    account.getRepositoryDisplayName(), account.getAccountDisplayName());
            final NotificationManager nm = (NotificationManager) context
                    .getSystemService(Context.NOTIFICATION_SERVICE);
            nm.createNotificationChannelGroup(
                    new NotificationChannelGroup(account.getAccountHash(), defaultChannelName));

            NotificationChannel channel = new NotificationChannel(account.getAccountHash(), defaultChannelName,
                    NotificationManager.IMPORTANCE_DEFAULT);
            channel.setDescription(context.getString(R.string.notifications_default_channel_description));
            channel.enableVibration(true);
            channel.enableLights(true);
            channel.setLightColor(ContextCompat.getColor(context, R.color.primaryDark));
            channel.setShowBadge(true);
            channel.setGroup(account.getAccountHash());
            nm.createNotificationChannel(channel);
        }
    }

    @TargetApi(Build.VERSION_CODES.O)
    public static void deleteNotificationChannel(Context context, Account account) {
        if (AndroidHelper.isApi26OrGreater()) {
            final NotificationManager nm = (NotificationManager) context
                    .getSystemService(Context.NOTIFICATION_SERVICE);
            nm.deleteNotificationChannelGroup(account.getAccountHash());
        }
    }
}