fdroid-client/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java

568 lines
27 KiB
Java

package org.fdroid.fdroid;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.StyleSpan;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.net.DownloaderService;
import org.fdroid.fdroid.views.AppDetailsActivity;
import org.fdroid.fdroid.views.main.MainActivity;
import java.util.ArrayList;
import java.util.Arrays;
@SuppressWarnings("LineLength")
public class NotificationHelper {
public static final String CHANNEL_SWAPS = "swap-channel";
public static final String CHANNEL_INSTALLS = "install-channel";
public static final String CHANNEL_UPDATES = "update-channel";
static final String BROADCAST_NOTIFICATIONS_ALL_UPDATES_CLEARED = "org.fdroid.fdroid.installer.notifications.allupdates.cleared";
static final String BROADCAST_NOTIFICATIONS_ALL_INSTALLED_CLEARED = "org.fdroid.fdroid.installer.notifications.allinstalled.cleared";
static final String BROADCAST_NOTIFICATIONS_UPDATE_CLEARED = "org.fdroid.fdroid.installer.notifications.update.cleared";
static final String BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED = "org.fdroid.fdroid.installer.notifications.installed.cleared";
private static final int NOTIFY_ID_UPDATES = 1;
private static final int NOTIFY_ID_INSTALLED = 2;
private static final int MAX_UPDATES_TO_SHOW = 5;
private static final int MAX_INSTALLED_TO_SHOW = 10;
private static final String GROUP_UPDATES = "updates";
private static final String GROUP_INSTALLED = "installed";
private final Context context;
private final NotificationManagerCompat notificationManager;
private final ArrayList<AppUpdateStatusManager.AppUpdateStatus> updates = new ArrayList<>();
private final ArrayList<AppUpdateStatusManager.AppUpdateStatus> installed = new ArrayList<>();
NotificationHelper(Context context) {
this.context = context;
notificationManager = NotificationManagerCompat.from(context);
final NotificationChannelCompat installChannel = new NotificationChannelCompat.Builder(CHANNEL_INSTALLS,
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(context.getString(R.string.notification_channel_installs_title))
.setDescription(context.getString(R.string.notification_channel_installs_description))
.build();
final NotificationChannelCompat swapChannel = new NotificationChannelCompat.Builder(CHANNEL_SWAPS,
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(context.getString(R.string.notification_channel_swaps_title))
.setDescription(context.getString(R.string.notification_channel_swaps_description))
.build();
final NotificationChannelCompat updateChannel = new NotificationChannelCompat.Builder(CHANNEL_UPDATES,
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(context.getString(R.string.notification_channel_updates_title))
.setDescription(context.getString(R.string.notification_channel_updates_description))
.build();
notificationManager.createNotificationChannelsCompat(Arrays.asList(installChannel, swapChannel,
updateChannel));
IntentFilter filter = new IntentFilter();
filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED);
filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED);
filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED);
filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED);
BroadcastReceiver receiverAppStatusChanges = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) {
return;
}
AppUpdateStatusManager appUpdateStatusManager = AppUpdateStatusManager.getInstance(context);
AppUpdateStatusManager.AppUpdateStatus entry;
String url;
switch (intent.getAction()) {
case AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED:
notificationManager.cancelAll();
updateStatusLists();
createSummaryNotifications();
for (AppUpdateStatusManager.AppUpdateStatus appUpdateStatus : appUpdateStatusManager.getAll()) {
createNotification(appUpdateStatus);
}
break;
case AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED:
updateStatusLists();
createSummaryNotifications();
url = intent.getStringExtra(DownloaderService.EXTRA_CANONICAL_URL);
entry = appUpdateStatusManager.get(url);
if (entry != null) {
createNotification(entry);
}
break;
case AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED:
url = intent.getStringExtra(DownloaderService.EXTRA_CANONICAL_URL);
entry = appUpdateStatusManager.get(url);
updateStatusLists();
if (entry != null) {
createNotification(entry);
}
if (intent.getBooleanExtra(AppUpdateStatusManager.EXTRA_IS_STATUS_UPDATE, false)) {
createSummaryNotifications();
}
break;
case AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED:
url = intent.getStringExtra(DownloaderService.EXTRA_CANONICAL_URL);
notificationManager.cancel(url, NOTIFY_ID_INSTALLED);
notificationManager.cancel(url, NOTIFY_ID_UPDATES);
updateStatusLists();
createSummaryNotifications();
break;
}
}
};
LocalBroadcastManager.getInstance(context).registerReceiver(receiverAppStatusChanges, filter);
}
private boolean useStackedNotifications() {
return Build.VERSION.SDK_INT >= 24;
}
/**
* Populate {@link NotificationHelper#updates} and {@link NotificationHelper#installed} with
* the relevant status entries from the {@link AppUpdateStatusManager}.
*/
private void updateStatusLists() {
if (!notificationManager.areNotificationsEnabled()) {
return;
}
updates.clear();
installed.clear();
AppUpdateStatusManager appUpdateStatusManager = AppUpdateStatusManager.getInstance(context);
for (AppUpdateStatusManager.AppUpdateStatus entry : appUpdateStatusManager.getAll()) {
if (entry.status == AppUpdateStatusManager.Status.Installed) {
installed.add(entry);
} else if (!shouldIgnoreEntry(entry)) {
updates.add(entry);
}
}
}
private boolean shouldIgnoreEntry(AppUpdateStatusManager.AppUpdateStatus entry) {
// Ignore unknown status
if (entry.status == AppUpdateStatusManager.Status.DownloadInterrupted) {
return true;
} else if ((entry.status == AppUpdateStatusManager.Status.Downloading ||
entry.status == AppUpdateStatusManager.Status.ReadyToInstall ||
entry.status == AppUpdateStatusManager.Status.InstallError) &&
AppDetailsActivity.isAppVisible(entry.app.packageName)) {
// Ignore downloading, readyToInstall and installError if we are showing the details screen for this app
return true;
}
return false;
}
private void createNotification(AppUpdateStatusManager.AppUpdateStatus entry) {
if (shouldIgnoreEntry(entry)) {
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES);
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED);
return;
}
if (!notificationManager.areNotificationsEnabled() || Preferences.get().hideAllNotifications()) {
return;
}
Notification notification;
if (entry.status == AppUpdateStatusManager.Status.Installed) {
if (useStackedNotifications()) {
notification = createInstalledNotification(entry);
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES);
notificationManager.notify(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED, notification);
} else if (installed.size() == 1) {
notification = createInstalledNotification(entry);
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES);
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED);
notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, notification);
}
} else {
if (useStackedNotifications()) {
notification = createUpdateNotification(entry);
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED);
notificationManager.notify(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES, notification);
} else if (updates.size() == 1) {
notification = createUpdateNotification(entry);
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_UPDATES);
notificationManager.cancel(entry.getCanonicalUrl(), NOTIFY_ID_INSTALLED);
notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, notification);
}
}
}
private void createSummaryNotifications() {
if (!notificationManager.areNotificationsEnabled() || Preferences.get().hideAllNotifications()) {
return;
}
Notification notification;
if (updates.size() != 1 || useStackedNotifications()) {
if (updates.isEmpty()) {
// No updates, remove summary
notificationManager.cancel(GROUP_UPDATES, NOTIFY_ID_UPDATES);
} else {
notification = createUpdateSummaryNotification(updates);
notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, notification);
}
}
if (installed.size() != 1 || useStackedNotifications()) {
if (installed.isEmpty()) {
// No installed, remove summary
notificationManager.cancel(GROUP_INSTALLED, NOTIFY_ID_INSTALLED);
} else {
notification = createInstalledSummaryNotification(installed);
notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, notification);
}
}
}
private NotificationCompat.Action getAction(AppUpdateStatusManager.AppUpdateStatus entry) {
if (entry.intent != null) {
switch (entry.status) {
case UpdateAvailable:
return new NotificationCompat.Action(R.drawable.ic_file_download, context.getString(R.string.notification_action_update), entry.intent);
case PendingInstall:
case Downloading:
case Installing:
return new NotificationCompat.Action(R.drawable.ic_cancel, context.getString(R.string.notification_action_cancel), entry.intent);
case ReadyToInstall:
return new NotificationCompat.Action(R.drawable.ic_file_install, context.getString(R.string.notification_action_install), entry.intent);
}
}
return null;
}
private String getSingleItemTitleString(App app, AppUpdateStatusManager.Status status) {
switch (status) {
case UpdateAvailable:
return context.getString(R.string.notification_title_single_update_available);
case PendingInstall:
case Downloading:
return app.name;
case ReadyToInstall:
return context.getString(app.isInstalled(context) ? R.string.notification_title_single_ready_to_install_update : R.string.notification_title_single_ready_to_install);
case Installing:
return app.name;
case Installed:
return app.name;
case InstallError:
return context.getString(R.string.notification_title_single_install_error);
}
return "";
}
private String getSingleItemContentString(App app, AppUpdateStatusManager.Status status) {
switch (status) {
case UpdateAvailable:
return app.name;
case PendingInstall:
case Downloading:
return context.getString(app.isInstalled(context) ? R.string.notification_content_single_downloading_update : R.string.notification_content_single_downloading, app.name);
case ReadyToInstall:
return app.name;
case Installing:
return context.getString(R.string.notification_content_single_installing, app.name);
case Installed:
return context.getString(R.string.notification_content_single_installed);
case InstallError:
return app.name;
}
return "";
}
private String getMultiItemContentString(App app, AppUpdateStatusManager.Status status) {
switch (status) {
case UpdateAvailable:
return context.getString(R.string.notification_title_summary_update_available);
case PendingInstall:
case Downloading:
return context.getString(app.isInstalled(context) ? R.string.notification_title_summary_downloading_update : R.string.notification_title_summary_downloading);
case ReadyToInstall:
return context.getString(app.isInstalled(context) ? R.string.notification_title_summary_ready_to_install_update : R.string.notification_title_summary_ready_to_install);
case Installing:
return context.getString(R.string.notification_title_summary_installing);
case Installed:
return context.getString(R.string.notification_title_summary_installed);
case InstallError:
return context.getString(R.string.notification_title_summary_install_error);
}
return "";
}
private Notification createUpdateNotification(AppUpdateStatusManager.AppUpdateStatus entry) {
App app = entry.app;
AppUpdateStatusManager.Status status = entry.status;
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context, CHANNEL_UPDATES)
.setAutoCancel(true)
.setContentTitle(getSingleItemTitleString(app, status))
.setContentText(getSingleItemContentString(app, status))
.setSmallIcon(R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, R.color.fdroid_blue))
.setLocalOnly(true)
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
.setContentIntent(entry.intent);
/* If using stacked notifications, use groups. Note that this would not work prior to Lollipop,
because of http://stackoverflow.com/a/34953411, but currently not an issue since stacked
notifications are used only on >= Nougat.
*/
if (useStackedNotifications()) {
builder.setGroup(GROUP_UPDATES);
}
// Handle actions
//
NotificationCompat.Action action = getAction(entry);
if (action != null) {
builder.addAction(action);
}
// Handle progress bar (for some states)
//
if (status == AppUpdateStatusManager.Status.Downloading) {
if (entry.progressMax == 0) {
builder.setProgress(100, 0, true);
} else {
builder.setProgress(Utils.bytesToKb(entry.progressMax),
Utils.bytesToKb(entry.progressCurrent), false);
}
} else if (status == AppUpdateStatusManager.Status.Installing) {
builder.setProgress(100, 0, true); // indeterminate bar
}
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_UPDATE_CLEARED);
intentDeleted.putExtra(DownloaderService.EXTRA_CANONICAL_URL, entry.getCanonicalUrl());
intentDeleted.setClass(context, NotificationBroadcastReceiver.class);
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setDeleteIntent(piDeleted);
loadLargeIconForEntry(entry, builder, NOTIFY_ID_UPDATES, entry.getCanonicalUrl());
return builder.build();
}
private Notification createUpdateSummaryNotification(ArrayList<AppUpdateStatusManager.AppUpdateStatus> updates) {
String title = context.getResources().getQuantityString(R.plurals.notification_summary_updates,
updates.size(), updates.size());
StringBuilder text = new StringBuilder();
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
inboxStyle.setBigContentTitle(title);
for (int i = 0; i < MAX_UPDATES_TO_SHOW && i < updates.size(); i++) {
AppUpdateStatusManager.AppUpdateStatus entry = updates.get(i);
App app = entry.app;
AppUpdateStatusManager.Status status = entry.status;
String content = getMultiItemContentString(app, status);
SpannableStringBuilder sb = new SpannableStringBuilder(app.name);
sb.setSpan(new StyleSpan(Typeface.BOLD), 0, sb.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
sb.append(" ");
sb.append(content);
inboxStyle.addLine(sb);
if (text.length() > 0) {
text.append(", ");
}
text.append(app.name);
}
if (updates.size() > MAX_UPDATES_TO_SHOW) {
int diff = updates.size() - MAX_UPDATES_TO_SHOW;
inboxStyle.setSummaryText(context.getResources().getQuantityString(R.plurals.notification_summary_more,
diff, diff));
}
// Intent to open main app list
Intent intentObject = new Intent(context, MainActivity.class);
intentObject.putExtra(MainActivity.EXTRA_VIEW_UPDATES, true);
PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context, CHANNEL_UPDATES)
.setAutoCancel(!useStackedNotifications())
.setSmallIcon(R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, R.color.fdroid_blue))
.setContentTitle(title)
.setContentText(text)
.setContentIntent(piAction)
.setLocalOnly(true)
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
.setStyle(inboxStyle);
if (useStackedNotifications()) {
builder.setGroup(GROUP_UPDATES)
.setGroupSummary(true);
}
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_ALL_UPDATES_CLEARED);
intentDeleted.setClass(context, NotificationBroadcastReceiver.class);
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setDeleteIntent(piDeleted);
return builder.build();
}
private Notification createInstalledNotification(AppUpdateStatusManager.AppUpdateStatus entry) {
App app = entry.app;
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context, CHANNEL_INSTALLS)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, R.color.fdroid_blue))
.setContentTitle(app.name)
.setContentText(context.getString(R.string.notification_content_single_installed))
.setLocalOnly(true)
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
.setContentIntent(entry.intent);
if (useStackedNotifications()) {
builder.setGroup(GROUP_INSTALLED);
}
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED);
intentDeleted.putExtra(DownloaderService.EXTRA_CANONICAL_URL, entry.getCanonicalUrl());
intentDeleted.setClass(context, NotificationBroadcastReceiver.class);
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setDeleteIntent(piDeleted);
loadLargeIconForEntry(entry, builder, NOTIFY_ID_INSTALLED, entry.getCanonicalUrl());
return builder.build();
}
private Notification createInstalledSummaryNotification(ArrayList<AppUpdateStatusManager.AppUpdateStatus> installed) {
String title = context.getResources().getQuantityString(R.plurals.notification_summary_installed,
installed.size(), installed.size());
StringBuilder text = new StringBuilder();
NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle();
bigTextStyle.setBigContentTitle(title);
for (int i = 0; i < MAX_INSTALLED_TO_SHOW && i < installed.size(); i++) {
AppUpdateStatusManager.AppUpdateStatus entry = installed.get(i);
App app = entry.app;
if (text.length() > 0) {
text.append(", ");
}
text.append(app.name);
}
bigTextStyle.bigText(text);
if (installed.size() > MAX_INSTALLED_TO_SHOW) {
int diff = installed.size() - MAX_INSTALLED_TO_SHOW;
bigTextStyle.setSummaryText(context.getResources().getQuantityString(R.plurals.notification_summary_more,
diff, diff));
}
// Intent to open main app list
Intent intentObject = new Intent(context, MainActivity.class);
PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context, CHANNEL_INSTALLS)
.setAutoCancel(!useStackedNotifications())
.setSmallIcon(R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, R.color.fdroid_blue))
.setContentTitle(title)
.setContentText(text)
.setContentIntent(piAction)
.setLocalOnly(true)
.setVisibility(NotificationCompat.VISIBILITY_SECRET);
if (useStackedNotifications()) {
builder.setGroup(GROUP_INSTALLED)
.setGroupSummary(true);
}
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_ALL_INSTALLED_CLEARED);
intentDeleted.setClass(context, NotificationBroadcastReceiver.class);
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setDeleteIntent(piDeleted);
return builder.build();
}
private Point getLargeIconSize() {
int w = context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
int h = context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
return new Point(w, h);
}
private void loadLargeIconForEntry(AppUpdateStatusManager.AppUpdateStatus entry,
NotificationCompat.Builder notificationBuilder,
int notificationId,
String notificationTag) {
final Point largeIconSize = getLargeIconSize();
if (entry.status == AppUpdateStatusManager.Status.Downloading
|| entry.status == AppUpdateStatusManager.Status.Installing) {
Bitmap bitmap = Bitmap.createBitmap(largeIconSize.x, largeIconSize.y, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Drawable downloadIcon = ContextCompat.getDrawable(context, R.drawable.ic_notification_download);
if (downloadIcon != null) {
downloadIcon.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
downloadIcon.draw(canvas);
}
Glide.with(context)
.asBitmap()
.load(bitmap)
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
// update the loaded large icon, but don't expand
notificationBuilder.setLargeIcon(resource);
Notification notification = notificationBuilder.build();
notificationManager.notify(notificationTag, notificationId, notification);
}
@Override
public void onLoadCleared(@Nullable Drawable drawable) {
}
});
} else {
App.loadBitmapWithGlide(context, entry.app.repoId, entry.app.iconFile)
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
// update the loaded large icon, but don't expand
notificationBuilder.setLargeIcon(resource);
Notification notification = notificationBuilder.build();
notificationManager.notify(notificationTag, notificationId, notification);
}
@Override
public void onLoadCleared(@Nullable Drawable drawable) {
}
});
}
}
}