Move app status handling to new AppUpdateStatusManager

This commit is contained in:
mvp76 2017-02-13 15:46:37 +01:00
parent 10cbf1ef7a
commit 55c7a21c90
5 changed files with 553 additions and 500 deletions

View 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);
}
}

View File

@ -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();

View File

@ -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;
}
}
}
};
}

View File

@ -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());
}
/**

View File

@ -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();
}
}