Create notification helper class
This commit is contained in:
parent
9e1458f41e
commit
10cbf1ef7a
@ -220,6 +220,8 @@ public class FDroidApp extends Application {
|
||||
curTheme = Preferences.get().getTheme();
|
||||
Preferences.get().configureProxy();
|
||||
|
||||
NotificationHelper.init(getApplicationContext());
|
||||
|
||||
InstalledAppProviderService.compareToPackageManager(this);
|
||||
|
||||
// If the user changes the preference to do with filtering rooted apps,
|
||||
|
522
app/src/main/java/org/fdroid/fdroid/NotificationHelper.java
Normal file
522
app/src/main/java/org/fdroid/fdroid/NotificationHelper.java
Normal file
@ -0,0 +1,522 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Typeface;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.support.v4.app.TaskStackBuilder;
|
||||
import android.support.v4.util.LongSparseArray;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.assist.ImageSize;
|
||||
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
public class NotificationHelper {
|
||||
|
||||
private static final String BROADCAST_NOTIFICATIONS_UPDATES_CLEARED = "org.fdroid.fdroid.installer.notifications.updates.cleared";
|
||||
private static final String BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED = "org.fdroid.fdroid.installer.notifications.installed.cleared";
|
||||
private static final String BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED = "org.fdroid.fdroid.installer.notifications.deleted";
|
||||
|
||||
private static final int NOTIFY_ID_UPDATES = 4711;
|
||||
private static final int NOTIFY_ID_INSTALLED = 4712;
|
||||
|
||||
private static final int MAX_UPDATES_TO_SHOW = 5;
|
||||
private static final int MAX_INSTALLED_TO_SHOW = 10;
|
||||
|
||||
private static final String EXTRA_NOTIFICATION_TAG = "tag";
|
||||
private static final String GROUP_UPDATES = "updates";
|
||||
private static final String GROUP_INSTALLED = "installed";
|
||||
|
||||
public enum Status {
|
||||
UpdateAvailable,
|
||||
Downloading,
|
||||
ReadyToInstall,
|
||||
Installing,
|
||||
Installed,
|
||||
Error
|
||||
}
|
||||
|
||||
private static NotificationHelper instance;
|
||||
|
||||
public static void init(Context context) {
|
||||
instance = new NotificationHelper(context);
|
||||
}
|
||||
|
||||
private static NotificationHelper getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private class AppEntry {
|
||||
App app;
|
||||
Apk apk;
|
||||
Status status;
|
||||
PendingIntent intent;
|
||||
int progressCurrent;
|
||||
int progressMax;
|
||||
|
||||
AppEntry(App app, Apk apk, Status status, PendingIntent intent) {
|
||||
this.app = app;
|
||||
this.apk = apk;
|
||||
this.status = status;
|
||||
this.intent = intent;
|
||||
}
|
||||
|
||||
String getTag() {
|
||||
return apk.getUrl();
|
||||
}
|
||||
|
||||
int getId() {
|
||||
return getTag().hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private final NotificationManagerCompat notificationManager;
|
||||
private HashMap<String, AppEntry> appMapping;
|
||||
private boolean isBatchUpdating;
|
||||
private ArrayList<AppEntry> updates;
|
||||
private ArrayList<AppEntry> installed;
|
||||
|
||||
private NotificationHelper(Context context) {
|
||||
this.context = context;
|
||||
notificationManager = NotificationManagerCompat.from(context);
|
||||
|
||||
// We need to listen to when notifications are cleared, so that we "forget" all that we currently know about updates
|
||||
// and installs.
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(BROADCAST_NOTIFICATIONS_UPDATES_CLEARED);
|
||||
filter.addAction(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED);
|
||||
filter.addAction(BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED);
|
||||
BroadcastReceiver mReceiverNotificationsCleared = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
switch (intent.getAction()) {
|
||||
case BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED:
|
||||
clearAllInstalledInternal();
|
||||
break;
|
||||
case BROADCAST_NOTIFICATIONS_UPDATES_CLEARED:
|
||||
clearAllUpdatesInternal();
|
||||
break;
|
||||
case BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED:
|
||||
String id = intent.getStringExtra(EXTRA_NOTIFICATION_TAG);
|
||||
// TODO
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
context.registerReceiver(mReceiverNotificationsCleared, filter);
|
||||
appMapping = new HashMap<>();
|
||||
updates = new ArrayList<>();
|
||||
installed = new ArrayList<>();
|
||||
}
|
||||
|
||||
private void setApkInternal(Apk apk, Status status, PendingIntent intent) {
|
||||
if (apk == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppEntry entry = appMapping.get(apk.getUrl());
|
||||
if (status == null) {
|
||||
// Remove
|
||||
if (entry != null) {
|
||||
appMapping.remove(apk.getUrl());
|
||||
notificationManager.cancel(entry.getTag(), entry.getId());
|
||||
}
|
||||
} else if (entry != null) {
|
||||
// Update
|
||||
boolean isStatusUpdate = (entry.status != status);
|
||||
entry.status = status;
|
||||
entry.intent = intent;
|
||||
createNotificationForAppEntry(entry);
|
||||
if (isStatusUpdate) {
|
||||
updateSummaryNotifications();
|
||||
}
|
||||
} else {
|
||||
// Add
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo);
|
||||
entry = new AppEntry(app, apk, status, intent);
|
||||
appMapping.put(apk.getUrl(), entry);
|
||||
createNotificationForAppEntry(entry);
|
||||
updateSummaryNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
private void setApkProgressInternal(Apk apk, int max, int current) {
|
||||
if (appMapping.get(apk.getUrl()) != null) {
|
||||
AppEntry entry = appMapping.get(apk.getUrl());
|
||||
entry.progressMax = max;
|
||||
entry.progressCurrent = current;
|
||||
createNotificationForAppEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearAllUpdatesInternal() {
|
||||
for(Iterator<Map.Entry<String, AppEntry>> it = appMapping.entrySet().iterator(); it.hasNext(); ) {
|
||||
Map.Entry<String, AppEntry> entry = it.next();
|
||||
if(entry.getValue().status != Status.Installed) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearAllInstalledInternal() {
|
||||
for(Iterator<Map.Entry<String, AppEntry>> it = appMapping.entrySet().iterator(); it.hasNext(); ) {
|
||||
Map.Entry<String, AppEntry> entry = it.next();
|
||||
if(entry.getValue().status == Status.Installed) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSummaryNotifications() {
|
||||
if (!isBatchUpdating) {
|
||||
// Get the list of updates and installed available
|
||||
updates.clear();
|
||||
installed.clear();
|
||||
for (Iterator<Map.Entry<String, AppEntry>> it = appMapping.entrySet().iterator(); it.hasNext(); ) {
|
||||
Map.Entry<String, AppEntry> entry = it.next();
|
||||
if (entry.getValue().status != Status.Installed) {
|
||||
updates.add(entry.getValue());
|
||||
} else {
|
||||
installed.add(entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
NotificationCompat.Builder builder;
|
||||
if (updates.size() == 0) {
|
||||
// No updates, remove summary
|
||||
notificationManager.cancel(GROUP_UPDATES, NOTIFY_ID_UPDATES);
|
||||
} else {
|
||||
builder = createUpdateSummaryNotification(updates);
|
||||
notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, builder.build());
|
||||
}
|
||||
if (installed.size() == 0) {
|
||||
// No installed, remove summary
|
||||
notificationManager.cancel(GROUP_INSTALLED, NOTIFY_ID_INSTALLED);
|
||||
} else {
|
||||
builder = createInstalledSummaryNotification(installed);
|
||||
notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, builder.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void createNotificationForAppEntry(AppEntry entry) {
|
||||
NotificationCompat.Builder builder;
|
||||
if (entry.status == Status.Installed) {
|
||||
builder = createInstalledNotification(entry);
|
||||
} else {
|
||||
builder = createUpdateNotification(entry);
|
||||
}
|
||||
notificationManager.notify(entry.getTag(), entry.getId(), builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an Apk to the notifications manager.
|
||||
* @param apk The apk to add.
|
||||
* @param status The current status of the app
|
||||
* @param pendingIntent Action when notification is clicked. Can be null for default action(s)
|
||||
*/
|
||||
public static void setApk(Apk apk, Status status, PendingIntent pendingIntent) {
|
||||
getInstance().setApkInternal(apk, status, pendingIntent);
|
||||
}
|
||||
|
||||
public static void removeApk(Apk apk) {
|
||||
getInstance().setApkInternal(apk, null, null);
|
||||
}
|
||||
|
||||
public static void setApkProgress(Apk apk, int max, int current) {
|
||||
getInstance().setApkProgressInternal(apk, max, current);
|
||||
}
|
||||
|
||||
public static void startBatchUpdates() {
|
||||
getInstance().isBatchUpdating = true;
|
||||
}
|
||||
|
||||
public static void endBatchUpdates() {
|
||||
getInstance().isBatchUpdating = false;
|
||||
getInstance().updateSummaryNotifications();
|
||||
}
|
||||
|
||||
public static void clearAllUpdates() {
|
||||
getInstance().clearAllUpdatesInternal();
|
||||
}
|
||||
|
||||
public static void clearAllInstalled() {
|
||||
getInstance().clearAllInstalledInternal();
|
||||
}
|
||||
|
||||
private NotificationCompat.Action getAction(AppEntry entry) {
|
||||
if (entry.status == Status.UpdateAvailable) {
|
||||
// Make sure we have an intent to install the app. If not set, we create an intent
|
||||
// to open up the app details page for the app. From there, the user can hit "install"
|
||||
PendingIntent intent = entry.intent;
|
||||
if (intent == null) {
|
||||
intent = getAppDetailsIntent(0, entry.apk);
|
||||
}
|
||||
return new NotificationCompat.Action(R.drawable.ic_notify_update_24dp, "Update", intent);
|
||||
} else if (entry.status == Status.Downloading || entry.status == Status.Installing) {
|
||||
PendingIntent intent = entry.intent;
|
||||
if (intent != null) {
|
||||
return new NotificationCompat.Action(R.drawable.ic_notify_cancel_24dp, "Cancel", intent);
|
||||
}
|
||||
} else if (entry.status == Status.ReadyToInstall) {
|
||||
// Make sure we have an intent to install the app. If not set, we create an intent
|
||||
// to open up the app details page for the app. From there, the user can hit "install"
|
||||
PendingIntent intent = entry.intent;
|
||||
if (intent == null) {
|
||||
intent = getAppDetailsIntent(0, entry.apk);
|
||||
}
|
||||
return new NotificationCompat.Action(R.drawable.ic_notify_install_24dp, "Install", intent);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getSingleItemTitleString(App app, Status status) {
|
||||
switch (status) {
|
||||
case UpdateAvailable:
|
||||
return "Update Available";
|
||||
case Downloading:
|
||||
return app.name;
|
||||
case ReadyToInstall:
|
||||
return "Update ready to install"; // TODO - "Update"? Should just be "ready to install"?
|
||||
case Installing:
|
||||
return app.name;
|
||||
case Installed:
|
||||
return app.name;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String getSingleItemContentString(App app, Status status) {
|
||||
switch (status) {
|
||||
case UpdateAvailable:
|
||||
return app.name;
|
||||
case Downloading:
|
||||
return String.format("Downloading update for \"%s\"...", app.name);
|
||||
case ReadyToInstall:
|
||||
return app.name;
|
||||
case Installing:
|
||||
return String.format("Installing \"%s\"...", app.name);
|
||||
case Installed:
|
||||
return "Successfully installed";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String getMultiItemContentString(App app, Status status) {
|
||||
switch (status) {
|
||||
case UpdateAvailable:
|
||||
return "Update available";
|
||||
case Downloading:
|
||||
return "Downloading update...";
|
||||
case ReadyToInstall:
|
||||
return "Ready to install";
|
||||
case Installing:
|
||||
return "Installing";
|
||||
case Installed:
|
||||
return "Successfully installed";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link PendingIntent} for a {@link Notification} to send when it
|
||||
* is clicked. {@link AppDetails} handles {@code Intent}s that are missing
|
||||
* or bad {@link AppDetails#EXTRA_APPID}, so it does not need to be checked
|
||||
* here.
|
||||
*/
|
||||
private PendingIntent getAppDetailsIntent(int requestCode, Apk apk) {
|
||||
Intent notifyIntent = new Intent(context, AppDetails.class)
|
||||
.putExtra(AppDetails.EXTRA_APPID, apk.packageName);
|
||||
return TaskStackBuilder.create(context)
|
||||
.addParentStack(AppDetails.class)
|
||||
.addNextIntent(notifyIntent)
|
||||
.getPendingIntent(requestCode, 0);
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder createUpdateNotification(AppEntry entry) {
|
||||
App app = entry.app;
|
||||
Status status = entry.status;
|
||||
|
||||
// TODO - async image loading
|
||||
int largeIconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
|
||||
Bitmap iconLarge = ImageLoader.getInstance().loadImageSync(app.iconUrl, new ImageSize(largeIconSize, largeIconSize));
|
||||
|
||||
NotificationCompat.Builder builder =
|
||||
new NotificationCompat.Builder(context)
|
||||
.setAutoCancel(true)
|
||||
.setLargeIcon(iconLarge)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_updates)
|
||||
.setContentTitle(getSingleItemTitleString(app, status))
|
||||
.setContentText(getSingleItemContentString(app, status))
|
||||
.setGroup(GROUP_UPDATES);
|
||||
|
||||
// Handle actions
|
||||
//
|
||||
NotificationCompat.Action action = getAction(entry);
|
||||
if (action != null) {
|
||||
builder.addAction(action);
|
||||
// TODO - also click on whole item?
|
||||
builder.setContentIntent(action.getActionIntent());
|
||||
} else if (entry.intent != null) {
|
||||
builder.setContentIntent(entry.intent);
|
||||
}
|
||||
|
||||
// Handle progress bar (for some states)
|
||||
//
|
||||
if (status == Status.Downloading) {
|
||||
if (entry.progressMax == 0)
|
||||
builder.setProgress(100, 0, true);
|
||||
else
|
||||
builder.setProgress(entry.progressMax, entry.progressCurrent, false);
|
||||
} else if (status == Status.Installing) {
|
||||
builder.setProgress(100, 0, true); // indeterminate bar
|
||||
}
|
||||
|
||||
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED);
|
||||
intentDeleted.putExtra(EXTRA_NOTIFICATION_TAG, entry.getId());
|
||||
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0);
|
||||
builder.setDeleteIntent(piDeleted);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder createUpdateSummaryNotification(ArrayList<AppEntry> updates) {
|
||||
String title = String.format("%d Updates", 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++) {
|
||||
AppEntry entry = updates.get(i);
|
||||
App app = entry.app;
|
||||
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.getString(R.string.update_notification_more, diff));
|
||||
//}
|
||||
|
||||
inboxStyle.setSummaryText(title);
|
||||
|
||||
|
||||
// Intent to open main app list
|
||||
Intent intentObject = new Intent(context, FDroid.class);
|
||||
PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, 0);
|
||||
|
||||
NotificationCompat.Builder builder =
|
||||
new NotificationCompat.Builder(context)
|
||||
.setAutoCancel(true)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setContentIntent(piAction)
|
||||
.setStyle(inboxStyle)
|
||||
.setGroup(GROUP_UPDATES)
|
||||
.setGroupSummary(true);
|
||||
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_UPDATES_CLEARED);
|
||||
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0);
|
||||
builder.setDeleteIntent(piDeleted);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder createInstalledNotification(AppEntry entry) {
|
||||
App app = entry.app;
|
||||
|
||||
int largeIconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
|
||||
Bitmap iconLarge = ImageLoader.getInstance().loadImageSync(app.iconUrl, new ImageSize(largeIconSize, largeIconSize));
|
||||
|
||||
NotificationCompat.Builder builder =
|
||||
new NotificationCompat.Builder(context)
|
||||
.setAutoCancel(true)
|
||||
.setLargeIcon(iconLarge)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_updates)
|
||||
.setContentTitle(app.name)
|
||||
.setContentText("Successfully Installed")
|
||||
.setGroup(GROUP_INSTALLED);
|
||||
|
||||
PackageManager pm = context.getPackageManager();
|
||||
Intent intentObject = pm.getLaunchIntentForPackage(app.packageName);
|
||||
PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, 0);
|
||||
builder.setContentIntent(piAction);
|
||||
|
||||
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED);
|
||||
intentDeleted.putExtra(EXTRA_NOTIFICATION_TAG, entry.getId());
|
||||
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0);
|
||||
builder.setDeleteIntent(piDeleted);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder createInstalledSummaryNotification(ArrayList<AppEntry> installed) {
|
||||
String title = String.format("%d Apps Installed", 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++) {
|
||||
AppEntry 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.getString(R.string.update_notification_more, diff));
|
||||
}
|
||||
|
||||
// Intent to open main app list
|
||||
Intent intentObject = new Intent(context, FDroid.class);
|
||||
PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, 0);
|
||||
|
||||
NotificationCompat.Builder builder =
|
||||
new NotificationCompat.Builder(context)
|
||||
.setAutoCancel(true)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setContentIntent(piAction)
|
||||
.setGroup(GROUP_INSTALLED)
|
||||
.setGroupSummary(true);
|
||||
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED);
|
||||
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0);
|
||||
builder.setDeleteIntent(piDeleted);
|
||||
return builder;
|
||||
}
|
||||
}
|
@ -520,6 +520,19 @@ public class UpdateService extends IntentService {
|
||||
}
|
||||
|
||||
private void showAppUpdatesNotification(Cursor hasUpdates) {
|
||||
|
||||
if (hasUpdates != null) {
|
||||
hasUpdates.moveToFirst();
|
||||
NotificationHelper.startBatchUpdates();
|
||||
for (int i = 0; i < Math.min(MAX_UPDATES_TO_SHOW, hasUpdates.getCount()); i++) {
|
||||
App app = new App(hasUpdates);
|
||||
hasUpdates.moveToNext();
|
||||
Apk apk = ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode);
|
||||
NotificationHelper.setApk(apk, NotificationHelper.Status.UpdateAvailable, null);
|
||||
}
|
||||
NotificationHelper.endBatchUpdates();
|
||||
}
|
||||
|
||||
Utils.debugLog(TAG, "Notifying " + hasUpdates.getCount() + " updates.");
|
||||
|
||||
final int icon = Build.VERSION.SDK_INT >= 11 ? R.drawable.ic_stat_notify_updates : R.drawable.ic_launcher;
|
||||
|
@ -22,6 +22,7 @@ import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.filefilter.WildcardFileFilter;
|
||||
import org.fdroid.fdroid.AppDetails;
|
||||
import org.fdroid.fdroid.Hasher;
|
||||
import org.fdroid.fdroid.NotificationHelper;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.compat.PackageManagerCompat;
|
||||
@ -98,7 +99,6 @@ public class InstallManagerService extends Service {
|
||||
private static final HashMap<String, App> ACTIVE_APPS = new HashMap<>(3);
|
||||
|
||||
private LocalBroadcastManager localBroadcastManager;
|
||||
private NotificationManager notificationManager;
|
||||
|
||||
/**
|
||||
* This service does not use binding, so no need to implement this method
|
||||
@ -113,7 +113,6 @@ public class InstallManagerService extends Service {
|
||||
super.onCreate();
|
||||
Utils.debugLog(TAG, "creating Service");
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(this);
|
||||
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
|
||||
BroadcastReceiver br = new BroadcastReceiver() {
|
||||
@Override
|
||||
@ -122,7 +121,7 @@ public class InstallManagerService extends Service {
|
||||
for (Map.Entry<String, Apk> entry : ACTIVE_APKS.entrySet()) {
|
||||
if (TextUtils.equals(packageName, entry.getValue().packageName)) {
|
||||
String urlString = entry.getKey();
|
||||
cancelNotification(urlString);
|
||||
NotificationHelper.removeApk(getApkFromActive(urlString));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -150,7 +149,7 @@ public class InstallManagerService extends Service {
|
||||
Apk apk = getApkFromActive(urlString);
|
||||
DownloaderService.cancel(this, apk.getPatchObbUrl());
|
||||
DownloaderService.cancel(this, apk.getMainObbUrl());
|
||||
cancelNotification(urlString);
|
||||
NotificationHelper.removeApk(apk);
|
||||
return START_NOT_STICKY;
|
||||
} else if (!ACTION_INSTALL.equals(action)) {
|
||||
Utils.debugLog(TAG, "Ignoring " + intent + " as it is not an " + ACTION_INSTALL + " intent");
|
||||
@ -166,7 +165,7 @@ public class InstallManagerService extends Service {
|
||||
&& !DownloaderService.isQueuedOrActive(urlString)) {
|
||||
// TODO is there a case where we should allow an active urlString to pass through?
|
||||
Utils.debugLog(TAG, urlString + " finished downloading while InstallManagerService was killed.");
|
||||
cancelNotification(urlString);
|
||||
NotificationHelper.removeApk(getApkFromActive(urlString));
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@ -178,12 +177,9 @@ public class InstallManagerService extends Service {
|
||||
}
|
||||
addToActive(urlString, app, apk);
|
||||
|
||||
NotificationCompat.Builder builder = createNotificationBuilder(urlString, apk);
|
||||
notificationManager.notify(urlString.hashCode(), builder.build());
|
||||
|
||||
registerApkDownloaderReceivers(urlString, builder);
|
||||
getObb(urlString, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256, builder);
|
||||
getObb(urlString, apk.getPatchObbUrl(), apk.getPatchObbFile(), apk.obbPatchFileSha256, builder);
|
||||
registerApkDownloaderReceivers(urlString);
|
||||
getObb(urlString, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256);
|
||||
getObb(urlString, apk.getPatchObbUrl(), apk.getPatchObbFile(), apk.obbPatchFileSha256);
|
||||
|
||||
File apkFilePath = ApkCache.getApkDownloadPath(this, intent.getData());
|
||||
long apkFileSize = apkFilePath.length();
|
||||
@ -217,8 +213,7 @@ public class InstallManagerService extends Service {
|
||||
* @see <a href="https://developer.android.com/google/play/expansion-files.html">APK Expansion Files</a>
|
||||
*/
|
||||
private void getObb(final String urlString, String obbUrlString,
|
||||
final File obbDestFile, final String sha256,
|
||||
final NotificationCompat.Builder builder) {
|
||||
final File obbDestFile, final String sha256) {
|
||||
if (obbDestFile == null || obbDestFile.exists() || TextUtils.isEmpty(obbUrlString)) {
|
||||
return;
|
||||
}
|
||||
@ -232,8 +227,7 @@ public class InstallManagerService extends Service {
|
||||
|
||||
int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0);
|
||||
int totalBytes = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 0);
|
||||
builder.setProgress(totalBytes, bytesRead, false);
|
||||
notificationManager.notify(urlString.hashCode(), builder.build());
|
||||
// TODO - handle obb notifications?
|
||||
} else if (Downloader.ACTION_COMPLETE.equals(action)) {
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
|
||||
@ -274,7 +268,7 @@ public class InstallManagerService extends Service {
|
||||
DownloaderService.getIntentFilter(obbUrlString));
|
||||
}
|
||||
|
||||
private void registerApkDownloaderReceivers(String urlString, final NotificationCompat.Builder builder) {
|
||||
private void registerApkDownloaderReceivers(String urlString) {
|
||||
|
||||
BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
@ -284,13 +278,17 @@ public class InstallManagerService extends Service {
|
||||
|
||||
switch (intent.getAction()) {
|
||||
case Downloader.ACTION_STARTED:
|
||||
Intent intentObject = new Intent(context, InstallManagerService.class);
|
||||
intentObject.setAction(ACTION_CANCEL);
|
||||
intentObject.setData(downloadUri);
|
||||
PendingIntent action = PendingIntent.getService(context, 0, intentObject, 0);
|
||||
NotificationHelper.setApk(getApkFromActive(urlString), NotificationHelper.Status.Downloading, action);
|
||||
// nothing to do
|
||||
break;
|
||||
case Downloader.ACTION_PROGRESS:
|
||||
int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0);
|
||||
int totalBytes = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 0);
|
||||
builder.setProgress(totalBytes, bytesRead, false);
|
||||
notificationManager.notify(urlString.hashCode(), builder.build());
|
||||
NotificationHelper.setApkProgress(getApkFromActive(urlString), totalBytes, bytesRead);
|
||||
break;
|
||||
case Downloader.ACTION_COMPLETE:
|
||||
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
|
||||
@ -298,17 +296,19 @@ public class InstallManagerService extends Service {
|
||||
|
||||
Utils.debugLog(TAG, "download completed of " + urlString + " to " + localApkUri);
|
||||
|
||||
NotificationHelper.setApk(getApkFromActive(urlString), NotificationHelper.Status.ReadyToInstall, null);
|
||||
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
registerInstallerReceivers(downloadUri);
|
||||
|
||||
Apk apk = ACTIVE_APKS.get(urlString);
|
||||
|
||||
InstallerService.install(context, localApkUri, downloadUri, apk);
|
||||
break;
|
||||
case Downloader.ACTION_INTERRUPTED:
|
||||
NotificationHelper.removeApk(getApkFromActive(urlString));
|
||||
|
||||
removeFromActive(urlString);
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
cancelNotification(urlString);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("intent action not handled!");
|
||||
@ -329,9 +329,10 @@ public class InstallManagerService extends Service {
|
||||
Apk apk;
|
||||
switch (intent.getAction()) {
|
||||
case Installer.ACTION_INSTALL_STARTED:
|
||||
// nothing to do
|
||||
NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.Installing, null);
|
||||
break;
|
||||
case Installer.ACTION_INSTALL_COMPLETE:
|
||||
NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.Installed, null);
|
||||
Apk apkComplete = removeFromActive(downloadUrl);
|
||||
|
||||
PackageManagerCompat.setInstaller(getPackageManager(), apkComplete.packageName);
|
||||
@ -339,6 +340,8 @@ public class InstallManagerService extends Service {
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
break;
|
||||
case Installer.ACTION_INSTALL_INTERRUPTED:
|
||||
NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.ReadyToInstall, null);
|
||||
|
||||
apk = intent.getParcelableExtra(Installer.EXTRA_APK);
|
||||
String errorMessage =
|
||||
intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
|
||||
@ -350,12 +353,7 @@ public class InstallManagerService extends Service {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo);
|
||||
}
|
||||
// show notification if app details is not visible
|
||||
if (app != null && AppDetails.isAppVisible(app.packageName)) {
|
||||
cancelNotification(downloadUrl);
|
||||
} else {
|
||||
notifyError(downloadUrl, app, errorMessage);
|
||||
}
|
||||
// TODO - show error
|
||||
}
|
||||
removeFromActive(downloadUrl);
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
@ -364,14 +362,7 @@ public class InstallManagerService extends Service {
|
||||
apk = intent.getParcelableExtra(Installer.EXTRA_APK);
|
||||
PendingIntent installPendingIntent =
|
||||
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
|
||||
|
||||
// show notification if app details is not visible
|
||||
if (AppDetails.isAppVisible(apk.packageName)) {
|
||||
cancelNotification(downloadUrl);
|
||||
} else {
|
||||
notifyDownloadComplete(apk, downloadUrl, installPendingIntent);
|
||||
}
|
||||
|
||||
NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.ReadyToInstall, installPendingIntent);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("intent action not handled!");
|
||||
@ -383,81 +374,10 @@ public class InstallManagerService extends Service {
|
||||
Installer.getInstallIntentFilter(downloadUri));
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder createNotificationBuilder(String urlString, Apk apk) {
|
||||
int downloadUrlId = urlString.hashCode();
|
||||
return new NotificationCompat.Builder(this)
|
||||
.setAutoCancel(false)
|
||||
.setOngoing(true)
|
||||
.setContentIntent(getAppDetailsIntent(downloadUrlId, apk))
|
||||
.setContentTitle(getString(R.string.downloading_apk, getAppName(apk)))
|
||||
.addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel),
|
||||
getCancelPendingIntent(urlString))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setContentText(urlString)
|
||||
.setProgress(100, 0, true);
|
||||
}
|
||||
|
||||
private String getAppName(Apk apk) {
|
||||
return ACTIVE_APPS.get(apk.packageName).name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link PendingIntent} for a {@link Notification} to send when it
|
||||
* is clicked. {@link AppDetails} handles {@code Intent}s that are missing
|
||||
* or bad {@link AppDetails#EXTRA_APPID}, so it does not need to be checked
|
||||
* here.
|
||||
*/
|
||||
private PendingIntent getAppDetailsIntent(int requestCode, Apk apk) {
|
||||
Intent notifyIntent = new Intent(getApplicationContext(), AppDetails.class)
|
||||
.putExtra(AppDetails.EXTRA_APPID, apk.packageName);
|
||||
return TaskStackBuilder.create(getApplicationContext())
|
||||
.addParentStack(AppDetails.class)
|
||||
.addNextIntent(notifyIntent)
|
||||
.getPendingIntent(requestCode, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post a notification about a completed download. {@code packageName} must be a valid
|
||||
* and currently in the app index database. This must create a new {@code Builder}
|
||||
* instance otherwise the progress/cancel stuff does not go away.
|
||||
*
|
||||
* @see <a href=https://code.google.com/p/android/issues/detail?id=47809> Issue 47809:
|
||||
* Removing the progress bar from a notification should cause the notification's content
|
||||
* text to return to normal size</a>
|
||||
*/
|
||||
private void notifyDownloadComplete(Apk apk, String urlString, PendingIntent installPendingIntent) {
|
||||
String title;
|
||||
try {
|
||||
PackageManager pm = getPackageManager();
|
||||
title = String.format(getString(R.string.tap_to_update_format),
|
||||
pm.getApplicationLabel(pm.getApplicationInfo(apk.packageName, 0)));
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
String name = getAppName(apk);
|
||||
if (TextUtils.isEmpty(name) || name.equals(new App().name)) {
|
||||
ContentResolver resolver = getContentResolver();
|
||||
App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo,
|
||||
new String[]{Schema.AppMetadataTable.Cols.NAME});
|
||||
if (app == null || TextUtils.isEmpty(app.name)) {
|
||||
return; // do not have a name to display, so leave notification as is
|
||||
}
|
||||
name = app.name;
|
||||
}
|
||||
title = String.format(getString(R.string.tap_to_install_format), name);
|
||||
}
|
||||
|
||||
int downloadUrlId = urlString.hashCode();
|
||||
notificationManager.cancel(downloadUrlId);
|
||||
Notification notification = new NotificationCompat.Builder(this)
|
||||
.setAutoCancel(true)
|
||||
.setOngoing(false)
|
||||
.setContentTitle(title)
|
||||
.setContentIntent(installPendingIntent)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setContentText(getString(R.string.tap_to_install))
|
||||
.build();
|
||||
notificationManager.notify(downloadUrlId, notification);
|
||||
}
|
||||
|
||||
private void notifyError(String urlString, App app, String text) {
|
||||
int downloadUrlId = urlString.hashCode();
|
||||
|
||||
@ -493,15 +413,6 @@ public class InstallManagerService extends Service {
|
||||
nm.notify(downloadUrlId, builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the {@link Notification} tied to {@code urlString}, which is the
|
||||
* unique ID used to represent a given APK file. {@link String#hashCode()}
|
||||
* converts {@code urlString} to the required {@code int}.
|
||||
*/
|
||||
private void cancelNotification(String urlString) {
|
||||
notificationManager.cancel(urlString.hashCode());
|
||||
}
|
||||
|
||||
private static void addToActive(String urlString, App app, Apk apk) {
|
||||
ACTIVE_APKS.put(urlString, apk);
|
||||
ACTIVE_APPS.put(app.packageName, app);
|
||||
|
4
app/src/main/res/drawable/ic_notify_cancel_24dp.xml
Normal file
4
app/src/main/res/drawable/ic_notify_cancel_24dp.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="32dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="32dp">
|
||||
<path android:fillColor="#30000000" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
6
app/src/main/res/drawable/ic_notify_install_24dp.xml
Normal file
6
app/src/main/res/drawable/ic_notify_install_24dp.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="32dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="32dp">
|
||||
<group android:pivotX="12" android:pivotY="12" android:rotation="-90">
|
||||
<path android:fillColor="#30000000" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
|
||||
</group>
|
||||
</vector>
|
4
app/src/main/res/drawable/ic_notify_update_24dp.xml
Normal file
4
app/src/main/res/drawable/ic_notify_update_24dp.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="32dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="32dp">
|
||||
<path android:fillColor="#30000000" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
|
||||
</vector>
|
Loading…
x
Reference in New Issue
Block a user