Move app status handling to new AppUpdateStatusManager
This commit is contained in:
parent
10cbf1ef7a
commit
55c7a21c90
310
app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java
Normal file
310
app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java
Normal file
@ -0,0 +1,310 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.v4.app.TaskStackBuilder;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.installer.ErrorDialogActivity;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
public class AppUpdateStatusManager {
|
||||
|
||||
static final String BROADCAST_APPSTATUS_LIST_CHANGED = "org.fdroid.fdroid.installer.appstatus.listchange";
|
||||
static final String BROADCAST_APPSTATUS_ADDED = "org.fdroid.fdroid.installer.appstatus.appchange.add";
|
||||
static final String BROADCAST_APPSTATUS_CHANGED = "org.fdroid.fdroid.installer.appstatus.appchange.change";
|
||||
static final String BROADCAST_APPSTATUS_REMOVED = "org.fdroid.fdroid.installer.appstatus.appchange.remove";
|
||||
static final String EXTRA_APK_URL = "urlstring";
|
||||
static final String EXTRA_IS_STATUS_UPDATE = "isstatusupdate";
|
||||
|
||||
private static final String LOGTAG = "AppUpdateStatusManager";
|
||||
|
||||
public enum Status {
|
||||
Unknown,
|
||||
UpdateAvailable,
|
||||
Downloading,
|
||||
ReadyToInstall,
|
||||
Installing,
|
||||
Installed,
|
||||
InstallError
|
||||
}
|
||||
|
||||
public static AppUpdateStatusManager getInstance(Context context) {
|
||||
return new AppUpdateStatusManager(context);
|
||||
}
|
||||
|
||||
class AppUpdateStatus {
|
||||
final App app;
|
||||
final Apk apk;
|
||||
Status status;
|
||||
PendingIntent intent;
|
||||
int progressCurrent;
|
||||
int progressMax;
|
||||
String errorText;
|
||||
|
||||
AppUpdateStatus(App app, Apk apk, Status status, PendingIntent intent) {
|
||||
this.app = app;
|
||||
this.apk = apk;
|
||||
this.status = status;
|
||||
this.intent = intent;
|
||||
}
|
||||
|
||||
String getUniqueKey() {
|
||||
return apk.getUrl();
|
||||
}
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private final LocalBroadcastManager localBroadcastManager;
|
||||
private static final HashMap<String, AppUpdateStatus> appMapping = new HashMap<>();
|
||||
private boolean isBatchUpdating;
|
||||
|
||||
private AppUpdateStatusManager(Context context) {
|
||||
this.context = context;
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(context.getApplicationContext());
|
||||
}
|
||||
|
||||
public AppUpdateStatus get(String key) {
|
||||
synchronized (appMapping) {
|
||||
return appMapping.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
public Collection<AppUpdateStatus> getAll() {
|
||||
synchronized (appMapping) {
|
||||
return appMapping.values();
|
||||
}
|
||||
}
|
||||
|
||||
private void setApkInternal(Apk apk, Status status, PendingIntent intent) {
|
||||
if (apk == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (appMapping) {
|
||||
AppUpdateStatus entry = appMapping.get(apk.getUrl());
|
||||
if (status == null) {
|
||||
// Remove
|
||||
Utils.debugLog(LOGTAG, "Remove APK " + apk.apkName);
|
||||
if (entry != null) {
|
||||
appMapping.remove(apk.getUrl());
|
||||
notifyRemove(entry);
|
||||
}
|
||||
} else if (entry != null) {
|
||||
// Update
|
||||
Utils.debugLog(LOGTAG, "Update APK " + apk.apkName + " state to " + status.name());
|
||||
boolean isStatusUpdate = (entry.status != status);
|
||||
entry.status = status;
|
||||
entry.intent = intent;
|
||||
// If intent not set, see if we need to create a default intent
|
||||
if (entry.intent == null) {
|
||||
entry.intent = getContentIntent(entry);
|
||||
}
|
||||
notifyChange(entry, isStatusUpdate);
|
||||
} else {
|
||||
// Add
|
||||
Utils.debugLog(LOGTAG, "Add APK " + apk.apkName + " with state " + status.name());
|
||||
entry = createAppEntry(apk, status, intent);
|
||||
// If intent not set, see if we need to create a default intent
|
||||
if (entry.intent == null) {
|
||||
entry.intent = getContentIntent(entry);
|
||||
}
|
||||
appMapping.put(entry.getUniqueKey(), entry);
|
||||
notifyAdd(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyChange() {
|
||||
if (!isBatchUpdating) {
|
||||
localBroadcastManager.sendBroadcast(new Intent(BROADCAST_APPSTATUS_LIST_CHANGED));
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyAdd(AppUpdateStatus entry) {
|
||||
if (!isBatchUpdating) {
|
||||
Intent broadcastIntent = new Intent(BROADCAST_APPSTATUS_ADDED);
|
||||
broadcastIntent.putExtra(EXTRA_APK_URL, entry.getUniqueKey());
|
||||
localBroadcastManager.sendBroadcast(broadcastIntent);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyChange(AppUpdateStatus entry, boolean isStatusUpdate) {
|
||||
if (!isBatchUpdating) {
|
||||
Intent broadcastIntent = new Intent(BROADCAST_APPSTATUS_CHANGED);
|
||||
broadcastIntent.putExtra(EXTRA_APK_URL, entry.getUniqueKey());
|
||||
broadcastIntent.putExtra(EXTRA_IS_STATUS_UPDATE, isStatusUpdate);
|
||||
localBroadcastManager.sendBroadcast(broadcastIntent);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyRemove(AppUpdateStatus entry) {
|
||||
if (!isBatchUpdating) {
|
||||
Intent broadcastIntent = new Intent(BROADCAST_APPSTATUS_REMOVED);
|
||||
broadcastIntent.putExtra(EXTRA_APK_URL, entry.getUniqueKey());
|
||||
localBroadcastManager.sendBroadcast(broadcastIntent);
|
||||
}
|
||||
}
|
||||
|
||||
private AppUpdateStatus createAppEntry(Apk apk, Status status, PendingIntent intent) {
|
||||
synchronized (appMapping) {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo);
|
||||
AppUpdateStatus ret = new AppUpdateStatus(app, apk, status, intent);
|
||||
appMapping.put(apk.getUrl(), ret);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an Apk to the AppUpdateStatusManager 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 void addApk(Apk apk, Status status, PendingIntent pendingIntent) {
|
||||
setApkInternal(apk, status, pendingIntent);
|
||||
}
|
||||
|
||||
public void updateApk(String key, Status status, PendingIntent pendingIntent) {
|
||||
synchronized (appMapping) {
|
||||
AppUpdateStatus entry = appMapping.get(key);
|
||||
if (entry != null) {
|
||||
setApkInternal(entry.apk, status, pendingIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Apk getApk(String key) {
|
||||
synchronized (appMapping) {
|
||||
AppUpdateStatus entry = appMapping.get(key);
|
||||
if (entry != null) {
|
||||
return entry.apk;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void removeApk(String key) {
|
||||
synchronized (appMapping) {
|
||||
AppUpdateStatus entry = appMapping.get(key);
|
||||
if (entry != null) {
|
||||
setApkInternal(entry.apk, null, null); // remove
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateApkProgress(String key, int max, int current) {
|
||||
synchronized (appMapping) {
|
||||
AppUpdateStatus entry = appMapping.get(key);
|
||||
if (entry != null) {
|
||||
entry.progressMax = max;
|
||||
entry.progressCurrent = current;
|
||||
notifyChange(entry, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setApkError(Apk apk, String errorText) {
|
||||
synchronized (appMapping) {
|
||||
AppUpdateStatus entry = appMapping.get(apk.getUrl());
|
||||
if (entry == null) {
|
||||
entry = createAppEntry(apk, Status.InstallError, null);
|
||||
}
|
||||
entry.status = Status.InstallError;
|
||||
entry.errorText = errorText;
|
||||
entry.intent = getAppErrorIntent(entry);
|
||||
notifyChange(entry, false);
|
||||
}
|
||||
}
|
||||
|
||||
void startBatchUpdates() {
|
||||
synchronized (appMapping) {
|
||||
isBatchUpdating = true;
|
||||
}
|
||||
}
|
||||
|
||||
void endBatchUpdates() {
|
||||
synchronized (appMapping) {
|
||||
isBatchUpdating = false;
|
||||
notifyChange();
|
||||
}
|
||||
}
|
||||
|
||||
void clearAllUpdates() {
|
||||
synchronized (appMapping) {
|
||||
for (Iterator<Map.Entry<String, AppUpdateStatus>> it = appMapping.entrySet().iterator(); it.hasNext(); ) {
|
||||
Map.Entry<String, AppUpdateStatus> entry = it.next();
|
||||
if (entry.getValue().status != Status.Installed) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
notifyChange();
|
||||
}
|
||||
}
|
||||
|
||||
void clearAllInstalled() {
|
||||
synchronized (appMapping) {
|
||||
for (Iterator<Map.Entry<String, AppUpdateStatus>> it = appMapping.entrySet().iterator(); it.hasNext(); ) {
|
||||
Map.Entry<String, AppUpdateStatus> entry = it.next();
|
||||
if (entry.getValue().status == Status.Installed) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
notifyChange();
|
||||
}
|
||||
}
|
||||
|
||||
private PendingIntent getContentIntent(AppUpdateStatus 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"
|
||||
return getAppDetailsIntent(entry.apk);
|
||||
} else if (entry.status == Status.ReadyToInstall) {
|
||||
return getAppDetailsIntent(entry.apk);
|
||||
} else if (entry.status == Status.InstallError) {
|
||||
return getAppErrorIntent(entry);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(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(0, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private PendingIntent getAppErrorIntent(AppUpdateStatus entry) {
|
||||
String title = String.format(context.getString(R.string.install_error_notify_title), entry.app.name);
|
||||
|
||||
Intent errorDialogIntent = new Intent(context, ErrorDialogActivity.class);
|
||||
errorDialogIntent.putExtra(
|
||||
ErrorDialogActivity.EXTRA_TITLE, title);
|
||||
errorDialogIntent.putExtra(
|
||||
ErrorDialogActivity.EXTRA_MESSAGE, entry.errorText);
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
errorDialogIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
}
|
@ -220,8 +220,6 @@ 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,
|
||||
@ -264,6 +262,7 @@ public class FDroidApp extends Application {
|
||||
|
||||
CleanCacheService.schedule(this);
|
||||
|
||||
NotificationHelper.create(getApplicationContext());
|
||||
UpdateService.schedule(getApplicationContext());
|
||||
bluetoothAdapter = getBluetoothAdapter();
|
||||
|
||||
|
@ -1,20 +1,17 @@
|
||||
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.os.Build;
|
||||
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.support.v4.content.LocalBroadcastManager;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.StyleSpan;
|
||||
@ -22,278 +19,143 @@ 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 {
|
||||
class NotificationHelper {
|
||||
|
||||
private static final String BROADCAST_NOTIFICATIONS_UPDATES_CLEARED = "org.fdroid.fdroid.installer.notifications.updates.cleared";
|
||||
private static final String BROADCAST_NOTIFICATIONS_ALL_UPDATES_CLEARED = "org.fdroid.fdroid.installer.notifications.allupdates.cleared";
|
||||
private static final String BROADCAST_NOTIFICATIONS_ALL_INSTALLED_CLEARED = "org.fdroid.fdroid.installer.notifications.allinstalled.cleared";
|
||||
private static final String BROADCAST_NOTIFICATIONS_UPDATE_CLEARED = "org.fdroid.fdroid.installer.notifications.update.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 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 EXTRA_NOTIFICATION_TAG = "tag";
|
||||
private static final String EXTRA_NOTIFICATION_KEY = "key";
|
||||
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 final String LOGTAG = "NotificationHelper";
|
||||
|
||||
private static NotificationHelper instance;
|
||||
|
||||
public static void init(Context context) {
|
||||
instance = new NotificationHelper(context);
|
||||
}
|
||||
|
||||
private static NotificationHelper getInstance() {
|
||||
public static NotificationHelper create(Context context) {
|
||||
if (instance == null) {
|
||||
instance = new NotificationHelper(context.getApplicationContext());
|
||||
}
|
||||
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 final AppUpdateStatusManager appUpdateStatusMananger;
|
||||
|
||||
private NotificationHelper(Context context) {
|
||||
this.context = context;
|
||||
appUpdateStatusMananger = AppUpdateStatusManager.getInstance(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_ALL_UPDATES_CLEARED);
|
||||
filter.addAction(BROADCAST_NOTIFICATIONS_ALL_INSTALLED_CLEARED);
|
||||
filter.addAction(BROADCAST_NOTIFICATIONS_UPDATE_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<>();
|
||||
context.registerReceiver(receiverNotificationsCleared, filter);
|
||||
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);
|
||||
LocalBroadcastManager.getInstance(context).registerReceiver(receiverAppStatusChanges, filter);
|
||||
}
|
||||
|
||||
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 boolean useStackedNotifications() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
if (!notificationManager.areNotificationsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationCompat.Builder builder;
|
||||
if (updates.size() == 0) {
|
||||
// No updates, remove summary
|
||||
notificationManager.cancel(GROUP_UPDATES, NOTIFY_ID_UPDATES);
|
||||
// Get the list of updates and installed available
|
||||
ArrayList<AppUpdateStatusManager.AppUpdateStatus> updates = new ArrayList<>();
|
||||
ArrayList<AppUpdateStatusManager.AppUpdateStatus> installed = new ArrayList<>();
|
||||
for (AppUpdateStatusManager.AppUpdateStatus entry : appUpdateStatusMananger.getAll()) {
|
||||
if (entry.status == AppUpdateStatusManager.Status.Unknown) {
|
||||
continue;
|
||||
} else if (entry.status != AppUpdateStatusManager.Status.Installed) {
|
||||
updates.add(entry);
|
||||
} 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());
|
||||
installed.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
NotificationCompat.Builder builder;
|
||||
if (updates.size() == 0) {
|
||||
// No updates, remove summary
|
||||
notificationManager.cancel(GROUP_UPDATES, NOTIFY_ID_UPDATES);
|
||||
} else if (updates.size() == 1 && !useStackedNotifications()) {
|
||||
// If we use stacked notifications we have already created one.
|
||||
doCreateNotification(updates.get(0));
|
||||
} 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 if (installed.size() == 1 && !useStackedNotifications()) {
|
||||
// If we use stacked notifications we have already created one.
|
||||
doCreateNotification(installed.get(0));
|
||||
} else {
|
||||
builder = createInstalledSummaryNotification(installed);
|
||||
notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, builder.build());
|
||||
}
|
||||
}
|
||||
|
||||
private void createNotificationForAppEntry(AppEntry entry) {
|
||||
private void createNotification(AppUpdateStatusManager.AppUpdateStatus entry) {
|
||||
if (useStackedNotifications() && notificationManager.areNotificationsEnabled() && entry.status != AppUpdateStatusManager.Status.Unknown) {
|
||||
doCreateNotification(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void doCreateNotification(AppUpdateStatusManager.AppUpdateStatus entry) {
|
||||
NotificationCompat.Builder builder;
|
||||
if (entry.status == Status.Installed) {
|
||||
int id;
|
||||
if (entry.status == AppUpdateStatusManager.Status.Installed) {
|
||||
builder = createInstalledNotification(entry);
|
||||
id = NOTIFY_ID_INSTALLED;
|
||||
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES);
|
||||
} else {
|
||||
builder = createUpdateNotification(entry);
|
||||
id = NOTIFY_ID_UPDATES;
|
||||
notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED);
|
||||
}
|
||||
notificationManager.notify(entry.getTag(), entry.getId(), builder.build());
|
||||
notificationManager.notify(entry.getUniqueKey(), id, 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);
|
||||
private NotificationCompat.Action getAction(AppUpdateStatusManager.AppUpdateStatus entry) {
|
||||
if (entry.intent != null) {
|
||||
if (entry.status == AppUpdateStatusManager.Status.UpdateAvailable) {
|
||||
return new NotificationCompat.Action(R.drawable.ic_notify_update_24dp, "Update", entry.intent);
|
||||
} else if (entry.status == AppUpdateStatusManager.Status.Downloading || entry.status == AppUpdateStatusManager.Status.Installing) {
|
||||
return new NotificationCompat.Action(R.drawable.ic_notify_cancel_24dp, "Cancel", entry.intent);
|
||||
} else if (entry.status == AppUpdateStatusManager.Status.ReadyToInstall) {
|
||||
return new NotificationCompat.Action(R.drawable.ic_notify_install_24dp, "Install", entry.intent);
|
||||
}
|
||||
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) {
|
||||
private String getSingleItemTitleString(App app, AppUpdateStatusManager.Status status) {
|
||||
switch (status) {
|
||||
case UpdateAvailable:
|
||||
return "Update Available";
|
||||
@ -305,11 +167,13 @@ public class NotificationHelper {
|
||||
return app.name;
|
||||
case Installed:
|
||||
return app.name;
|
||||
case InstallError:
|
||||
return "Install Failed";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String getSingleItemContentString(App app, Status status) {
|
||||
private String getSingleItemContentString(App app, AppUpdateStatusManager.Status status) {
|
||||
switch (status) {
|
||||
case UpdateAvailable:
|
||||
return app.name;
|
||||
@ -321,11 +185,13 @@ public class NotificationHelper {
|
||||
return String.format("Installing \"%s\"...", app.name);
|
||||
case Installed:
|
||||
return "Successfully installed";
|
||||
case InstallError:
|
||||
return "Install Failed";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String getMultiItemContentString(App app, Status status) {
|
||||
private String getMultiItemContentString(App app, AppUpdateStatusManager.Status status) {
|
||||
switch (status) {
|
||||
case UpdateAvailable:
|
||||
return "Update available";
|
||||
@ -337,72 +203,64 @@ public class NotificationHelper {
|
||||
return "Installing";
|
||||
case Installed:
|
||||
return "Successfully installed";
|
||||
case InstallError:
|
||||
return "Install Failed";
|
||||
}
|
||||
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) {
|
||||
private NotificationCompat.Builder createUpdateNotification(AppUpdateStatusManager.AppUpdateStatus entry) {
|
||||
App app = entry.app;
|
||||
Status status = entry.status;
|
||||
AppUpdateStatusManager.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));
|
||||
|
||||
// TODO - why?
|
||||
final int icon = Build.VERSION.SDK_INT >= 11 ? R.drawable.ic_stat_notify_updates : R.drawable.ic_launcher;
|
||||
|
||||
NotificationCompat.Builder builder =
|
||||
new NotificationCompat.Builder(context)
|
||||
.setAutoCancel(true)
|
||||
.setAutoCancel(false)
|
||||
.setLargeIcon(iconLarge)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_updates)
|
||||
.setSmallIcon(icon)
|
||||
.setContentTitle(getSingleItemTitleString(app, status))
|
||||
.setContentText(getSingleItemContentString(app, status))
|
||||
.setGroup(GROUP_UPDATES);
|
||||
|
||||
// Handle intents
|
||||
//
|
||||
if (entry.intent != null) {
|
||||
builder.setContentIntent(entry.intent);
|
||||
}
|
||||
|
||||
// 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 (status == AppUpdateStatusManager.Status.Downloading) {
|
||||
if (entry.progressMax == 0)
|
||||
builder.setProgress(100, 0, true);
|
||||
else
|
||||
builder.setProgress(entry.progressMax, entry.progressCurrent, false);
|
||||
} else if (status == Status.Installing) {
|
||||
} else if (status == AppUpdateStatusManager.Status.Installing) {
|
||||
builder.setProgress(100, 0, true); // indeterminate bar
|
||||
}
|
||||
|
||||
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED);
|
||||
intentDeleted.putExtra(EXTRA_NOTIFICATION_TAG, entry.getId());
|
||||
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_UPDATE_CLEARED);
|
||||
intentDeleted.putExtra(EXTRA_NOTIFICATION_KEY, entry.getUniqueKey());
|
||||
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0);
|
||||
builder.setDeleteIntent(piDeleted);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder createUpdateSummaryNotification(ArrayList<AppEntry> updates) {
|
||||
private NotificationCompat.Builder createUpdateSummaryNotification(ArrayList<AppUpdateStatusManager.AppUpdateStatus> updates) {
|
||||
String title = String.format("%d Updates", updates.size());
|
||||
StringBuilder text = new StringBuilder();
|
||||
|
||||
@ -410,9 +268,9 @@ public class NotificationHelper {
|
||||
inboxStyle.setBigContentTitle(title);
|
||||
|
||||
for (int i = 0; i < MAX_UPDATES_TO_SHOW && i < updates.size(); i++) {
|
||||
AppEntry entry = updates.get(i);
|
||||
AppUpdateStatusManager.AppUpdateStatus entry = updates.get(i);
|
||||
App app = entry.app;
|
||||
Status status = entry.status;
|
||||
AppUpdateStatusManager.Status status = entry.status;
|
||||
|
||||
String content = getMultiItemContentString(app, status);
|
||||
SpannableStringBuilder sb = new SpannableStringBuilder(app.name);
|
||||
@ -425,14 +283,10 @@ public class NotificationHelper {
|
||||
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);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
// Intent to open main app list
|
||||
Intent intentObject = new Intent(context, FDroid.class);
|
||||
@ -445,16 +299,21 @@ public class NotificationHelper {
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setContentIntent(piAction)
|
||||
.setStyle(inboxStyle)
|
||||
.setGroup(GROUP_UPDATES)
|
||||
.setGroupSummary(true);
|
||||
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_UPDATES_CLEARED);
|
||||
.setStyle(inboxStyle);
|
||||
if (BuildConfig.DEBUG) {
|
||||
builder.setPriority(NotificationCompat.PRIORITY_LOW); // To make not at top of list!
|
||||
}
|
||||
if (useStackedNotifications()) {
|
||||
builder.setGroup(GROUP_UPDATES)
|
||||
.setGroupSummary(true);
|
||||
}
|
||||
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_ALL_UPDATES_CLEARED);
|
||||
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0);
|
||||
builder.setDeleteIntent(piDeleted);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder createInstalledNotification(AppEntry entry) {
|
||||
private NotificationCompat.Builder createInstalledNotification(AppUpdateStatusManager.AppUpdateStatus entry) {
|
||||
App app = entry.app;
|
||||
|
||||
int largeIconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
|
||||
@ -474,14 +333,14 @@ public class NotificationHelper {
|
||||
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());
|
||||
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED);
|
||||
intentDeleted.putExtra(EXTRA_NOTIFICATION_KEY, entry.getUniqueKey());
|
||||
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0);
|
||||
builder.setDeleteIntent(piDeleted);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder createInstalledSummaryNotification(ArrayList<AppEntry> installed) {
|
||||
private NotificationCompat.Builder createInstalledSummaryNotification(ArrayList<AppUpdateStatusManager.AppUpdateStatus> installed) {
|
||||
String title = String.format("%d Apps Installed", installed.size());
|
||||
StringBuilder text = new StringBuilder();
|
||||
|
||||
@ -489,7 +348,7 @@ public class NotificationHelper {
|
||||
bigTextStyle.setBigContentTitle(title);
|
||||
|
||||
for (int i = 0; i < MAX_INSTALLED_TO_SHOW && i < installed.size(); i++) {
|
||||
AppEntry entry = installed.get(i);
|
||||
AppUpdateStatusManager.AppUpdateStatus entry = installed.get(i);
|
||||
App app = entry.app;
|
||||
if (text.length() > 0)
|
||||
text.append(", ");
|
||||
@ -511,12 +370,76 @@ public class NotificationHelper {
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setContentIntent(piAction)
|
||||
.setGroup(GROUP_INSTALLED)
|
||||
.setGroupSummary(true);
|
||||
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED);
|
||||
.setContentIntent(piAction);
|
||||
if (useStackedNotifications()) {
|
||||
builder.setGroup(GROUP_INSTALLED)
|
||||
.setGroupSummary(true);
|
||||
}
|
||||
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_ALL_INSTALLED_CLEARED);
|
||||
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0);
|
||||
builder.setDeleteIntent(piDeleted);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private BroadcastReceiver receiverNotificationsCleared = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
switch (intent.getAction()) {
|
||||
case BROADCAST_NOTIFICATIONS_ALL_UPDATES_CLEARED:
|
||||
appUpdateStatusMananger.clearAllUpdates();
|
||||
break;
|
||||
case BROADCAST_NOTIFICATIONS_ALL_INSTALLED_CLEARED:
|
||||
appUpdateStatusMananger.clearAllInstalled();
|
||||
break;
|
||||
case BROADCAST_NOTIFICATIONS_UPDATE_CLEARED:
|
||||
break;
|
||||
case BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED:
|
||||
String key = intent.getStringExtra(EXTRA_NOTIFICATION_KEY);
|
||||
appUpdateStatusMananger.removeApk(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private BroadcastReceiver receiverAppStatusChanges = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
switch (intent.getAction()) {
|
||||
case AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED:
|
||||
notificationManager.cancelAll();
|
||||
for (AppUpdateStatusManager.AppUpdateStatus entry : appUpdateStatusMananger.getAll()) {
|
||||
createNotification(entry);
|
||||
}
|
||||
updateSummaryNotifications();
|
||||
break;
|
||||
case AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED: {
|
||||
String url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL);
|
||||
AppUpdateStatusManager.AppUpdateStatus entry = appUpdateStatusMananger.get(url);
|
||||
if (entry != null) {
|
||||
createNotification(entry);
|
||||
}
|
||||
updateSummaryNotifications();
|
||||
break;
|
||||
}
|
||||
case AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED: {
|
||||
String url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL);
|
||||
AppUpdateStatusManager.AppUpdateStatus entry = appUpdateStatusMananger.get(url);
|
||||
if (entry != null) {
|
||||
createNotification(entry);
|
||||
}
|
||||
if (intent.getBooleanExtra(AppUpdateStatusManager.EXTRA_IS_STATUS_UPDATE, false)) {
|
||||
updateSummaryNotifications();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED: {
|
||||
String url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL);
|
||||
notificationManager.cancel(url, NOTIFY_ID_INSTALLED);
|
||||
notificationManager.cancel(url, NOTIFY_ID_UPDATES);
|
||||
updateSummaryNotifications();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -37,7 +37,6 @@ import android.os.Process;
|
||||
import android.os.SystemClock;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.TaskStackBuilder;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
@ -79,7 +78,6 @@ public class UpdateService extends IntentService {
|
||||
private static final String STATE_LAST_UPDATED = "lastUpdateCheck";
|
||||
|
||||
private static final int NOTIFY_ID_UPDATING = 0;
|
||||
private static final int NOTIFY_ID_UPDATES_AVAILABLE = 1;
|
||||
|
||||
private static final int FLAG_NET_UNAVAILABLE = 0;
|
||||
private static final int FLAG_NET_METERED = 1;
|
||||
@ -89,6 +87,7 @@ public class UpdateService extends IntentService {
|
||||
|
||||
private NotificationManager notificationManager;
|
||||
private NotificationCompat.Builder notificationBuilder;
|
||||
private AppUpdateStatusManager appUpdateStatusManager;
|
||||
|
||||
public UpdateService() {
|
||||
super("UpdateService");
|
||||
@ -147,6 +146,7 @@ public class UpdateService extends IntentService {
|
||||
.setOngoing(true)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setContentTitle(getString(R.string.update_notification_title));
|
||||
appUpdateStatusManager = AppUpdateStatusManager.getInstance(this);
|
||||
|
||||
// Android docs are a little sketchy, however it seems that Gingerbread is the last
|
||||
// sdk that made a content intent mandatory:
|
||||
@ -469,39 +469,6 @@ public class UpdateService extends IntentService {
|
||||
}
|
||||
}
|
||||
|
||||
private PendingIntent createNotificationIntent() {
|
||||
Intent notifyIntent = new Intent(this, FDroid.class).putExtra(FDroid.EXTRA_TAB_UPDATE, true);
|
||||
TaskStackBuilder stackBuilder = TaskStackBuilder
|
||||
.create(this).addParentStack(FDroid.class)
|
||||
.addNextIntent(notifyIntent);
|
||||
return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private static final int MAX_UPDATES_TO_SHOW = 5;
|
||||
|
||||
private NotificationCompat.Style createNotificationBigStyle(Cursor hasUpdates) {
|
||||
|
||||
final String contentText = hasUpdates.getCount() > 1
|
||||
? getString(R.string.many_updates_available, hasUpdates.getCount())
|
||||
: getString(R.string.one_update_available);
|
||||
|
||||
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
|
||||
inboxStyle.setBigContentTitle(contentText);
|
||||
hasUpdates.moveToFirst();
|
||||
for (int i = 0; i < Math.min(hasUpdates.getCount(), MAX_UPDATES_TO_SHOW); i++) {
|
||||
App app = new App(hasUpdates);
|
||||
hasUpdates.moveToNext();
|
||||
inboxStyle.addLine(app.name + " (" + app.installedVersionName + " → " + app.getSuggestedVersionName() + ")");
|
||||
}
|
||||
|
||||
if (hasUpdates.getCount() > MAX_UPDATES_TO_SHOW) {
|
||||
int diff = hasUpdates.getCount() - MAX_UPDATES_TO_SHOW;
|
||||
inboxStyle.setSummaryText(getString(R.string.update_notification_more, diff));
|
||||
}
|
||||
|
||||
return inboxStyle;
|
||||
}
|
||||
|
||||
private void autoDownloadUpdates() {
|
||||
Cursor cursor = getContentResolver().query(
|
||||
AppProvider.getCanUpdateUri(),
|
||||
@ -520,37 +487,17 @@ 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++) {
|
||||
appUpdateStatusManager.startBatchUpdates();
|
||||
for (int i = 0; i < 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);
|
||||
appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.UpdateAvailable, null);
|
||||
}
|
||||
NotificationHelper.endBatchUpdates();
|
||||
appUpdateStatusManager.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;
|
||||
|
||||
final String contentText = hasUpdates.getCount() > 1
|
||||
? getString(R.string.many_updates_available, hasUpdates.getCount())
|
||||
: getString(R.string.one_update_available);
|
||||
|
||||
NotificationCompat.Builder builder =
|
||||
new NotificationCompat.Builder(this)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(getString(R.string.fdroid_updates_available))
|
||||
.setSmallIcon(icon)
|
||||
.setContentIntent(createNotificationIntent())
|
||||
.setContentText(contentText)
|
||||
.setStyle(createNotificationBigStyle(hasUpdates));
|
||||
|
||||
notificationManager.notify(NOTIFY_ID_UPDATES_AVAILABLE, builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,44 +1,31 @@
|
||||
package org.fdroid.fdroid.installer;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
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.net.Uri;
|
||||
import android.os.IBinder;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.TaskStackBuilder;
|
||||
import android.support.v4.content.IntentCompat;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.filefilter.WildcardFileFilter;
|
||||
import org.fdroid.fdroid.AppDetails;
|
||||
import org.fdroid.fdroid.AppUpdateStatusManager;
|
||||
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;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.Schema;
|
||||
import org.fdroid.fdroid.net.Downloader;
|
||||
import org.fdroid.fdroid.net.DownloaderService;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Manages the whole process when a background update triggers an install or the user
|
||||
@ -89,16 +76,17 @@ public class InstallManagerService extends Service {
|
||||
* matching the {@link App}s in {@code ACTIVE_APPS}. The key is the download URL, as
|
||||
* in {@link Apk#getUrl()} or {@code urlString}.
|
||||
*/
|
||||
private static final HashMap<String, Apk> ACTIVE_APKS = new HashMap<>(3);
|
||||
//private static final HashMap<String, Apk> ACTIVE_APKS = new HashMap<>(3);
|
||||
|
||||
/**
|
||||
* The collection of {@link App}s that are actively going through this whole process,
|
||||
* matching the {@link Apk}s in {@code ACTIVE_APKS}. The key is the
|
||||
* {@code packageName} of the app.
|
||||
*/
|
||||
private static final HashMap<String, App> ACTIVE_APPS = new HashMap<>(3);
|
||||
//private static final HashMap<String, App> ACTIVE_APPS = new HashMap<>(3);
|
||||
|
||||
private LocalBroadcastManager localBroadcastManager;
|
||||
private AppUpdateStatusManager appUpdateStatusManager;
|
||||
|
||||
/**
|
||||
* This service does not use binding, so no need to implement this method
|
||||
@ -113,18 +101,14 @@ public class InstallManagerService extends Service {
|
||||
super.onCreate();
|
||||
Utils.debugLog(TAG, "creating Service");
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(this);
|
||||
appUpdateStatusManager = AppUpdateStatusManager.getInstance(this);
|
||||
|
||||
BroadcastReceiver br = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String packageName = intent.getData().getSchemeSpecificPart();
|
||||
for (Map.Entry<String, Apk> entry : ACTIVE_APKS.entrySet()) {
|
||||
if (TextUtils.equals(packageName, entry.getValue().packageName)) {
|
||||
String urlString = entry.getKey();
|
||||
NotificationHelper.removeApk(getApkFromActive(urlString));
|
||||
break;
|
||||
}
|
||||
}
|
||||
//TODO: do we need to mark as installed, or is this handled by other code already?
|
||||
//appUpdateStatusManager.removeApk(packageName);
|
||||
}
|
||||
};
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
@ -146,10 +130,12 @@ public class InstallManagerService extends Service {
|
||||
String action = intent.getAction();
|
||||
if (ACTION_CANCEL.equals(action)) {
|
||||
DownloaderService.cancel(this, urlString);
|
||||
Apk apk = getApkFromActive(urlString);
|
||||
DownloaderService.cancel(this, apk.getPatchObbUrl());
|
||||
DownloaderService.cancel(this, apk.getMainObbUrl());
|
||||
NotificationHelper.removeApk(apk);
|
||||
Apk apk = appUpdateStatusManager.getApk(urlString);
|
||||
if (apk != null) {
|
||||
DownloaderService.cancel(this, apk.getPatchObbUrl());
|
||||
DownloaderService.cancel(this, apk.getMainObbUrl());
|
||||
}
|
||||
appUpdateStatusManager.removeApk(urlString);
|
||||
return START_NOT_STICKY;
|
||||
} else if (!ACTION_INSTALL.equals(action)) {
|
||||
Utils.debugLog(TAG, "Ignoring " + intent + " as it is not an " + ACTION_INSTALL + " intent");
|
||||
@ -165,7 +151,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.");
|
||||
NotificationHelper.removeApk(getApkFromActive(urlString));
|
||||
appUpdateStatusManager.removeApk(urlString);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@ -175,7 +161,7 @@ public class InstallManagerService extends Service {
|
||||
Utils.debugLog(TAG, "Intent had null EXTRA_APP and/or EXTRA_APK: " + intent);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
addToActive(urlString, app, apk);
|
||||
appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.Unknown, null);
|
||||
|
||||
registerApkDownloaderReceivers(urlString);
|
||||
getObb(urlString, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256);
|
||||
@ -282,32 +268,31 @@ public class InstallManagerService extends Service {
|
||||
intentObject.setAction(ACTION_CANCEL);
|
||||
intentObject.setData(downloadUri);
|
||||
PendingIntent action = PendingIntent.getService(context, 0, intentObject, 0);
|
||||
NotificationHelper.setApk(getApkFromActive(urlString), NotificationHelper.Status.Downloading, action);
|
||||
appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.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);
|
||||
NotificationHelper.setApkProgress(getApkFromActive(urlString), totalBytes, bytesRead);
|
||||
appUpdateStatusManager.updateApkProgress(urlString, totalBytes, bytesRead);
|
||||
break;
|
||||
case Downloader.ACTION_COMPLETE:
|
||||
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
|
||||
Uri localApkUri = Uri.fromFile(localFile);
|
||||
|
||||
Utils.debugLog(TAG, "download completed of " + urlString + " to " + localApkUri);
|
||||
|
||||
NotificationHelper.setApk(getApkFromActive(urlString), NotificationHelper.Status.ReadyToInstall, null);
|
||||
appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.ReadyToInstall, null);
|
||||
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
registerInstallerReceivers(downloadUri);
|
||||
|
||||
Apk apk = ACTIVE_APKS.get(urlString);
|
||||
InstallerService.install(context, localApkUri, downloadUri, apk);
|
||||
Apk apk = appUpdateStatusManager.getApk(urlString);
|
||||
if (apk != null) {
|
||||
InstallerService.install(context, localApkUri, downloadUri, apk);
|
||||
}
|
||||
break;
|
||||
case Downloader.ACTION_INTERRUPTED:
|
||||
NotificationHelper.removeApk(getApkFromActive(urlString));
|
||||
|
||||
removeFromActive(urlString);
|
||||
appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.UpdateAvailable, null);
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
break;
|
||||
default:
|
||||
@ -329,18 +314,18 @@ public class InstallManagerService extends Service {
|
||||
Apk apk;
|
||||
switch (intent.getAction()) {
|
||||
case Installer.ACTION_INSTALL_STARTED:
|
||||
NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.Installing, null);
|
||||
appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.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);
|
||||
|
||||
appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.Installed, null);
|
||||
Apk apkComplete = appUpdateStatusManager.getApk(downloadUrl);
|
||||
if (apkComplete != null) {
|
||||
PackageManagerCompat.setInstaller(getPackageManager(), apkComplete.packageName);
|
||||
}
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
break;
|
||||
case Installer.ACTION_INSTALL_INTERRUPTED:
|
||||
NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.ReadyToInstall, null);
|
||||
appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.ReadyToInstall, null);
|
||||
|
||||
apk = intent.getParcelableExtra(Installer.EXTRA_APK);
|
||||
String errorMessage =
|
||||
@ -348,21 +333,21 @@ public class InstallManagerService extends Service {
|
||||
|
||||
// show notification if app details is not visible
|
||||
if (!TextUtils.isEmpty(errorMessage)) {
|
||||
App app = getAppFromActive(downloadUrl);
|
||||
if (app == null) {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo);
|
||||
}
|
||||
appUpdateStatusManager.setApkError(apk, errorMessage);
|
||||
// App app = getAppFromActive(downloadUrl);
|
||||
// if (app == null) {
|
||||
// ContentResolver resolver = context.getContentResolver();
|
||||
// app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo);
|
||||
// }
|
||||
// TODO - show error
|
||||
}
|
||||
removeFromActive(downloadUrl);
|
||||
localBroadcastManager.unregisterReceiver(this);
|
||||
break;
|
||||
case Installer.ACTION_INSTALL_USER_INTERACTION:
|
||||
apk = intent.getParcelableExtra(Installer.EXTRA_APK);
|
||||
PendingIntent installPendingIntent =
|
||||
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
|
||||
NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.ReadyToInstall, installPendingIntent);
|
||||
appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.ReadyToInstall, installPendingIntent);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("intent action not handled!");
|
||||
@ -374,98 +359,6 @@ public class InstallManagerService extends Service {
|
||||
Installer.getInstallIntentFilter(downloadUri));
|
||||
}
|
||||
|
||||
private String getAppName(Apk apk) {
|
||||
return ACTIVE_APPS.get(apk.packageName).name;
|
||||
}
|
||||
|
||||
private void notifyError(String urlString, App app, String text) {
|
||||
int downloadUrlId = urlString.hashCode();
|
||||
|
||||
String name;
|
||||
if (app == null) {
|
||||
// if we have nothing else, show the APK filename
|
||||
String path = Uri.parse(urlString).getPath();
|
||||
name = path.substring(path.lastIndexOf('/'), path.length());
|
||||
} else {
|
||||
name = app.name;
|
||||
}
|
||||
String title = String.format(getString(R.string.install_error_notify_title), name);
|
||||
|
||||
Intent errorDialogIntent = new Intent(this, ErrorDialogActivity.class);
|
||||
errorDialogIntent.putExtra(
|
||||
ErrorDialogActivity.EXTRA_TITLE, title);
|
||||
errorDialogIntent.putExtra(
|
||||
ErrorDialogActivity.EXTRA_MESSAGE, text);
|
||||
PendingIntent errorDialogPendingIntent = PendingIntent.getActivity(
|
||||
getApplicationContext(),
|
||||
downloadUrlId,
|
||||
errorDialogIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
NotificationCompat.Builder builder =
|
||||
new NotificationCompat.Builder(this)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(title)
|
||||
.setContentIntent(errorDialogPendingIntent)
|
||||
.setSmallIcon(R.drawable.ic_issues)
|
||||
.setContentText(text);
|
||||
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
nm.notify(downloadUrlId, builder.build());
|
||||
}
|
||||
|
||||
private static void addToActive(String urlString, App app, Apk apk) {
|
||||
ACTIVE_APKS.put(urlString, apk);
|
||||
ACTIVE_APPS.put(app.packageName, app);
|
||||
}
|
||||
|
||||
/**
|
||||
* Always returns an {@link Apk} instance to avoid annoying null guards.
|
||||
*/
|
||||
private static Apk getApkFromActive(String urlString) {
|
||||
Apk apk = ACTIVE_APKS.get(urlString);
|
||||
if (apk == null) {
|
||||
return new Apk();
|
||||
} else {
|
||||
return apk;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the {@link App} and {@Apk} instances that are associated with
|
||||
* {@code urlString} from the {@link Map} of active apps. This can be
|
||||
* called after this service has been destroyed and recreated based on the
|
||||
* {@link BroadcastReceiver}s, in which case {@code urlString} would not
|
||||
* find anything in the active maps.
|
||||
*/
|
||||
private static App getAppFromActive(String urlString) {
|
||||
return ACTIVE_APPS.get(getApkFromActive(urlString).packageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the URL from this service, and return the {@link Apk}. This returns
|
||||
* an empty {@code Apk} instance if we get a null one so the code doesn't need
|
||||
* lots of null guards.
|
||||
*/
|
||||
private static Apk removeFromActive(String urlString) {
|
||||
Apk apk = ACTIVE_APKS.remove(urlString);
|
||||
if (apk == null) {
|
||||
return new Apk();
|
||||
}
|
||||
ACTIVE_APPS.remove(apk.packageName);
|
||||
return apk;
|
||||
}
|
||||
|
||||
private PendingIntent getCancelPendingIntent(String urlString) {
|
||||
Intent intent = new Intent(this, InstallManagerService.class)
|
||||
.setData(Uri.parse(urlString))
|
||||
.setAction(ACTION_CANCEL)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | IntentCompat.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
return PendingIntent.getService(this,
|
||||
urlString.hashCode(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install an APK, checking the cache and downloading if necessary before starting the process.
|
||||
* All notifications are sent as an {@link Intent} via local broadcasts to be received by
|
||||
@ -492,23 +385,4 @@ public class InstallManagerService extends Service {
|
||||
intent.setData(Uri.parse(urlString));
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Set} of the {@code urlString}s that are currently active.
|
||||
* {@code urlString}s are used as unique IDs throughout the
|
||||
* {@code InstallManagerService} process, either as a {@code String} or as an
|
||||
* {@code int} from {@link String#hashCode()}.
|
||||
*/
|
||||
public static Set<String> getActiveDownloadUrls() {
|
||||
return ACTIVE_APKS.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Set} of the {@code packageName}s that are currently active.
|
||||
* {@code packageName}s are used as unique IDs for apps throughout all of
|
||||
* Android, F-Droid, and other apps stores.
|
||||
*/
|
||||
public static Set<String> getActivePackageNames() {
|
||||
return ACTIVE_APPS.keySet();
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user