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/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java
index 8ff6ee17b..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(
diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java
index 52e06311f..96686b35a 100644
--- a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java
+++ b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java
@@ -64,6 +64,15 @@ 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();
@@ -121,6 +130,49 @@ 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());
+ }
+ }
+ }
+
+ /**
+ * 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
@@ -364,7 +416,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
new file mode 100644
index 000000000..8ca9fa56d
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java
@@ -0,0 +1,378 @@
+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.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;
+
+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.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+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 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";
+ 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) {
+ if (instance == null) {
+ instance = new AppUpdateStatusManager(context.getApplicationContext());
+ }
+ return instance;
+ }
+
+ private static AppUpdateStatusManager instance;
+
+ 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;
+ this.apk = apk;
+ this.status = status;
+ this.intent = intent;
+ }
+
+ public String getUniqueKey() {
+ return apk.getUrl();
+ }
+ }
+
+ private final Context context;
+ private final LocalBroadcastManager localBroadcastManager;
+ private final HashMap appMapping = new HashMap<>();
+ private boolean isBatchUpdating;
+
+ private AppUpdateStatusManager(Context context) {
+ this.context = context;
+ localBroadcastManager = LocalBroadcastManager.getInstance(context.getApplicationContext());
+ }
+
+ @Nullable
+ public AppUpdateStatus get(String key) {
+ synchronized (appMapping) {
+ return appMapping.get(key);
+ }
+ }
+
+ public Collection getAll() {
+ synchronized (appMapping) {
+ return appMapping.values();
+ }
+ }
+
+ /**
+ * 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 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);
+ }
+
+ 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() {
+ 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;
+ }
+ }
+
+ public void addApks(List apksToUpdate, Status status) {
+ startBatchUpdates();
+ for (Apk apk : apksToUpdate) {
+ addApk(apk, status, null);
+ }
+ endBatchUpdates();
+ }
+
+ /**
+ * 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, @Nullable PendingIntent 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);
+ }
+ }
+ }
+
+ /**
+ * @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) {
+ updateApkInternal(entry, status, pendingIntent);
+ }
+ }
+ }
+
+ @Nullable
+ 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) {
+ Utils.debugLog(LOGTAG, "Remove APK " + entry.apk.apkName);
+ appMapping.remove(entry.apk.getUrl());
+ notifyRemove(entry);
+ }
+ }
+ }
+
+ 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);
+ 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);
+ }
+ }
+
+ private void startBatchUpdates() {
+ synchronized (appMapping) {
+ isBatchUpdating = true;
+ }
+ }
+
+ private 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) {
+ 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;
+ }
+
+ /**
+ * 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)
+ .putExtra(ErrorDialogActivity.EXTRA_TITLE, title)
+ .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 7624f710b..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();
@@ -98,6 +103,14 @@ 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 {
SPONGYCASTLE_PROVIDER = new org.spongycastle.jce.provider.BouncyCastleProvider();
enableSpongyCastle();
@@ -262,6 +275,7 @@ public class FDroidApp extends Application {
CleanCacheService.schedule(this);
+ 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
new file mode 100644
index 000000000..ea89f6e66
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java
@@ -0,0 +1,575 @@
+package org.fdroid.fdroid;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.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;
+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 org.fdroid.fdroid.data.App;
+
+import java.util.ArrayList;
+
+class NotificationHelper {
+
+ 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 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_KEY = "key";
+ private static final String GROUP_UPDATES = "updates";
+ private static final String GROUP_INSTALLED = "installed";
+
+ private final Context context;
+ private final NotificationManagerCompat notificationManager;
+ private final AppUpdateStatusManager appUpdateStatusManager;
+ private final DisplayImageOptions displayImageOptions;
+ private final ArrayList updates = new ArrayList<>();
+ private final ArrayList installed = new ArrayList<>();
+
+ NotificationHelper(Context context) {
+ this.context = 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.
+ IntentFilter filter = new IntentFilter();
+ 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);
+ 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:
+ // 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:
+ appUpdateStatusManager.removeApk(intent.getStringExtra(EXTRA_NOTIFICATION_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) {
+ AppUpdateStatusManager.AppUpdateStatus entry;
+ String url;
+
+ switch (intent.getAction()) {
+ case AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED:
+ notificationManager.cancelAll();
+ updateStatusLists();
+ createSummaryNotifications();
+ for (AppUpdateStatusManager.AppUpdateStatus appUpdateStatus : appUpdateStatusManager.getAll()) {
+ createNotification(appUpdateStatus);
+ }
+ break;
+ case AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED:
+ updateStatusLists();
+ createSummaryNotifications();
+ url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL);
+ entry = appUpdateStatusManager.get(url);
+ if (entry != null) {
+ createNotification(entry);
+ }
+ break;
+ case AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED:
+ url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL);
+ entry = appUpdateStatusManager.get(url);
+ updateStatusLists();
+ if (entry != null) {
+ createNotification(entry);
+ }
+ if (intent.getBooleanExtra(AppUpdateStatusManager.EXTRA_IS_STATUS_UPDATE, false)) {
+ createSummaryNotifications();
+ }
+ break;
+ case AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED:
+ url = intent.getStringExtra(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);
+ }
+
+ private boolean useStackedNotifications() {
+ 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;
+ }
+
+ updates.clear();
+ installed.clear();
+
+ for (AppUpdateStatusManager.AppUpdateStatus entry : appUpdateStatusManager.getAll()) {
+ if (entry.status == AppUpdateStatusManager.Status.Installed) {
+ installed.add(entry);
+ } else if (!shouldIgnoreEntry(entry)) {
+ updates.add(entry);
+ }
+ }
+ }
+
+ private boolean shouldIgnoreEntry(AppUpdateStatusManager.AppUpdateStatus entry) {
+ // Ignore unknown status
+ if (entry.status == AppUpdateStatusManager.Status.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
+ return true;
+ }
+ return false;
+ }
+
+ private void createNotification(AppUpdateStatusManager.AppUpdateStatus entry) {
+ if (shouldIgnoreEntry(entry)) {
+ notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES);
+ notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED);
+ return;
+ }
+
+ 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);
+ }
+ }
+ }
+
+ private void createSummaryNotifications() {
+ if (!notificationManager.areNotificationsEnabled()) {
+ return;
+ }
+
+ Notification notification;
+ if (updates.size() != 1 || useStackedNotifications()) {
+ if (updates.size() == 0) {
+ // No updates, remove summary
+ notificationManager.cancel(GROUP_UPDATES, NOTIFY_ID_UPDATES);
+ } else {
+ notification = createUpdateSummaryNotification(updates);
+ notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, notification);
+ }
+ }
+ if (installed.size() != 1 || useStackedNotifications()) {
+ if (installed.size() == 0) {
+ // No installed, remove summary
+ notificationManager.cancel(GROUP_INSTALLED, NOTIFY_ID_INSTALLED);
+ } else {
+ notification = createInstalledSummaryNotification(installed);
+ notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, notification);
+ }
+ }
+ }
+
+ private NotificationCompat.Action getAction(AppUpdateStatusManager.AppUpdateStatus entry) {
+ if (entry.intent != null) {
+ switch (entry.status) {
+ case UpdateAvailable:
+ return new NotificationCompat.Action(R.drawable.ic_file_download, context.getString(R.string.notification_action_update), entry.intent);
+
+ case Downloading:
+ case Installing:
+ return new NotificationCompat.Action(R.drawable.ic_cancel, context.getString(R.string.notification_action_cancel), entry.intent);
+
+ case ReadyToInstall:
+ return new NotificationCompat.Action(R.drawable.ic_file_install, context.getString(R.string.notification_action_install), entry.intent);
+ }
+ }
+ return null;
+ }
+
+ private String getSingleItemTitleString(App app, AppUpdateStatusManager.Status status) {
+ switch (status) {
+ case UpdateAvailable:
+ return context.getString(R.string.notification_title_single_update_available);
+ case 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);
+ case Installing:
+ return app.name;
+ case Installed:
+ return app.name;
+ case InstallError:
+ return context.getString(R.string.notification_title_single_install_error);
+ }
+ return "";
+ }
+
+ private String getSingleItemContentString(App app, AppUpdateStatusManager.Status status) {
+ switch (status) {
+ case UpdateAvailable:
+ return app.name;
+ case Downloading:
+ 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:
+ return context.getString(R.string.notification_content_single_installing, app.name);
+ case Installed:
+ return context.getString(R.string.notification_content_single_installed);
+ case InstallError:
+ return app.name;
+ }
+ return "";
+ }
+
+ private String getMultiItemContentString(App app, AppUpdateStatusManager.Status status) {
+ switch (status) {
+ case UpdateAvailable:
+ return context.getString(R.string.notification_title_summary_update_available);
+ case Downloading:
+ return context.getString(app.isInstalled() ? R.string.notification_title_summary_downloading_update : R.string.notification_title_summary_downloading);
+ case ReadyToInstall:
+ 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:
+ return context.getString(R.string.notification_title_summary_installed);
+ case InstallError:
+ return context.getString(R.string.notification_title_summary_install_error);
+ }
+ return "";
+ }
+
+ private Notification createUpdateNotification(AppUpdateStatusManager.AppUpdateStatus entry) {
+ App app = entry.app;
+ AppUpdateStatusManager.Status status = entry.status;
+
+ int iconSmall = R.drawable.ic_launcher;
+ Bitmap iconLarge = getLargeIconForEntry(entry);
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(context)
+ .setAutoCancel(true)
+ .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)
+ .setContentIntent(entry.intent);
+
+ /* If using stacked notifications, use groups. Note that this would not work prior to Lollipop,
+ because of http://stackoverflow.com/a/34953411, but currently not an issue since stacked
+ notifications are used only on >= Nougat.
+ */
+ if (useStackedNotifications()) {
+ builder.setGroup(GROUP_UPDATES);
+ }
+
+ // Handle actions
+ //
+ NotificationCompat.Action action = getAction(entry);
+ if (action != null) {
+ builder.addAction(action);
+ }
+
+ // Handle progress bar (for some states)
+ //
+ if (status == AppUpdateStatusManager.Status.Downloading) {
+ if (entry.progressMax == 0) {
+ builder.setProgress(100, 0, true);
+ } else {
+ builder.setProgress(entry.progressMax, entry.progressCurrent, false);
+ }
+ } else if (status == AppUpdateStatusManager.Status.Installing) {
+ builder.setProgress(100, 0, true); // indeterminate bar
+ }
+
+ Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_UPDATE_CLEARED);
+ intentDeleted.putExtra(EXTRA_NOTIFICATION_KEY, entry.getUniqueKey());
+ PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0);
+ builder.setDeleteIntent(piDeleted);
+ return builder.build();
+ }
+
+ private Notification createUpdateSummaryNotification(ArrayList updates) {
+ String title = context.getString(R.string.notification_summary_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++) {
+ AppUpdateStatusManager.AppUpdateStatus entry = updates.get(i);
+ App app = entry.app;
+ AppUpdateStatusManager.Status status = entry.status;
+
+ String content = getMultiItemContentString(app, status);
+ SpannableStringBuilder sb = new SpannableStringBuilder(app.name);
+ sb.setSpan(new StyleSpan(Typeface.BOLD), 0, sb.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ sb.append(" ");
+ sb.append(content);
+ inboxStyle.addLine(sb);
+
+ if (text.length() > 0) {
+ text.append(", ");
+ }
+ text.append(app.name);
+ }
+
+ if (updates.size() > MAX_UPDATES_TO_SHOW) {
+ int diff = updates.size() - MAX_UPDATES_TO_SHOW;
+ inboxStyle.setSummaryText(context.getString(R.string.notification_summary_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(!useStackedNotifications())
+ .setSmallIcon(R.drawable.ic_launcher)
+ .setColor(ContextCompat.getColor(context, R.color.fdroid_blue))
+ .setContentTitle(title)
+ .setContentText(text)
+ .setContentIntent(piAction)
+ .setLocalOnly(true)
+ .setVisibility(NotificationCompat.VISIBILITY_SECRET)
+ .setStyle(inboxStyle);
+
+ if (useStackedNotifications()) {
+ builder.setGroup(GROUP_UPDATES)
+ .setGroupSummary(true);
+ }
+
+ Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_ALL_UPDATES_CLEARED);
+ PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0);
+ builder.setDeleteIntent(piDeleted);
+ return builder.build();
+ }
+
+ private Notification createInstalledNotification(AppUpdateStatusManager.AppUpdateStatus entry) {
+ App app = entry.app;
+
+ Bitmap iconLarge = getLargeIconForEntry(entry);
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(context)
+ .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)
+ .setVisibility(NotificationCompat.VISIBILITY_SECRET)
+ .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);
+ builder.setDeleteIntent(piDeleted);
+ return builder.build();
+ }
+
+ private Notification createInstalledSummaryNotification(ArrayList installed) {
+ String title = context.getString(R.string.notification_summary_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++) {
+ AppUpdateStatusManager.AppUpdateStatus entry = installed.get(i);
+ App app = entry.app;
+ if (text.length() > 0) {
+ text.append(", ");
+ }
+ text.append(app.name);
+ }
+ bigTextStyle.bigText(text);
+ if (installed.size() > MAX_INSTALLED_TO_SHOW) {
+ int diff = installed.size() - MAX_INSTALLED_TO_SHOW;
+ bigTextStyle.setSummaryText(context.getString(R.string.notification_summary_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(!useStackedNotifications())
+ .setSmallIcon(R.drawable.ic_launcher)
+ .setColor(ContextCompat.getColor(context, R.color.fdroid_blue))
+ .setContentTitle(title)
+ .setContentText(text)
+ .setContentIntent(piAction)
+ .setLocalOnly(true)
+ .setVisibility(NotificationCompat.VISIBILITY_SECRET);
+ if (useStackedNotifications()) {
+ builder.setGroup(GROUP_INSTALLED)
+ .setGroupSummary(true);
+ }
+ Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_ALL_INSTALLED_CLEARED);
+ PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0);
+ builder.setDeleteIntent(piDeleted);
+ return builder.build();
+ }
+
+ 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 = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
+ h = w;
+ }
+ return new Point(w, h);
+ }
+
+ private Bitmap getLargeIconForEntry(AppUpdateStatusManager.AppUpdateStatus entry) {
+ final Point largeIconSize = getLargeIconSize();
+ Bitmap iconLarge = 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!
+ 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;
+ }
+
+ @Override
+ public void onLoadingStarted(String imageUri, View view) {
+
+ }
+
+ @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/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java
index fbf780caa..58b3ce6d4 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,24 +487,16 @@ public class UpdateService extends IntentService {
}
private void showAppUpdatesNotification(Cursor hasUpdates) {
- 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());
+ if (hasUpdates != null) {
+ hasUpdates.moveToFirst();
+ List apksToUpdate = new ArrayList<>(hasUpdates.getCount());
+ for (int i = 0; i < hasUpdates.getCount(); i++) {
+ App app = new App(hasUpdates);
+ hasUpdates.moveToNext();
+ apksToUpdate.add(ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode));
+ }
+ appUpdateStatusManager.addApks(apksToUpdate, AppUpdateStatusManager.Status.UpdateAvailable);
+ }
}
/**
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..51cc09f3d 100644
--- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java
+++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java
@@ -1,43 +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.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
@@ -83,22 +71,8 @@ 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 NotificationManager notificationManager;
+ private AppUpdateStatusManager appUpdateStatusManager;
/**
* This service does not use binding, so no need to implement this method
@@ -113,18 +87,14 @@ public class InstallManagerService extends Service {
super.onCreate();
Utils.debugLog(TAG, "creating Service");
localBroadcastManager = LocalBroadcastManager.getInstance(this);
- notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ 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();
- cancelNotification(urlString);
- break;
- }
+ for (AppUpdateStatusManager.AppUpdateStatus status : appUpdateStatusManager.getByPackageName(packageName)) {
+ appUpdateStatusManager.updateApk(status.getUniqueKey(), AppUpdateStatusManager.Status.Installed, null);
}
}
};
@@ -147,10 +117,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());
- cancelNotification(urlString);
+ 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");
@@ -166,7 +138,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);
+ appUpdateStatusManager.removeApk(urlString);
return START_NOT_STICKY;
}
@@ -176,14 +148,11 @@ 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);
- 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 +186,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 +200,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());
+ 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));
@@ -274,7 +241,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,31 +251,36 @@ public class InstallManagerService extends Service {
switch (intent.getAction()) {
case Downloader.ACTION_STARTED:
- // nothing to do
+ // 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);
+ PendingIntent action = PendingIntent.getService(context, 0, intentObject, 0);
+ appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.Downloading, action);
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());
+ 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);
+ 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:
- removeFromActive(urlString);
+ appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.Unknown, null);
localBroadcastManager.unregisterReceiver(this);
- cancelNotification(urlString);
break;
default:
throw new RuntimeException("intent action not handled!");
@@ -329,49 +301,31 @@ public class InstallManagerService extends Service {
Apk apk;
switch (intent.getAction()) {
case Installer.ACTION_INSTALL_STARTED:
- // nothing to do
+ appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.Installing, null);
break;
case Installer.ACTION_INSTALL_COMPLETE:
- 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:
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)) {
- App app = getAppFromActive(downloadUrl);
- if (app == null) {
- 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);
- }
+ appUpdateStatusManager.setApkError(apk, errorMessage);
+ } else {
+ appUpdateStatusManager.removeApk(downloadUrl);
}
- 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);
-
- // show notification if app details is not visible
- if (AppDetails.isAppVisible(apk.packageName)) {
- cancelNotification(downloadUrl);
- } else {
- notifyDownloadComplete(apk, downloadUrl, installPendingIntent);
- }
-
+ PendingIntent installPendingIntent = intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
+ appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.ReadyToInstall, installPendingIntent);
break;
default:
throw new RuntimeException("intent action not handled!");
@@ -383,178 +337,6 @@ 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();
-
- 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());
- }
-
- /**
- * 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);
- }
-
- /**
- * 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
@@ -581,23 +363,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();
- }
}
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_cancel.xml b/app/src/main/res/drawable-anydpi-v21/ic_cancel.xml
new file mode 100644
index 000000000..784cc0b1e
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi-v21/ic_cancel.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_file_download.xml b/app/src/main/res/drawable-anydpi-v21/ic_file_download.xml
new file mode 100644
index 000000000..11b5def17
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi-v21/ic_file_download.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_file_install.xml b/app/src/main/res/drawable-anydpi-v21/ic_file_install.xml
new file mode 100644
index 000000000..644cb6070
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi-v21/ic_file_install.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
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 000000000..e18c7aef9
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_cancel.png differ
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 000000000..52fa71dc4
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_file_download.png differ
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 000000000..81ca22780
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_file_install.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_cancel.png b/app/src/main/res/drawable-mdpi/ic_cancel.png
new file mode 100644
index 000000000..418410737
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_cancel.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_file_download.png b/app/src/main/res/drawable-mdpi/ic_file_download.png
new file mode 100644
index 000000000..7c65d4c76
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_file_download.png differ
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 000000000..4cc23747a
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_file_install.png differ
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 000000000..3e85c8a97
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_cancel.png differ
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 000000000..2923dd893
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_file_download.png differ
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 000000000..1de323ce1
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_file_install.png differ
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 000000000..155ddbc59
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_cancel.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_file_download.png b/app/src/main/res/drawable-xxhdpi/ic_file_download.png
new file mode 100644
index 000000000..afe04b6d4
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_file_download.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_file_install.png b/app/src/main/res/drawable-xxhdpi/ic_file_install.png
new file mode 100644
index 000000000..c9f7923c8
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_file_install.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_cancel.png b/app/src/main/res/drawable-xxxhdpi/ic_cancel.png
new file mode 100644
index 000000000..67d8ea595
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_cancel.png differ
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 000000000..025407ad2
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_file_download.png differ
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 000000000..c38be0310
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_file_install.png differ
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/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 d4e8e770e..b162d7bc9 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -119,10 +119,8 @@
تم التثبيت
تم التثبيت (%d)
تحديثات (%d)
- يتوفر تحديث.
- %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..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
@@ -214,7 +212,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..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 метод за изпращане
@@ -181,7 +179,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..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
@@ -181,7 +179,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..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
@@ -152,7 +150,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..3082002c7 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -75,10 +75,8 @@
Opdateringer
Installeret (%d)
Opdateringer (%d)
- 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..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
@@ -182,7 +180,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..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
@@ -142,7 +140,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..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
@@ -151,7 +149,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..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
@@ -182,7 +180,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..db97687ce 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -78,10 +78,8 @@
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…
+ +%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..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
@@ -99,7 +97,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..97406bfaa 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -114,9 +114,7 @@
نصب شده
نصب شده (%d)
بهروز رسانیها (%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..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
@@ -183,7 +181,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..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
@@ -284,7 +282,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..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
@@ -101,7 +99,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..01d438602 100644
--- a/app/src/main/res/values-he/strings.xml
+++ b/app/src/main/res/values-he/strings.xml
@@ -54,10 +54,8 @@
זמינות
עדכונים
- עדכון 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..be431ba09 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -147,10 +147,8 @@
सारी रेपोसितोरिएस नवीनतम हैं
सभी दुसरे रेपो ने कोई गलती नहीं बनाई|
अपडेट के दौरान एरर: %s
- १ अपडेट उपलब्ध है|
- %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..8c0061383 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -78,10 +78,8 @@
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…
+ +%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..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)
@@ -126,7 +124,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..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
@@ -166,7 +164,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..1117d9c81 100644
--- a/app/src/main/res/values-is/strings.xml
+++ b/app/src/main/res/values-is/strings.xml
@@ -66,10 +66,8 @@
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…
+ +%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..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
@@ -333,7 +331,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..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の送信方法を選択してください
@@ -181,7 +179,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..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 업데이트를 사용할 수 있습니다.
블루투스 전송 방법을 찾을 수 없습니다. 선택하세요!
블루투스 전송 방법 선택
@@ -194,7 +192,7 @@
설치됨
설치됨 (%d)
업데이트 (%d)
- +%1$d 이상…
+ +%1$d 이상…
올바르지 않은 핑거프린트
올바른 URL이 아닙니다.
설정
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 f71147699..603041a05 100644
--- a/app/src/main/res/values-my/strings.xml
+++ b/app/src/main/res/values-my/strings.xml
@@ -74,10 +74,8 @@
အသစ္မြမး္မံမႈမ်ား
သြင္းထားခဲ့သည္ (%d)
အသစ္မြမ္းမံမႈမ်ား (%d)
- အသစ္မြမ္းမံမႈ ၁ ခုရရွိႏိုင္သည္
- အသစ္မြမ္းမံမႈ %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..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
@@ -185,7 +183,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..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
@@ -179,7 +177,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..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,8 +153,7 @@ Uwaga: Wszystkie poprzednio zainstalowane aplikacje zostaną na urządzeniu.Linki
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..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
@@ -186,7 +184,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..cb98498d8 100644
--- a/app/src/main/res/values-pt-rPT/strings.xml
+++ b/app/src/main/res/values-pt-rPT/strings.xml
@@ -63,10 +63,8 @@
Disponíveis
Instaladas
Atualizações
- 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..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
@@ -97,7 +95,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..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
@@ -212,7 +210,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..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
@@ -184,7 +182,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..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
@@ -187,7 +185,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..99b30f468 100644
--- a/app/src/main/res/values-sn/strings.xml
+++ b/app/src/main/res/values-sn/strings.xml
@@ -73,10 +73,8 @@
Zvekunatsa
Zvavakirirwa (%d)
Zvekunatsa (%d)
- 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..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 нових надоградњи је доступно.
Доступне су надоградње на Ф-дроиду
Нема начина за слања блутутом, одредите један!
Одредите начин слања блутутом
@@ -188,7 +186,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..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
@@ -191,7 +189,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-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 f85cf6149..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 โปรแกรมมีอัพเดต
แสดงรายละเอียดสถานะส่วนขยายช่วยติดตั้ง และ/หรือทำการอัพเดต/ถอนการติดตั้งส่วนขยายนี้
เลือกวิธีการส่งต่อผ่านบลูทูธ
ยังไม่ได้เลือกวิธีส่งต่อทางบลูทูธ, โปรดเลือกก่อน!
@@ -314,7 +312,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..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ç
@@ -203,7 +201,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-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 9b3c0a68d..b25caafe2 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -87,10 +87,8 @@
Увімкнути
Додати ключ
Встановлено
- Одне оновлення доступно.
- %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..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
@@ -168,7 +166,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..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
未找到蓝牙发送方式,请选择一个!
选择蓝牙发送方式
@@ -168,7 +166,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..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 更新
請選擇一個藍牙傳送的方式!
選擇藍牙的傳送方式
@@ -228,7 +226,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..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 更新
透過藍牙傳送
@@ -257,7 +255,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..2976c7f7a 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,30 @@
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 \"%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
+ Install Failed
+ Update
+ Cancel
+ Install
+