From 10cbf1ef7a6e668e80b4716e82d0961f30531613 Mon Sep 17 00:00:00 2001 From: mvp76 Date: Mon, 28 Nov 2016 17:30:58 +0100 Subject: [PATCH 01/31] Create notification helper class --- .../java/org/fdroid/fdroid/FDroidApp.java | 2 + .../org/fdroid/fdroid/NotificationHelper.java | 522 ++++++++++++++++++ .../java/org/fdroid/fdroid/UpdateService.java | 13 + .../installer/InstallManagerService.java | 141 +---- .../res/drawable/ic_notify_cancel_24dp.xml | 4 + .../res/drawable/ic_notify_install_24dp.xml | 6 + .../res/drawable/ic_notify_update_24dp.xml | 4 + 7 files changed, 577 insertions(+), 115 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/NotificationHelper.java create mode 100644 app/src/main/res/drawable/ic_notify_cancel_24dp.xml create mode 100644 app/src/main/res/drawable/ic_notify_install_24dp.xml create mode 100644 app/src/main/res/drawable/ic_notify_update_24dp.xml diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 7624f710b..2507c46d2 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -220,6 +220,8 @@ public class FDroidApp extends Application { curTheme = Preferences.get().getTheme(); Preferences.get().configureProxy(); + NotificationHelper.init(getApplicationContext()); + InstalledAppProviderService.compareToPackageManager(this); // If the user changes the preference to do with filtering rooted apps, diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java new file mode 100644 index 000000000..18c8190f5 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -0,0 +1,522 @@ +package org.fdroid.fdroid; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Typeface; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.app.TaskStackBuilder; +import android.support.v4.util.LongSparseArray; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.StyleSpan; + +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.assist.ImageSize; + +import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.ApkProvider; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class NotificationHelper { + + private static final String BROADCAST_NOTIFICATIONS_UPDATES_CLEARED = "org.fdroid.fdroid.installer.notifications.updates.cleared"; + private static final String BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED = "org.fdroid.fdroid.installer.notifications.installed.cleared"; + private static final String BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED = "org.fdroid.fdroid.installer.notifications.deleted"; + + private static final int NOTIFY_ID_UPDATES = 4711; + private static final int NOTIFY_ID_INSTALLED = 4712; + + private static final int MAX_UPDATES_TO_SHOW = 5; + private static final int MAX_INSTALLED_TO_SHOW = 10; + + private static final String EXTRA_NOTIFICATION_TAG = "tag"; + private static final String GROUP_UPDATES = "updates"; + private static final String GROUP_INSTALLED = "installed"; + + public enum Status { + UpdateAvailable, + Downloading, + ReadyToInstall, + Installing, + Installed, + Error + } + + private static NotificationHelper instance; + + public static void init(Context context) { + instance = new NotificationHelper(context); + } + + private static NotificationHelper getInstance() { + return instance; + } + + private class AppEntry { + App app; + Apk apk; + Status status; + PendingIntent intent; + int progressCurrent; + int progressMax; + + AppEntry(App app, Apk apk, Status status, PendingIntent intent) { + this.app = app; + this.apk = apk; + this.status = status; + this.intent = intent; + } + + String getTag() { + return apk.getUrl(); + } + + int getId() { + return getTag().hashCode(); + } + } + + private final Context context; + private final NotificationManagerCompat notificationManager; + private HashMap appMapping; + private boolean isBatchUpdating; + private ArrayList updates; + private ArrayList installed; + + private NotificationHelper(Context context) { + this.context = context; + notificationManager = NotificationManagerCompat.from(context); + + // We need to listen to when notifications are cleared, so that we "forget" all that we currently know about updates + // and installs. + IntentFilter filter = new IntentFilter(); + filter.addAction(BROADCAST_NOTIFICATIONS_UPDATES_CLEARED); + filter.addAction(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED); + filter.addAction(BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED); + BroadcastReceiver mReceiverNotificationsCleared = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED: + clearAllInstalledInternal(); + break; + case BROADCAST_NOTIFICATIONS_UPDATES_CLEARED: + clearAllUpdatesInternal(); + break; + case BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED: + String id = intent.getStringExtra(EXTRA_NOTIFICATION_TAG); + // TODO + break; + } + } + }; + context.registerReceiver(mReceiverNotificationsCleared, filter); + appMapping = new HashMap<>(); + updates = new ArrayList<>(); + installed = new ArrayList<>(); + } + + private void setApkInternal(Apk apk, Status status, PendingIntent intent) { + if (apk == null) { + return; + } + + AppEntry entry = appMapping.get(apk.getUrl()); + if (status == null) { + // Remove + if (entry != null) { + appMapping.remove(apk.getUrl()); + notificationManager.cancel(entry.getTag(), entry.getId()); + } + } else if (entry != null) { + // Update + boolean isStatusUpdate = (entry.status != status); + entry.status = status; + entry.intent = intent; + createNotificationForAppEntry(entry); + if (isStatusUpdate) { + updateSummaryNotifications(); + } + } else { + // Add + ContentResolver resolver = context.getContentResolver(); + App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo); + entry = new AppEntry(app, apk, status, intent); + appMapping.put(apk.getUrl(), entry); + createNotificationForAppEntry(entry); + updateSummaryNotifications(); + } + } + + private void setApkProgressInternal(Apk apk, int max, int current) { + if (appMapping.get(apk.getUrl()) != null) { + AppEntry entry = appMapping.get(apk.getUrl()); + entry.progressMax = max; + entry.progressCurrent = current; + createNotificationForAppEntry(entry); + } + } + + private void clearAllUpdatesInternal() { + for(Iterator> it = appMapping.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = it.next(); + if(entry.getValue().status != Status.Installed) { + it.remove(); + } + } + } + + private void clearAllInstalledInternal() { + for(Iterator> it = appMapping.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = it.next(); + if(entry.getValue().status == Status.Installed) { + it.remove(); + } + } + } + + private void updateSummaryNotifications() { + if (!isBatchUpdating) { + // Get the list of updates and installed available + updates.clear(); + installed.clear(); + for (Iterator> it = appMapping.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = it.next(); + if (entry.getValue().status != Status.Installed) { + updates.add(entry.getValue()); + } else { + installed.add(entry.getValue()); + } + } + + NotificationCompat.Builder builder; + if (updates.size() == 0) { + // No updates, remove summary + notificationManager.cancel(GROUP_UPDATES, NOTIFY_ID_UPDATES); + } else { + builder = createUpdateSummaryNotification(updates); + notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, builder.build()); + } + if (installed.size() == 0) { + // No installed, remove summary + notificationManager.cancel(GROUP_INSTALLED, NOTIFY_ID_INSTALLED); + } else { + builder = createInstalledSummaryNotification(installed); + notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, builder.build()); + } + } + } + + private void createNotificationForAppEntry(AppEntry entry) { + NotificationCompat.Builder builder; + if (entry.status == Status.Installed) { + builder = createInstalledNotification(entry); + } else { + builder = createUpdateNotification(entry); + } + notificationManager.notify(entry.getTag(), entry.getId(), builder.build()); + } + + /** + * Add an Apk to the notifications manager. + * @param apk The apk to add. + * @param status The current status of the app + * @param pendingIntent Action when notification is clicked. Can be null for default action(s) + */ + public static void setApk(Apk apk, Status status, PendingIntent pendingIntent) { + getInstance().setApkInternal(apk, status, pendingIntent); + } + + public static void removeApk(Apk apk) { + getInstance().setApkInternal(apk, null, null); + } + + public static void setApkProgress(Apk apk, int max, int current) { + getInstance().setApkProgressInternal(apk, max, current); + } + + public static void startBatchUpdates() { + getInstance().isBatchUpdating = true; + } + + public static void endBatchUpdates() { + getInstance().isBatchUpdating = false; + getInstance().updateSummaryNotifications(); + } + + public static void clearAllUpdates() { + getInstance().clearAllUpdatesInternal(); + } + + public static void clearAllInstalled() { + getInstance().clearAllInstalledInternal(); + } + + private NotificationCompat.Action getAction(AppEntry entry) { + if (entry.status == Status.UpdateAvailable) { + // Make sure we have an intent to install the app. If not set, we create an intent + // to open up the app details page for the app. From there, the user can hit "install" + PendingIntent intent = entry.intent; + if (intent == null) { + intent = getAppDetailsIntent(0, entry.apk); + } + return new NotificationCompat.Action(R.drawable.ic_notify_update_24dp, "Update", intent); + } else if (entry.status == Status.Downloading || entry.status == Status.Installing) { + PendingIntent intent = entry.intent; + if (intent != null) { + return new NotificationCompat.Action(R.drawable.ic_notify_cancel_24dp, "Cancel", intent); + } + } else if (entry.status == Status.ReadyToInstall) { + // Make sure we have an intent to install the app. If not set, we create an intent + // to open up the app details page for the app. From there, the user can hit "install" + PendingIntent intent = entry.intent; + if (intent == null) { + intent = getAppDetailsIntent(0, entry.apk); + } + return new NotificationCompat.Action(R.drawable.ic_notify_install_24dp, "Install", intent); + } + return null; + } + + private String getSingleItemTitleString(App app, Status status) { + switch (status) { + case UpdateAvailable: + return "Update Available"; + case Downloading: + return app.name; + case ReadyToInstall: + return "Update ready to install"; // TODO - "Update"? Should just be "ready to install"? + case Installing: + return app.name; + case Installed: + return app.name; + } + return ""; + } + + private String getSingleItemContentString(App app, Status status) { + switch (status) { + case UpdateAvailable: + return app.name; + case Downloading: + return String.format("Downloading update for \"%s\"...", app.name); + case ReadyToInstall: + return app.name; + case Installing: + return String.format("Installing \"%s\"...", app.name); + case Installed: + return "Successfully installed"; + } + return ""; + } + + private String getMultiItemContentString(App app, Status status) { + switch (status) { + case UpdateAvailable: + return "Update available"; + case Downloading: + return "Downloading update..."; + case ReadyToInstall: + return "Ready to install"; + case Installing: + return "Installing"; + case Installed: + return "Successfully installed"; + } + return ""; + } + + /** + * Get a {@link PendingIntent} for a {@link Notification} to send when it + * is clicked. {@link AppDetails} handles {@code Intent}s that are missing + * or bad {@link AppDetails#EXTRA_APPID}, so it does not need to be checked + * here. + */ + private PendingIntent getAppDetailsIntent(int requestCode, Apk apk) { + Intent notifyIntent = new Intent(context, AppDetails.class) + .putExtra(AppDetails.EXTRA_APPID, apk.packageName); + return TaskStackBuilder.create(context) + .addParentStack(AppDetails.class) + .addNextIntent(notifyIntent) + .getPendingIntent(requestCode, 0); + } + + private NotificationCompat.Builder createUpdateNotification(AppEntry entry) { + App app = entry.app; + Status status = entry.status; + + // TODO - async image loading + int largeIconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); + Bitmap iconLarge = ImageLoader.getInstance().loadImageSync(app.iconUrl, new ImageSize(largeIconSize, largeIconSize)); + + NotificationCompat.Builder builder = + new NotificationCompat.Builder(context) + .setAutoCancel(true) + .setLargeIcon(iconLarge) + .setSmallIcon(R.drawable.ic_stat_notify_updates) + .setContentTitle(getSingleItemTitleString(app, status)) + .setContentText(getSingleItemContentString(app, status)) + .setGroup(GROUP_UPDATES); + + // Handle actions + // + NotificationCompat.Action action = getAction(entry); + if (action != null) { + builder.addAction(action); + // TODO - also click on whole item? + builder.setContentIntent(action.getActionIntent()); + } else if (entry.intent != null) { + builder.setContentIntent(entry.intent); + } + + // Handle progress bar (for some states) + // + if (status == Status.Downloading) { + if (entry.progressMax == 0) + builder.setProgress(100, 0, true); + else + builder.setProgress(entry.progressMax, entry.progressCurrent, false); + } else if (status == Status.Installing) { + builder.setProgress(100, 0, true); // indeterminate bar + } + + Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED); + intentDeleted.putExtra(EXTRA_NOTIFICATION_TAG, entry.getId()); + PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0); + builder.setDeleteIntent(piDeleted); + return builder; + } + + private NotificationCompat.Builder createUpdateSummaryNotification(ArrayList updates) { + String title = String.format("%d Updates", updates.size()); + StringBuilder text = new StringBuilder(); + + NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); + inboxStyle.setBigContentTitle(title); + + for (int i = 0; i < MAX_UPDATES_TO_SHOW && i < updates.size(); i++) { + AppEntry entry = updates.get(i); + App app = entry.app; + Status status = entry.status; + + String content = getMultiItemContentString(app, status); + SpannableStringBuilder sb = new SpannableStringBuilder(app.name); + sb.setSpan(new StyleSpan(Typeface.BOLD), 0, sb.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + sb.append(" "); + sb.append(content); + inboxStyle.addLine(sb); + + if (text.length() > 0) + text.append(", "); + text.append(app.name); + } + + //if (updates.size() > MAX_UPDATES_TO_SHOW) { + // int diff = updates.size() - MAX_UPDATES_TO_SHOW; + // inboxStyle.setSummaryText(context.getString(R.string.update_notification_more, diff)); + //} + + inboxStyle.setSummaryText(title); + + + // Intent to open main app list + Intent intentObject = new Intent(context, FDroid.class); + PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, 0); + + NotificationCompat.Builder builder = + new NotificationCompat.Builder(context) + .setAutoCancel(true) + .setSmallIcon(R.drawable.ic_launcher) + .setContentTitle(title) + .setContentText(text) + .setContentIntent(piAction) + .setStyle(inboxStyle) + .setGroup(GROUP_UPDATES) + .setGroupSummary(true); + Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_UPDATES_CLEARED); + PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0); + builder.setDeleteIntent(piDeleted); + return builder; + } + + private NotificationCompat.Builder createInstalledNotification(AppEntry entry) { + App app = entry.app; + + int largeIconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); + Bitmap iconLarge = ImageLoader.getInstance().loadImageSync(app.iconUrl, new ImageSize(largeIconSize, largeIconSize)); + + NotificationCompat.Builder builder = + new NotificationCompat.Builder(context) + .setAutoCancel(true) + .setLargeIcon(iconLarge) + .setSmallIcon(R.drawable.ic_stat_notify_updates) + .setContentTitle(app.name) + .setContentText("Successfully Installed") + .setGroup(GROUP_INSTALLED); + + PackageManager pm = context.getPackageManager(); + Intent intentObject = pm.getLaunchIntentForPackage(app.packageName); + PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, 0); + builder.setContentIntent(piAction); + + Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED); + intentDeleted.putExtra(EXTRA_NOTIFICATION_TAG, entry.getId()); + PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0); + builder.setDeleteIntent(piDeleted); + return builder; + } + + private NotificationCompat.Builder createInstalledSummaryNotification(ArrayList installed) { + String title = String.format("%d Apps Installed", installed.size()); + StringBuilder text = new StringBuilder(); + + NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle(); + bigTextStyle.setBigContentTitle(title); + + for (int i = 0; i < MAX_INSTALLED_TO_SHOW && i < installed.size(); i++) { + AppEntry entry = installed.get(i); + App app = entry.app; + if (text.length() > 0) + text.append(", "); + text.append(app.name); + } + bigTextStyle.bigText(text); + if (installed.size() > MAX_INSTALLED_TO_SHOW) { + int diff = installed.size() - MAX_INSTALLED_TO_SHOW; + bigTextStyle.setSummaryText(context.getString(R.string.update_notification_more, diff)); + } + + // Intent to open main app list + Intent intentObject = new Intent(context, FDroid.class); + PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, 0); + + NotificationCompat.Builder builder = + new NotificationCompat.Builder(context) + .setAutoCancel(true) + .setSmallIcon(R.drawable.ic_launcher) + .setContentTitle(title) + .setContentText(text) + .setContentIntent(piAction) + .setGroup(GROUP_INSTALLED) + .setGroupSummary(true); + Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED); + PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0); + builder.setDeleteIntent(piDeleted); + return builder; + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index fbf780caa..ce83afce8 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -520,6 +520,19 @@ public class UpdateService extends IntentService { } private void showAppUpdatesNotification(Cursor hasUpdates) { + + if (hasUpdates != null) { + hasUpdates.moveToFirst(); + NotificationHelper.startBatchUpdates(); + for (int i = 0; i < Math.min(MAX_UPDATES_TO_SHOW, hasUpdates.getCount()); i++) { + App app = new App(hasUpdates); + hasUpdates.moveToNext(); + Apk apk = ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode); + NotificationHelper.setApk(apk, NotificationHelper.Status.UpdateAvailable, null); + } + NotificationHelper.endBatchUpdates(); + } + Utils.debugLog(TAG, "Notifying " + hasUpdates.getCount() + " updates."); final int icon = Build.VERSION.SDK_INT >= 11 ? R.drawable.ic_stat_notify_updates : R.drawable.ic_launcher; diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index 226aafd6e..e50d2ccec 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -22,6 +22,7 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.WildcardFileFilter; import org.fdroid.fdroid.AppDetails; import org.fdroid.fdroid.Hasher; +import org.fdroid.fdroid.NotificationHelper; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.compat.PackageManagerCompat; @@ -98,7 +99,6 @@ public class InstallManagerService extends Service { private static final HashMap ACTIVE_APPS = new HashMap<>(3); private LocalBroadcastManager localBroadcastManager; - private NotificationManager notificationManager; /** * This service does not use binding, so no need to implement this method @@ -113,7 +113,6 @@ public class InstallManagerService extends Service { super.onCreate(); Utils.debugLog(TAG, "creating Service"); localBroadcastManager = LocalBroadcastManager.getInstance(this); - notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); BroadcastReceiver br = new BroadcastReceiver() { @Override @@ -122,7 +121,7 @@ public class InstallManagerService extends Service { for (Map.Entry entry : ACTIVE_APKS.entrySet()) { if (TextUtils.equals(packageName, entry.getValue().packageName)) { String urlString = entry.getKey(); - cancelNotification(urlString); + NotificationHelper.removeApk(getApkFromActive(urlString)); break; } } @@ -150,7 +149,7 @@ public class InstallManagerService extends Service { Apk apk = getApkFromActive(urlString); DownloaderService.cancel(this, apk.getPatchObbUrl()); DownloaderService.cancel(this, apk.getMainObbUrl()); - cancelNotification(urlString); + NotificationHelper.removeApk(apk); return START_NOT_STICKY; } else if (!ACTION_INSTALL.equals(action)) { Utils.debugLog(TAG, "Ignoring " + intent + " as it is not an " + ACTION_INSTALL + " intent"); @@ -166,7 +165,7 @@ public class InstallManagerService extends Service { && !DownloaderService.isQueuedOrActive(urlString)) { // TODO is there a case where we should allow an active urlString to pass through? Utils.debugLog(TAG, urlString + " finished downloading while InstallManagerService was killed."); - cancelNotification(urlString); + NotificationHelper.removeApk(getApkFromActive(urlString)); return START_NOT_STICKY; } @@ -178,12 +177,9 @@ public class InstallManagerService extends Service { } addToActive(urlString, app, apk); - NotificationCompat.Builder builder = createNotificationBuilder(urlString, apk); - notificationManager.notify(urlString.hashCode(), builder.build()); - - registerApkDownloaderReceivers(urlString, builder); - getObb(urlString, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256, builder); - getObb(urlString, apk.getPatchObbUrl(), apk.getPatchObbFile(), apk.obbPatchFileSha256, builder); + registerApkDownloaderReceivers(urlString); + getObb(urlString, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256); + getObb(urlString, apk.getPatchObbUrl(), apk.getPatchObbFile(), apk.obbPatchFileSha256); File apkFilePath = ApkCache.getApkDownloadPath(this, intent.getData()); long apkFileSize = apkFilePath.length(); @@ -217,8 +213,7 @@ public class InstallManagerService extends Service { * @see APK Expansion Files */ private void getObb(final String urlString, String obbUrlString, - final File obbDestFile, final String sha256, - final NotificationCompat.Builder builder) { + final File obbDestFile, final String sha256) { if (obbDestFile == null || obbDestFile.exists() || TextUtils.isEmpty(obbUrlString)) { return; } @@ -232,8 +227,7 @@ public class InstallManagerService extends Service { int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0); int totalBytes = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 0); - builder.setProgress(totalBytes, bytesRead, false); - notificationManager.notify(urlString.hashCode(), builder.build()); + // TODO - handle obb notifications? } else if (Downloader.ACTION_COMPLETE.equals(action)) { localBroadcastManager.unregisterReceiver(this); File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH)); @@ -274,7 +268,7 @@ public class InstallManagerService extends Service { DownloaderService.getIntentFilter(obbUrlString)); } - private void registerApkDownloaderReceivers(String urlString, final NotificationCompat.Builder builder) { + private void registerApkDownloaderReceivers(String urlString) { BroadcastReceiver downloadReceiver = new BroadcastReceiver() { @Override @@ -284,13 +278,17 @@ public class InstallManagerService extends Service { switch (intent.getAction()) { case Downloader.ACTION_STARTED: + Intent intentObject = new Intent(context, InstallManagerService.class); + intentObject.setAction(ACTION_CANCEL); + intentObject.setData(downloadUri); + PendingIntent action = PendingIntent.getService(context, 0, intentObject, 0); + NotificationHelper.setApk(getApkFromActive(urlString), NotificationHelper.Status.Downloading, action); // nothing to do break; case Downloader.ACTION_PROGRESS: int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0); int totalBytes = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 0); - builder.setProgress(totalBytes, bytesRead, false); - notificationManager.notify(urlString.hashCode(), builder.build()); + NotificationHelper.setApkProgress(getApkFromActive(urlString), totalBytes, bytesRead); break; case Downloader.ACTION_COMPLETE: File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH)); @@ -298,17 +296,19 @@ public class InstallManagerService extends Service { Utils.debugLog(TAG, "download completed of " + urlString + " to " + localApkUri); + NotificationHelper.setApk(getApkFromActive(urlString), NotificationHelper.Status.ReadyToInstall, null); + localBroadcastManager.unregisterReceiver(this); registerInstallerReceivers(downloadUri); Apk apk = ACTIVE_APKS.get(urlString); - InstallerService.install(context, localApkUri, downloadUri, apk); break; case Downloader.ACTION_INTERRUPTED: + NotificationHelper.removeApk(getApkFromActive(urlString)); + removeFromActive(urlString); localBroadcastManager.unregisterReceiver(this); - cancelNotification(urlString); break; default: throw new RuntimeException("intent action not handled!"); @@ -329,9 +329,10 @@ public class InstallManagerService extends Service { Apk apk; switch (intent.getAction()) { case Installer.ACTION_INSTALL_STARTED: - // nothing to do + NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.Installing, null); break; case Installer.ACTION_INSTALL_COMPLETE: + NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.Installed, null); Apk apkComplete = removeFromActive(downloadUrl); PackageManagerCompat.setInstaller(getPackageManager(), apkComplete.packageName); @@ -339,6 +340,8 @@ public class InstallManagerService extends Service { localBroadcastManager.unregisterReceiver(this); break; case Installer.ACTION_INSTALL_INTERRUPTED: + NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.ReadyToInstall, null); + apk = intent.getParcelableExtra(Installer.EXTRA_APK); String errorMessage = intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE); @@ -350,12 +353,7 @@ public class InstallManagerService extends Service { ContentResolver resolver = context.getContentResolver(); app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo); } - // show notification if app details is not visible - if (app != null && AppDetails.isAppVisible(app.packageName)) { - cancelNotification(downloadUrl); - } else { - notifyError(downloadUrl, app, errorMessage); - } + // TODO - show error } removeFromActive(downloadUrl); localBroadcastManager.unregisterReceiver(this); @@ -364,14 +362,7 @@ public class InstallManagerService extends Service { apk = intent.getParcelableExtra(Installer.EXTRA_APK); PendingIntent installPendingIntent = intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI); - - // show notification if app details is not visible - if (AppDetails.isAppVisible(apk.packageName)) { - cancelNotification(downloadUrl); - } else { - notifyDownloadComplete(apk, downloadUrl, installPendingIntent); - } - + NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.ReadyToInstall, installPendingIntent); break; default: throw new RuntimeException("intent action not handled!"); @@ -383,81 +374,10 @@ public class InstallManagerService extends Service { Installer.getInstallIntentFilter(downloadUri)); } - private NotificationCompat.Builder createNotificationBuilder(String urlString, Apk apk) { - int downloadUrlId = urlString.hashCode(); - return new NotificationCompat.Builder(this) - .setAutoCancel(false) - .setOngoing(true) - .setContentIntent(getAppDetailsIntent(downloadUrlId, apk)) - .setContentTitle(getString(R.string.downloading_apk, getAppName(apk))) - .addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel), - getCancelPendingIntent(urlString)) - .setSmallIcon(android.R.drawable.stat_sys_download) - .setContentText(urlString) - .setProgress(100, 0, true); - } - private String getAppName(Apk apk) { return ACTIVE_APPS.get(apk.packageName).name; } - /** - * Get a {@link PendingIntent} for a {@link Notification} to send when it - * is clicked. {@link AppDetails} handles {@code Intent}s that are missing - * or bad {@link AppDetails#EXTRA_APPID}, so it does not need to be checked - * here. - */ - private PendingIntent getAppDetailsIntent(int requestCode, Apk apk) { - Intent notifyIntent = new Intent(getApplicationContext(), AppDetails.class) - .putExtra(AppDetails.EXTRA_APPID, apk.packageName); - return TaskStackBuilder.create(getApplicationContext()) - .addParentStack(AppDetails.class) - .addNextIntent(notifyIntent) - .getPendingIntent(requestCode, PendingIntent.FLAG_UPDATE_CURRENT); - } - - /** - * Post a notification about a completed download. {@code packageName} must be a valid - * and currently in the app index database. This must create a new {@code Builder} - * instance otherwise the progress/cancel stuff does not go away. - * - * @see Issue 47809: - * Removing the progress bar from a notification should cause the notification's content - * text to return to normal size - */ - private void notifyDownloadComplete(Apk apk, String urlString, PendingIntent installPendingIntent) { - String title; - try { - PackageManager pm = getPackageManager(); - title = String.format(getString(R.string.tap_to_update_format), - pm.getApplicationLabel(pm.getApplicationInfo(apk.packageName, 0))); - } catch (PackageManager.NameNotFoundException e) { - String name = getAppName(apk); - if (TextUtils.isEmpty(name) || name.equals(new App().name)) { - ContentResolver resolver = getContentResolver(); - App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo, - new String[]{Schema.AppMetadataTable.Cols.NAME}); - if (app == null || TextUtils.isEmpty(app.name)) { - return; // do not have a name to display, so leave notification as is - } - name = app.name; - } - title = String.format(getString(R.string.tap_to_install_format), name); - } - - int downloadUrlId = urlString.hashCode(); - notificationManager.cancel(downloadUrlId); - Notification notification = new NotificationCompat.Builder(this) - .setAutoCancel(true) - .setOngoing(false) - .setContentTitle(title) - .setContentIntent(installPendingIntent) - .setSmallIcon(android.R.drawable.stat_sys_download_done) - .setContentText(getString(R.string.tap_to_install)) - .build(); - notificationManager.notify(downloadUrlId, notification); - } - private void notifyError(String urlString, App app, String text) { int downloadUrlId = urlString.hashCode(); @@ -493,15 +413,6 @@ public class InstallManagerService extends Service { nm.notify(downloadUrlId, builder.build()); } - /** - * Cancel the {@link Notification} tied to {@code urlString}, which is the - * unique ID used to represent a given APK file. {@link String#hashCode()} - * converts {@code urlString} to the required {@code int}. - */ - private void cancelNotification(String urlString) { - notificationManager.cancel(urlString.hashCode()); - } - private static void addToActive(String urlString, App app, Apk apk) { ACTIVE_APKS.put(urlString, apk); ACTIVE_APPS.put(app.packageName, app); diff --git a/app/src/main/res/drawable/ic_notify_cancel_24dp.xml b/app/src/main/res/drawable/ic_notify_cancel_24dp.xml new file mode 100644 index 000000000..784cc0b1e --- /dev/null +++ b/app/src/main/res/drawable/ic_notify_cancel_24dp.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_notify_install_24dp.xml b/app/src/main/res/drawable/ic_notify_install_24dp.xml new file mode 100644 index 000000000..644cb6070 --- /dev/null +++ b/app/src/main/res/drawable/ic_notify_install_24dp.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_notify_update_24dp.xml b/app/src/main/res/drawable/ic_notify_update_24dp.xml new file mode 100644 index 000000000..11b5def17 --- /dev/null +++ b/app/src/main/res/drawable/ic_notify_update_24dp.xml @@ -0,0 +1,4 @@ + + + + From 55c7a21c90385f145e323fdffde8b962954c11c4 Mon Sep 17 00:00:00 2001 From: mvp76 Date: Mon, 13 Feb 2017 15:46:37 +0100 Subject: [PATCH 02/31] Move app status handling to new AppUpdateStatusManager --- .../fdroid/fdroid/AppUpdateStatusManager.java | 310 ++++++++++++ .../java/org/fdroid/fdroid/FDroidApp.java | 3 +- .../org/fdroid/fdroid/NotificationHelper.java | 475 ++++++++---------- .../java/org/fdroid/fdroid/UpdateService.java | 65 +-- .../installer/InstallManagerService.java | 200 ++------ 5 files changed, 553 insertions(+), 500 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java new file mode 100644 index 000000000..6ccdea17f --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -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 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 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> it = appMapping.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = it.next(); + if (entry.getValue().status != Status.Installed) { + it.remove(); + } + } + notifyChange(); + } + } + + void clearAllInstalled() { + synchronized (appMapping) { + for (Iterator> it = appMapping.entrySet().iterator(); it.hasNext(); ) { + Map.Entry 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); + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 2507c46d2..ec57e4d45 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -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(); diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index 18c8190f5..2b7bcae46 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -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 appMapping; - private boolean isBatchUpdating; - private ArrayList updates; - private ArrayList 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> it = appMapping.entrySet().iterator(); it.hasNext(); ) { - Map.Entry entry = it.next(); - if(entry.getValue().status != Status.Installed) { - it.remove(); - } - } - } - - private void clearAllInstalledInternal() { - for(Iterator> it = appMapping.entrySet().iterator(); it.hasNext(); ) { - Map.Entry 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> it = appMapping.entrySet().iterator(); it.hasNext(); ) { - Map.Entry 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 updates = new ArrayList<>(); + ArrayList 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 updates) { + private NotificationCompat.Builder createUpdateSummaryNotification(ArrayList 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 installed) { + private NotificationCompat.Builder createInstalledSummaryNotification(ArrayList 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; + } + } + } + }; } diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index ce83afce8..11a11cf4f 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -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()); } /** diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index e50d2ccec..528c9bc7b 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -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 ACTIVE_APKS = new HashMap<>(3); + //private static final HashMap 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 ACTIVE_APPS = new HashMap<>(3); + //private static final HashMap 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 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 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 getActivePackageNames() { - return ACTIVE_APPS.keySet(); - } } From 7efa62891d43b0779546252dcf1140ab8e81c6e1 Mon Sep 17 00:00:00 2001 From: mvp76 Date: Wed, 15 Feb 2017 15:51:08 +0100 Subject: [PATCH 03/31] Use strings, async image loading, "unknown" status added. --- .../fdroid/fdroid/AppUpdateStatusManager.java | 36 +- .../java/org/fdroid/fdroid/FDroidApp.java | 3 +- .../org/fdroid/fdroid/NotificationHelper.java | 371 +++++++++++------- .../installer/InstallManagerService.java | 25 +- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-ast/strings.xml | 2 +- app/src/main/res/values-bg/strings.xml | 2 +- app/src/main/res/values-ca/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-da/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-el/strings.xml | 2 +- app/src/main/res/values-eo/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-et/strings.xml | 2 +- app/src/main/res/values-eu/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 2 +- app/src/main/res/values-fi/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-gl/strings.xml | 2 +- app/src/main/res/values-he/strings.xml | 2 +- app/src/main/res/values-hi/strings.xml | 2 +- app/src/main/res/values-hr/strings.xml | 2 +- app/src/main/res/values-hu/strings.xml | 2 +- app/src/main/res/values-id/strings.xml | 2 +- app/src/main/res/values-is/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values-my/strings.xml | 2 +- app/src/main/res/values-nb/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 2 +- app/src/main/res/values-pt-rPT/strings.xml | 2 +- app/src/main/res/values-ro/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-sc/strings.xml | 2 +- app/src/main/res/values-sk/strings.xml | 2 +- app/src/main/res/values-sn/strings.xml | 2 +- app/src/main/res/values-sr/strings.xml | 2 +- app/src/main/res/values-sv/strings.xml | 2 +- app/src/main/res/values-th/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 2 +- app/src/main/res/values-zh-rHK/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values/strings.xml | 28 +- 50 files changed, 335 insertions(+), 218 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 6ccdea17f..fb34c9db3 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -13,6 +13,7 @@ import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.installer.ErrorDialogActivity; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; @@ -43,14 +44,14 @@ public class AppUpdateStatusManager { return new AppUpdateStatusManager(context); } - class AppUpdateStatus { - final App app; - final Apk apk; - Status status; - PendingIntent intent; - int progressCurrent; - int progressMax; - String errorText; + public class AppUpdateStatus { + public final App app; + public final Apk apk; + public Status status; + public PendingIntent intent; + public int progressCurrent; + public int progressMax; + public String errorText; AppUpdateStatus(App app, Apk apk, Status status, PendingIntent intent) { this.app = app; @@ -59,7 +60,7 @@ public class AppUpdateStatusManager { this.intent = intent; } - String getUniqueKey() { + public String getUniqueKey() { return apk.getUrl(); } } @@ -86,6 +87,23 @@ public class AppUpdateStatusManager { } } + /** + * Get all entries associated with a package name. There may be several. + * @param packageName Package name of the app + * @return A list of entries, or an empty list + */ + public Collection getByPackageName(String packageName) { + ArrayList returnValues = new ArrayList<>(); + synchronized (appMapping) { + for (AppUpdateStatus entry : appMapping.values()) { + if (entry.apk.packageName.equalsIgnoreCase(packageName)) { + returnValues.add(entry); + } + } + } + return returnValues; + } + private void setApkInternal(Apk apk, Status status, PendingIntent intent) { if (apk == null) { return; diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index ec57e4d45..9b1e9bb6c 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -97,6 +97,7 @@ public class FDroidApp extends Application { @SuppressWarnings("unused") BluetoothAdapter bluetoothAdapter; + NotificationHelper notificationHelper; static { SPONGYCASTLE_PROVIDER = new org.spongycastle.jce.provider.BouncyCastleProvider(); @@ -262,7 +263,7 @@ public class FDroidApp extends Application { CleanCacheService.schedule(this); - NotificationHelper.create(getApplicationContext()); + notificationHelper = new NotificationHelper(getApplicationContext()); UpdateService.schedule(getApplicationContext()); bluetoothAdapter = getBluetoothAdapter(); diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index 2b7bcae46..64a10b1d1 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -7,6 +7,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.graphics.Bitmap; +import android.graphics.Point; import android.graphics.Typeface; import android.os.Build; import android.support.v4.app.NotificationCompat; @@ -15,9 +16,16 @@ import android.support.v4.content.LocalBroadcastManager; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.StyleSpan; +import android.view.View; +import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.assist.ImageScaleType; import com.nostra13.universalimageloader.core.assist.ImageSize; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; +import com.nostra13.universalimageloader.utils.DiskCacheUtils; +import com.nostra13.universalimageloader.utils.MemoryCacheUtils; import org.fdroid.fdroid.data.App; @@ -40,25 +48,23 @@ class NotificationHelper { private static final String GROUP_UPDATES = "updates"; private static final String GROUP_INSTALLED = "installed"; - private static final String LOGTAG = "NotificationHelper"; - - private static NotificationHelper instance; - - public static NotificationHelper create(Context context) { - if (instance == null) { - instance = new NotificationHelper(context.getApplicationContext()); - } - return instance; - } - private final Context context; private final NotificationManagerCompat notificationManager; - private final AppUpdateStatusManager appUpdateStatusMananger; + private final AppUpdateStatusManager appUpdateStatusManager; + private final DisplayImageOptions displayImageOptions; + private ArrayList updates; + private ArrayList installed; - private NotificationHelper(Context context) { + NotificationHelper(Context context) { this.context = context; - appUpdateStatusMananger = AppUpdateStatusManager.getInstance(context); + appUpdateStatusManager = AppUpdateStatusManager.getInstance(context); notificationManager = NotificationManagerCompat.from(context); + displayImageOptions = new DisplayImageOptions.Builder() + .cacheInMemory(true) + .cacheOnDisk(true) + .imageScaleType(ImageScaleType.NONE) + .bitmapConfig(Bitmap.Config.RGB_565) + .build(); // We need to listen to when notifications are cleared, so that we "forget" all that we currently know about updates // and installs. @@ -67,12 +73,76 @@ class NotificationHelper { filter.addAction(BROADCAST_NOTIFICATIONS_ALL_INSTALLED_CLEARED); filter.addAction(BROADCAST_NOTIFICATIONS_UPDATE_CLEARED); filter.addAction(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED); + BroadcastReceiver receiverNotificationsCleared = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case BROADCAST_NOTIFICATIONS_ALL_UPDATES_CLEARED: + appUpdateStatusManager.clearAllUpdates(); + break; + case BROADCAST_NOTIFICATIONS_ALL_INSTALLED_CLEARED: + appUpdateStatusManager.clearAllInstalled(); + break; + case BROADCAST_NOTIFICATIONS_UPDATE_CLEARED: + break; + case BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED: + String key = intent.getStringExtra(EXTRA_NOTIFICATION_KEY); + appUpdateStatusManager.removeApk(key); + break; + } + } + }; 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); + BroadcastReceiver receiverAppStatusChanges = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED: + notificationManager.cancelAll(); + updateStatusLists(); + createSummaryNotifications(); + for (AppUpdateStatusManager.AppUpdateStatus entry : appUpdateStatusManager.getAll()) { + createNotification(entry); + } + break; + case AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED: { + updateStatusLists(); + createSummaryNotifications(); + String url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); + AppUpdateStatusManager.AppUpdateStatus entry = appUpdateStatusManager.get(url); + if (entry != null) { + createNotification(entry); + } + break; + } + case AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED: { + String url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); + AppUpdateStatusManager.AppUpdateStatus entry = appUpdateStatusManager.get(url); + updateStatusLists(); + if (entry != null) { + createNotification(entry); + } + if (intent.getBooleanExtra(AppUpdateStatusManager.EXTRA_IS_STATUS_UPDATE, false)) { + createSummaryNotifications(); + } + break; + } + case AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED: { + String url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); + notificationManager.cancel(url, NOTIFY_ID_INSTALLED); + notificationManager.cancel(url, NOTIFY_ID_UPDATES); + updateStatusLists(); + createSummaryNotifications(); + break; + } + } + } + }; LocalBroadcastManager.getInstance(context).registerReceiver(receiverAppStatusChanges, filter); } @@ -80,76 +150,100 @@ class NotificationHelper { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; } - private void updateSummaryNotifications() { + private void updateStatusLists() { if (!notificationManager.areNotificationsEnabled()) { return; } // Get the list of updates and installed available - ArrayList updates = new ArrayList<>(); - ArrayList 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 { + if (updates == null) { + updates = new ArrayList<>(); + } else { + updates.clear(); + } + if (installed == null) { + installed = new ArrayList<>(); + } else { + installed.clear(); + } + for (AppUpdateStatusManager.AppUpdateStatus entry : appUpdateStatusManager.getAll()) { + if (entry.status == AppUpdateStatusManager.Status.Installed) { installed.add(entry); + } else if (entry.status != AppUpdateStatusManager.Status.Unknown) { + updates.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 createNotification(AppUpdateStatusManager.AppUpdateStatus entry) { - if (useStackedNotifications() && notificationManager.areNotificationsEnabled() && entry.status != AppUpdateStatusManager.Status.Unknown) { - doCreateNotification(entry); + if (entry.status == AppUpdateStatusManager.Status.Unknown) { + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); + return; + } + if (notificationManager.areNotificationsEnabled()) { + NotificationCompat.Builder builder; + if (entry.status == AppUpdateStatusManager.Status.Installed) { + if (useStackedNotifications()) { + builder = createInstalledNotification(entry); + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); + notificationManager.notify(entry.getUniqueKey(), NOTIFY_ID_INSTALLED, builder.build()); + } else if (installed.size() == 1) { + builder = createInstalledNotification(entry); + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); + notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, builder.build()); + } + } else { + if (useStackedNotifications()) { + builder = createUpdateNotification(entry); + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); + notificationManager.notify(entry.getUniqueKey(), NOTIFY_ID_UPDATES, builder.build()); + } else if (updates.size() == 1) { + builder = createUpdateNotification(entry); + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); + notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, builder.build()); + } + } } } - private void doCreateNotification(AppUpdateStatusManager.AppUpdateStatus entry) { - NotificationCompat.Builder builder; - 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); + + private void createSummaryNotifications() { + if (!notificationManager.areNotificationsEnabled()) { + return; + } + + NotificationCompat.Builder builder; + if (updates.size() != 1 || useStackedNotifications()) { + if (updates.size() == 0) { + // No updates, remove summary + notificationManager.cancel(GROUP_UPDATES, NOTIFY_ID_UPDATES); + } else { + builder = createUpdateSummaryNotification(updates); + notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, builder.build()); + } + } + if (installed.size() != 1 || useStackedNotifications()) { + 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()); + } } - notificationManager.notify(entry.getUniqueKey(), id, builder.build()); } 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); + return new NotificationCompat.Action(R.drawable.ic_notify_update_24dp, context.getString(R.string.notification_action_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); + return new NotificationCompat.Action(R.drawable.ic_notify_cancel_24dp, context.getString(R.string.notification_action_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_install_24dp, context.getString(R.string.notification_action_install), entry.intent); } } return null; @@ -158,17 +252,17 @@ class NotificationHelper { private String getSingleItemTitleString(App app, AppUpdateStatusManager.Status status) { switch (status) { case UpdateAvailable: - return "Update Available"; + return context.getString(R.string.notification_title_single_update_available); case Downloading: return app.name; case ReadyToInstall: - return "Update ready to install"; // TODO - "Update"? Should just be "ready to install"? + return context.getString(app.isInstalled() ? R.string.notification_title_single_ready_to_install_update : R.string.notification_title_single_ready_to_install); // TODO - "Update"? Should just be "ready to install"? case Installing: return app.name; case Installed: return app.name; case InstallError: - return "Install Failed"; + return context.getString(R.string.notification_title_single_install_error); } return ""; } @@ -178,15 +272,15 @@ class NotificationHelper { case UpdateAvailable: return app.name; case Downloading: - return String.format("Downloading update for \"%s\"...", app.name); + return context.getString(R.string.notification_content_single_downloading, app.name); case ReadyToInstall: return app.name; case Installing: - return String.format("Installing \"%s\"...", app.name); + return context.getString(R.string.notification_content_single_installing, app.name); case Installed: - return "Successfully installed"; + return context.getString(R.string.notification_content_single_installed); case InstallError: - return "Install Failed"; + return context.getString(R.string.notification_content_single_install_error); } return ""; } @@ -194,17 +288,17 @@ class NotificationHelper { private String getMultiItemContentString(App app, AppUpdateStatusManager.Status status) { switch (status) { case UpdateAvailable: - return "Update available"; + return context.getString(R.string.notification_title_summary_update_available); case Downloading: - return "Downloading update..."; + return context.getString(app.isInstalled() ? R.string.notification_title_summary_downloading_update : R.string.notification_title_summary_downloading); case ReadyToInstall: - return "Ready to install"; + return context.getString(app.isInstalled() ? R.string.notification_title_summary_ready_to_install_update : R.string.notification_title_summary_ready_to_install); case Installing: - return "Installing"; + return context.getString(R.string.notification_title_summary_installing); case Installed: - return "Successfully installed"; + return context.getString(R.string.notification_title_summary_installed); case InstallError: - return "Install Failed"; + return context.getString(R.string.notification_title_summary_install_error); } return ""; } @@ -213,18 +307,12 @@ class NotificationHelper { App app = entry.app; 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; - + Bitmap iconLarge = getLargeIconForEntry(entry); NotificationCompat.Builder builder = new NotificationCompat.Builder(context) .setAutoCancel(false) .setLargeIcon(iconLarge) - .setSmallIcon(icon) + .setSmallIcon(R.drawable.ic_launcher) .setContentTitle(getSingleItemTitleString(app, status)) .setContentText(getSingleItemContentString(app, status)) .setGroup(GROUP_UPDATES); @@ -261,7 +349,7 @@ class NotificationHelper { } private NotificationCompat.Builder createUpdateSummaryNotification(ArrayList updates) { - String title = String.format("%d Updates", updates.size()); + String title = context.getString(R.string.notification_summary_updates, updates.size()); StringBuilder text = new StringBuilder(); NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); @@ -285,7 +373,7 @@ class NotificationHelper { } 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(context.getString(R.string.notification_summary_more, diff)); } // Intent to open main app list @@ -316,16 +404,14 @@ class NotificationHelper { private NotificationCompat.Builder createInstalledNotification(AppUpdateStatusManager.AppUpdateStatus entry) { App app = entry.app; - int largeIconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); - Bitmap iconLarge = ImageLoader.getInstance().loadImageSync(app.iconUrl, new ImageSize(largeIconSize, largeIconSize)); - + Bitmap iconLarge = getLargeIconForEntry(entry); NotificationCompat.Builder builder = new NotificationCompat.Builder(context) .setAutoCancel(true) .setLargeIcon(iconLarge) .setSmallIcon(R.drawable.ic_stat_notify_updates) .setContentTitle(app.name) - .setContentText("Successfully Installed") + .setContentText(context.getString(R.string.notification_content_single_installed)) .setGroup(GROUP_INSTALLED); PackageManager pm = context.getPackageManager(); @@ -341,7 +427,7 @@ class NotificationHelper { } private NotificationCompat.Builder createInstalledSummaryNotification(ArrayList installed) { - String title = String.format("%d Apps Installed", installed.size()); + String title = context.getString(R.string.notification_summary_installed, installed.size()); StringBuilder text = new StringBuilder(); NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle(); @@ -357,7 +443,7 @@ class NotificationHelper { bigTextStyle.bigText(text); if (installed.size() > MAX_INSTALLED_TO_SHOW) { int diff = installed.size() - MAX_INSTALLED_TO_SHOW; - bigTextStyle.setSummaryText(context.getString(R.string.update_notification_more, diff)); + bigTextStyle.setSummaryText(context.getString(R.string.notification_summary_more, diff)); } // Intent to open main app list @@ -381,65 +467,58 @@ class NotificationHelper { 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 Point getLargeIconSize() { + int w; + int h; + if (Build.VERSION.SDK_INT >= 11) { + w = context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width); + h = context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height); + } else { + w = h = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); } - }; + return new Point(w, h); + } - 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; + private Bitmap getLargeIconForEntry(AppUpdateStatusManager.AppUpdateStatus entry) { + final Point largeIconSize = getLargeIconSize(); + Bitmap iconLarge = null; + if (DiskCacheUtils.findInCache(entry.app.iconUrl, ImageLoader.getInstance().getDiskCache()) != null) { + iconLarge = ImageLoader.getInstance().loadImageSync(entry.app.iconUrl, new ImageSize(largeIconSize.x, largeIconSize.y), displayImageOptions); + } else { + // Load it for later! + ImageLoader.getInstance().loadImage(entry.app.iconUrl, new ImageSize(largeIconSize.x, largeIconSize.y), displayImageOptions, new ImageLoadingListener() { + AppUpdateStatusManager.AppUpdateStatus entry; + ImageLoadingListener init(AppUpdateStatusManager.AppUpdateStatus entry) { + this.entry = entry; + return this; } - 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; + + @Override + public void onLoadingStarted(String imageUri, View view) { + } - 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; + + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + } - } + + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + // Need to check that the notification is still valid, and also that the image + // is indeed cached now, so we won't get stuck in an endless loop. + AppUpdateStatusManager.AppUpdateStatus oldEntry = appUpdateStatusManager.get(entry.getUniqueKey()); + if (oldEntry != null && DiskCacheUtils.findInCache(oldEntry.app.iconUrl, ImageLoader.getInstance().getDiskCache()) != null) { + createNotification(oldEntry); // Update with new image! + } + } + + @Override + public void onLoadingCancelled(String imageUri, View view) { + + } + }.init(entry)); } - }; + return iconLarge; + } } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index 528c9bc7b..daec607db 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -14,6 +14,7 @@ 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.Utils; @@ -107,8 +108,9 @@ public class InstallManagerService extends Service { @Override public void onReceive(Context context, Intent intent) { String packageName = intent.getData().getSchemeSpecificPart(); - //TODO: do we need to mark as installed, or is this handled by other code already? - //appUpdateStatusManager.removeApk(packageName); + for (AppUpdateStatusManager.AppUpdateStatus status : appUpdateStatusManager.getByPackageName(packageName)) { + appUpdateStatusManager.updateApk(status.getUniqueKey(), AppUpdateStatusManager.Status.Installed, null); + } } }; IntentFilter intentFilter = new IntentFilter(); @@ -292,7 +294,7 @@ public class InstallManagerService extends Service { } break; case Downloader.ACTION_INTERRUPTED: - appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.UpdateAvailable, null); + appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.Unknown, null); localBroadcastManager.unregisterReceiver(this); break; default: @@ -325,28 +327,23 @@ public class InstallManagerService extends Service { localBroadcastManager.unregisterReceiver(this); break; case Installer.ACTION_INSTALL_INTERRUPTED: - appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.ReadyToInstall, null); - + AppUpdateStatusManager.AppUpdateStatus status = appUpdateStatusManager.get(downloadUrl); + appUpdateStatusManager.removeApk(downloadUrl); apk = intent.getParcelableExtra(Installer.EXTRA_APK); String errorMessage = intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE); // show notification if app details is not visible if (!TextUtils.isEmpty(errorMessage)) { - 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 + if (status == null || status.app == null || !AppDetails.isAppVisible(status.app.packageName)) { + appUpdateStatusManager.setApkError(apk, errorMessage); + } } 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); + PendingIntent installPendingIntent = intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI); appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.ReadyToInstall, installPendingIntent); break; default: diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index d4e8e770e..274e49035 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -122,7 +122,7 @@ يتوفر تحديث. %d يتوفر على تحديثات. تحديثات اف-درويد متوفرة - +%1$d المزيد… + +%1$d المزيد… لا توجد طريقة إرسال بلوتوث، فضلاً إختر واحدة ! هذه المستودعات بالفعل موجودة، بهذا سوف يتم إضافة مفتاح معلومات جديد. هذه المستودعات بالفعل موجودة، تأكد إن كنت ترغب في إعادة تمكينه. diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml index 84d7f50cb..d769396e7 100644 --- a/app/src/main/res/values-ast/strings.xml +++ b/app/src/main/res/values-ast/strings.xml @@ -214,7 +214,7 @@ rehabilitar esti repositoriu pa instalar aplicaciones dende elli. Atrás Instaláu - +%1$d más… + +%1$d más… Buelga incorreuta Esto nun ye una URL válida. Rexistru de cambeos diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 8db31a1f1..8f6df2c82 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -181,7 +181,7 @@ Изходен код Инсталирани - + още %1$d… + + още %1$d… Актуализиране на хранилищата Инсталирането се провали поради неизвестна грешка Деинсталирането се провали поради неизвестна грешка diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 24eca82b3..43cd7a666 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -181,7 +181,7 @@ tornar a habilitar el dipòsit per instal·lar aplicacions d\'aquest. Enllaços Torna - +%1$d més… + +%1$d més… Idioma Wi-Fi Gràfics diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index b8f9cf24e..9d7c9f706 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -152,7 +152,7 @@ Pro instalaci aplikací z tohoto repozitáře ho bude nejprve třeba znovu povol Odkazy Zpět - +%1$d více… + +%1$d více… Nerozpoznána žádná Bluetooth metoda přenosu, nějakou vyberte! Vyberte metodu přenosu Bluetooth Tento repozitář je již nastaven, budou jen přidány nové klíče. diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 93d9dc9aa..1388c2e2a 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -78,7 +78,7 @@ 1 opdatering er tilgængelig. %d opdateringer er tilgængelige. F-Droid Opdateringer Tilgængelige - +%1$d flere… + +%1$d flere… Ingen Bluetooth afsendelsesmetode fundet, vælg en! Vælg Bluetooth afsendelsesmetode Send over Bluetooth diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b5a7e4e8f..44e75d98e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -182,7 +182,7 @@ um Anwendungen daraus installieren zu können. Inkompatibel Verweise Zurück - +%1$d weitere … + +%1$d weitere … Falscher Fingerabdruck Das ist keine gültige Adresse. Paketquellen werden aktualisiert diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index f546703ae..f004b3e07 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -142,7 +142,7 @@ Πίσω Εγκατεστημένο - +%1$d περισσότερα… + +%1$d περισσότερα… Αποστολή μέσω Bluetooth Λανθασμένο δακτυλικό αποτύπωμα diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 4d06f7d1f..b301ab46a 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -151,7 +151,7 @@ Instalitaj Instalitaj (%d) Ĝisdatigoj (%d) - ankoraŭ +%1$d… + ankoraŭ +%1$d… Neniu Bludenta metodo de sendo trovita, elektu iun! Elektu Bludentan metodon de sendo Fingrospuro (malnepra) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index bf26e5baf..a2923498a 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -182,7 +182,7 @@ Para ver las aplicaciones que ofrece tienes que activarlo. Enlaces Volver - +%1$d más… + +%1$d más… Actualiza/Desinstala la extensión con permisos de sistema Abre la pantalla de detalles de la extensión con permisos de sistema para actualizarla/desinstalarla Huella digital incorrecta diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index f9fbbac08..2b75d1e18 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -81,7 +81,7 @@ Üks värskendus on saadaval. %d värskendust on saadaval. F-Droid: värskendused saadaval - +%1$d veel… + +%1$d veel… Bluetoothiga saatmisviise ei leitud. Valige üks! Vali Bluetoothiga saatmise viis Saada Bluetoothiga diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index fbe4f6ee6..8ca03d7c1 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -99,7 +99,7 @@ Loturak Atzera - +%1$d gehiago… + +%1$d gehiago… Ez da aurkitu Bluetooth bidez bidaltzeko metodorik, aukeratu bat! Aukeratu Bluetooth bidez bidaltzeko metodoa Bidali Bluetooth bidez diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 23fea4a60..eeca7fdf3 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -116,7 +116,7 @@ به‌روز رسانی‌ها (%d) ۱ به‌روز رسانی موجود است. %d به روز رسانی موجود است. - +%1$d بیش‌تر… + +%1$d بیش‌تر… ارسال با بلوتوث این مخزن از پیش برپا شده است. این کار، اظّلاعات کلید جدیدی را می‌افزاید. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index af32eda3f..b97a36663 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -183,7 +183,7 @@ Asennettu Asennettu (%d) Päivitykset (%d) - +%1$d lisää… + +%1$d lisää… Lataa päivitykset automaattisesti Lataa päivitykset taustalla Päivitä/Poista Privileged Extension diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2947b2114..1d130254c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -284,7 +284,7 @@ perdues. Elle ne requiert aucune autorisation particulière. Toutes les heures Fourni par %1$s. Ouvrir l\'écran des détails de l\'appli F-Droid Privileged Extension pour la mettre à jour/la désinstaller - %1$d de plus… + %1$d de plus… Téléchargement de \n%2$s depuis \n%1$s diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 23933a733..2f795d276 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -101,7 +101,7 @@ Sobreescribir Instalado - +%1$d máis… + +%1$d máis… Non se atopou ningún método de transmisión Bluetooth, escolla un! Escolla o método de transmisión Bluetooth Enviar por Bluetooth diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index d11df15e2..18fd1eeb8 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -57,7 +57,7 @@ עדכון 1 זמין. %d עדכונים זמינים. עדכוני F-Droid זמינים - +%1$d עוד… + +%1$d עוד… לא נמצאה שיטת שליחה של Bluetooth, בחר אחת! בחר שיטת שליחה של Bluetooth שלח דרך Bluetooth diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 6ae62ef52..276900dc7 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -150,7 +150,7 @@ १ अपडेट उपलब्ध है| %d अपडेटस उपलब्ध हैं| F-Droid अपडेट उपलब्ध - +%1$d और… + +%1$d और… Bluettoth से भेजने का तरीका उपलब नहीं है, एक चुने! Bluetooth से भेजने का तरीका चुने Bluetooth द्वारा भेजे diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 8a87b3402..68d0c5ea7 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -81,7 +81,7 @@ 1 ažuriranje je dostupno. %d ažuriranja je dostupno. Ažuriranja za F-Droid dostupna - +%1$d više… + +%1$d više… Nije pronađena metoda slanja preko Bluetootha, odaberite jednu! Odaberi metodu slanja preko Bluetootha Pošalji preko Bluetootha diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index a0e71c4b3..f57f2d214 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -126,7 +126,7 @@ Engedélyeznie kell, hogy megtekinthesse az általa kínált appokat. Telepítve Telepítve (%d) Frissítések (%d) - +%1$d további… + +%1$d további… Küldés Bluetooth-on Rossz ujjlenyomat diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 0c422dde7..2cef07854 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -166,7 +166,7 @@ Terakhir diperbarui Nama Tidak diketahui - +%1$d lainnya… + +%1$d lainnya… Pengaturan Jangan bedakan warna apl yang membutuhkan izin root Ini artinya daftar diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index ee53c696a..a539847ed 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -69,7 +69,7 @@ 1 uppfærsla er tiltæk. %d uppfærslur eru tiltækar. Uppfærslur á F-Droid eru tiltækar - +%1$d fleiri… + +%1$d fleiri… Engin aðferð til sendingar með Bluetooth fannst, veldu eina! Veldu aðferð til sendingar með Bluetooth Senda með Bluetooth diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 6816d5f7d..087465c82 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -333,7 +333,7 @@ non saranno rimossi. Non è richiesto alcun accesso speciale. di questa app integrata? I dati presenti non saranno rimossi. Non richiede alcun permesso speciale. Tutte - +%1$d rimanente… + +%1$d rimanente… Scambio ravvicinato Nessuna app installata corrispondente. Puoi aggiungere altre informazioni e commenti qui: diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1c99463ab..3ebec0d32 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -181,7 +181,7 @@ リンク 戻る - +%1$d 以上… + +%1$d 以上… フィンガープリントが違います 有効な URL ではありません。 変更履歴 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index b6e1f9fee..0a8977657 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -194,7 +194,7 @@ 설치됨 설치됨 (%d) 업데이트 (%d) - +%1$d 이상… + +%1$d 이상… 올바르지 않은 핑거프린트 올바른 URL이 아닙니다. 설정 diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index f71147699..2bfc0f244 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -77,7 +77,7 @@ အသစ္မြမ္းမံမႈ ၁ ခုရရွိႏိုင္သည္ အသစ္မြမ္းမံမႈ %d ခုရရွိႏိုင္သည္. F-Droid အသစ္မြမ္းမံမႈရရွိႏိုင္သည္ - +%1$d ေနာက္ထပ္.. + +%1$d ေနာက္ထပ္.. ဘလူးသုဒ့္ႏွင့္ပို႔ရန္နည္းလမ္းရွာမေတြ႕ပါ။ တစ္ခုေရြးပါ! ဘလူးသုဒ့္ႏွင့္ပို႔ေသာနည္းလမ္းကိုေရြးမည္ ဘလူးသုဒ့္မွတစ္ဆင့္ပို႔မည္ diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 3b481e6eb..64a3c0ee5 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -185,7 +185,7 @@ skru på denne pakkebrønnen igjen for å installere programmer fra den.Tilbake Installert - +%1$d mer… + +%1$d mer… Feil i fingeravtrykk Dette er ikke en gyldig nettadresse. Endringslogg diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index b3a5681a4..6b9e58fbd 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -179,7 +179,7 @@ Je moet deze bron weer inschakelen indien je er apps van wil installeren.Links Terug - +%1$d meer… + +%1$d meer… Slechte vingerafdruk Dit is geen correcte URL. Lijst van veranderingen diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index bd646f2f5..7570096e5 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -155,7 +155,7 @@ Uwaga: Wszystkie poprzednio zainstalowane aplikacje zostaną na urządzeniu.Powrót Liczba dostępnych aktualizacji: %d. - +%1$d więcej… + +%1$d więcej… Nie znaleziono metody do wysłania przez Bluetooth! Wybierz metodę wysłania przez Bluetooth To repozytorium jest już dodane. Klucz zostanie zaktualizowany. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index f05d62636..b2c72e12f 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -186,7 +186,7 @@ reativar este repositório para instalar aplicativos a partir dele. Links Voltar - Mais +%1$d… + Mais +%1$d… Falha na fingerprint Esta não é uma URL válida. Changelog diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index df67aa098..618a31938 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -66,7 +66,7 @@ 1 atualização disponível. %d atualizações disponíveis. Atualizações F-Droid disponíveis - +%1$d… + +%1$d… Nenhum método de envio Bluetooth encontrado. Escolha um! Escolha o método de envio Bluetooth Enviar por Bluetooth diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 4c2cf84d7..4ae5ada7b 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -97,7 +97,7 @@ Instalat (%d) Actualizari (%d) Actualizari disponibile in F-Droid - +%1$d mai mult… + +%1$d mai mult… Nu a fost gasita o metoda Bluetooth de a trimite, alegeti una! Alegeti metoda de a trimite prin Bluetooth Trimite prin Bluetooth diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 164deaf8f..b3cfbf3cf 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -212,7 +212,7 @@ Меньше Назад - Детали +%1$d … + Детали +%1$d … Неверный отпечаток ключа URL некорректен. Список изменений diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml index b27a228ba..f5e0fbd99 100644 --- a/app/src/main/res/values-sc/strings.xml +++ b/app/src/main/res/values-sc/strings.xml @@ -184,7 +184,7 @@ Depes Ligàmenes In dae segus - +%1$d àteru(os)… + +%1$d àteru(os)… Custu no est unu ligàmene vàlidu. Lista modìficas Agiornende sos depòsitos diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index f8ddc6acc..53b71a12e 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -187,7 +187,7 @@ znovu povoliť tento repozitár pre inštaláciu aplikácií z neho. Späť Nainštalované - +%1$d viac… + +%1$d viac… Posielanie cez Bluetooth zlyhalo, zvoľte inú metódu! Vyberte posielanie cez Bluetooth Tento repozitár je už nastavený a povolený. diff --git a/app/src/main/res/values-sn/strings.xml b/app/src/main/res/values-sn/strings.xml index 4b6c7189e..521acf544 100644 --- a/app/src/main/res/values-sn/strings.xml +++ b/app/src/main/res/values-sn/strings.xml @@ -76,7 +76,7 @@ Chekunatsa 1 chiripo. Zvekunatsa %d zviripo. Zvekunatsa F-Droid zviripo - zvimwe +%1$d … + zvimwe +%1$d … Hapana mutowo wekutumira neBluetooth wawanikwa, sarudza imwe chete! Sarudza mutowo weBluetooth wekutumira nawo Tumira kuburikira neBluetooth diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 5e872ecc4..62d7a0b6a 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -188,7 +188,7 @@ Назад Инсталиране - +још %1$d… + +још %1$d… Лош отисак Ово није исправна адреса. Дневник измена diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index b5ce211ad..64f7ef44a 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -191,7 +191,7 @@ Du kommer Bakåt Installerade - +%1$d mer… + +%1$d mer… Felaktigt fingeravtryck Det här är inte en giltig hemsideadress. Inställningar diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index f85cf6149..88791c754 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -314,7 +314,7 @@ ไม่แสดงตัวบน Wi-Fi ไม่พบคนรอบข้างที่สามารถจะแบ่งปันโปรแกรมด้วยได้ QR Code ที่อ่านได้ ไม่ใช่โค้ดที่ใช้แบ่งปันโปรแกรม - มีอีก +%1$d… + มีอีก +%1$d… จัดให้โดย %1$s ไม่เจอคนที่ตามหาหรือ? แลกเปลี่ยนโปรแกรมกับคนข้างๆ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index c395ba64e..701ec9fc9 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -203,7 +203,7 @@ bu depoyu tekrar etkinleştirmeniz gerekecektir. Kurulu Kurulu (%d) Güncellemeler (%d) - +%1$d daha… + +%1$d daha… Yanlış parmak izi Bu, geçerli bir URL değildir. Ayarlar diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 9b3c0a68d..8a045d226 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -90,7 +90,7 @@ Одне оновлення доступно. %d оновлень доступно. F-Droid: доступні оновлення - +%1$d більше… + +%1$d більше… Надіслати через Bluetooth Поділитися diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 52bd4cf62..7f61fc486 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -168,7 +168,7 @@ Mã nguồn Không tương thích Đã cài đặt - +%1$d ứng dụng khác… + +%1$d ứng dụng khác… Đây không phải là URL hợp lệ. Lịch sử sửa đổi Bitcoin diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 414b45921..6cc9d8922 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -168,7 +168,7 @@ 已安装 已安装(%d) 可更新(%d) - +%1$d 更多… + +%1$d 更多… 用蓝牙发送 指纹错误 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index f92b5f6fd..6c4698543 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -228,7 +228,7 @@ 您想更新此應用程式嗎? 您不會失去現有的數據, 已更新的程式亦不需要任何特別的存取權。 - 還有 +%1$d 個… + 還有 +%1$d 個… 下一步 %1$s 至 %2$s 應用程式的新版本使用了不同的鑰匙簽署。若要安裝新版本,您必須先將舊版本卸載,然後再嘗試安裝。(注意:卸載將會把應用程式內的資料刪除) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index b5ac9b000..8d4018a3a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -257,7 +257,7 @@ 使用私密連線 空的使用者名稱,憑證未改變 - +%1$d 更多… + +%1$d 更多… 此應用軟體倉庫已經設立。確認您要重新啟用它。 進入的儲存庫已設立並已啟用。 略過異常的應用軟體倉庫 URI:%s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ea70e6a6c..54eeb414a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -88,10 +88,7 @@ Updates Installed (%d) Updates (%d) - 1 update is available. - %d updates are available. F-Droid Updates Available - +%1$d more… No Bluetooth send method found, choose one! Choose Bluetooth send method Send via Bluetooth @@ -400,4 +397,29 @@ forcing the application to stop. Would you like to e-mail the details to help fix the issue? You can add extra information and comments here: + + + +%1$d more… + Update Available + Ready to install + Update ready to install + Install Failed + Downloading update for \"%1$s\"… + Installing \"%1$s\"… + Successfully installed + Install Failed + %1$d Updates + %1$d Apps Installed + Update available + Downloading… + Downloading update… + Ready to install + Update ready to install + Installing + Successfully installed + Install Failed + Update + Cancel + Install + From 046186047966225cd3cfec060faf78600b5db518 Mon Sep 17 00:00:00 2001 From: mvp76 Date: Wed, 15 Feb 2017 15:55:31 +0100 Subject: [PATCH 04/31] Removed "installed" notification when entering Details screen. --- app/src/main/java/org/fdroid/fdroid/AppDetails.java | 8 ++++++++ app/src/main/java/org/fdroid/fdroid/AppDetails2.java | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java index 8ff6ee17b..dc8c00e9c 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java @@ -701,6 +701,14 @@ public class AppDetails extends AppCompatActivity { app = newApp; startingPrefs = app.getPrefs(this).createClone(); + + // Remove all "installed" statuses for this app, since we are now viewing it. + AppUpdateStatusManager appUpdateStatusManager = AppUpdateStatusManager.getInstance(this); + for (AppUpdateStatusManager.AppUpdateStatus status : appUpdateStatusManager.getByPackageName(app.packageName)) { + if (status.status == AppUpdateStatusManager.Status.Installed) { + appUpdateStatusManager.removeApk(status.getUniqueKey()); + } + } } private void refreshApkList() { diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java index 52e06311f..317f21084 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java @@ -121,6 +121,14 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog return; } app = newApp; + + // Remove all "installed" statuses for this app, since we are now viewing it. + AppUpdateStatusManager appUpdateStatusManager = AppUpdateStatusManager.getInstance(this); + for (AppUpdateStatusManager.AppUpdateStatus status : appUpdateStatusManager.getByPackageName(app.packageName)) { + if (status.status == AppUpdateStatusManager.Status.Installed) { + appUpdateStatusManager.removeApk(status.getUniqueKey()); + } + } } @Override From 28d683e16a790d48b0526859f4566d1294815cb0 Mon Sep 17 00:00:00 2001 From: mvp76 Date: Wed, 22 Feb 2017 09:57:47 +0100 Subject: [PATCH 05/31] Add intent for "installed" state --- .../java/org/fdroid/fdroid/AppUpdateStatusManager.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index fb34c9db3..6e57b22d3 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -5,6 +5,7 @@ import android.app.PendingIntent; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.support.v4.app.TaskStackBuilder; import android.support.v4.content.LocalBroadcastManager; @@ -292,6 +293,15 @@ public class AppUpdateStatusManager { return getAppDetailsIntent(entry.apk); } else if (entry.status == Status.InstallError) { return getAppErrorIntent(entry); + } else if (entry.status == Status.Installed) { + PackageManager pm = context.getPackageManager(); + Intent intentObject = pm.getLaunchIntentForPackage(entry.app.packageName); + if (intentObject != null) { + return PendingIntent.getActivity(context, 0, intentObject, 0); + } else { + // Could not get launch intent, maybe not launchable, e.g. a keyboard + return getAppDetailsIntent(entry.apk); + } } return null; } From f7e12b4f2559f1a650cb7f69a521ff288609393d Mon Sep 17 00:00:00 2001 From: mvp76 Date: Thu, 23 Feb 2017 11:42:44 +0100 Subject: [PATCH 06/31] Icon fixes and general tweaks --- .../org/fdroid/fdroid/NotificationHelper.java | 77 ++++++++++++------- .../res/drawable/ic_notification_download.xml | 13 ++++ app/src/main/res/values/strings.xml | 5 +- 3 files changed, 65 insertions(+), 30 deletions(-) create mode 100644 app/src/main/res/drawable/ic_notification_download.xml diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index 64a10b1d1..a2c96d300 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -5,11 +5,13 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; import android.graphics.Bitmap; +import android.graphics.Canvas; import android.graphics.Point; import android.graphics.Typeface; +import android.graphics.drawable.Drawable; import android.os.Build; +import android.support.graphics.drawable.VectorDrawableCompat; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; import android.support.v4.content.LocalBroadcastManager; @@ -25,7 +27,6 @@ import com.nostra13.universalimageloader.core.assist.ImageScaleType; import com.nostra13.universalimageloader.core.assist.ImageSize; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import com.nostra13.universalimageloader.utils.DiskCacheUtils; -import com.nostra13.universalimageloader.utils.MemoryCacheUtils; import org.fdroid.fdroid.data.App; @@ -169,14 +170,24 @@ class NotificationHelper { for (AppUpdateStatusManager.AppUpdateStatus entry : appUpdateStatusManager.getAll()) { if (entry.status == AppUpdateStatusManager.Status.Installed) { installed.add(entry); - } else if (entry.status != AppUpdateStatusManager.Status.Unknown) { + } else if (!shouldIgnoreEntry(entry)) { updates.add(entry); } } } + private boolean shouldIgnoreEntry(AppUpdateStatusManager.AppUpdateStatus entry) { + // Ignore unknown status + if (entry.status == AppUpdateStatusManager.Status.Unknown) + return true; + // Ignore first time install downloads, assumed to be done from UI + else if (!entry.app.isInstalled() && (entry.status == AppUpdateStatusManager.Status.Downloading || entry.status == AppUpdateStatusManager.Status.ReadyToInstall)) + return true; + return false; + } + private void createNotification(AppUpdateStatusManager.AppUpdateStatus entry) { - if (entry.status == AppUpdateStatusManager.Status.Unknown) { + if (shouldIgnoreEntry(entry)) { notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); return; @@ -256,7 +267,7 @@ class NotificationHelper { case Downloading: return app.name; case ReadyToInstall: - return context.getString(app.isInstalled() ? R.string.notification_title_single_ready_to_install_update : R.string.notification_title_single_ready_to_install); // TODO - "Update"? Should just be "ready to install"? + return context.getString(R.string.notification_title_single_ready_to_install_update); case Installing: return app.name; case Installed: @@ -272,7 +283,7 @@ class NotificationHelper { case UpdateAvailable: return app.name; case Downloading: - return context.getString(R.string.notification_content_single_downloading, app.name); + return context.getString(R.string.notification_content_single_downloading_update, app.name); case ReadyToInstall: return app.name; case Installing: @@ -280,19 +291,19 @@ class NotificationHelper { case Installed: return context.getString(R.string.notification_content_single_installed); case InstallError: - return context.getString(R.string.notification_content_single_install_error); + return app.name; } return ""; } - private String getMultiItemContentString(App app, AppUpdateStatusManager.Status status) { + private String getMultiItemContentString(AppUpdateStatusManager.Status status) { switch (status) { case UpdateAvailable: return context.getString(R.string.notification_title_summary_update_available); case Downloading: - return context.getString(app.isInstalled() ? R.string.notification_title_summary_downloading_update : R.string.notification_title_summary_downloading); + return context.getString(R.string.notification_title_summary_downloading_update); case ReadyToInstall: - return context.getString(app.isInstalled() ? R.string.notification_title_summary_ready_to_install_update : R.string.notification_title_summary_ready_to_install); + return context.getString(R.string.notification_title_summary_ready_to_install_update); case Installing: return context.getString(R.string.notification_title_summary_installing); case Installed: @@ -307,14 +318,17 @@ class NotificationHelper { App app = entry.app; AppUpdateStatusManager.Status status = entry.status; + int iconSmall = R.drawable.ic_launcher; Bitmap iconLarge = getLargeIconForEntry(entry); NotificationCompat.Builder builder = new NotificationCompat.Builder(context) - .setAutoCancel(false) - .setLargeIcon(iconLarge) - .setSmallIcon(R.drawable.ic_launcher) + .setAutoCancel(true) .setContentTitle(getSingleItemTitleString(app, status)) .setContentText(getSingleItemContentString(app, status)) + .setSmallIcon(iconSmall) + .setLargeIcon(iconLarge) + .setLocalOnly(true) + .setVisibility(NotificationCompat.VISIBILITY_SECRET) .setGroup(GROUP_UPDATES); // Handle intents @@ -360,7 +374,7 @@ class NotificationHelper { App app = entry.app; AppUpdateStatusManager.Status status = entry.status; - String content = getMultiItemContentString(app, status); + String content = getMultiItemContentString(status); SpannableStringBuilder sb = new SpannableStringBuilder(app.name); sb.setSpan(new StyleSpan(Typeface.BOLD), 0, sb.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); sb.append(" "); @@ -382,15 +396,14 @@ class NotificationHelper { NotificationCompat.Builder builder = new NotificationCompat.Builder(context) - .setAutoCancel(true) + .setAutoCancel(!useStackedNotifications()) .setSmallIcon(R.drawable.ic_launcher) .setContentTitle(title) .setContentText(text) .setContentIntent(piAction) + .setLocalOnly(true) + .setVisibility(NotificationCompat.VISIBILITY_SECRET) .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); @@ -409,15 +422,16 @@ class NotificationHelper { new NotificationCompat.Builder(context) .setAutoCancel(true) .setLargeIcon(iconLarge) - .setSmallIcon(R.drawable.ic_stat_notify_updates) + .setSmallIcon(R.drawable.ic_launcher) .setContentTitle(app.name) .setContentText(context.getString(R.string.notification_content_single_installed)) + .setLocalOnly(true) + .setVisibility(NotificationCompat.VISIBILITY_SECRET) .setGroup(GROUP_INSTALLED); - PackageManager pm = context.getPackageManager(); - Intent intentObject = pm.getLaunchIntentForPackage(app.packageName); - PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, 0); - builder.setContentIntent(piAction); + if (entry.intent != null) { + builder.setContentIntent(entry.intent); + } Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED); intentDeleted.putExtra(EXTRA_NOTIFICATION_KEY, entry.getUniqueKey()); @@ -452,11 +466,13 @@ class NotificationHelper { NotificationCompat.Builder builder = new NotificationCompat.Builder(context) - .setAutoCancel(true) + .setAutoCancel(!useStackedNotifications()) .setSmallIcon(R.drawable.ic_launcher) .setContentTitle(title) .setContentText(text) - .setContentIntent(piAction); + .setContentIntent(piAction) + .setLocalOnly(true) + .setVisibility(NotificationCompat.VISIBILITY_SECRET); if (useStackedNotifications()) { builder.setGroup(GROUP_INSTALLED) .setGroupSummary(true); @@ -482,7 +498,16 @@ class NotificationHelper { private Bitmap getLargeIconForEntry(AppUpdateStatusManager.AppUpdateStatus entry) { final Point largeIconSize = getLargeIconSize(); Bitmap iconLarge = null; - if (DiskCacheUtils.findInCache(entry.app.iconUrl, ImageLoader.getInstance().getDiskCache()) != null) { + if (entry.status == AppUpdateStatusManager.Status.Downloading || entry.status == AppUpdateStatusManager.Status.Installing) { + Bitmap bitmap = Bitmap.createBitmap(largeIconSize.x, largeIconSize.y, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Drawable downloadIcon = VectorDrawableCompat.create(context.getResources(), R.drawable.ic_notification_download, context.getTheme()); + if (downloadIcon != null) { + downloadIcon.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + downloadIcon.draw(canvas); + } + return bitmap; + } else if (DiskCacheUtils.findInCache(entry.app.iconUrl, ImageLoader.getInstance().getDiskCache()) != null) { iconLarge = ImageLoader.getInstance().loadImageSync(entry.app.iconUrl, new ImageSize(largeIconSize.x, largeIconSize.y), displayImageOptions); } else { // Load it for later! diff --git a/app/src/main/res/drawable/ic_notification_download.xml b/app/src/main/res/drawable/ic_notification_download.xml new file mode 100644 index 000000000..5dcc185c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_download.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 54eeb414a..f1839d6e6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -401,19 +401,16 @@ +%1$d more… Update Available - Ready to install Update ready to install Install Failed - Downloading update for \"%1$s\"… + Downloading update for \"%1$s\"… Installing \"%1$s\"… Successfully installed Install Failed %1$d Updates %1$d Apps Installed Update available - Downloading… Downloading update… - Ready to install Update ready to install Installing Successfully installed From c81b2c7fe44a0e199550f8c0b585269c2a7a4967 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 24 Feb 2017 12:14:06 +1100 Subject: [PATCH 07/31] Minor formatting. Neither pmd/checkstyle/many-devs are particularly precious about hard rules of when to wrap a line. In this case we could've brought the `ErrorDialogActivity` onto the same line as `putExtra`, but instead opted to chain the method calls as per the `getAppDetailsIntent` method. --- .../java/org/fdroid/fdroid/AppUpdateStatusManager.java | 10 +++++----- .../java/org/fdroid/fdroid/NotificationHelper.java | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 6e57b22d3..dac5b099d 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -315,6 +315,7 @@ public class AppUpdateStatusManager { 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) @@ -324,11 +325,10 @@ public class AppUpdateStatusManager { 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); + Intent errorDialogIntent = new Intent(context, ErrorDialogActivity.class) + .putExtra(ErrorDialogActivity.EXTRA_TITLE, title) + .putExtra(ErrorDialogActivity.EXTRA_MESSAGE, entry.errorText); + return PendingIntent.getActivity( context, 0, diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index a2c96d300..8bb088003 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -385,6 +385,7 @@ 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.notification_summary_more, diff)); @@ -404,10 +405,12 @@ class NotificationHelper { .setLocalOnly(true) .setVisibility(NotificationCompat.VISIBILITY_SECRET) .setStyle(inboxStyle); + if (useStackedNotifications()) { builder.setGroup(GROUP_UPDATES) .setGroupSummary(true); } + Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_ALL_UPDATES_CLEARED); PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0); builder.setDeleteIntent(piDeleted); @@ -512,7 +515,9 @@ class NotificationHelper { } else { // Load it for later! ImageLoader.getInstance().loadImage(entry.app.iconUrl, new ImageSize(largeIconSize.x, largeIconSize.y), displayImageOptions, new ImageLoadingListener() { + AppUpdateStatusManager.AppUpdateStatus entry; + ImageLoadingListener init(AppUpdateStatusManager.AppUpdateStatus entry) { this.entry = entry; return this; From 1a9f5d60bf0f65d551b77e61868e979eb59cc835 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 24 Feb 2017 10:30:45 +1100 Subject: [PATCH 08/31] Remove dead code --- .../fdroid/installer/InstallManagerService.java | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index daec607db..949d5bdf3 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -72,20 +72,6 @@ public class InstallManagerService extends Service { private static final String EXTRA_APP = "org.fdroid.fdroid.installer.extra.APP"; private static final String EXTRA_APK = "org.fdroid.fdroid.installer.extra.APK"; - /** - * The collection of {@link Apk}s that are actively going through this whole process, - * 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 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 ACTIVE_APPS = new HashMap<>(3); - private LocalBroadcastManager localBroadcastManager; private AppUpdateStatusManager appUpdateStatusManager; @@ -271,7 +257,6 @@ public class InstallManagerService extends Service { intentObject.setData(downloadUri); PendingIntent action = PendingIntent.getService(context, 0, intentObject, 0); appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.Downloading, action); - // nothing to do break; case Downloader.ACTION_PROGRESS: int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0); From 49f20f64b3e45d5be049650b2858b6e77b40faff Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 24 Feb 2017 11:45:09 +1100 Subject: [PATCH 09/31] Added documentation. --- .../java/org/fdroid/fdroid/installer/InstallManagerService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index 949d5bdf3..23fe520dd 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -252,6 +252,7 @@ public class InstallManagerService extends Service { switch (intent.getAction()) { case Downloader.ACTION_STARTED: + // App should currently be in the "Unknown" state, so this changes it to "Downloading". Intent intentObject = new Intent(context, InstallManagerService.class); intentObject.setAction(ACTION_CANCEL); intentObject.setData(downloadUri); From 875b0d091fc6b7bdcbf790a882ee2abe29aeb41a Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 24 Feb 2017 12:11:49 +1100 Subject: [PATCH 10/31] Replace if/else with switch This is common throughout the F-Droid code base. --- .../fdroid/fdroid/AppUpdateStatusManager.java | 34 ++++++++++--------- .../org/fdroid/fdroid/NotificationHelper.java | 16 +++++---- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index dac5b099d..bdac704cd 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -285,23 +285,25 @@ public class AppUpdateStatusManager { } 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); - } else if (entry.status == Status.Installed) { - PackageManager pm = context.getPackageManager(); - Intent intentObject = pm.getLaunchIntentForPackage(entry.app.packageName); - if (intentObject != null) { - return PendingIntent.getActivity(context, 0, intentObject, 0); - } else { - // Could not get launch intent, maybe not launchable, e.g. a keyboard + switch (entry.status) { + case UpdateAvailable: + case 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" return getAppDetailsIntent(entry.apk); - } + + case InstallError: + return getAppErrorIntent(entry); + + case Installed: + PackageManager pm = context.getPackageManager(); + Intent intentObject = pm.getLaunchIntentForPackage(entry.app.packageName); + if (intentObject != null) { + return PendingIntent.getActivity(context, 0, intentObject, 0); + } else { + // Could not get launch intent, maybe not launchable, e.g. a keyboard + return getAppDetailsIntent(entry.apk); + } } return null; } diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index 8bb088003..cf51be63d 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -249,12 +249,16 @@ class NotificationHelper { 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, context.getString(R.string.notification_action_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, context.getString(R.string.notification_action_cancel), entry.intent); - } else if (entry.status == AppUpdateStatusManager.Status.ReadyToInstall) { - return new NotificationCompat.Action(R.drawable.ic_notify_install_24dp, context.getString(R.string.notification_action_install), entry.intent); + switch (entry.status) { + case UpdateAvailable: + return new NotificationCompat.Action(R.drawable.ic_notify_update_24dp, context.getString(R.string.notification_action_update), entry.intent); + + case Downloading: + case Installing: + return new NotificationCompat.Action(R.drawable.ic_notify_cancel_24dp, context.getString(R.string.notification_action_cancel), entry.intent); + + case ReadyToInstall: + return new NotificationCompat.Action(R.drawable.ic_notify_install_24dp, context.getString(R.string.notification_action_install), entry.intent); } } return null; From 0b1940c4bfc683c1cbfb7ab54aa34f48a867e405 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 24 Feb 2017 10:29:01 +1100 Subject: [PATCH 11/31] Make it explicit that it may return null. Given that some places where this method is invoked does a null check, it seems reasonable to make this assumption explicit. That way if devs use the method in the future, then they will be aware of the contract. --- .../main/java/org/fdroid/fdroid/AppUpdateStatusManager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index bdac704cd..90f00c7ad 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -6,6 +6,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.support.annotation.Nullable; import android.support.v4.app.TaskStackBuilder; import android.support.v4.content.LocalBroadcastManager; @@ -76,6 +77,7 @@ public class AppUpdateStatusManager { localBroadcastManager = LocalBroadcastManager.getInstance(context.getApplicationContext()); } + @Nullable public AppUpdateStatus get(String key) { synchronized (appMapping) { return appMapping.get(key); @@ -204,6 +206,7 @@ public class AppUpdateStatusManager { } } + @Nullable public Apk getApk(String key) { synchronized (appMapping) { AppUpdateStatus entry = appMapping.get(key); From a2997747afb15256ca1ed577a51f3e06f8ce64f0 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 24 Feb 2017 12:19:08 +1100 Subject: [PATCH 12/31] Clarify the unused status of `notificationManager` --- app/src/main/java/org/fdroid/fdroid/FDroidApp.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 9b1e9bb6c..0c32ccaa2 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -97,6 +97,13 @@ public class FDroidApp extends Application { @SuppressWarnings("unused") BluetoothAdapter bluetoothAdapter; + + /** + * The construction of this notification helper has side effects including listening and + * responding to local broadcasts. It is kept as a reference on the app object here so that + * it doesn't get GC'ed. + */ + @SuppressWarnings("unused") NotificationHelper notificationHelper; static { From ef7d8dfce92b04baf7e70d3d5a1f8812840d2d92 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 24 Feb 2017 12:43:44 +1100 Subject: [PATCH 13/31] Make updates/installed final to slightly simplify code. --- .../org/fdroid/fdroid/NotificationHelper.java | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index cf51be63d..bc3f8e5ed 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -53,8 +53,8 @@ class NotificationHelper { private final NotificationManagerCompat notificationManager; private final AppUpdateStatusManager appUpdateStatusManager; private final DisplayImageOptions displayImageOptions; - private ArrayList updates; - private ArrayList installed; + private final ArrayList updates = new ArrayList<>(); + private final ArrayList installed = new ArrayList<>(); NotificationHelper(Context context) { this.context = context; @@ -151,22 +151,18 @@ class NotificationHelper { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; } + /** + * Populate {@link NotificationHelper#updates} and {@link NotificationHelper#installed} with + * the relevant status entries from the {@link AppUpdateStatusManager}. + */ private void updateStatusLists() { if (!notificationManager.areNotificationsEnabled()) { return; } - // Get the list of updates and installed available - if (updates == null) { - updates = new ArrayList<>(); - } else { - updates.clear(); - } - if (installed == null) { - installed = new ArrayList<>(); - } else { - installed.clear(); - } + updates.clear(); + installed.clear(); + for (AppUpdateStatusManager.AppUpdateStatus entry : appUpdateStatusManager.getAll()) { if (entry.status == AppUpdateStatusManager.Status.Installed) { installed.add(entry); From 7329dc5d8d0b9bb5ad741ae28f74a61c3d365b34 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 24 Feb 2017 14:31:57 +1100 Subject: [PATCH 14/31] Return notification instead of builder. Given that none of the callers need the functionality of the builder, lets make it explicit that we don't expect the builder to be further customized once it is returned. Instead, return a notification to hint that no further customization is required/desired. --- .../org/fdroid/fdroid/NotificationHelper.java | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index bc3f8e5ed..edee5abf7 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -1,5 +1,6 @@ package org.fdroid.fdroid; +import android.app.Notification; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; @@ -189,28 +190,28 @@ class NotificationHelper { return; } if (notificationManager.areNotificationsEnabled()) { - NotificationCompat.Builder builder; + Notification notification; if (entry.status == AppUpdateStatusManager.Status.Installed) { if (useStackedNotifications()) { - builder = createInstalledNotification(entry); + notification = createInstalledNotification(entry); notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); - notificationManager.notify(entry.getUniqueKey(), NOTIFY_ID_INSTALLED, builder.build()); + notificationManager.notify(entry.getUniqueKey(), NOTIFY_ID_INSTALLED, notification); } else if (installed.size() == 1) { - builder = createInstalledNotification(entry); + notification = createInstalledNotification(entry); notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); - notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, builder.build()); + notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, notification); } } else { if (useStackedNotifications()) { - builder = createUpdateNotification(entry); + notification = createUpdateNotification(entry); notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); - notificationManager.notify(entry.getUniqueKey(), NOTIFY_ID_UPDATES, builder.build()); + notificationManager.notify(entry.getUniqueKey(), NOTIFY_ID_UPDATES, notification); } else if (updates.size() == 1) { - builder = createUpdateNotification(entry); + notification = createUpdateNotification(entry); notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); - notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, builder.build()); + notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, notification); } } } @@ -222,14 +223,14 @@ class NotificationHelper { return; } - NotificationCompat.Builder builder; + Notification notification; if (updates.size() != 1 || useStackedNotifications()) { if (updates.size() == 0) { // No updates, remove summary notificationManager.cancel(GROUP_UPDATES, NOTIFY_ID_UPDATES); } else { - builder = createUpdateSummaryNotification(updates); - notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, builder.build()); + notification = createUpdateSummaryNotification(updates); + notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, notification); } } if (installed.size() != 1 || useStackedNotifications()) { @@ -237,8 +238,8 @@ class NotificationHelper { // No installed, remove summary notificationManager.cancel(GROUP_INSTALLED, NOTIFY_ID_INSTALLED); } else { - builder = createInstalledSummaryNotification(installed); - notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, builder.build()); + notification = createInstalledSummaryNotification(installed); + notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, notification); } } } @@ -314,7 +315,7 @@ class NotificationHelper { return ""; } - private NotificationCompat.Builder createUpdateNotification(AppUpdateStatusManager.AppUpdateStatus entry) { + private Notification createUpdateNotification(AppUpdateStatusManager.AppUpdateStatus entry) { App app = entry.app; AppUpdateStatusManager.Status status = entry.status; @@ -359,10 +360,10 @@ class NotificationHelper { intentDeleted.putExtra(EXTRA_NOTIFICATION_KEY, entry.getUniqueKey()); PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0); builder.setDeleteIntent(piDeleted); - return builder; + return builder.build(); } - private NotificationCompat.Builder createUpdateSummaryNotification(ArrayList updates) { + private Notification createUpdateSummaryNotification(ArrayList updates) { String title = context.getString(R.string.notification_summary_updates, updates.size()); StringBuilder text = new StringBuilder(); @@ -414,10 +415,10 @@ class NotificationHelper { Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_ALL_UPDATES_CLEARED); PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0); builder.setDeleteIntent(piDeleted); - return builder; + return builder.build(); } - private NotificationCompat.Builder createInstalledNotification(AppUpdateStatusManager.AppUpdateStatus entry) { + private Notification createInstalledNotification(AppUpdateStatusManager.AppUpdateStatus entry) { App app = entry.app; Bitmap iconLarge = getLargeIconForEntry(entry); @@ -440,10 +441,10 @@ class NotificationHelper { intentDeleted.putExtra(EXTRA_NOTIFICATION_KEY, entry.getUniqueKey()); PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0); builder.setDeleteIntent(piDeleted); - return builder; + return builder.build(); } - private NotificationCompat.Builder createInstalledSummaryNotification(ArrayList installed) { + private Notification createInstalledSummaryNotification(ArrayList installed) { String title = context.getString(R.string.notification_summary_installed, installed.size()); StringBuilder text = new StringBuilder(); @@ -483,7 +484,7 @@ class NotificationHelper { Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_ALL_INSTALLED_CLEARED); PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0); builder.setDeleteIntent(piDeleted); - return builder; + return builder.build(); } private Point getLargeIconSize() { From d2ef357403c43ee4df0c453c7d0fc10ff3429dcc Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 24 Feb 2017 14:35:35 +1100 Subject: [PATCH 15/31] Invert if statement to bail early. --- .../org/fdroid/fdroid/NotificationHelper.java | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index edee5abf7..6fedc9d95 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -189,30 +189,33 @@ class NotificationHelper { notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); return; } - if (notificationManager.areNotificationsEnabled()) { - Notification notification; - if (entry.status == AppUpdateStatusManager.Status.Installed) { - if (useStackedNotifications()) { - notification = createInstalledNotification(entry); - notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); - notificationManager.notify(entry.getUniqueKey(), NOTIFY_ID_INSTALLED, notification); - } else if (installed.size() == 1) { - notification = createInstalledNotification(entry); - notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); - notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); - notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, notification); - } - } else { - if (useStackedNotifications()) { - notification = createUpdateNotification(entry); - notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); - notificationManager.notify(entry.getUniqueKey(), NOTIFY_ID_UPDATES, notification); - } else if (updates.size() == 1) { - notification = createUpdateNotification(entry); - notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); - notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); - notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, notification); - } + + if (!notificationManager.areNotificationsEnabled()) { + return; + } + + Notification notification; + if (entry.status == AppUpdateStatusManager.Status.Installed) { + if (useStackedNotifications()) { + notification = createInstalledNotification(entry); + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); + notificationManager.notify(entry.getUniqueKey(), NOTIFY_ID_INSTALLED, notification); + } else if (installed.size() == 1) { + notification = createInstalledNotification(entry); + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); + notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, notification); + } + } else { + if (useStackedNotifications()) { + notification = createUpdateNotification(entry); + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); + notificationManager.notify(entry.getUniqueKey(), NOTIFY_ID_UPDATES, notification); + } else if (updates.size() == 1) { + notification = createUpdateNotification(entry); + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); + notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, notification); } } } From aa945367c93375389dd9865fe3b1aec0f851f279 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 24 Feb 2017 15:21:49 +1100 Subject: [PATCH 16/31] Pass potentially nullable argument instead of guarding. It seems that `setContentIntent()` will do fine if we pass it a null argument. The default value is `null` anyway, and it doesn't mandate a non-null argument. --- .../org/fdroid/fdroid/NotificationHelper.java | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index 6fedc9d95..445688ddc 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -333,13 +333,9 @@ class NotificationHelper { .setLargeIcon(iconLarge) .setLocalOnly(true) .setVisibility(NotificationCompat.VISIBILITY_SECRET) - .setGroup(GROUP_UPDATES); + .setGroup(GROUP_UPDATES) + .setContentIntent(entry.intent); - // Handle intents - // - if (entry.intent != null) { - builder.setContentIntent(entry.intent); - } // Handle actions // @@ -434,11 +430,8 @@ class NotificationHelper { .setContentText(context.getString(R.string.notification_content_single_installed)) .setLocalOnly(true) .setVisibility(NotificationCompat.VISIBILITY_SECRET) - .setGroup(GROUP_INSTALLED); - - if (entry.intent != null) { - builder.setContentIntent(entry.intent); - } + .setGroup(GROUP_INSTALLED) + .setContentIntent(entry.intent); Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED); intentDeleted.putExtra(EXTRA_NOTIFICATION_KEY, entry.getUniqueKey()); From 30d3f8efccbc828e0684730d8fe1f886af458922 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 24 Feb 2017 11:42:15 +1100 Subject: [PATCH 17/31] Make "batchUpdates" mode an implementation detail of the `AddUpdateStatusManager` class. Because of the way that this can be misused without the compiler knowing (e.g. by forgetting to call `endBatchUpdates()`) it may be safer to move it to an internal implementation detail of the class. It could probably be done away with completely if the `notify*` methods were moved out of the respective `*ApkInternal()` methods, but that requires more significant refactoring to get right without code duplication. --- .../org/fdroid/fdroid/AppUpdateStatusManager.java | 14 ++++++++++++-- .../main/java/org/fdroid/fdroid/UpdateService.java | 7 +++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 90f00c7ad..545abf37f 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; public class AppUpdateStatusManager { @@ -187,6 +188,15 @@ public class AppUpdateStatusManager { } } + + public void addApks(List apksToUpdate, Status status) { + startBatchUpdates(); + for (Apk apk : apksToUpdate) { + addApk(apk, status, null); + } + endBatchUpdates(); + } + /** * Add an Apk to the AppUpdateStatusManager manager. * @param apk The apk to add. @@ -250,13 +260,13 @@ public class AppUpdateStatusManager { } } - void startBatchUpdates() { + private void startBatchUpdates() { synchronized (appMapping) { isBatchUpdating = true; } } - void endBatchUpdates() { + private void endBatchUpdates() { synchronized (appMapping) { isBatchUpdating = false; notifyChange(); diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index 11a11cf4f..58b3ce6d4 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -489,14 +489,13 @@ public class UpdateService extends IntentService { private void showAppUpdatesNotification(Cursor hasUpdates) { if (hasUpdates != null) { hasUpdates.moveToFirst(); - appUpdateStatusManager.startBatchUpdates(); + List apksToUpdate = new ArrayList<>(hasUpdates.getCount()); 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); - appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.UpdateAvailable, null); + apksToUpdate.add(ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode)); } - appUpdateStatusManager.endBatchUpdates(); + appUpdateStatusManager.addApks(apksToUpdate, AppUpdateStatusManager.Status.UpdateAvailable); } } From d00de6997454cd81622aef2d3e5da48d5e544599 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 24 Feb 2017 10:51:22 +1100 Subject: [PATCH 18/31] Extract "remove apk" code to simplify setApkInternal(). The only time `status == null` was when coming from `removeApk()`. By moving the logic out of `setApkInternal()` into `removeApk()` it makes it easier to reason about `setApkInternal()` as it now does less. Also, it was doubling up on the `syncrhonized (appMapping)` and `if (entry != null)` logic which is no longer required, because `removeApk()` was already doing that. While here, also make explicit the fact that `status` can no longer be `null`. --- .../fdroid/fdroid/AppUpdateStatusManager.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 545abf37f..9c07890c5 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -6,6 +6,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.TaskStackBuilder; import android.support.v4.content.LocalBroadcastManager; @@ -108,21 +109,14 @@ public class AppUpdateStatusManager { return returnValues; } - private void setApkInternal(Apk apk, Status status, PendingIntent intent) { + private void setApkInternal(Apk apk, @NonNull 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) { + if (entry != null) { // Update Utils.debugLog(LOGTAG, "Update APK " + apk.apkName + " state to " + status.name()); boolean isStatusUpdate = (entry.status != status); @@ -203,11 +197,11 @@ public class AppUpdateStatusManager { * @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) { + public void addApk(Apk apk, @NonNull Status status, PendingIntent pendingIntent) { setApkInternal(apk, status, pendingIntent); } - public void updateApk(String key, Status status, PendingIntent pendingIntent) { + public void updateApk(String key, @NonNull Status status, PendingIntent pendingIntent) { synchronized (appMapping) { AppUpdateStatus entry = appMapping.get(key); if (entry != null) { @@ -231,7 +225,9 @@ public class AppUpdateStatusManager { synchronized (appMapping) { AppUpdateStatus entry = appMapping.get(key); if (entry != null) { - setApkInternal(entry.apk, null, null); // remove + Utils.debugLog(LOGTAG, "Remove APK " + entry.apk.apkName); + appMapping.remove(entry.apk.getUrl()); + notifyRemove(entry); } } } From 2ad61a4eb7e07cbc434549a17d49b118507295e0 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 24 Feb 2017 11:35:20 +1100 Subject: [PATCH 19/31] Replace `setApkInternal` with more specific functions. The `setApkInternal` method had to infer the intent of the caller based on the arguments which were passed on, and then do specific things depending on the input. Instead, this change has three distinct actions which can happen (add/remove/update). Each of these methods does only one thing, and doesn't have to guess the intent of the caller. The only exception may be "add", which will (for convenience) delegate to "update" if it already knows about the apk in question. --- .../fdroid/fdroid/AppUpdateStatusManager.java | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 9c07890c5..ae7176588 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -109,36 +109,27 @@ public class AppUpdateStatusManager { return returnValues; } - private void setApkInternal(Apk apk, @NonNull Status status, PendingIntent intent) { - if (apk == null) { - return; + private void updateApkInternal(@NonNull AppUpdateStatus entry, @NonNull Status status, PendingIntent intent) { + Utils.debugLog(LOGTAG, "Update APK " + entry.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); + } - synchronized (appMapping) { - AppUpdateStatus entry = appMapping.get(apk.getUrl()); - 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 addApkInternal(@NonNull Apk apk, @NonNull Status status, PendingIntent intent) { + Utils.debugLog(LOGTAG, "Add APK " + apk.apkName + " with state " + status.name()); + AppUpdateStatus 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() { @@ -192,20 +183,31 @@ public class AppUpdateStatusManager { } /** - * Add an Apk to the AppUpdateStatusManager manager. + * Add an Apk to the AppUpdateStatusManager manager (or update it if we already know about it). * @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, @NonNull Status status, PendingIntent pendingIntent) { - setApkInternal(apk, status, pendingIntent); + if (apk == null) { + return; + } + + synchronized (appMapping) { + AppUpdateStatus entry = appMapping.get(apk.getUrl()); + if (entry != null) { + updateApkInternal(entry, status, pendingIntent); + } else { + addApkInternal(apk, status, pendingIntent); + } + } } public void updateApk(String key, @NonNull Status status, PendingIntent pendingIntent) { synchronized (appMapping) { AppUpdateStatus entry = appMapping.get(key); if (entry != null) { - setApkInternal(entry.apk, status, pendingIntent); + updateApkInternal(entry, status, pendingIntent); } } } From 583ea96816d62719a3f6e4b46e377baf3571af05 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Fri, 24 Feb 2017 12:16:19 +1100 Subject: [PATCH 20/31] Clarify that the pendingIntent arg can be null. --- .../java/org/fdroid/fdroid/AppUpdateStatusManager.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index ae7176588..0ea683021 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -188,7 +188,7 @@ public class AppUpdateStatusManager { * @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, @NonNull Status status, PendingIntent pendingIntent) { + public void addApk(Apk apk, @NonNull Status status, @Nullable PendingIntent pendingIntent) { if (apk == null) { return; } @@ -203,7 +203,10 @@ public class AppUpdateStatusManager { } } - public void updateApk(String key, @NonNull Status status, PendingIntent pendingIntent) { + /** + * @param pendingIntent Action when notification is clicked. Can be null for default action(s) + */ + public void updateApk(String key, @NonNull Status status, @Nullable PendingIntent pendingIntent) { synchronized (appMapping) { AppUpdateStatus entry = appMapping.get(key); if (entry != null) { From 40a061ae73e445e54000094d26771b7c3334d230 Mon Sep 17 00:00:00 2001 From: mvp76 Date: Tue, 28 Feb 2017 15:10:05 +0100 Subject: [PATCH 21/31] Fix for old devices They can't use vector drawables in notifications. Also, groups can not be used, see http://stackoverflow.com/a/34953411. --- app/build.gradle | 1 + .../java/org/fdroid/fdroid/FDroidApp.java | 5 +++++ .../org/fdroid/fdroid/NotificationHelper.java | 20 +++++++++++++----- .../ic_cancel.xml} | 0 .../ic_file_download.xml} | 0 .../ic_file_install.xml} | 0 app/src/main/res/drawable-hdpi/ic_cancel.png | Bin 0 -> 579 bytes .../res/drawable-hdpi/ic_file_download.png | Bin 0 -> 367 bytes .../res/drawable-hdpi/ic_file_install.png | Bin 0 -> 690 bytes app/src/main/res/drawable-mdpi/ic_cancel.png | Bin 0 -> 541 bytes .../res/drawable-mdpi/ic_file_download.png | Bin 0 -> 263 bytes .../res/drawable-mdpi/ic_file_install.png | Bin 0 -> 506 bytes app/src/main/res/drawable-xhdpi/ic_cancel.png | Bin 0 -> 846 bytes .../res/drawable-xhdpi/ic_file_download.png | Bin 0 -> 351 bytes .../res/drawable-xhdpi/ic_file_install.png | Bin 0 -> 793 bytes .../main/res/drawable-xxhdpi/ic_cancel.png | Bin 0 -> 1124 bytes .../res/drawable-xxhdpi/ic_file_download.png | Bin 0 -> 450 bytes .../res/drawable-xxhdpi/ic_file_install.png | Bin 0 -> 972 bytes .../main/res/drawable-xxxhdpi/ic_cancel.png | Bin 0 -> 1298 bytes .../res/drawable-xxxhdpi/ic_file_download.png | Bin 0 -> 548 bytes .../res/drawable-xxxhdpi/ic_file_install.png | Bin 0 -> 1216 bytes 21 files changed, 21 insertions(+), 5 deletions(-) rename app/src/main/res/{drawable/ic_notify_cancel_24dp.xml => drawable-anydpi-v21/ic_cancel.xml} (100%) rename app/src/main/res/{drawable/ic_notify_update_24dp.xml => drawable-anydpi-v21/ic_file_download.xml} (100%) rename app/src/main/res/{drawable/ic_notify_install_24dp.xml => drawable-anydpi-v21/ic_file_install.xml} (100%) create mode 100644 app/src/main/res/drawable-hdpi/ic_cancel.png create mode 100644 app/src/main/res/drawable-hdpi/ic_file_download.png create mode 100644 app/src/main/res/drawable-hdpi/ic_file_install.png create mode 100644 app/src/main/res/drawable-mdpi/ic_cancel.png create mode 100644 app/src/main/res/drawable-mdpi/ic_file_download.png create mode 100644 app/src/main/res/drawable-mdpi/ic_file_install.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_cancel.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_file_download.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_file_install.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_cancel.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_file_download.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_file_install.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_cancel.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_file_download.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_file_install.png diff --git a/app/build.gradle b/app/build.gradle index 65913a79c..094e1aea2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -184,6 +184,7 @@ android { versionCode 102050 versionName getVersionName() testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary = true } testOptions { diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 0c32ccaa2..67d773a63 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -37,6 +37,7 @@ import android.os.Build; import android.os.Environment; import android.os.StrictMode; import android.preference.PreferenceManager; +import android.support.v7.app.AppCompatDelegate; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; @@ -78,6 +79,10 @@ import sun.net.www.protocol.bluetooth.Handler; ) public class FDroidApp extends Application { + static { + AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); + } + private static final String TAG = "FDroidApp"; public static final String SYSTEM_DIR_NAME = Environment.getRootDirectory().getAbsolutePath(); diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index 445688ddc..f4acb08e2 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -15,6 +15,7 @@ import android.os.Build; import android.support.graphics.drawable.VectorDrawableCompat; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.content.ContextCompat; import android.support.v4.content.LocalBroadcastManager; import android.text.SpannableStringBuilder; import android.text.Spanned; @@ -251,14 +252,14 @@ class NotificationHelper { if (entry.intent != null) { switch (entry.status) { case UpdateAvailable: - return new NotificationCompat.Action(R.drawable.ic_notify_update_24dp, context.getString(R.string.notification_action_update), entry.intent); + return new NotificationCompat.Action(R.drawable.ic_file_download, context.getString(R.string.notification_action_update), entry.intent); case Downloading: case Installing: - return new NotificationCompat.Action(R.drawable.ic_notify_cancel_24dp, context.getString(R.string.notification_action_cancel), entry.intent); + return new NotificationCompat.Action(R.drawable.ic_cancel, context.getString(R.string.notification_action_cancel), entry.intent); case ReadyToInstall: - return new NotificationCompat.Action(R.drawable.ic_notify_install_24dp, context.getString(R.string.notification_action_install), entry.intent); + return new NotificationCompat.Action(R.drawable.ic_file_install, context.getString(R.string.notification_action_install), entry.intent); } } return null; @@ -333,9 +334,15 @@ class NotificationHelper { .setLargeIcon(iconLarge) .setLocalOnly(true) .setVisibility(NotificationCompat.VISIBILITY_SECRET) - .setGroup(GROUP_UPDATES) .setContentIntent(entry.intent); + /* If using stacked notifications, use groups. Note that this would not work prior to Lollipop, + because of http://stackoverflow.com/a/34953411, but currently not an issue since stacked + notifications are used only on >= Nougat. + */ + if (useStackedNotifications()) { + builder.setGroup(GROUP_UPDATES); + } // Handle actions // @@ -430,9 +437,12 @@ class NotificationHelper { .setContentText(context.getString(R.string.notification_content_single_installed)) .setLocalOnly(true) .setVisibility(NotificationCompat.VISIBILITY_SECRET) - .setGroup(GROUP_INSTALLED) .setContentIntent(entry.intent); + if (useStackedNotifications()) { + builder.setGroup(GROUP_INSTALLED); + } + Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED); intentDeleted.putExtra(EXTRA_NOTIFICATION_KEY, entry.getUniqueKey()); PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0); diff --git a/app/src/main/res/drawable/ic_notify_cancel_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_cancel.xml similarity index 100% rename from app/src/main/res/drawable/ic_notify_cancel_24dp.xml rename to app/src/main/res/drawable-anydpi-v21/ic_cancel.xml diff --git a/app/src/main/res/drawable/ic_notify_update_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_file_download.xml similarity index 100% rename from app/src/main/res/drawable/ic_notify_update_24dp.xml rename to app/src/main/res/drawable-anydpi-v21/ic_file_download.xml diff --git a/app/src/main/res/drawable/ic_notify_install_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_file_install.xml similarity index 100% rename from app/src/main/res/drawable/ic_notify_install_24dp.xml rename to app/src/main/res/drawable-anydpi-v21/ic_file_install.xml diff --git a/app/src/main/res/drawable-hdpi/ic_cancel.png b/app/src/main/res/drawable-hdpi/ic_cancel.png new file mode 100644 index 0000000000000000000000000000000000000000..e18c7aef903ff9373323c67727b15776cd8fdbe9 GIT binary patch literal 579 zcmV-J0=)f+P)Om`JHLm>&TgNb5n}P*O^Vn7;r#00>kZ_kG{rQ3fVetxy@5RDp`) zzEENKo`C^?4h!)irF3h6afx}8fdK%g*=W285KRE=a>jOvc{88Qvar{{IK;dL(9QXM z5L!>^$S|A2U&AVknFNLp-g>J)}l(NGQR!+RRvYnM=sE{P}ht-2~^hc zwb?xjWE!A~slEtQG>~pIR27?{5Ksc;67whWPuYF)&HcwjPoC#(i^M4mlt7)!RGy5_ zE+ZgAs8R>1F;GFM(xpa}0jg8?J!Muc>RWC5LTl(!r$<^3nW%>fI?wwm9aa@rtJ)8% z8Z@g~SgV?RtFA#-T@-HtV*(|>JU?NuXQEz}FCd=h1xz%+U#U4(M+-3%t#oe_yLm9T3MGa Rrz`*f002ovPDHLkV1l1o?hOC{ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_file_download.png b/app/src/main/res/drawable-hdpi/ic_file_download.png new file mode 100644 index 0000000000000000000000000000000000000000..52fa71dc43451a4253cf7101335a758acfebf28b GIT binary patch literal 367 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBSc;uILpXq-h9ji|$mcBZh%9Dc z;GPD;j2yzHwLn4164!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq=1Uvc)B=- zM6|xWzF)A(K;-zx_$!8YT)Yc7uTS8fyYR8>x`!gmrufPnIu((z)cxO{1CAf}sdDZ- z|8xgKgUZvT7Ur?H%HB`8RViM!&i?9G+tRnZ>`$g>G0b5umSQ-$K$P7im5EDrK>!oi zQ(KA7jNMFq^~~o)f6Q?`Y?;1r%HO?znASzTX6I&Mke$eOq3xM#n$}wB`z@szt+TiO zU8pmk;g7WulNy(akQSFP?VA=bvrwcy4KoeKzopr0DyRji2wiq literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_file_install.png b/app/src/main/res/drawable-hdpi/ic_file_install.png new file mode 100644 index 0000000000000000000000000000000000000000..81ca22780f5cc3d7d835704c2dd3b27fbf3062ec GIT binary patch literal 690 zcmV;j0!{siP)jJbo2gfUwJKiL#lLv%~oWE)9MPC@m2?7w2+ey29*arqYfPgZ_%+I#l zyFGMy0Re378?#`o-IBM+7YJZaN#sGN(<$XC@d*Mr_O9E#o~NLkSo_Q=A+l}IWmOGl z;`mKH4CjUdA#!Ax;D8Z@yGfEP1wruAHSGJy{!E25eH6#>PS;>jAb=eP9!F8M$t6@I z2w?yC*w}qlm8^lHK`2ZnaU5pdFB}9QA~$Zu@kEy0k$?b`!rZL2d;hyQ3XRK*3X~c+ zo8`54Bp_A2dN~Xivg{TP5||IdFwVMPH0agx%E~zr(X4?YK_5~$8AZ`Imr%JNOb=sD zg+cJ%HCRUVxl`iz>&LzU8!B+2KEyvgKsjhMj=BUAYmX!H@0~>q;gPSvyrtuk4+8Y#F+JHbf z&k=?-gb+wh5Slxb7=Fl%azPVFi1-E|6cQ@F6|f7E9USDL;+246m*V}b>{JnB&66K=J+F8uI)*@KjK3mW`@BT^Dg2FWy;rp~n0afXQSsj6|-~m@jR_%dr~J fyG->*k|gt2qhe4nJ zu}eLz9U64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq=1TAJY5_^ zJUZV_G30Ad;Bj3Y=etMnW!G%qewEebMVIexnc~*Vw`Zf~f5QXp84@L?-aaKBpF1+< zz29s8=@QF*?jzv`TsRoIjkR5m{Irr;a@O>DE>|LZ!$-dzS_NKLJ}g=g6vSiB+IECz zy^zMFRR!igQ)Xz`%KgdS(yw-Hb8E=`_2I6MtNl_QnkBC03EmBK4}+(xpUXO@geCxi C#9h_^ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_file_install.png b/app/src/main/res/drawable-mdpi/ic_file_install.png new file mode 100644 index 0000000000000000000000000000000000000000..4cc23747a8e358a7c8f14ae53dfabbc3e617c61d GIT binary patch literal 506 zcmV000d>0sP;YmjD0&8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H10e4A6K~zYIwUjYS!$25@-|w1&i{RoQ2vS{Ky6WocQV_8n6uLOL z8j~gsI0vx3|(10D$dtzMQ6jB(U3P zG-lHh0KgE0IN_X+#~sjiYPHEIAe3^1`j^Hsz;O@+bGoj-Dg_Z@^y#&W2^RqJV(~1RNQail^cZ2$lO literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_cancel.png b/app/src/main/res/drawable-xhdpi/ic_cancel.png new file mode 100644 index 0000000000000000000000000000000000000000..3e85c8a971ef2ce782658257da2eb0f316f2d922 GIT binary patch literal 846 zcmV-U1F`&xP)(Fa2c|Nhj#VomAy|kMpiy{- zVs*@DIei7Bss$>!0}=xgv_qc(bwi@GXr3g~&&NNB0*N9zI!J{d{tAGK7)97+&?w=oqgq5(XZ)H`=s3DIqcNles~) zTU9y5z>gzv_c#0bEAC5pg9f&k1%q9{Df5P)(eT);z$0F)!)3LY{DfLjTd z@Q_ge+(@{Fha3V>Ori{U$SDAJ5@o@Ig#hG}0REH(50(OO+wQ9BK4~4}ZD#HxBAZ*j ze_+Gm*&GD0<@*OJ(k7)9CrwbbYYPtr&VWhAa*B(SR-~{1SV=f*2Av2%E(xcdto0a} zjtqy+evDk->*7N)UgsxMpAxcwH>rLQ7l1b@EMYwccEYfYNcwF2Z!(KZ1Cia)8jw}2 z1TwGNsCKhVc16~ma+i=zV^-#!T`qwlU1eDknFU<5#AQfi5^&kiyOu~5DC1_trNm5u zGVi2ZNhAuC32)*;B357?@Fqni{u7udyh#y>I|5a}o7hT>1*(KMv5^p{2fRsM38@pj zNgfFg&wG}(qlj$I^I5U)`-dXZA*~hnybW)u+3X^c1Xp<7#x@z&oubRDtG5ENe>H%L zpVFHolv8v$7`*QFde`Ht(3C_i;7z6^#_%vT4;Tr7uT_-Itn7qgTjA+av)OI6TH~?t Y7p~ax>L0Z7B>(^b07*qoM6N<$f{@j1I{*Lx literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_file_download.png b/app/src/main/res/drawable-xhdpi/ic_file_download.png new file mode 100644 index 0000000000000000000000000000000000000000..2923dd8931588978b70d4a7a5a108c016778e4d3 GIT binary patch literal 351 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmSQK*5Dp-y;YjHK@;M7UB8wRq z_`#TQ-qjK~}U&Kt*>vT^vI) z8sFY9_;Bt{arng}6iQE?vPd%1A^((m$r0`_x^)(KkU!RzH{V3zoDW`J8OD|-7 z`(R@~UFg%h+9QfHIVa3zxCtdTHK0EkJYD@<);T3K0RZsQg%kh) literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_file_install.png b/app/src/main/res/drawable-xhdpi/ic_file_install.png new file mode 100644 index 0000000000000000000000000000000000000000..1de323ce100864e59df66542e99c38f015545d73 GIT binary patch literal 793 zcmV+!1LpjRP)1Jc-tShye&Q5OqAv9YqTv9OaMf`wR!OA^+dIX1z^!fJ9K zGxL|j-0#fCy!bD{Fzt4G<8r@$r0RJKrmTM@YPA-?d}{S^VG8c`dc?q;D2n!&vUFlB zFiZ(a@;D5`ZI*Z=$Cdy9Y!$#$rBvA>^S`kv006tDCMNDNGg;=Y8QTJ2;6xOLo^9@0 zu`U1rL^OXR3};-hSI3C}0FeOqZwA3mNBrM|9I`hgM2n3^V~fKXP6fcg^twXfky2{C zs~JuOhS@h&D&2MkPeTAqbgmTy=UuQ<5dZ)pYDdlHe%st)6h#M&2yr|)>MRJ+xz%j$ zzF4b$Fq-)nG)Z0<5ooNR?&cLCo&`a$*Z2KzMstTr?GgGTI|@V(I-O3z1}qB!066rq z+r4H3l$8LO*}oo!r%gGT2>^hNB9y5BEEJ)v1pvb$lm`L8pa|tfK)VR#MgY(&Lb(#q zNv;H#`O0O_TO4OiZUhK;C_OJ6cU@it-Y)g~XNic%U6==f&k1l;Db>?iT(o^PV`cy$ zj(Jjk(gkEHK)`HO$~PvA%mj!isLst9Z^>u%4m{sJJ$=;%C<}r2G4shvr4rkK6(Phy zBSIWcg6PSJXMZgw$zfTmS?R+7P?S=>Gyw!buxZ3{yMiRC*xZo-QhbD%`GTy~UfJfR z@)aWHyVCcaY%iv92VO4?22QpW0mlLabbrM0ai!F#00000NkvXXu0mjf9TrTM literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_cancel.png b/app/src/main/res/drawable-xxhdpi/ic_cancel.png new file mode 100644 index 0000000000000000000000000000000000000000..155ddbc59a67cd904d2ec3e1dc38776057d0b2cc GIT binary patch literal 1124 zcmV-q1e^PbP)eY@A~z9xh}f8E)6pZA$o-oY%(He3h?4t|_Y zChzw4_PV;aY9H@?`*ydx4?M=Xzkl1^eUSG#clAKT0XnmTAspRmw|{C%Pl`AJez~+H&vcF#+<6J&z3=q8-Gh=i0Bu^W=dIyr^s|Ex724kHbvg%i z9s->AoWFq0(kF*-G#rh#+=UC3?Siul)$!-H=yw{PoILd2pUnP?H3>-y2d<2Rnt&vU z134qw*KqKrkzk|4WrRFdUx$t#s~iJbomJ z0}4Q}U{9(Ng`N|0OZtKUgTY{WZ+(6L>y?#nfx8-Q%%X?`NKZKK%k6dfCI`v zpoCsp-EB98WS5E880cB0ErJA4UyD=)JD}t zNUA{UQw{?pH6XM3m@-mhCaYRV;^Uz6a>qO1eudKyf&YMSN}c80op5&c-S_M3`-8#Y zv_iYZmzKPkL?sY7OT9RGF|{eVUK}CUVFfp)7DfBK=|f$Rk}vyQH@B(-QqqOPc_yBz zk&-PvmzkBPL`t%Gp-Raj6_WY(mqqG1DUeJzP)QWKtdY#N;7L{jEs;z%acY!Ft&r5W z0c+NfEs#_x={7y`TI)9zWyr-pBdl+{X0u^mQx1 zE1=w$4B<#)!=y&M_b0>g_+f>YCrkqTQR(F-b?-TODfx4o1m~Xg`~454UyrklI5Be~ z&2XUP3zn7{C*}sEOE^&C6nayWew&~!-a5UO# q8cvM&zSCP>eG}mKe!rhP6!-*uVF+uv6X%-%00004nJ zi0gtdnvPC8XgN28k_JicHKK8yEga0?FF%$Zk@mVt?Kn*f7{9T z^{4kwzx-0T;+>`XMm?DqEKN%%?kY@R5YXu~VPR)uJQ(D0xFA76K&R0}gq@A~V91h( z784j)f+qwOC~$IkO}tc~Ajsi0A*fJ6k|Sv1rNRXa96=Mj3Knp1v?)z}=%6UT7j>aYBMS$X2=ls8p+`+t0Pn&9Bb#L6k6G6570GiT~vH=nw5Ui89cGwguj O&EV^yL z|3h35fD$}!dZOLlDZR+Qut5+2Z0>vBOcX_%q!;-cTLb~Xu7XkzrlzI}(u*v`CP4sj z{w$76e#OwAn65yl5+SljMx)5F*+=;D}Zyir)G`@HCT!RzZ=7ZgGcd;6bU$ zi6}ZS9t3ZAgWW--Xmd%BNYUn$phZ}VHWvi}j76KPg4l{SX9a<&XmeQ*z)-ZgE(kDO zwB32DW--Jfe!}yP)qVd{Rs-`Sh!ikZ_5GJw4$adMi>medWY$CTBIw!n zp`ptr2IoQ0yFSs0k&%(UiQ%rlgT5BhwBk1!-%JI#`~)o^O^3%DjrS%)oE1bw7yL%! zx#=8N1>L9x!A*`J7X`ie(d%903Uf-30zNGC`^UpD{Kb{&lA!M%QKi{ze&Gx)Dy7bv z4k4nG01gc>A_dVX_stFfC{}B=+onT0NwOD2g9vI$+zkR8IARKzuGVUaRKD$kGAq7x zu~|^2#g{I&{uj%<_|nBjK}L!%U2HsJMv5<8Y!hUx_|nBDK}L%&U2J&=885zcvEd0~ zDZX?O7sOS3=^`zNt@zSKQV?tLrHhCliQ-Ea2|*&omoD6bQV%#KQGDsbEvR{^UY}vk u90)c+(#4l9EML+DyegDR4<(B)U48@CTOeSSg6@z20000DF6Tf8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H11dmBXK~#90?cB|38&?>I@#l<8j0gyNE!otYLQ6|SyA0$qylF(D zc9E7+6qu}Z6NNxmh0x+%H|{3aQV2AvNj#^Uys~A@r{;XTXJ-COTZr)dB*h~gWN6rC zvsqldd-t1xpPxK?_E*#Gr$L_QXQn8Ad0!NdR;$%Nb+^sxe!1Cf7XNLxe`3s+)|l5X zU%dFt?LT#R%k%t<>+3%(e10;UU98vZPwH-Os(vBy4Sag*$Nw5@znPv1<&*drv)KL6 zm^UA;ufM8#CX@nH#FtG7B`b|Dt3fybXoxSH5N@qDzN`Xa51?gy*@QsL__7j&9YDwU zvQZ;+iZ8o^VBqNZvQZ)&6<>A>!SswT8x=yY__B`(+0}OYs%LzF`xbuB^Zcym{ivUl z_yE>EILWeKGgA~l0JT?Zt!B)EqPQ6H#hSDD0M@?E;70@Sa=m_Fv+M$>y=rT@LB1S0 zk8jM^-#vZ$_wKKt388wv;=`TmfC=GnKI22lmGFdcGvD!{;=P~=AxwOzc{g;-2%+Lb z4FHT2A!K}L0Dy5JgpLm_0AL&lG2%l@02nqxtoYCY0EUSWGd^?#fMFrTjt@rwz%URr z#D^mRAZ-LK@u3F*q=}#@KJ)~Dv=Fq#2L}L<27<=;;0OS8BWR5e4go+Xg68<(7yxu3 zXpavb06+&q3h}`c0K`Q|B|dlrfH(*##Rty-5Edb|_z(a9!XTs=9|8e@UxZZSLqGuV zgJ5T~_o>E*zyRR>(F)+~zGkzB%jNR2_kDF(Z~$=rOz6J|k9=7a08oSwF+M~D00jt< z<3nTsh=`ydJ}3Zy2nb5zgAxD;h@dDwC<1^02+HDvG63+1pfElt1ON{RO5;Pr*VV+D zJvq6DwKHdTXw1x7d(YYZIh?v@fYY}s_NU@3>fNaV@EBjF4nYk-fcP?12r2*q#+Ru< zhz<}TzDxx|RDg)_Wzi9$0Vs$si;55hKv8^IG=$(6V$MI3l#eC*E!y`T1qJYNdcj!x z1qpxOQLvXFpGQYDoJBo$K>$LW+esC{J3z=oQz;^N1&DECEj0vh05OjXri9=HK*L$h zR1lm4XoxRM0l_JNruZ`L2+jaB#+PYEZ~~w?zDz4ZZ-5ly%QPbN0!S&oOdG<{04c_o zX+k&(Am#WnEeM?fh7ez-0ihGXkmAc?BeVt>VtiRlgjN7UjxURa&hX1);26uH9bC=(@Gt~G1PgbipWZAdQ4g?$XeMXi&0{VTl_VJM)`pb@DY`I)sIyw-1c{-mz zmj46pQxva(?k9r>{<2fGo-aq2r}O#4^Yin!xBm+2jxf^rvLg|8@!{|~pgqFKi2@G5vKU1Yd|QC5B2Yc9wJQfYgd6#86TQI54b^?;6~}{EVV_emb8&u8R-wKjb4!XhjbqZ~y=R07*qo IM6N<$g2DPgGynhq literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_file_download.png b/app/src/main/res/drawable-xxxhdpi/ic_file_download.png new file mode 100644 index 0000000000000000000000000000000000000000..025407ad23d395379f7c1af4383c83657e0803c7 GIT binary patch literal 548 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#Vf4nJ z$UO&P#*0pdF+f4d64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<_&7kaul zhE&XXd&kg^*-(P@A>%r?9f4bNUu^j1vE->=$pz&WjvICRC;#M2sJy>rH}@nJPaxVQ zTK5v71>P-}9y@G5_}rUUbpbPVlugc$U? zHk|pfA%NjWXv@-L?MImo^b1atmKWn;xbGHG^ylf@wR68+^LttC0Sv-Rs%|yPW?$}~ p&)oap_HFni)bIkS03+USoI2T;A|kW2zX4;E!PC{xWt~$(69Dgr&>jE) literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_file_install.png b/app/src/main/res/drawable-xxxhdpi/ic_file_install.png new file mode 100644 index 0000000000000000000000000000000000000000..c38be03106a0033547504472c3c399b5180b9435 GIT binary patch literal 1216 zcmV;x1V8(UP)B);1gg1XhlE6u09aslPpfsdD2n^Iw}e8JfSN-yQ&S&v zj|hcC0kA|z2W9yh_l^*V6aWB3^x<%2l_m)QfY-H!oTdp-7jhaW0Lns669oW@LQYc!07^nmdtl5b+#`r+2G;tx^xiwM2DOh1OS3VPAmcdp&=(G z0f4}e6Po~7V19Kl=yLDaAvysy%Thv4tOCS@oLB__LPAc=0z`zIs0GxphMXkeI&2{) z38>);IZ40`SVB$`a5KR}P7-h{2UWQ2rV0VH%k&^`MPLPn31l$?# zAtwo7rp-M-INQjw=kiYHH(v)N0lQ-fIJVg7eDCv+^b)i?Wr#mzNdj)qw~y`Le=@{D zNx-dK8d>||!otER#9`?@;AZ}8Rn=Ul-~TJrf$0vo!Bte%{E2@5=a7dc0X5dzH#+_P zccITs0x7TC&qQE2oxcJZ3)|*_1M=gMW&8wr)^F>ko$F(750e@%K&Mhr1{lPgR zRsmH8ynLe9`;mJ{tOAy#d{cXL0ti@N>~;s-J9daozzG0NUL=f0cVgS(f`+ zL_P#u*f%@-j)&_`j3&PcUm98aLa4CfN>kqhf`=7Xns^7WgcVmBCx9!gxY9HMY+=Qf zCJA5-E3Py`fP}E(N)rT#2rI5=7a%6AxT0Eswf1c(VZ{}-0ti^|vmmiy#T88gM28huG`$1`d156rS_E7O^~6eOGzd@;R$P((9-ttBQYI*I5Ixx@x0000 Date: Tue, 28 Feb 2017 15:18:02 +0100 Subject: [PATCH 22/31] Set blue color on notifications --- app/src/main/java/org/fdroid/fdroid/NotificationHelper.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index f4acb08e2..f1149601b 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -331,6 +331,7 @@ class NotificationHelper { .setContentTitle(getSingleItemTitleString(app, status)) .setContentText(getSingleItemContentString(app, status)) .setSmallIcon(iconSmall) + .setColor(ContextCompat.getColor(context, R.color.fdroid_blue)) .setLargeIcon(iconLarge) .setLocalOnly(true) .setVisibility(NotificationCompat.VISIBILITY_SECRET) @@ -406,6 +407,7 @@ class NotificationHelper { new NotificationCompat.Builder(context) .setAutoCancel(!useStackedNotifications()) .setSmallIcon(R.drawable.ic_launcher) + .setColor(ContextCompat.getColor(context, R.color.fdroid_blue)) .setContentTitle(title) .setContentText(text) .setContentIntent(piAction) @@ -433,6 +435,7 @@ class NotificationHelper { .setAutoCancel(true) .setLargeIcon(iconLarge) .setSmallIcon(R.drawable.ic_launcher) + .setColor(ContextCompat.getColor(context, R.color.fdroid_blue)) .setContentTitle(app.name) .setContentText(context.getString(R.string.notification_content_single_installed)) .setLocalOnly(true) @@ -478,6 +481,7 @@ class NotificationHelper { new NotificationCompat.Builder(context) .setAutoCancel(!useStackedNotifications()) .setSmallIcon(R.drawable.ic_launcher) + .setColor(ContextCompat.getColor(context, R.color.fdroid_blue)) .setContentTitle(title) .setContentText(text) .setContentIntent(piAction) From 04d2d004a3eb74b9dd8e5e565c752c2497cd34a8 Mon Sep 17 00:00:00 2001 From: mvp76 Date: Tue, 28 Feb 2017 16:33:37 +0100 Subject: [PATCH 23/31] Add logic to show/hide notifications based on appdetails screen Also, add code to AppDetails2 to match AppDetails, keeping track of currently viewed app. Moved the nulling of this info to onStop instead of onPause, since alerts may be shown on top of the details page, while still visible. --- .../java/org/fdroid/fdroid/AppDetails.java | 39 +++++++++++---- .../java/org/fdroid/fdroid/AppDetails2.java | 47 ++++++++++++++++++- .../fdroid/fdroid/AppUpdateStatusManager.java | 10 ++++ .../org/fdroid/fdroid/NotificationHelper.java | 5 +- .../installer/InstallManagerService.java | 11 ++--- 5 files changed, 92 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java index dc8c00e9c..15e0539e4 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java @@ -432,6 +432,12 @@ public class AppDetails extends AppCompatActivity { myAppObserver); } + @Override + protected void onResume() { + super.onResume(); + updateNotificationsForApp(); + } + @Override protected void onResumeFragments() { // Must be called before super.onResumeFragments(), as the fragments depend on the active @@ -462,13 +468,34 @@ public class AppDetails extends AppCompatActivity { protected void onStop() { super.onStop(); + visiblePackageName = null; getContentResolver().unregisterContentObserver(myAppObserver); + + // When leaving the app details, make sure to refresh app status for this app, since + // we might want to show notifications for it now. + updateNotificationsForApp(); + } + + /** + * Some notifications (like "downloading" and "installed") are not shown for this app if it is open in app details. + * When closing, we need to refresh the notifications, so they are displayed again. + */ + private void updateNotificationsForApp() { + if (app != null) { + AppUpdateStatusManager appUpdateStatusManager = AppUpdateStatusManager.getInstance(this); + for (AppUpdateStatusManager.AppUpdateStatus status : appUpdateStatusManager.getByPackageName(app.packageName)) { + if (status.status == AppUpdateStatusManager.Status.Installed) { + appUpdateStatusManager.removeApk(status.getUniqueKey()); + } else { + appUpdateStatusManager.refreshApk(status.getUniqueKey()); + } + } + } } @Override protected void onPause() { super.onPause(); - visiblePackageName = null; // save the active URL for this app in case we come back getPreferences(MODE_PRIVATE) .edit() @@ -554,7 +581,7 @@ public class AppDetails extends AppCompatActivity { String errorMessage = intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE); - if (!TextUtils.isEmpty(errorMessage)) { + if (!TextUtils.isEmpty(errorMessage) && !isFinishing()) { Log.e(TAG, "install aborted with errorMessage: " + errorMessage); String title = String.format( @@ -701,14 +728,6 @@ public class AppDetails extends AppCompatActivity { app = newApp; startingPrefs = app.getPrefs(this).createClone(); - - // Remove all "installed" statuses for this app, since we are now viewing it. - AppUpdateStatusManager appUpdateStatusManager = AppUpdateStatusManager.getInstance(this); - for (AppUpdateStatusManager.AppUpdateStatus status : appUpdateStatusManager.getByPackageName(app.packageName)) { - if (status.status == AppUpdateStatusManager.Status.Installed) { - appUpdateStatusManager.removeApk(status.getUniqueKey()); - } - } } private void refreshApkList() { diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java index 317f21084..d1c395d65 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java @@ -64,6 +64,16 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog private LocalBroadcastManager localBroadcastManager; private String activeDownloadUrlString; + /** + * Check if {@code packageName} is currently visible to the user. + */ + public static boolean isAppVisible(String packageName) { + return packageName != null && packageName.equals(visiblePackageName); + } + + private static String visiblePackageName; + + @Override protected void onCreate(Bundle savedInstanceState) { fdroidApp = (FDroidApp) getApplication(); @@ -131,6 +141,41 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog } } + /** + * Some notifications (like "downloading" and "installed") are not shown for this app if it is open in app details. + * When closing, we need to refresh the notifications, so they are displayed again. + */ + private void updateNotificationsForApp() { + if (app != null) { + AppUpdateStatusManager appUpdateStatusManager = AppUpdateStatusManager.getInstance(this); + for (AppUpdateStatusManager.AppUpdateStatus status : appUpdateStatusManager.getByPackageName(app.packageName)) { + if (status.status == AppUpdateStatusManager.Status.Installed) { + appUpdateStatusManager.removeApk(status.getUniqueKey()); + } else { + appUpdateStatusManager.refreshApk(status.getUniqueKey()); + } + } + } + } + + @Override + protected void onResume() { + super.onResume(); + if (app != null) { + visiblePackageName = app.packageName; + } + updateNotificationsForApp(); + } + + protected void onStop() { + super.onStop(); + visiblePackageName = null; + + // When leaving the app details, make sure to refresh app status for this app, since + // we might want to show notifications for it now. + updateNotificationsForApp(); + } + @Override public boolean onCreateOptionsMenu(Menu menu) { boolean ret = super.onCreateOptionsMenu(menu); @@ -372,7 +417,7 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog String errorMessage = intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE); - if (!TextUtils.isEmpty(errorMessage)) { + if (!TextUtils.isEmpty(errorMessage) && !isFinishing()) { Log.e(TAG, "install aborted with errorMessage: " + errorMessage); String title = String.format( diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 0ea683021..92ad49a82 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -237,6 +237,16 @@ public class AppUpdateStatusManager { } } + public void refreshApk(String key) { + synchronized (appMapping) { + AppUpdateStatus entry = appMapping.get(key); + if (entry != null) { + Utils.debugLog(LOGTAG, "Refresh APK " + entry.apk.apkName); + notifyChange(entry, true); + } + } + } + public void updateApkProgress(String key, int max, int current) { synchronized (appMapping) { AppUpdateStatus entry = appMapping.get(key); diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index f1149601b..e06a27594 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -178,8 +178,9 @@ class NotificationHelper { // Ignore unknown status if (entry.status == AppUpdateStatusManager.Status.Unknown) return true; - // Ignore first time install downloads, assumed to be done from UI - else if (!entry.app.isInstalled() && (entry.status == AppUpdateStatusManager.Status.Downloading || entry.status == AppUpdateStatusManager.Status.ReadyToInstall)) + // Ignore downloading, readyToInstall and installError if we are showing the details screen for this app + else if ((entry.status == AppUpdateStatusManager.Status.Downloading || entry.status == AppUpdateStatusManager.Status.ReadyToInstall || entry.status == AppUpdateStatusManager.Status.InstallError) && + (AppDetails.isAppVisible(entry.app.packageName) || AppDetails2.isAppVisible(entry.app.packageName))) return true; return false; } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index 23fe520dd..ebd53031d 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -15,6 +15,7 @@ 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.AppDetails2; import org.fdroid.fdroid.AppUpdateStatusManager; import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.Utils; @@ -313,17 +314,13 @@ public class InstallManagerService extends Service { localBroadcastManager.unregisterReceiver(this); break; case Installer.ACTION_INSTALL_INTERRUPTED: - AppUpdateStatusManager.AppUpdateStatus status = appUpdateStatusManager.get(downloadUrl); - appUpdateStatusManager.removeApk(downloadUrl); apk = intent.getParcelableExtra(Installer.EXTRA_APK); String errorMessage = intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE); - - // show notification if app details is not visible if (!TextUtils.isEmpty(errorMessage)) { - if (status == null || status.app == null || !AppDetails.isAppVisible(status.app.packageName)) { - appUpdateStatusManager.setApkError(apk, errorMessage); - } + appUpdateStatusManager.setApkError(apk, errorMessage); + } else { + appUpdateStatusManager.removeApk(downloadUrl); } localBroadcastManager.unregisterReceiver(this); break; From a4c187f3e18b2b7b32396e56ea285452165efa75 Mon Sep 17 00:00:00 2001 From: mvp76 Date: Tue, 28 Feb 2017 17:15:42 +0100 Subject: [PATCH 24/31] Add strings for first time installs --- .../java/org/fdroid/fdroid/NotificationHelper.java | 12 ++++++------ app/src/main/res/values/strings.xml | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index e06a27594..6b1a2ca87 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -273,7 +273,7 @@ class NotificationHelper { case Downloading: return app.name; case ReadyToInstall: - return context.getString(R.string.notification_title_single_ready_to_install_update); + return context.getString(app.isInstalled() ? R.string.notification_title_single_ready_to_install_update : R.string.notification_title_single_ready_to_install); case Installing: return app.name; case Installed: @@ -289,7 +289,7 @@ class NotificationHelper { case UpdateAvailable: return app.name; case Downloading: - return context.getString(R.string.notification_content_single_downloading_update, app.name); + return context.getString(app.isInstalled() ? R.string.notification_content_single_downloading_update : R.string.notification_content_single_downloading, app.name); case ReadyToInstall: return app.name; case Installing: @@ -302,14 +302,14 @@ class NotificationHelper { return ""; } - private String getMultiItemContentString(AppUpdateStatusManager.Status status) { + private String getMultiItemContentString(App app, AppUpdateStatusManager.Status status) { switch (status) { case UpdateAvailable: return context.getString(R.string.notification_title_summary_update_available); case Downloading: - return context.getString(R.string.notification_title_summary_downloading_update); + return context.getString(app.isInstalled() ? R.string.notification_title_summary_downloading_update : R.string.notification_title_summary_downloading); case ReadyToInstall: - return context.getString(R.string.notification_title_summary_ready_to_install_update); + return context.getString(app.isInstalled() ? R.string.notification_title_summary_ready_to_install_update : R.string.notification_title_summary_ready_to_install); case Installing: return context.getString(R.string.notification_title_summary_installing); case Installed: @@ -383,7 +383,7 @@ class NotificationHelper { App app = entry.app; AppUpdateStatusManager.Status status = entry.status; - String content = getMultiItemContentString(status); + String content = getMultiItemContentString(app, status); SpannableStringBuilder sb = new SpannableStringBuilder(app.name); sb.setSpan(new StyleSpan(Typeface.BOLD), 0, sb.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); sb.append(" "); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f1839d6e6..2976c7f7a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -401,8 +401,10 @@ +%1$d more… Update Available + Ready to install Update ready to install Install Failed + Downloading \"%1$s\"… Downloading update for \"%1$s\"… Installing \"%1$s\"… Successfully installed @@ -410,7 +412,9 @@ %1$d Updates %1$d Apps Installed Update available + Downloading… Downloading update… + Ready to install Update ready to install Installing Successfully installed From 7b446daf3fa9d7cf2c202be7aaaa64a1b55d206d Mon Sep 17 00:00:00 2001 From: mvp76 Date: Tue, 28 Feb 2017 21:57:51 +0100 Subject: [PATCH 25/31] Make AppUpdateStatusManager a singleton --- .../main/java/org/fdroid/fdroid/AppUpdateStatusManager.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 92ad49a82..3362299a6 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -45,8 +45,12 @@ public class AppUpdateStatusManager { } public static AppUpdateStatusManager getInstance(Context context) { - return new AppUpdateStatusManager(context); + if (instance == null) { + instance = new AppUpdateStatusManager(context.getApplicationContext()); + } + return instance; } + private static AppUpdateStatusManager instance; public class AppUpdateStatus { public final App app; From a8509ae8331c404c8235a7a7534ecd226c6c2b4b Mon Sep 17 00:00:00 2001 From: mvp76 Date: Tue, 28 Feb 2017 22:07:40 +0100 Subject: [PATCH 26/31] Added a comment about the key being the downloadUrl --- .../java/org/fdroid/fdroid/AppUpdateStatusManager.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 3362299a6..ccb27aaea 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -23,6 +23,16 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +/** + * Manages the state of APKs that are being installed or that have updates available. + *

+ * The full URL for the APK file to download is used as the unique ID to + * represent the status of the APK throughout F-Droid. The full download URL is guaranteed + * to be unique since it points to files on a filesystem, where there cannot be multiple files with + * the same name. This provides a unique ID beyond just {@code packageName} + * and {@code versionCode} since there could be different copies of the same + * APK on different servers, signed by different keys, or even different builds. + */ public class AppUpdateStatusManager { static final String BROADCAST_APPSTATUS_LIST_CHANGED = "org.fdroid.fdroid.installer.appstatus.listchange"; From 1418a419eab3dbcf04776cf6a450d7bbfe1bc128 Mon Sep 17 00:00:00 2001 From: mvp76 Date: Tue, 28 Feb 2017 22:24:26 +0100 Subject: [PATCH 27/31] Remove "InstallError" entries when clearing notification I.e. either the user "swiped" the notification away or clicked on it to view the error message (and thus the notification was auto canceled) --- .../main/java/org/fdroid/fdroid/NotificationHelper.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index 6b1a2ca87..c3273d0ee 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -87,10 +87,15 @@ class NotificationHelper { appUpdateStatusManager.clearAllInstalled(); break; case BROADCAST_NOTIFICATIONS_UPDATE_CLEARED: + // If clearing apps in state "InstallError" (like when auto-cancelling) we + // remove them from the status manager entirely. + AppUpdateStatusManager.AppUpdateStatus appUpdateStatus = appUpdateStatusManager.get(intent.getStringExtra(EXTRA_NOTIFICATION_KEY)); + if (appUpdateStatus != null && appUpdateStatus.status == AppUpdateStatusManager.Status.InstallError) { + appUpdateStatusManager.removeApk(intent.getStringExtra(EXTRA_NOTIFICATION_KEY)); + } break; case BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED: - String key = intent.getStringExtra(EXTRA_NOTIFICATION_KEY); - appUpdateStatusManager.removeApk(key); + appUpdateStatusManager.removeApk(intent.getStringExtra(EXTRA_NOTIFICATION_KEY)); break; } } From 238dff5c104e598101fb2ab064c8d6555b898330 Mon Sep 17 00:00:00 2001 From: mvp76 Date: Tue, 28 Feb 2017 22:45:41 +0100 Subject: [PATCH 28/31] Update download progress for OBB files --- .../java/org/fdroid/fdroid/installer/InstallManagerService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index ebd53031d..93bb02147 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -202,7 +202,7 @@ public class InstallManagerService extends Service { int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0); int totalBytes = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 0); - // TODO - handle obb notifications? + appUpdateStatusManager.updateApkProgress(urlString, totalBytes, bytesRead); } else if (Downloader.ACTION_COMPLETE.equals(action)) { localBroadcastManager.unregisterReceiver(this); File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH)); From 0b53f694270d1dddeae879d6fb552cf53b89a994 Mon Sep 17 00:00:00 2001 From: mvp76 Date: Tue, 28 Feb 2017 23:15:31 +0100 Subject: [PATCH 29/31] Remove static from appMapping --- app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index ccb27aaea..7aa3d6476 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -85,7 +85,7 @@ public class AppUpdateStatusManager { private final Context context; private final LocalBroadcastManager localBroadcastManager; - private static final HashMap appMapping = new HashMap<>(); + private final HashMap appMapping = new HashMap<>(); private boolean isBatchUpdating; private AppUpdateStatusManager(Context context) { From d43b31a701624b141c2719e32007dfc4413e71b0 Mon Sep 17 00:00:00 2001 From: mvp76 Date: Tue, 28 Feb 2017 23:43:23 +0100 Subject: [PATCH 30/31] Fix checkstyle errors --- .../java/org/fdroid/fdroid/AppDetails2.java | 1 - .../fdroid/fdroid/AppUpdateStatusManager.java | 10 ++-- .../org/fdroid/fdroid/NotificationHelper.java | 48 ++++++++++--------- .../installer/InstallManagerService.java | 2 - 4 files changed, 31 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java index d1c395d65..96686b35a 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java @@ -73,7 +73,6 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog private static String visiblePackageName; - @Override protected void onCreate(Bundle savedInstanceState) { fdroidApp = (FDroidApp) getApplication(); diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 7aa3d6476..8ca9fa56d 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -33,7 +33,7 @@ import java.util.Map; * and {@code versionCode} since there could be different copies of the same * APK on different servers, signed by different keys, or even different builds. */ -public class AppUpdateStatusManager { +public final 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"; @@ -60,6 +60,7 @@ public class AppUpdateStatusManager { } return instance; } + private static AppUpdateStatusManager instance; public class AppUpdateStatus { @@ -125,7 +126,7 @@ public class AppUpdateStatusManager { private void updateApkInternal(@NonNull AppUpdateStatus entry, @NonNull Status status, PendingIntent intent) { Utils.debugLog(LOGTAG, "Update APK " + entry.apk.apkName + " state to " + status.name()); - boolean isStatusUpdate = (entry.status != status); + boolean isStatusUpdate = entry.status != status; entry.status = status; entry.intent = intent; // If intent not set, see if we need to create a default intent @@ -187,7 +188,6 @@ public class AppUpdateStatusManager { } } - public void addApks(List apksToUpdate, Status status) { startBatchUpdates(); for (Apk apk : apksToUpdate) { @@ -300,7 +300,7 @@ public class AppUpdateStatusManager { void clearAllUpdates() { synchronized (appMapping) { - for (Iterator> it = appMapping.entrySet().iterator(); it.hasNext(); ) { + for (Iterator> it = appMapping.entrySet().iterator(); it.hasNext();) { Map.Entry entry = it.next(); if (entry.getValue().status != Status.Installed) { it.remove(); @@ -312,7 +312,7 @@ public class AppUpdateStatusManager { void clearAllInstalled() { synchronized (appMapping) { - for (Iterator> it = appMapping.entrySet().iterator(); it.hasNext(); ) { + for (Iterator> it = appMapping.entrySet().iterator(); it.hasNext();) { Map.Entry entry = it.next(); if (entry.getValue().status == Status.Installed) { it.remove(); diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index c3273d0ee..ea89f6e66 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -109,28 +109,30 @@ class NotificationHelper { BroadcastReceiver receiverAppStatusChanges = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { + AppUpdateStatusManager.AppUpdateStatus entry; + String url; + switch (intent.getAction()) { case AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED: notificationManager.cancelAll(); updateStatusLists(); createSummaryNotifications(); - for (AppUpdateStatusManager.AppUpdateStatus entry : appUpdateStatusManager.getAll()) { - createNotification(entry); + for (AppUpdateStatusManager.AppUpdateStatus appUpdateStatus : appUpdateStatusManager.getAll()) { + createNotification(appUpdateStatus); } break; - case AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED: { + case AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED: updateStatusLists(); createSummaryNotifications(); - String url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); - AppUpdateStatusManager.AppUpdateStatus entry = appUpdateStatusManager.get(url); + url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); + entry = appUpdateStatusManager.get(url); if (entry != null) { createNotification(entry); } break; - } - case AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED: { - String url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); - AppUpdateStatusManager.AppUpdateStatus entry = appUpdateStatusManager.get(url); + case AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED: + url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); + entry = appUpdateStatusManager.get(url); updateStatusLists(); if (entry != null) { createNotification(entry); @@ -139,15 +141,13 @@ class NotificationHelper { createSummaryNotifications(); } break; - } - case AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED: { - String url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); + case AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED: + url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); notificationManager.cancel(url, NOTIFY_ID_INSTALLED); notificationManager.cancel(url, NOTIFY_ID_UPDATES); updateStatusLists(); createSummaryNotifications(); break; - } } } }; @@ -181,12 +181,13 @@ class NotificationHelper { private boolean shouldIgnoreEntry(AppUpdateStatusManager.AppUpdateStatus entry) { // Ignore unknown status - if (entry.status == AppUpdateStatusManager.Status.Unknown) + if (entry.status == AppUpdateStatusManager.Status.Unknown) { return true; + } else if ((entry.status == AppUpdateStatusManager.Status.Downloading || entry.status == AppUpdateStatusManager.Status.ReadyToInstall || entry.status == AppUpdateStatusManager.Status.InstallError) && + (AppDetails.isAppVisible(entry.app.packageName) || AppDetails2.isAppVisible(entry.app.packageName))) { // Ignore downloading, readyToInstall and installError if we are showing the details screen for this app - else if ((entry.status == AppUpdateStatusManager.Status.Downloading || entry.status == AppUpdateStatusManager.Status.ReadyToInstall || entry.status == AppUpdateStatusManager.Status.InstallError) && - (AppDetails.isAppVisible(entry.app.packageName) || AppDetails2.isAppVisible(entry.app.packageName))) return true; + } return false; } @@ -227,7 +228,6 @@ class NotificationHelper { } } - private void createSummaryNotifications() { if (!notificationManager.areNotificationsEnabled()) { return; @@ -361,10 +361,11 @@ class NotificationHelper { // Handle progress bar (for some states) // if (status == AppUpdateStatusManager.Status.Downloading) { - if (entry.progressMax == 0) + if (entry.progressMax == 0) { builder.setProgress(100, 0, true); - else + } else { builder.setProgress(entry.progressMax, entry.progressCurrent, false); + } } else if (status == AppUpdateStatusManager.Status.Installing) { builder.setProgress(100, 0, true); // indeterminate bar } @@ -395,8 +396,9 @@ class NotificationHelper { sb.append(content); inboxStyle.addLine(sb); - if (text.length() > 0) + if (text.length() > 0) { text.append(", "); + } text.append(app.name); } @@ -469,8 +471,9 @@ class NotificationHelper { for (int i = 0; i < MAX_INSTALLED_TO_SHOW && i < installed.size(); i++) { AppUpdateStatusManager.AppUpdateStatus entry = installed.get(i); App app = entry.app; - if (text.length() > 0) + if (text.length() > 0) { text.append(", "); + } text.append(app.name); } bigTextStyle.bigText(text); @@ -510,7 +513,8 @@ class NotificationHelper { w = context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width); h = context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height); } else { - w = h = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); + w = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); + h = w; } return new Point(w, h); } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index 93bb02147..51cc09f3d 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -14,8 +14,6 @@ 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.AppDetails2; import org.fdroid.fdroid.AppUpdateStatusManager; import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.Utils; From eb3c6e6e0b1ca32384597999537be8703ad023f7 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Wed, 1 Mar 2017 13:41:23 +1100 Subject: [PATCH 31/31] Fix lint warnings. Removed unused translations from values-* folders. Used `app:srcCompat` instead of android:src for some `ImageView`s --- app/src/main/res/layout/donate_bitcoin.xml | 3 ++- app/src/main/res/layout/donate_flattr.xml | 3 ++- app/src/main/res/layout/donate_litecoin.xml | 3 ++- app/src/main/res/layout/share_header_item.xml | 9 +++++---- app/src/main/res/values-ar/strings.xml | 2 -- app/src/main/res/values-ast/strings.xml | 2 -- app/src/main/res/values-bg/strings.xml | 2 -- app/src/main/res/values-ca/strings.xml | 2 -- app/src/main/res/values-cs/strings.xml | 2 -- app/src/main/res/values-da/strings.xml | 2 -- app/src/main/res/values-de/strings.xml | 2 -- app/src/main/res/values-el/strings.xml | 2 -- app/src/main/res/values-eo/strings.xml | 2 -- app/src/main/res/values-es/strings.xml | 2 -- app/src/main/res/values-et/strings.xml | 2 -- app/src/main/res/values-eu/strings.xml | 2 -- app/src/main/res/values-fa/strings.xml | 2 -- app/src/main/res/values-fi/strings.xml | 2 -- app/src/main/res/values-fr/strings.xml | 2 -- app/src/main/res/values-gl/strings.xml | 2 -- app/src/main/res/values-he/strings.xml | 2 -- app/src/main/res/values-hi/strings.xml | 2 -- app/src/main/res/values-hr/strings.xml | 2 -- app/src/main/res/values-hu/strings.xml | 2 -- app/src/main/res/values-id/strings.xml | 2 -- app/src/main/res/values-is/strings.xml | 2 -- app/src/main/res/values-it/strings.xml | 2 -- app/src/main/res/values-ja/strings.xml | 2 -- app/src/main/res/values-ko/strings.xml | 2 -- app/src/main/res/values-lt/strings.xml | 2 -- app/src/main/res/values-lv/strings.xml | 2 -- app/src/main/res/values-my/strings.xml | 2 -- app/src/main/res/values-nb/strings.xml | 2 -- app/src/main/res/values-nl/strings.xml | 2 -- app/src/main/res/values-pl/strings.xml | 2 -- app/src/main/res/values-pt-rBR/strings.xml | 2 -- app/src/main/res/values-pt-rPT/strings.xml | 2 -- app/src/main/res/values-ro/strings.xml | 2 -- app/src/main/res/values-ru/strings.xml | 2 -- app/src/main/res/values-sc/strings.xml | 2 -- app/src/main/res/values-sk/strings.xml | 2 -- app/src/main/res/values-sn/strings.xml | 2 -- app/src/main/res/values-sr/strings.xml | 2 -- app/src/main/res/values-sv/strings.xml | 2 -- app/src/main/res/values-ta/strings.xml | 2 -- app/src/main/res/values-th/strings.xml | 2 -- app/src/main/res/values-tr/strings.xml | 2 -- app/src/main/res/values-ug/strings.xml | 2 -- app/src/main/res/values-uk/strings.xml | 2 -- app/src/main/res/values-vi/strings.xml | 2 -- app/src/main/res/values-zh-rCN/strings.xml | 2 -- app/src/main/res/values-zh-rHK/strings.xml | 2 -- app/src/main/res/values-zh-rTW/strings.xml | 2 -- 53 files changed, 11 insertions(+), 105 deletions(-) diff --git a/app/src/main/res/layout/donate_bitcoin.xml b/app/src/main/res/layout/donate_bitcoin.xml index 2ba601ee6..9e538fb8e 100644 --- a/app/src/main/res/layout/donate_bitcoin.xml +++ b/app/src/main/res/layout/donate_bitcoin.xml @@ -1,11 +1,12 @@ \ No newline at end of file diff --git a/app/src/main/res/layout/donate_flattr.xml b/app/src/main/res/layout/donate_flattr.xml index e78df12c6..ad9092e77 100644 --- a/app/src/main/res/layout/donate_flattr.xml +++ b/app/src/main/res/layout/donate_flattr.xml @@ -1,11 +1,12 @@ \ No newline at end of file diff --git a/app/src/main/res/layout/donate_litecoin.xml b/app/src/main/res/layout/donate_litecoin.xml index beca15e9f..7a84eb367 100644 --- a/app/src/main/res/layout/donate_litecoin.xml +++ b/app/src/main/res/layout/donate_litecoin.xml @@ -1,7 +1,8 @@ \ No newline at end of file diff --git a/app/src/main/res/layout/share_header_item.xml b/app/src/main/res/layout/share_header_item.xml index ddf0aa942..daa2d000d 100644 --- a/app/src/main/res/layout/share_header_item.xml +++ b/app/src/main/res/layout/share_header_item.xml @@ -1,13 +1,14 @@ - + android:paddingBottom="20dp"> diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 274e49035..b162d7bc9 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -119,8 +119,6 @@ تم التثبيت تم التثبيت (%d) تحديثات (%d) - يتوفر تحديث. - %d يتوفر على تحديثات. تحديثات اف-درويد متوفرة +%1$d المزيد… لا توجد طريقة إرسال بلوتوث، فضلاً إختر واحدة ! diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml index d769396e7..90c5be67b 100644 --- a/app/src/main/res/values-ast/strings.xml +++ b/app/src/main/res/values-ast/strings.xml @@ -45,8 +45,6 @@ Sobrescribir Disponible Anovamientos - Ta disponible 1 anovamientu. - Tán disponibles %d anovamientos. Anovamientos de F-Droid disponibles Nun s\'alcontró dal métodu d\'unviu pente Bluetooth, ¡escueyi ún! Escueyi\'l métodu d\'unvu pente Bluetooth diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 8f6df2c82..b4ebb534d 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -42,8 +42,6 @@ Добавете ключ Налични Обновявания - 1 налична актуализация. - %d налични актуализации. Налични актуализации от F-Droid Не е намерен Bluetooth метод за изпращане, изберете един! Изберете Bluetooth метод за изпращане diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 43cd7a666..b26d706d2 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -44,8 +44,6 @@ Sobreescriu Disponible Actualitzacions - Hi ha 1 actualització disponible. - Hi ha %d actualitzacions disponibles. Hi ha actualitzacions disponibles de l\'F-Droid Trieu un mètode d\'enviament per Bluetooth! Envia per Bluetooth diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 9d7c9f706..549a1e09a 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -41,8 +41,6 @@ Přepsat Dostupné Aktualizace - 1 aktualizace je dostupná. - %d aktualizací je dostupných. Dostupné aktualizace F-Droid Poslat přes bluetooth Adresa repositáře diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 1388c2e2a..3082002c7 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -75,8 +75,6 @@ Opdateringer Installeret (%d) Opdateringer (%d) - 1 opdatering er tilgængelig. - %d opdateringer er tilgængelige. F-Droid Opdateringer Tilgængelige +%1$d flere… Ingen Bluetooth afsendelsesmetode fundet, vælg en! diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 44e75d98e..f613a9547 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -45,8 +45,6 @@ Überschreiben Verfügbar Aktualisierungen - Eine Aktualisierung ist verfügbar. - %d Aktualisierungen sind verfügbar. F-Droid: Aktualisierungen verfügbar Keine Bluetooth-Sendemethode gefunden, bitte eine auswählen! Bluetooth-Sendemethode auswählen diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index f004b3e07..3d910a241 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -37,8 +37,6 @@ Αντικατάσταση Διαθέσιμα Ενημερώσεις - 1 διαθέσιμη ενημερώση. - %d διαθέσιμες ενημερώσεις. Υπάρχουν ενημερώσεις του F-Droid Καμία μέθοδος αποστολής με Bluetooth δεν βρέθηκε, επιλέξετε μία! Επιλέξτε τη μέθοδο αποστολής με Bluetooth diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index b301ab46a..9606e1696 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -28,8 +28,6 @@ Anstataŭigi Disponeblaj Ĝisdatigeblaj - 1 ĝisdatigo estas disponebla. - %d ĝisdatigoj estas disponeblaj. F-Droidaj ĝisdatigoj disponeblaj Sendi per Bludento Deponeja adreso diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index a2923498a..46ceb91e3 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -45,8 +45,6 @@ Sobrescribir Disponible Actualizaciones - 1 actualización disponible. - %d actualizaciones disponibles. Actualizaciones de F-Droid disponibles No se encontró método de envío Bluetooth, ¡elige uno! Elegir el método de envío Bluetooth diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 2b75d1e18..db97687ce 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -78,8 +78,6 @@ Värskendused Paigaldatud (%d) Värskendused (%d) - Üks värskendus on saadaval. - %d värskendust on saadaval. F-Droid: värskendused saadaval +%1$d veel… Bluetoothiga saatmisviise ei leitud. Valige üks! diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 8ca03d7c1..a67ac036d 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -31,8 +31,6 @@ Gainidatzi Eskuragarri Eguneraketak - Eguneraketa 1 eskuragarri. - %d eguneraketa eskuragarri. F-Droid eguneraketak eskuragarri Biltegiaren helbidea Eguneratu biltegiak diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index eeca7fdf3..97406bfaa 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -114,8 +114,6 @@ نصب شده نصب شده (%d) به‌روز رسانی‌ها (%d) - ۱ به‌روز رسانی موجود است. - %d به روز رسانی موجود است. +%1$d بیش‌تر… ارسال با بلوتوث diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index b97a36663..2a092b69a 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -44,8 +44,6 @@ Korvaa Saatavilla Päivityksiä - 1 päivitys saatavilla. - %d päivitystä saatavilla. F-Droid: Päivityksiä saatavilla Bluetooth -lähetystapaa ei löytynyt, valitse yksi! Valitse Bluetooth -lähetystapa diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1d130254c..a2e8a5ebb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -45,8 +45,6 @@ Écraser Disponibles Mises à jour - Une mise à jour est disponible. - %d mises à jour sont disponibles. Des mises à jour F-Droid sont disponibles Aucune méthode d\'envoi Bluetooth n\'a été trouvée, choisissez-en une ! Choisir une méthode d\'envoi Bluetooth diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 2f795d276..e8adef054 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -27,8 +27,6 @@ Cancelar Dispoñíbel Actualizacións - 1 Actualización dispoñíbel. - %d actualizacións dispoñíbeis. Actualizacións de F-Droid dispoñíbeis Enderezo do repositorio Actualizar repositorios diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 18fd1eeb8..01d438602 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -54,8 +54,6 @@ זמינות עדכונים - עדכון 1 זמין. - %d עדכונים זמינים. עדכוני F-Droid זמינים +%1$d עוד… לא נמצאה שיטת שליחה של Bluetooth, בחר אחת! diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 276900dc7..be431ba09 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -147,8 +147,6 @@ सारी रेपोसितोरिएस नवीनतम हैं सभी दुसरे रेपो ने कोई गलती नहीं बनाई| अपडेट के दौरान एरर: %s - १ अपडेट उपलब्ध है| - %d अपडेटस उपलब्ध हैं| F-Droid अपडेट उपलब्ध +%1$d और… Bluettoth से भेजने का तरीका उपलब नहीं है, एक चुने! diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 68d0c5ea7..8c0061383 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -78,8 +78,6 @@ Ažuriranja Instalirano (%d) Ažuriranja (%d) - 1 ažuriranje je dostupno. - %d ažuriranja je dostupno. Ažuriranja za F-Droid dostupna +%1$d više… Nije pronađena metoda slanja preko Bluetootha, odaberite jednu! diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index f57f2d214..1cfffa6e3 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -35,8 +35,6 @@ Felülírás Elérhető Frissítések - 1 frissítés érhető el. - %d frissítés érhető el. F-Droid frissítés érhető el Repo cím Ujjlenyomat (opcionális) diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 2cef07854..b03620d02 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -63,8 +63,6 @@ Tersedia Terpasang Pembaruan - 1 pembaruan tersedia. - %d pembaruan tersedia. Tersedia Pembaruan F-Droid Metode pengiriman Bluetooth tidak ditemukan, pilih salah satu! Pilih metode pengiriman Bluetooth diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index a539847ed..1117d9c81 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -66,8 +66,6 @@ Uppfærslur Uppsett (%d) Uppfærslur (%d) - 1 uppfærsla er tiltæk. - %d uppfærslur eru tiltækar. Uppfærslur á F-Droid eru tiltækar +%1$d fleiri… Engin aðferð til sendingar með Bluetooth fannst, veldu eina! diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 087465c82..ed46188be 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -43,8 +43,6 @@ Sovrascrivi Disponibile Aggiornamenti - 1 aggiornamento disponibile. - %d aggiornamenti disponibili. Aggiornamenti per F-Droid disponibili Non è possibile inviare via Bluetooth, scegliere un altro metodo! Scegli invio mediante Bluetooth diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 3ebec0d32..ccaa0e567 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -45,8 +45,6 @@ 上書き 入手可能 更新 - 1個の更新があります。 - %d個の更新があります. F-Droidの更新があります Bluetoothの送信方法がありません、選択してください! Bluetoothの送信方法を選択してください diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 0a8977657..116523830 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -45,8 +45,6 @@ 덮어쓰기 사용 가능 업데이트 - 1개의 업데이트를 사용할 수 있습니다. - %d개의 업데이트를 사용할 수 있습니다. F-Droid 업데이트를 사용할 수 있습니다. 블루투스 전송 방법을 찾을 수 없습니다. 선택하세요! 블루투스 전송 방법 선택 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index c6368478e..db5271564 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -29,8 +29,6 @@ Perrašyti Prieinamos programos Atnaujinimai - Atsirado 1 atnaujinimas. - Galite įdiegti %d atnaujinimus. Siųsti per Bluetooth Saugyklos adresas Atnaujinti saugyklas diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index bd0e49336..eda01cbc3 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -40,8 +40,6 @@ Pārrakstīt Pieejams Atjauninājumi - 1 atjauninājums pieejams. - %d atjauninājumi pieejami. Nav Bluetooth aktivitāšu, izvēlies vienu! Izvēlies Bluesūtīšanas metodi Bluesūtīt diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 2bfc0f244..603041a05 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -74,8 +74,6 @@ အသစ္မြမး္မံမႈမ်ား သြင္းထားခဲ့သည္ (%d) အသစ္မြမ္းမံမႈမ်ား (%d) - အသစ္မြမ္းမံမႈ ၁ ခုရရွိႏိုင္သည္ - အသစ္မြမ္းမံမႈ %d ခုရရွိႏိုင္သည္. F-Droid အသစ္မြမ္းမံမႈရရွိႏိုင္သည္ +%1$d ေနာက္ထပ္.. ဘလူးသုဒ့္ႏွင့္ပို႔ရန္နည္းလမ္းရွာမေတြ႕ပါ။ တစ္ခုေရြးပါ! diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 64a3c0ee5..d15ffb55d 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -45,8 +45,6 @@ Overskriv Tilgjengelig Oppdateringer - 1 oppdatering tilgjengelig. - %d oppdateringer tilgjengelig. F-Droid: Oppdateringer tilgjengelig Ingen forsendelsesmåte for Blåtann funnet, velg en! Velg forsendelsesmåte for Blåtann diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 6b9e58fbd..1d4fe6593 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -45,8 +45,6 @@ Overschrijven Beschikbaar Updates - 1 update beschikbaar. - %d updates beschikbaar. F-Droid-updates beschikbaar Geen Bluetooth-verzendmethode gevonden, kies er een! Kies Bluetooth-verzendmethode diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 7570096e5..b74969e7d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -34,7 +34,6 @@ Nadpisz Dostępne Aktualizacje - Dostępne jest 1 uaktualnienie. Uaktualnienie F-Droid jest dostępne Wyślij przez Bluetooth Adres repozytorium @@ -154,7 +153,6 @@ Uwaga: Wszystkie poprzednio zainstalowane aplikacje zostaną na urządzeniu.Linki Powrót - Liczba dostępnych aktualizacji: %d. +%1$d więcej… Nie znaleziono metody do wysłania przez Bluetooth! Wybierz metodę wysłania przez Bluetooth diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index b2c72e12f..500814b7b 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -45,8 +45,6 @@ Sobrescrever Disponível Atualizações - 1 atualização disponível. - %d atualizações disponíveis. Atualizações do F-Droid disponíveis Nenhum método de envio por Bluetooth foi encontrado, escolha um! Escolher o método de envio Bluetooth diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 618a31938..cb98498d8 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -63,8 +63,6 @@ Disponíveis Instaladas Atualizações - 1 atualização disponível. - %d atualizações disponíveis. Atualizações F-Droid disponíveis +%1$d… Nenhum método de envio Bluetooth encontrado. Escolha um! diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 4ae5ada7b..1d29bf79a 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -22,8 +22,6 @@ Activeaza Disponibil Actualizari - O actualizare este disponibila. - %d actualizari sunt disponibile. Despre Cauta Porneste diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b3cfbf3cf..e26220762 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -45,8 +45,6 @@ Перезаписать Доступно Обновления - Доступно 1 обновление. - Доступно %d обновлений. Доступны обновления для F-Droid Методы отправки по Bluetooth не найдены, выберите какой-либо! Выберите метод отправки по Bluetooth diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml index f5e0fbd99..568c29aa7 100644 --- a/app/src/main/res/values-sc/strings.xml +++ b/app/src/main/res/values-sc/strings.xml @@ -45,8 +45,6 @@ Subraiscrie Disponìbiles Agiornamentos - 1 agiornamentu est disponìbile. - %d agiornamentos sunt disponìbiles. Agiornamentos pro F-Droid disponìbiles Perunu mètodu de imbiu Bluetooth agatadu, issèberane unu! Issèbera su mètodu de imbiu Bluetooth diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 53b71a12e..d95e46614 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -45,8 +45,6 @@ Prepísať Dostupné Aktualizácie - Dostupná 1 aktualizácia. - Dostupných %d aktualizácií. Dostupné aktualizácie F-Droidu Poslať cez Bluetooth Adresa repozitára diff --git a/app/src/main/res/values-sn/strings.xml b/app/src/main/res/values-sn/strings.xml index 521acf544..99b30f468 100644 --- a/app/src/main/res/values-sn/strings.xml +++ b/app/src/main/res/values-sn/strings.xml @@ -73,8 +73,6 @@ Zvekunatsa Zvavakirirwa (%d) Zvekunatsa (%d) - Chekunatsa 1 chiripo. - Zvekunatsa %d zviripo. Zvekunatsa F-Droid zviripo zvimwe +%1$d … Hapana mutowo wekutumira neBluetooth wawanikwa, sarudza imwe chete! diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 62d7a0b6a..6edaab0c9 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -44,8 +44,6 @@ Пребриши Доступне Надоградње - 1 надоградња је доступна. - %d нових надоградњи је доступно. Доступне су надоградње на Ф-дроиду Нема начина за слања блутутом, одредите један! Одредите начин слања блутутом diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 64f7ef44a..ec7146078 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -45,8 +45,6 @@ Skriv över Tillgängliga Uppdateringar - 1 uppdatering finns tillgänglig. - %d uppdateringar finns tillgängliga. Uppdateringar för F-Droid tillgängliga Ingen metod för att kunna skicka med Bluetooth kunde hittas, välj en! Välj en sändningsmetod för Bluetooth diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index c045e4335..586b60d61 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -55,8 +55,6 @@ மேம்படுத்தல்கள் நிறுவப்பட்டது (%d) மேம்படுத்தல்கள் (%d) - 1 மேம்படுத்தல் உள்ளது. - %d மேம்படுத்தல்கள் உள்ளன. F-Droid மேம்படுத்தல்கள் உள்ளன ப்ளூடூத் வழியாக அனுப்ப diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 88791c754..4b1753e5a 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -262,8 +262,6 @@ เพิ่มกุญแจเข้ารหัส เขียนทับ - 1 โปรแกรมมีอัพเดต - %d โปรแกรมมีอัพเดต แสดงรายละเอียดสถานะส่วนขยายช่วยติดตั้ง และ/หรือทำการอัพเดต/ถอนการติดตั้งส่วนขยายนี้ เลือกวิธีการส่งต่อผ่านบลูทูธ ยังไม่ได้เลือกวิธีส่งต่อทางบลูทูธ, โปรดเลือกก่อน! diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 701ec9fc9..d272a40e6 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -45,8 +45,6 @@ Üzerine yaz Mevcut Güncellemeler - 1 güncelleme bulunmaktadır. - %d güncelleme bulunmaktadır. F-Droid güncellemeleri bulunmaktadır Hiçbir Bluetooth gönderme yöntemi bulunamadı, birisini seçin! Bluetooth gönderme yöntemi seç diff --git a/app/src/main/res/values-ug/strings.xml b/app/src/main/res/values-ug/strings.xml index d4ac3aee8..3adca7768 100644 --- a/app/src/main/res/values-ug/strings.xml +++ b/app/src/main/res/values-ug/strings.xml @@ -27,8 +27,6 @@ ۋاز كەچ ئىشلىتىشچان يېڭىلانمىلار - 1 يېڭىلانما بار. - %d يېڭىلانما بار. F-Droid يېڭىلانمىلىرى بار خەزىنە ئادرېسى خەزىنە يېڭىلا diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8a045d226..b25caafe2 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -87,8 +87,6 @@ Увімкнути Додати ключ Встановлено - Одне оновлення доступно. - %d оновлень доступно. F-Droid: доступні оновлення +%1$d більше… Надіслати через Bluetooth diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 7f61fc486..ddf6152ea 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -44,8 +44,6 @@ Ghi đè Hiện có Cập nhật - Có 1 bản cập nhật. - Có %d bản cập nhật. Có cập nhật F-Droid Không tìm thấy phương pháp gửi qua Bluetooth mặc định, hãy chọn phương pháp! Chọn phương pháp gửi qua Bluetooth diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6cc9d8922..1db72a224 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -38,8 +38,6 @@ 覆盖 可安装 更新 - 有 1 个可用的更新。 - 有 %d 个可用的更新。 可更新 F-Droid 未找到蓝牙发送方式,请选择一个! 选择蓝牙发送方式 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 6c4698543..238ed0fe3 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -41,8 +41,6 @@ 覆寫 可安裝 更新 - 您有 1 個更新。 - 您有 %d 個更新。 有尚未安裝的 F-Droid 更新 請選擇一個藍牙傳送的方式! 選擇藍牙的傳送方式 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8d4018a3a..4bfb344d5 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -64,8 +64,6 @@ 軟體更新 已安裝 (%d) 軟體更新 (%d) - 有 1 個軟體更新。 - 有 %d 個軟體更新。 有可用的 F-Droid 更新 透過藍牙傳送