Merge branch 'mvp76--notifications_742' into 'master'

Notification improvements from mvp76

See merge request !443
This commit is contained in:
Peter Serwylo 2017-03-01 03:42:39 +00:00
commit 04182b64aa
81 changed files with 1214 additions and 489 deletions

View File

@ -184,6 +184,7 @@ android {
versionCode 102050
versionName getVersionName()
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
testOptions {

View File

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

View File

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

View File

@ -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.
* <p>
* 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<String, AppUpdateStatus> 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<AppUpdateStatus> 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<AppUpdateStatus> getByPackageName(String packageName) {
ArrayList<AppUpdateStatus> 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<Apk> 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<Map.Entry<String, AppUpdateStatus>> it = appMapping.entrySet().iterator(); it.hasNext();) {
Map.Entry<String, AppUpdateStatus> entry = it.next();
if (entry.getValue().status != Status.Installed) {
it.remove();
}
}
notifyChange();
}
}
void clearAllInstalled() {
synchronized (appMapping) {
for (Iterator<Map.Entry<String, AppUpdateStatus>> it = appMapping.entrySet().iterator(); it.hasNext();) {
Map.Entry<String, AppUpdateStatus> entry = it.next();
if (entry.getValue().status == Status.Installed) {
it.remove();
}
}
notifyChange();
}
}
private PendingIntent getContentIntent(AppUpdateStatus entry) {
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);
}
}

View File

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

View File

@ -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<AppUpdateStatusManager.AppUpdateStatus> updates = new ArrayList<>();
private final ArrayList<AppUpdateStatusManager.AppUpdateStatus> 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<AppUpdateStatusManager.AppUpdateStatus> 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<AppUpdateStatusManager.AppUpdateStatus> 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;
}
}

View File

@ -37,7 +37,6 @@ import android.os.Process;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
@ -79,7 +78,6 @@ public class UpdateService extends IntentService {
private static final String STATE_LAST_UPDATED = "lastUpdateCheck";
private static final int NOTIFY_ID_UPDATING = 0;
private static final int NOTIFY_ID_UPDATES_AVAILABLE = 1;
private static final int FLAG_NET_UNAVAILABLE = 0;
private static final int FLAG_NET_METERED = 1;
@ -89,6 +87,7 @@ public class UpdateService extends IntentService {
private NotificationManager notificationManager;
private NotificationCompat.Builder notificationBuilder;
private AppUpdateStatusManager appUpdateStatusManager;
public UpdateService() {
super("UpdateService");
@ -147,6 +146,7 @@ public class UpdateService extends IntentService {
.setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setContentTitle(getString(R.string.update_notification_title));
appUpdateStatusManager = AppUpdateStatusManager.getInstance(this);
// Android docs are a little sketchy, however it seems that Gingerbread is the last
// sdk that made a content intent mandatory:
@ -469,39 +469,6 @@ public class UpdateService extends IntentService {
}
}
private PendingIntent createNotificationIntent() {
Intent notifyIntent = new Intent(this, FDroid.class).putExtra(FDroid.EXTRA_TAB_UPDATE, true);
TaskStackBuilder stackBuilder = TaskStackBuilder
.create(this).addParentStack(FDroid.class)
.addNextIntent(notifyIntent);
return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
private static final int MAX_UPDATES_TO_SHOW = 5;
private NotificationCompat.Style createNotificationBigStyle(Cursor hasUpdates) {
final String contentText = hasUpdates.getCount() > 1
? getString(R.string.many_updates_available, hasUpdates.getCount())
: getString(R.string.one_update_available);
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
inboxStyle.setBigContentTitle(contentText);
hasUpdates.moveToFirst();
for (int i = 0; i < Math.min(hasUpdates.getCount(), MAX_UPDATES_TO_SHOW); i++) {
App app = new App(hasUpdates);
hasUpdates.moveToNext();
inboxStyle.addLine(app.name + " (" + app.installedVersionName + "" + app.getSuggestedVersionName() + ")");
}
if (hasUpdates.getCount() > MAX_UPDATES_TO_SHOW) {
int diff = hasUpdates.getCount() - MAX_UPDATES_TO_SHOW;
inboxStyle.setSummaryText(getString(R.string.update_notification_more, diff));
}
return inboxStyle;
}
private void autoDownloadUpdates() {
Cursor cursor = getContentResolver().query(
AppProvider.getCanUpdateUri(),
@ -520,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<Apk> 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);
}
}
/**

View File

@ -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<String, Apk> ACTIVE_APKS = new HashMap<>(3);
/**
* The collection of {@link App}s that are actively going through this whole process,
* matching the {@link Apk}s in {@code ACTIVE_APKS}. The key is the
* {@code packageName} of the app.
*/
private static final HashMap<String, App> ACTIVE_APPS = new HashMap<>(3);
private 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<String, Apk> 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 <a href="https://developer.android.com/google/play/expansion-files.html">APK Expansion Files</a>
*/
private void getObb(final String urlString, String obbUrlString,
final File obbDestFile, final String sha256,
final NotificationCompat.Builder builder) {
final File obbDestFile, final String sha256) {
if (obbDestFile == null || obbDestFile.exists() || TextUtils.isEmpty(obbUrlString)) {
return;
}
@ -232,8 +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 <a href=https://code.google.com/p/android/issues/detail?id=47809> Issue 47809:
* Removing the progress bar from a notification should cause the notification's content
* text to return to normal size</a>
*/
private void notifyDownloadComplete(Apk apk, String urlString, PendingIntent installPendingIntent) {
String title;
try {
PackageManager pm = getPackageManager();
title = String.format(getString(R.string.tap_to_update_format),
pm.getApplicationLabel(pm.getApplicationInfo(apk.packageName, 0)));
} catch (PackageManager.NameNotFoundException e) {
String name = getAppName(apk);
if (TextUtils.isEmpty(name) || name.equals(new App().name)) {
ContentResolver resolver = getContentResolver();
App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo,
new String[]{Schema.AppMetadataTable.Cols.NAME});
if (app == null || TextUtils.isEmpty(app.name)) {
return; // do not have a name to display, so leave notification as is
}
name = app.name;
}
title = String.format(getString(R.string.tap_to_install_format), name);
}
int downloadUrlId = urlString.hashCode();
notificationManager.cancel(downloadUrlId);
Notification notification = new NotificationCompat.Builder(this)
.setAutoCancel(true)
.setOngoing(false)
.setContentTitle(title)
.setContentIntent(installPendingIntent)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentText(getString(R.string.tap_to_install))
.build();
notificationManager.notify(downloadUrlId, notification);
}
private void notifyError(String urlString, App app, String text) {
int downloadUrlId = urlString.hashCode();
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<String> getActiveDownloadUrls() {
return ACTIVE_APKS.keySet();
}
/**
* Returns a {@link Set} of the {@code packageName}s that are currently active.
* {@code packageName}s are used as unique IDs for apps throughout all of
* Android, F-Droid, and other apps stores.
*/
public static Set<String> getActivePackageNames() {
return ACTIVE_APPS.keySet();
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="32dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="32dp">
<path android:fillColor="#30000000" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="32dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="32dp">
<path android:fillColor="#30000000" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="32dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="32dp">
<group android:pivotX="12" android:pivotY="12" android:rotation="-90">
<path android:fillColor="#30000000" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,13 @@
<vector android:height="36dp" android:viewportHeight="90.0"
android:viewportWidth="90.0" android:width="36dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#0066cc"
android:pathData="m45,7.73c-20.57,0 -37.27,16.7 -37.27,37.27 0,20.57 16.7,37.27 37.27,37.27 20.57,0 37.27,-16.7 37.27,-37.27 0,-20.57 -16.7,-37.27 -37.27,-37.27z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="2"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M30.78,56.85h27.97v4.04h-27.97z"
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m38.8,26.93 l0,12.18 -7.71,0 13.86,13.57 13.86,-13.57 -7.83,0 0,-12.18z"
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
</vector>

View File

@ -1,11 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="80dp"
android:layout_height="20dp"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"
android:src="@drawable/donation_option_bitcoin"
app:srcCompat="@drawable/donation_option_bitcoin"
android:contentDescription="@string/menu_bitcoin" />

View File

@ -1,11 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="80dp"
android:layout_height="16dp"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"
android:src="@drawable/donation_option_flattr"
app:srcCompat="@drawable/donation_option_flattr"
android:contentDescription="@string/menu_flattr" />

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="80dp"
android:layout_height="20dp"
android:src="@drawable/donation_option_litecoin"
app:srcCompat="@drawable/donation_option_litecoin"
android:contentDescription="@string/menu_litecoin" />

View File

@ -1,13 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingBottom="20dp"
>
android:paddingBottom="20dp">
<ImageView
android:id="@+id/icon"
@ -18,7 +19,7 @@
android:layout_alignParentTop="true"
android:scaleType="centerCrop"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_nearby_share"
app:srcCompat="@drawable/ic_nearby_share"
android:layout_marginRight="10dp"
android:layout_marginEnd="10dp"
/>

View File

@ -119,10 +119,8 @@
<string name="tab_installed_apps">تم التثبيت</string>
<string name="tab_installed_apps_count">تم التثبيت (%d)</string>
<string name="tab_updates_count">تحديثات (%d)</string>
<string name="one_update_available">يتوفر تحديث.</string>
<string name="many_updates_available">%d يتوفر على تحديثات.</string>
<string name="fdroid_updates_available">تحديثات اف-درويد متوفرة</string>
<string name="update_notification_more">+%1$d المزيد…</string>
<string name="notification_summary_more">+%1$d المزيد…</string>
<string name="bluetooth_activity_not_found">لا توجد طريقة إرسال بلوتوث، فضلاً إختر واحدة !</string>
<string name="repo_exists_add_fingerprint">هذه المستودعات بالفعل موجودة، بهذا سوف يتم إضافة مفتاح معلومات جديد.</string>
<string name="repo_exists_enable">هذه المستودعات بالفعل موجودة، تأكد إن كنت ترغب في إعادة تمكينه.</string>

View File

@ -45,8 +45,6 @@
<string name="overwrite">Sobrescribir</string>
<string name="tab_available_apps">Disponible</string>
<string name="tab_updates">Anovamientos</string>
<string name="one_update_available">Ta disponible 1 anovamientu.</string>
<string name="many_updates_available">Tán disponibles %d anovamientos.</string>
<string name="fdroid_updates_available">Anovamientos de F-Droid disponibles</string>
<string name="bluetooth_activity_not_found">Nun s\'alcontró dal métodu d\'unviu pente Bluetooth, ¡escueyi ún!</string>
<string name="choose_bt_send">Escueyi\'l métodu d\'unvu pente Bluetooth</string>
@ -214,7 +212,7 @@ rehabilitar esti repositoriu pa instalar aplicaciones dende elli.</string>
<string name="back">Atrás</string>
<string name="tab_installed_apps">Instaláu</string>
<string name="update_notification_more">+%1$d más…</string>
<string name="notification_summary_more">+%1$d más…</string>
<string name="bad_fingerprint">Buelga incorreuta</string>
<string name="invalid_url">Esto nun ye una URL válida.</string>
<string name="menu_changelog">Rexistru de cambeos</string>

View File

@ -42,8 +42,6 @@
<string name="add_key">Добавете ключ</string>
<string name="tab_available_apps">Налични</string>
<string name="tab_updates">Обновявания</string>
<string name="one_update_available">1 налична актуализация.</string>
<string name="many_updates_available">%d налични актуализации.</string>
<string name="fdroid_updates_available">Налични актуализации от F-Droid</string>
<string name="bluetooth_activity_not_found">Не е намерен Bluetooth метод за изпращане, изберете един!</string>
<string name="choose_bt_send">Изберете Bluetooth метод за изпращане</string>
@ -181,7 +179,7 @@
<string name="about_source">Изходен код</string>
<string name="tab_installed_apps">Инсталирани</string>
<string name="update_notification_more">+ още %1$d…</string>
<string name="notification_summary_more">+ още %1$d…</string>
<string name="update_notification_title">Актуализиране на хранилищата</string>
<string name="install_error_unknown">Инсталирането се провали поради неизвестна грешка</string>
<string name="uninstall_error_unknown">Деинсталирането се провали поради неизвестна грешка</string>

View File

@ -44,8 +44,6 @@
<string name="overwrite">Sobreescriu</string>
<string name="tab_available_apps">Disponible</string>
<string name="tab_updates">Actualitzacions</string>
<string name="one_update_available">Hi ha 1 actualització disponible.</string>
<string name="many_updates_available">Hi ha %d actualitzacions disponibles.</string>
<string name="fdroid_updates_available">Hi ha actualitzacions disponibles de l\'F-Droid</string>
<string name="bluetooth_activity_not_found">Trieu un mètode d\'enviament per Bluetooth!</string>
<string name="send_via_bluetooth">Envia per Bluetooth</string>
@ -181,7 +179,7 @@ tornar a habilitar el dipòsit per instal·lar aplicacions d\'aquest.</string>
<string name="links">Enllaços</string>
<string name="back">Torna</string>
<string name="update_notification_more">+%1$d més…</string>
<string name="notification_summary_more">+%1$d més…</string>
<string name="pref_language">Idioma</string>
<string name="wifi">Wi-Fi</string>
<string name="category_Graphics">Gràfics</string>

View File

@ -41,8 +41,6 @@
<string name="overwrite">Přepsat</string>
<string name="tab_available_apps">Dostupné</string>
<string name="tab_updates">Aktualizace</string>
<string name="one_update_available">1 aktualizace je dostupná.</string>
<string name="many_updates_available">%d aktualizací je dostupných.</string>
<string name="fdroid_updates_available">Dostupné aktualizace F-Droid</string>
<string name="send_via_bluetooth">Poslat přes bluetooth</string>
<string name="repo_add_url">Adresa repositáře</string>
@ -152,7 +150,7 @@ Pro instalaci aplikací z tohoto repozitáře ho bude nejprve třeba znovu povol
<string name="links">Odkazy</string>
<string name="back">Zpět</string>
<string name="update_notification_more">+%1$d více…</string>
<string name="notification_summary_more">+%1$d více…</string>
<string name="bluetooth_activity_not_found">Nerozpoznána žádná Bluetooth metoda přenosu, nějakou vyberte!</string>
<string name="choose_bt_send">Vyberte metodu přenosu Bluetooth</string>
<string name="repo_exists_add_fingerprint">Tento repozitář je již nastaven, budou jen přidány nové klíče.</string>

View File

@ -75,10 +75,8 @@
<string name="tab_updates">Opdateringer</string>
<string name="tab_installed_apps_count">Installeret (%d)</string>
<string name="tab_updates_count">Opdateringer (%d)</string>
<string name="one_update_available">1 opdatering er tilgængelig.</string>
<string name="many_updates_available">%d opdateringer er tilgængelige.</string>
<string name="fdroid_updates_available">F-Droid Opdateringer Tilgængelige</string>
<string name="update_notification_more">+%1$d flere…</string>
<string name="notification_summary_more">+%1$d flere…</string>
<string name="bluetooth_activity_not_found">Ingen Bluetooth afsendelsesmetode fundet, vælg en!</string>
<string name="choose_bt_send">Vælg Bluetooth afsendelsesmetode</string>
<string name="send_via_bluetooth">Send over Bluetooth</string>

View File

@ -45,8 +45,6 @@
<string name="overwrite">Überschreiben</string>
<string name="tab_available_apps">Verfügbar</string>
<string name="tab_updates">Aktualisierungen</string>
<string name="one_update_available">Eine Aktualisierung ist verfügbar.</string>
<string name="many_updates_available">%d Aktualisierungen sind verfügbar.</string>
<string name="fdroid_updates_available">F-Droid: Aktualisierungen verfügbar</string>
<string name="bluetooth_activity_not_found">Keine Bluetooth-Sendemethode gefunden, bitte eine auswählen!</string>
<string name="choose_bt_send">Bluetooth-Sendemethode auswählen</string>
@ -182,7 +180,7 @@ um Anwendungen daraus installieren zu können.</string>
<string name="app_incompatible">Inkompatibel</string>
<string name="links">Verweise</string>
<string name="back">Zurück</string>
<string name="update_notification_more">+%1$d weitere …</string>
<string name="notification_summary_more">+%1$d weitere …</string>
<string name="bad_fingerprint">Falscher Fingerabdruck</string>
<string name="invalid_url">Das ist keine gültige Adresse.</string>
<string name="update_notification_title">Paketquellen werden aktualisiert</string>

View File

@ -37,8 +37,6 @@
<string name="overwrite">Αντικατάσταση</string>
<string name="tab_available_apps">Διαθέσιμα</string>
<string name="tab_updates">Ενημερώσεις</string>
<string name="one_update_available">1 διαθέσιμη ενημερώση.</string>
<string name="many_updates_available">%d διαθέσιμες ενημερώσεις.</string>
<string name="fdroid_updates_available">Υπάρχουν ενημερώσεις του F-Droid</string>
<string name="bluetooth_activity_not_found">Καμία μέθοδος αποστολής με Bluetooth δεν βρέθηκε, επιλέξετε μία!</string>
<string name="choose_bt_send">Επιλέξτε τη μέθοδο αποστολής με Bluetooth</string>
@ -142,7 +140,7 @@
<string name="back">Πίσω</string>
<string name="tab_installed_apps">Εγκατεστημένο</string>
<string name="update_notification_more">+%1$d περισσότερα…</string>
<string name="notification_summary_more">+%1$d περισσότερα…</string>
<string name="send_via_bluetooth">Αποστολή μέσω Bluetooth</string>
<string name="bad_fingerprint">Λανθασμένο δακτυλικό αποτύπωμα</string>

View File

@ -28,8 +28,6 @@
<string name="overwrite">Anstataŭigi</string>
<string name="tab_available_apps">Disponeblaj</string>
<string name="tab_updates">Ĝisdatigeblaj</string>
<string name="one_update_available">1 ĝisdatigo estas disponebla.</string>
<string name="many_updates_available">%d ĝisdatigoj estas disponeblaj.</string>
<string name="fdroid_updates_available">F-Droidaj ĝisdatigoj disponeblaj</string>
<string name="send_via_bluetooth">Sendi per Bludento</string>
<string name="repo_add_url">Deponeja adreso</string>
@ -151,7 +149,7 @@
<string name="tab_installed_apps">Instalitaj</string>
<string name="tab_installed_apps_count">Instalitaj (%d)</string>
<string name="tab_updates_count">Ĝisdatigoj (%d)</string>
<string name="update_notification_more">ankoraŭ +%1$d…</string>
<string name="notification_summary_more">ankoraŭ +%1$d…</string>
<string name="bluetooth_activity_not_found">Neniu Bludenta metodo de sendo trovita, elektu iun!</string>
<string name="choose_bt_send">Elektu Bludentan metodon de sendo</string>
<string name="repo_add_fingerprint">Fingrospuro (malnepra)</string>

View File

@ -45,8 +45,6 @@
<string name="overwrite">Sobrescribir</string>
<string name="tab_available_apps">Disponible</string>
<string name="tab_updates">Actualizaciones</string>
<string name="one_update_available">1 actualización disponible.</string>
<string name="many_updates_available">%d actualizaciones disponibles.</string>
<string name="fdroid_updates_available">Actualizaciones de F-Droid disponibles</string>
<string name="bluetooth_activity_not_found">No se encontró método de envío Bluetooth, ¡elige uno!</string>
<string name="choose_bt_send">Elegir el método de envío Bluetooth</string>
@ -182,7 +180,7 @@ Para ver las aplicaciones que ofrece tienes que activarlo.</string>
<string name="links">Enlaces</string>
<string name="back">Volver</string>
<string name="update_notification_more">+%1$d más…</string>
<string name="notification_summary_more">+%1$d más…</string>
<string name="uninstall_system">Actualiza/Desinstala la extensión con permisos de sistema</string>
<string name="uninstall_system_summary">Abre la pantalla de detalles de la extensión con permisos de sistema para actualizarla/desinstalarla</string>
<string name="bad_fingerprint">Huella digital incorrecta</string>

View File

@ -78,10 +78,8 @@
<string name="tab_updates">Värskendused</string>
<string name="tab_installed_apps_count">Paigaldatud (%d)</string>
<string name="tab_updates_count">Värskendused (%d)</string>
<string name="one_update_available">Üks värskendus on saadaval.</string>
<string name="many_updates_available">%d värskendust on saadaval.</string>
<string name="fdroid_updates_available">F-Droid: värskendused saadaval</string>
<string name="update_notification_more">+%1$d veel…</string>
<string name="notification_summary_more">+%1$d veel…</string>
<string name="bluetooth_activity_not_found">Bluetoothiga saatmisviise ei leitud. Valige üks!</string>
<string name="choose_bt_send">Vali Bluetoothiga saatmise viis</string>
<string name="send_via_bluetooth">Saada Bluetoothiga</string>

View File

@ -31,8 +31,6 @@
<string name="overwrite">Gainidatzi</string>
<string name="tab_available_apps">Eskuragarri</string>
<string name="tab_updates">Eguneraketak</string>
<string name="one_update_available">Eguneraketa 1 eskuragarri.</string>
<string name="many_updates_available">%d eguneraketa eskuragarri.</string>
<string name="fdroid_updates_available">F-Droid eguneraketak eskuragarri</string>
<string name="repo_add_url">Biltegiaren helbidea</string>
<string name="menu_update_repo">Eguneratu biltegiak</string>
@ -99,7 +97,7 @@
<string name="links">Loturak</string>
<string name="back">Atzera</string>
<string name="update_notification_more">+%1$d gehiago…</string>
<string name="notification_summary_more">+%1$d gehiago…</string>
<string name="bluetooth_activity_not_found">Ez da aurkitu Bluetooth bidez bidaltzeko metodorik, aukeratu bat!</string>
<string name="choose_bt_send">Aukeratu Bluetooth bidez bidaltzeko metodoa</string>
<string name="send_via_bluetooth">Bidali Bluetooth bidez</string>

View File

@ -114,9 +114,7 @@
<string name="tab_installed_apps">نصب شده</string>
<string name="tab_installed_apps_count">نصب شده (%d)</string>
<string name="tab_updates_count">به‌روز رسانی‌ها (%d)</string>
<string name="one_update_available">۱ به‌روز رسانی موجود است.</string>
<string name="many_updates_available">%d به روز رسانی موجود است.</string>
<string name="update_notification_more">+%1$d بیش‌تر…</string>
<string name="notification_summary_more">+%1$d بیش‌تر…</string>
<string name="send_via_bluetooth">ارسال با بلوتوث</string>
<string name="repo_exists_add_fingerprint">این مخزن از پیش برپا شده است. این کار، اظّلاعات کلید جدیدی را می‌افزاید.</string>

View File

@ -44,8 +44,6 @@
<string name="overwrite">Korvaa</string>
<string name="tab_available_apps">Saatavilla</string>
<string name="tab_updates">Päivityksiä</string>
<string name="one_update_available">1 päivitys saatavilla.</string>
<string name="many_updates_available">%d päivitystä saatavilla.</string>
<string name="fdroid_updates_available">F-Droid: Päivityksiä saatavilla</string>
<string name="bluetooth_activity_not_found">Bluetooth -lähetystapaa ei löytynyt, valitse yksi!</string>
<string name="choose_bt_send">Valitse Bluetooth -lähetystapa</string>
@ -183,7 +181,7 @@
<string name="tab_installed_apps">Asennettu</string>
<string name="tab_installed_apps_count">Asennettu (%d)</string>
<string name="tab_updates_count">Päivitykset (%d)</string>
<string name="update_notification_more">+%1$d lisää…</string>
<string name="notification_summary_more">+%1$d lisää…</string>
<string name="update_auto_download">Lataa päivitykset automaattisesti</string>
<string name="update_auto_download_summary">Lataa päivitykset taustalla</string>
<string name="uninstall_system">Päivitä/Poista Privileged Extension</string>

View File

@ -45,8 +45,6 @@
<string name="overwrite">Écraser</string>
<string name="tab_available_apps">Disponibles</string>
<string name="tab_updates">Mises à jour</string>
<string name="one_update_available">Une mise à jour est disponible.</string>
<string name="many_updates_available">%d mises à jour sont disponibles.</string>
<string name="fdroid_updates_available">Des mises à jour F-Droid sont disponibles</string>
<string name="bluetooth_activity_not_found">Aucune méthode d\'envoi Bluetooth n\'a été trouvée, choisissez-en une !</string>
<string name="choose_bt_send">Choisir une méthode d\'envoi Bluetooth</string>
@ -284,7 +282,7 @@ perdues. Elle ne requiert aucune autorisation particulière.</string>
<string name="interval_1h">Toutes les heures</string>
<string name="perms_description_app">Fourni par %1$s.</string>
<string name="uninstall_system_summary">Ouvrir l\'écran des détails de l\'appli F-Droid Privileged Extension pour la mettre à jour/la désinstaller</string>
<string name="update_notification_more">%1$d de plus…</string>
<string name="notification_summary_more">%1$d de plus…</string>
<string name="status_download_unknown_size">Téléchargement de
\n%2$s depuis
\n%1$s</string>

View File

@ -27,8 +27,6 @@
<string name="cancel">Cancelar</string>
<string name="tab_available_apps">Dispoñíbel</string>
<string name="tab_updates">Actualizacións</string>
<string name="one_update_available">1 Actualización dispoñíbel.</string>
<string name="many_updates_available">%d actualizacións dispoñíbeis.</string>
<string name="fdroid_updates_available">Actualizacións de F-Droid dispoñíbeis</string>
<string name="repo_add_url">Enderezo do repositorio</string>
<string name="menu_update_repo">Actualizar repositorios</string>
@ -101,7 +99,7 @@
<string name="overwrite">Sobreescribir</string>
<string name="tab_installed_apps">Instalado</string>
<string name="update_notification_more">+%1$d máis…</string>
<string name="notification_summary_more">+%1$d máis…</string>
<string name="bluetooth_activity_not_found">Non se atopou ningún método de transmisión Bluetooth, escolla un!</string>
<string name="choose_bt_send">Escolla o método de transmisión Bluetooth</string>
<string name="send_via_bluetooth">Enviar por Bluetooth</string>

View File

@ -54,10 +54,8 @@
<string name="tab_available_apps">זמינות</string>
<string name="tab_updates">עדכונים</string>
<string name="one_update_available">עדכון 1 זמין.</string>
<string name="many_updates_available">%d עדכונים זמינים.</string>
<string name="fdroid_updates_available">עדכוני F-Droid זמינים</string>
<string name="update_notification_more">+%1$d עוד…</string>
<string name="notification_summary_more">+%1$d עוד…</string>
<string name="bluetooth_activity_not_found">לא נמצאה שיטת שליחה של Bluetooth, בחר אחת!</string>
<string name="choose_bt_send">בחר שיטת שליחה של Bluetooth</string>
<string name="send_via_bluetooth">שלח דרך Bluetooth</string>

View File

@ -147,10 +147,8 @@
<string name="repos_unchanged">सारी रेपोसितोरिएस नवीनतम हैं</string>
<string name="all_other_repos_fine">सभी दुसरे रेपो ने कोई गलती नहीं बनाई|</string>
<string name="global_error_updating_repos">अपडेट के दौरान एरर: %s</string>
<string name="one_update_available">१ अपडेट उपलब्ध है|</string>
<string name="many_updates_available">%d अपडेटस उपलब्ध हैं|</string>
<string name="fdroid_updates_available">F-Droid अपडेट उपलब्ध</string>
<string name="update_notification_more">+%1$d और…</string>
<string name="notification_summary_more">+%1$d और…</string>
<string name="bluetooth_activity_not_found">Bluettoth से भेजने का तरीका उपलब नहीं है, एक चुने!</string>
<string name="choose_bt_send">Bluetooth से भेजने का तरीका चुने</string>
<string name="send_via_bluetooth">Bluetooth द्वारा भेजे</string>

View File

@ -78,10 +78,8 @@
<string name="tab_updates">Ažuriranja</string>
<string name="tab_installed_apps_count">Instalirano (%d)</string>
<string name="tab_updates_count">Ažuriranja (%d)</string>
<string name="one_update_available">1 ažuriranje je dostupno.</string>
<string name="many_updates_available">%d ažuriranja je dostupno.</string>
<string name="fdroid_updates_available">Ažuriranja za F-Droid dostupna</string>
<string name="update_notification_more">+%1$d više…</string>
<string name="notification_summary_more">+%1$d više…</string>
<string name="bluetooth_activity_not_found">Nije pronađena metoda slanja preko Bluetootha, odaberite jednu!</string>
<string name="choose_bt_send">Odaberi metodu slanja preko Bluetootha</string>
<string name="send_via_bluetooth">Pošalji preko Bluetootha</string>

View File

@ -35,8 +35,6 @@
<string name="overwrite">Felülírás</string>
<string name="tab_available_apps">Elérhető</string>
<string name="tab_updates">Frissítések</string>
<string name="one_update_available">1 frissítés érhető el.</string>
<string name="many_updates_available">%d frissítés érhető el.</string>
<string name="fdroid_updates_available">F-Droid frissítés érhető el</string>
<string name="repo_add_url">Repo cím</string>
<string name="repo_add_fingerprint">Ujjlenyomat (opcionális)</string>
@ -126,7 +124,7 @@ Engedélyeznie kell, hogy megtekinthesse az általa kínált appokat.</string>
<string name="tab_installed_apps">Telepítve</string>
<string name="tab_installed_apps_count">Telepítve (%d)</string>
<string name="tab_updates_count">Frissítések (%d)</string>
<string name="update_notification_more">+%1$d további…</string>
<string name="notification_summary_more">+%1$d további…</string>
<string name="send_via_bluetooth">Küldés Bluetooth-on</string>
<string name="bad_fingerprint">Rossz ujjlenyomat</string>

View File

@ -63,8 +63,6 @@
<string name="tab_available_apps">Tersedia</string>
<string name="tab_installed_apps">Terpasang</string>
<string name="tab_updates">Pembaruan</string>
<string name="one_update_available">1 pembaruan tersedia.</string>
<string name="many_updates_available">%d pembaruan tersedia.</string>
<string name="fdroid_updates_available">Tersedia Pembaruan F-Droid</string>
<string name="bluetooth_activity_not_found">Metode pengiriman Bluetooth tidak ditemukan, pilih salah satu!</string>
<string name="choose_bt_send">Pilih metode pengiriman Bluetooth</string>
@ -166,7 +164,7 @@
<string name="repo_last_update">Terakhir diperbarui</string>
<string name="repo_name">Nama</string>
<string name="unknown">Tidak diketahui</string>
<string name="update_notification_more">+%1$d lainnya…</string>
<string name="notification_summary_more">+%1$d lainnya…</string>
<string name="menu_settings">Pengaturan</string>
<string name="rooted_on">Jangan bedakan warna apl yang membutuhkan izin root</string>
<string name="unsigned_description">Ini artinya daftar

View File

@ -66,10 +66,8 @@
<string name="tab_updates">Uppfærslur</string>
<string name="tab_installed_apps_count">Uppsett (%d)</string>
<string name="tab_updates_count">Uppfærslur (%d)</string>
<string name="one_update_available">1 uppfærsla er tiltæk.</string>
<string name="many_updates_available">%d uppfærslur eru tiltækar.</string>
<string name="fdroid_updates_available">Uppfærslur á F-Droid eru tiltækar</string>
<string name="update_notification_more">+%1$d fleiri…</string>
<string name="notification_summary_more">+%1$d fleiri…</string>
<string name="bluetooth_activity_not_found">Engin aðferð til sendingar með Bluetooth fannst, veldu eina!</string>
<string name="choose_bt_send">Veldu aðferð til sendingar með Bluetooth</string>
<string name="send_via_bluetooth">Senda með Bluetooth</string>

View File

@ -43,8 +43,6 @@
<string name="overwrite">Sovrascrivi</string>
<string name="tab_available_apps">Disponibile</string>
<string name="tab_updates">Aggiornamenti</string>
<string name="one_update_available">1 aggiornamento disponibile.</string>
<string name="many_updates_available">%d aggiornamenti disponibili.</string>
<string name="fdroid_updates_available">Aggiornamenti per F-Droid disponibili</string>
<string name="bluetooth_activity_not_found">Non è possibile inviare via Bluetooth, scegliere un altro metodo!</string>
<string name="choose_bt_send">Scegli invio mediante Bluetooth</string>
@ -333,7 +331,7 @@ non saranno rimossi. Non è richiesto alcun accesso speciale.</string>
di questa app integrata? I dati presenti
non saranno rimossi. Non richiede alcun permesso speciale.</string>
<string name="allPerms">Tutte</string>
<string name="update_notification_more">+%1$d rimanente…</string>
<string name="notification_summary_more">+%1$d rimanente…</string>
<string name="swap_nearby">Scambio ravvicinato</string>
<string name="empty_search_installed_app_list">Nessuna app installata corrispondente.</string>
<string name="crash_dialog_comment_prompt">Puoi aggiungere altre informazioni e commenti qui:</string>

View File

@ -45,8 +45,6 @@
<string name="overwrite">上書き</string>
<string name="tab_available_apps">入手可能</string>
<string name="tab_updates">更新</string>
<string name="one_update_available">1個の更新があります。</string>
<string name="many_updates_available">%d個の更新があります.</string>
<string name="fdroid_updates_available">F-Droidの更新があります</string>
<string name="bluetooth_activity_not_found">Bluetoothの送信方法がありません、選択してください!</string>
<string name="choose_bt_send">Bluetoothの送信方法を選択してください</string>
@ -181,7 +179,7 @@
<string name="links">リンク</string>
<string name="back">戻る</string>
<string name="update_notification_more">+%1$d 以上…</string>
<string name="notification_summary_more">+%1$d 以上…</string>
<string name="bad_fingerprint">フィンガープリントが違います</string>
<string name="invalid_url">有効な URL ではありません。</string>
<string name="menu_changelog">変更履歴</string>

View File

@ -45,8 +45,6 @@
<string name="overwrite">덮어쓰기</string>
<string name="tab_available_apps">사용 가능</string>
<string name="tab_updates">업데이트</string>
<string name="one_update_available">1개의 업데이트를 사용할 수 있습니다.</string>
<string name="many_updates_available">%d개의 업데이트를 사용할 수 있습니다.</string>
<string name="fdroid_updates_available">F-Droid 업데이트를 사용할 수 있습니다.</string>
<string name="bluetooth_activity_not_found">블루투스 전송 방법을 찾을 수 없습니다. 선택하세요!</string>
<string name="choose_bt_send">블루투스 전송 방법 선택</string>
@ -194,7 +192,7 @@
<string name="tab_installed_apps">설치됨</string>
<string name="tab_installed_apps_count">설치됨 (%d)</string>
<string name="tab_updates_count">업데이트 (%d)</string>
<string name="update_notification_more">+%1$d 이상…</string>
<string name="notification_summary_more">+%1$d 이상…</string>
<string name="bad_fingerprint">올바르지 않은 핑거프린트</string>
<string name="invalid_url">올바른 URL이 아닙니다.</string>
<string name="menu_settings">설정</string>

View File

@ -29,8 +29,6 @@
<string name="overwrite">Perrašyti</string>
<string name="tab_available_apps">Prieinamos programos</string>
<string name="tab_updates">Atnaujinimai</string>
<string name="one_update_available">Atsirado 1 atnaujinimas.</string>
<string name="many_updates_available">Galite įdiegti %d atnaujinimus.</string>
<string name="send_via_bluetooth">Siųsti per Bluetooth</string>
<string name="repo_add_url">Saugyklos adresas</string>
<string name="menu_update_repo">Atnaujinti saugyklas</string>

View File

@ -40,8 +40,6 @@
<string name="overwrite">Pārrakstīt</string>
<string name="tab_available_apps">Pieejams</string>
<string name="tab_updates">Atjauninājumi</string>
<string name="one_update_available">1 atjauninājums pieejams.</string>
<string name="many_updates_available">%d atjauninājumi pieejami.</string>
<string name="bluetooth_activity_not_found">Nav Bluetooth aktivitāšu, izvēlies vienu!</string>
<string name="choose_bt_send">Izvēlies Bluesūtīšanas metodi</string>
<string name="send_via_bluetooth">Bluesūtīt</string>

View File

@ -74,10 +74,8 @@
<string name="tab_updates">အသစ္မြမး္မံမႈမ်ား</string>
<string name="tab_installed_apps_count">သြင္းထားခဲ့သည္ (%d)</string>
<string name="tab_updates_count">အသစ္မြမ္းမံမႈမ်ား (%d)</string>
<string name="one_update_available">အသစ္မြမ္းမံမႈ ၁ ခုရရွိႏိုင္သည္</string>
<string name="many_updates_available">အသစ္မြမ္းမံမႈ %d ခုရရွိႏိုင္သည္.</string>
<string name="fdroid_updates_available">F-Droid အသစ္မြမ္းမံမႈရရွိႏိုင္သည္</string>
<string name="update_notification_more">+%1$d ေနာက္ထပ္..</string>
<string name="notification_summary_more">+%1$d ေနာက္ထပ္..</string>
<string name="bluetooth_activity_not_found">ဘလူးသုဒ့္ႏွင့္ပို႔ရန္နည္းလမ္းရွာမေတြ႕ပါ။ တစ္ခုေရြးပါ!</string>
<string name="choose_bt_send">ဘလူးသုဒ့္ႏွင့္ပို႔ေသာနည္းလမ္းကိုေရြးမည္</string>
<string name="send_via_bluetooth">ဘလူးသုဒ့္မွတစ္ဆင့္ပို႔မည္</string>

View File

@ -45,8 +45,6 @@
<string name="overwrite">Overskriv</string>
<string name="tab_available_apps">Tilgjengelig</string>
<string name="tab_updates">Oppdateringer</string>
<string name="one_update_available">1 oppdatering tilgjengelig.</string>
<string name="many_updates_available">%d oppdateringer tilgjengelig.</string>
<string name="fdroid_updates_available">F-Droid: Oppdateringer tilgjengelig</string>
<string name="bluetooth_activity_not_found">Ingen forsendelsesmåte for Blåtann funnet, velg en!</string>
<string name="choose_bt_send">Velg forsendelsesmåte for Blåtann</string>
@ -185,7 +183,7 @@ skru på denne pakkebrønnen igjen for å installere programmer fra den.</string
<string name="back">Tilbake</string>
<string name="tab_installed_apps">Installert</string>
<string name="update_notification_more">+%1$d mer…</string>
<string name="notification_summary_more">+%1$d mer…</string>
<string name="bad_fingerprint">Feil i fingeravtrykk</string>
<string name="invalid_url">Dette er ikke en gyldig nettadresse.</string>
<string name="menu_changelog">Endringslogg</string>

View File

@ -45,8 +45,6 @@
<string name="overwrite">Overschrijven</string>
<string name="tab_available_apps">Beschikbaar</string>
<string name="tab_updates">Updates</string>
<string name="one_update_available">1 update beschikbaar.</string>
<string name="many_updates_available">%d updates beschikbaar.</string>
<string name="fdroid_updates_available">F-Droid-updates beschikbaar</string>
<string name="bluetooth_activity_not_found">Geen Bluetooth-verzendmethode gevonden, kies er een!</string>
<string name="choose_bt_send">Kies Bluetooth-verzendmethode</string>
@ -179,7 +177,7 @@ Je moet deze bron weer inschakelen indien je er apps van wil installeren.</strin
<string name="links">Links</string>
<string name="back">Terug</string>
<string name="update_notification_more">+%1$d meer…</string>
<string name="notification_summary_more">+%1$d meer…</string>
<string name="bad_fingerprint">Slechte vingerafdruk</string>
<string name="invalid_url">Dit is geen correcte URL.</string>
<string name="menu_changelog">Lijst van veranderingen</string>

View File

@ -34,7 +34,6 @@
<string name="overwrite">Nadpisz</string>
<string name="tab_available_apps">Dostępne</string>
<string name="tab_updates">Aktualizacje</string>
<string name="one_update_available">Dostępne jest 1 uaktualnienie.</string>
<string name="fdroid_updates_available">Uaktualnienie F-Droid jest dostępne</string>
<string name="send_via_bluetooth">Wyślij przez Bluetooth</string>
<string name="repo_add_url">Adres repozytorium</string>
@ -154,8 +153,7 @@ Uwaga: Wszystkie poprzednio zainstalowane aplikacje zostaną na urządzeniu.</st
<string name="links">Linki</string>
<string name="back">Powrót</string>
<string name="many_updates_available">Liczba dostępnych aktualizacji: %d.</string>
<string name="update_notification_more">+%1$d więcej…</string>
<string name="notification_summary_more">+%1$d więcej…</string>
<string name="bluetooth_activity_not_found">Nie znaleziono metody do wysłania przez Bluetooth!</string>
<string name="choose_bt_send">Wybierz metodę wysłania przez Bluetooth</string>
<string name="repo_exists_add_fingerprint">To repozytorium jest już dodane. Klucz zostanie zaktualizowany.</string>

View File

@ -45,8 +45,6 @@
<string name="overwrite">Sobrescrever</string>
<string name="tab_available_apps">Disponível</string>
<string name="tab_updates">Atualizações</string>
<string name="one_update_available">1 atualização disponível.</string>
<string name="many_updates_available">%d atualizações disponíveis.</string>
<string name="fdroid_updates_available">Atualizações do F-Droid disponíveis</string>
<string name="bluetooth_activity_not_found">Nenhum método de envio por Bluetooth foi encontrado, escolha um!</string>
<string name="choose_bt_send">Escolher o método de envio Bluetooth</string>
@ -186,7 +184,7 @@ reativar este repositório para instalar aplicativos a partir dele.</string>
<string name="links">Links</string>
<string name="back">Voltar</string>
<string name="update_notification_more">Mais +%1$d…</string>
<string name="notification_summary_more">Mais +%1$d…</string>
<string name="bad_fingerprint">Falha na fingerprint</string>
<string name="invalid_url">Esta não é uma URL válida.</string>
<string name="menu_changelog">Changelog</string>

View File

@ -63,10 +63,8 @@
<string name="tab_available_apps">Disponíveis</string>
<string name="tab_installed_apps">Instaladas</string>
<string name="tab_updates">Atualizações</string>
<string name="one_update_available">1 atualização disponível.</string>
<string name="many_updates_available">%d atualizações disponíveis.</string>
<string name="fdroid_updates_available">Atualizações F-Droid disponíveis</string>
<string name="update_notification_more">+%1$d…</string>
<string name="notification_summary_more">+%1$d…</string>
<string name="bluetooth_activity_not_found">Nenhum método de envio Bluetooth encontrado. Escolha um!</string>
<string name="choose_bt_send">Escolha o método de envio Bluetooth</string>
<string name="send_via_bluetooth">Enviar por Bluetooth</string>

View File

@ -22,8 +22,6 @@
<string name="enable">Activeaza</string>
<string name="tab_available_apps">Disponibil</string>
<string name="tab_updates">Actualizari</string>
<string name="one_update_available">O actualizare este disponibila.</string>
<string name="many_updates_available">%d actualizari sunt disponibile.</string>
<string name="menu_about">Despre</string>
<string name="menu_search">Cauta</string>
<string name="menu_launch">Porneste</string>
@ -97,7 +95,7 @@
<string name="tab_installed_apps_count">Instalat (%d)</string>
<string name="tab_updates_count">Actualizari (%d)</string>
<string name="fdroid_updates_available">Actualizari disponibile in F-Droid</string>
<string name="update_notification_more">+%1$d mai mult…</string>
<string name="notification_summary_more">+%1$d mai mult…</string>
<string name="bluetooth_activity_not_found">Nu a fost gasita o metoda Bluetooth de a trimite, alegeti una!</string>
<string name="choose_bt_send">Alegeti metoda de a trimite prin Bluetooth</string>
<string name="send_via_bluetooth">Trimite prin Bluetooth</string>

View File

@ -45,8 +45,6 @@
<string name="overwrite">Перезаписать</string>
<string name="tab_available_apps">Доступно</string>
<string name="tab_updates">Обновления</string>
<string name="one_update_available">Доступно 1 обновление.</string>
<string name="many_updates_available">Доступно %d обновлений.</string>
<string name="fdroid_updates_available">Доступны обновления для F-Droid</string>
<string name="bluetooth_activity_not_found">Методы отправки по Bluetooth не найдены, выберите какой-либо!</string>
<string name="choose_bt_send">Выберите метод отправки по Bluetooth</string>
@ -212,7 +210,7 @@
<string name="less">Меньше</string>
<string name="back">Назад</string>
<string name="update_notification_more">Детали +%1$d …</string>
<string name="notification_summary_more">Детали +%1$d …</string>
<string name="bad_fingerprint">Неверный отпечаток ключа</string>
<string name="invalid_url">URL некорректен.</string>
<string name="menu_changelog">Список изменений</string>

View File

@ -45,8 +45,6 @@
<string name="overwrite">Subraiscrie</string>
<string name="tab_available_apps">Disponìbiles</string>
<string name="tab_updates">Agiornamentos</string>
<string name="one_update_available">1 agiornamentu est disponìbile.</string>
<string name="many_updates_available">%d agiornamentos sunt disponìbiles.</string>
<string name="fdroid_updates_available">Agiornamentos pro F-Droid disponìbiles</string>
<string name="bluetooth_activity_not_found">Perunu mètodu de imbiu Bluetooth agatadu, issèberane unu!</string>
<string name="choose_bt_send">Issèbera su mètodu de imbiu Bluetooth</string>
@ -184,7 +182,7 @@ Depes
<string name="links">Ligàmenes</string>
<string name="back">In dae segus</string>
<string name="update_notification_more">+%1$d àteru(os)…</string>
<string name="notification_summary_more">+%1$d àteru(os)…</string>
<string name="invalid_url">Custu no est unu ligàmene vàlidu.</string>
<string name="menu_changelog">Lista modìficas</string>
<string name="update_notification_title">Agiornende sos depòsitos</string>

View File

@ -45,8 +45,6 @@
<string name="overwrite">Prepísať</string>
<string name="tab_available_apps">Dostupné</string>
<string name="tab_updates">Aktualizácie</string>
<string name="one_update_available">Dostupná 1 aktualizácia.</string>
<string name="many_updates_available">Dostupných %d aktualizácií.</string>
<string name="fdroid_updates_available">Dostupné aktualizácie F-Droidu</string>
<string name="send_via_bluetooth">Poslať cez Bluetooth</string>
<string name="repo_add_url">Adresa repozitára</string>
@ -187,7 +185,7 @@ znovu povoliť tento repozitár pre inštaláciu aplikácií z neho.</string>
<string name="back">Späť</string>
<string name="tab_installed_apps">Nainštalované</string>
<string name="update_notification_more">+%1$d viac…</string>
<string name="notification_summary_more">+%1$d viac…</string>
<string name="bluetooth_activity_not_found">Posielanie cez Bluetooth zlyhalo, zvoľte inú metódu!</string>
<string name="choose_bt_send">Vyberte posielanie cez Bluetooth</string>
<string name="repo_exists_and_enabled">Tento repozitár je už nastavený a povolený.</string>

View File

@ -73,10 +73,8 @@
<string name="tab_updates">Zvekunatsa</string>
<string name="tab_installed_apps_count">Zvavakirirwa (%d)</string>
<string name="tab_updates_count">Zvekunatsa (%d)</string>
<string name="one_update_available">Chekunatsa 1 chiripo.</string>
<string name="many_updates_available">Zvekunatsa %d zviripo.</string>
<string name="fdroid_updates_available">Zvekunatsa F-Droid zviripo</string>
<string name="update_notification_more">zvimwe +%1$d …</string>
<string name="notification_summary_more">zvimwe +%1$d …</string>
<string name="bluetooth_activity_not_found">Hapana mutowo wekutumira neBluetooth wawanikwa, sarudza imwe chete!</string>
<string name="choose_bt_send">Sarudza mutowo weBluetooth wekutumira nawo</string>
<string name="send_via_bluetooth">Tumira kuburikira neBluetooth</string>

View File

@ -44,8 +44,6 @@
<string name="overwrite">Пребриши</string>
<string name="tab_available_apps">Доступне</string>
<string name="tab_updates">Надоградње</string>
<string name="one_update_available">1 надоградња је доступна.</string>
<string name="many_updates_available">%d нових надоградњи је доступно.</string>
<string name="fdroid_updates_available">Доступне су надоградње на Ф-дроиду</string>
<string name="bluetooth_activity_not_found">Нема начина за слања блутутом, одредите један!</string>
<string name="choose_bt_send">Одредите начин слања блутутом</string>
@ -188,7 +186,7 @@
<string name="back">Назад</string>
<string name="tab_installed_apps">Инсталиране</string>
<string name="update_notification_more">+још %1$d…</string>
<string name="notification_summary_more">+још %1$d…</string>
<string name="bad_fingerprint">Лош отисак</string>
<string name="invalid_url">Ово није исправна адреса.</string>
<string name="menu_changelog">Дневник измена</string>

View File

@ -45,8 +45,6 @@
<string name="overwrite">Skriv över</string>
<string name="tab_available_apps">Tillgängliga</string>
<string name="tab_updates">Uppdateringar</string>
<string name="one_update_available">1 uppdatering finns tillgänglig.</string>
<string name="many_updates_available">%d uppdateringar finns tillgängliga.</string>
<string name="fdroid_updates_available">Uppdateringar för F-Droid tillgängliga</string>
<string name="bluetooth_activity_not_found">Ingen metod för att kunna skicka med Bluetooth kunde hittas, välj en!</string>
<string name="choose_bt_send">Välj en sändningsmetod för Bluetooth</string>
@ -191,7 +189,7 @@ Du kommer
<string name="back">Bakåt</string>
<string name="tab_installed_apps">Installerade</string>
<string name="update_notification_more">+%1$d mer…</string>
<string name="notification_summary_more">+%1$d mer…</string>
<string name="bad_fingerprint">Felaktigt fingeravtryck</string>
<string name="invalid_url">Det här är inte en giltig hemsideadress.</string>
<string name="menu_settings">Inställningar</string>

View File

@ -55,8 +55,6 @@
<string name="tab_updates">மேம்படுத்தல்கள்</string>
<string name="tab_installed_apps_count">நிறுவப்பட்டது (%d)</string>
<string name="tab_updates_count">மேம்படுத்தல்கள் (%d)</string>
<string name="one_update_available">1 மேம்படுத்தல் உள்ளது.</string>
<string name="many_updates_available">%d மேம்படுத்தல்கள் உள்ளன.</string>
<string name="fdroid_updates_available">F-Droid மேம்படுத்தல்கள் உள்ளன</string>
<string name="send_via_bluetooth">ப்ளூடூத் வழியாக அனுப்ப</string>

View File

@ -262,8 +262,6 @@
<string name="add_key">เพิ่มกุญแจเข้ารหัส</string>
<string name="overwrite">เขียนทับ</string>
<string name="one_update_available">1 โปรแกรมมีอัพเดต</string>
<string name="many_updates_available">%d โปรแกรมมีอัพเดต</string>
<string name="uninstall_system_summary">แสดงรายละเอียดสถานะส่วนขยายช่วยติดตั้ง และ/หรือทำการอัพเดต/ถอนการติดตั้งส่วนขยายนี้</string>
<string name="choose_bt_send">เลือกวิธีการส่งต่อผ่านบลูทูธ</string>
<string name="bluetooth_activity_not_found">ยังไม่ได้เลือกวิธีส่งต่อทางบลูทูธ, โปรดเลือกก่อน!</string>
@ -314,7 +312,7 @@
<string name="swap_not_visible_wifi">ไม่แสดงตัวบน Wi-Fi</string>
<string name="swap_no_peers_nearby">ไม่พบคนรอบข้างที่สามารถจะแบ่งปันโปรแกรมด้วยได้</string>
<string name="swap_qr_isnt_for_swap">QR Code ที่อ่านได้ ไม่ใช่โค้ดที่ใช้แบ่งปันโปรแกรม</string>
<string name="update_notification_more">มีอีก +%1$d…</string>
<string name="notification_summary_more">มีอีก +%1$d…</string>
<string name="perms_description_app">จัดให้โดย %1$s</string>
<string name="swap_cant_find_peers">ไม่เจอคนที่ตามหาหรือ?</string>
<string name="swap_nearby">แลกเปลี่ยนโปรแกรมกับคนข้างๆ</string>

View File

@ -45,8 +45,6 @@
<string name="overwrite">Üzerine yaz</string>
<string name="tab_available_apps">Mevcut</string>
<string name="tab_updates">Güncellemeler</string>
<string name="one_update_available">1 güncelleme bulunmaktadır.</string>
<string name="many_updates_available">%d güncelleme bulunmaktadır.</string>
<string name="fdroid_updates_available">F-Droid güncellemeleri bulunmaktadır</string>
<string name="bluetooth_activity_not_found">Hiçbir Bluetooth gönderme yöntemi bulunamadı, birisini seçin!</string>
<string name="choose_bt_send">Bluetooth gönderme yöntemi seç</string>
@ -203,7 +201,7 @@ bu depoyu tekrar etkinleştirmeniz gerekecektir.</string>
<string name="tab_installed_apps">Kurulu</string>
<string name="tab_installed_apps_count">Kurulu (%d)</string>
<string name="tab_updates_count">Güncellemeler (%d)</string>
<string name="update_notification_more">+%1$d daha…</string>
<string name="notification_summary_more">+%1$d daha…</string>
<string name="bad_fingerprint">Yanlış parmak izi</string>
<string name="invalid_url">Bu, geçerli bir URL değildir.</string>
<string name="menu_settings">Ayarlar</string>

View File

@ -27,8 +27,6 @@
<string name="cancel">ۋاز كەچ</string>
<string name="tab_available_apps">ئىشلىتىشچان</string>
<string name="tab_updates">يېڭىلانمىلار</string>
<string name="one_update_available">1 يېڭىلانما بار.</string>
<string name="many_updates_available">%d يېڭىلانما بار.</string>
<string name="fdroid_updates_available">F-Droid يېڭىلانمىلىرى بار</string>
<string name="repo_add_url">خەزىنە ئادرېسى</string>
<string name="menu_update_repo">خەزىنە يېڭىلا</string>

View File

@ -87,10 +87,8 @@
<string name="enable">Увімкнути</string>
<string name="add_key">Додати ключ</string>
<string name="tab_installed_apps">Встановлено</string>
<string name="one_update_available">Одне оновлення доступно.</string>
<string name="many_updates_available">%d оновлень доступно.</string>
<string name="fdroid_updates_available">F-Droid: доступні оновлення</string>
<string name="update_notification_more">+%1$d більше…</string>
<string name="notification_summary_more">+%1$d більше…</string>
<string name="send_via_bluetooth">Надіслати через Bluetooth</string>
<string name="menu_share">Поділитися</string>

View File

@ -44,8 +44,6 @@
<string name="overwrite">Ghi đè</string>
<string name="tab_available_apps">Hiện có</string>
<string name="tab_updates">Cập nhật</string>
<string name="one_update_available">Có 1 bản cập nhật.</string>
<string name="many_updates_available">Có %d bản cập nhật.</string>
<string name="fdroid_updates_available">Có cập nhật F-Droid</string>
<string name="bluetooth_activity_not_found">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!</string>
<string name="choose_bt_send">Chọn phương pháp gửi qua Bluetooth</string>
@ -168,7 +166,7 @@
<string name="about_source">Mã nguồn</string>
<string name="app_incompatible">Không tương thích</string>
<string name="tab_installed_apps">Đã cài đặt</string>
<string name="update_notification_more">+%1$d ứng dụng khác…</string>
<string name="notification_summary_more">+%1$d ứng dụng khác…</string>
<string name="invalid_url">Đây không phải là URL hợp lệ.</string>
<string name="menu_changelog">Lịch sử sửa đổi</string>
<string name="menu_bitcoin">Bitcoin</string>

View File

@ -38,8 +38,6 @@
<string name="overwrite">覆盖</string>
<string name="tab_available_apps">可安装</string>
<string name="tab_updates">更新</string>
<string name="one_update_available">有 1 个可用的更新。</string>
<string name="many_updates_available">有 %d 个可用的更新。</string>
<string name="fdroid_updates_available">可更新 F-Droid</string>
<string name="bluetooth_activity_not_found">未找到蓝牙发送方式,请选择一个!</string>
<string name="choose_bt_send">选择蓝牙发送方式</string>
@ -168,7 +166,7 @@
<string name="tab_installed_apps">已安装</string>
<string name="tab_installed_apps_count">已安装(%d</string>
<string name="tab_updates_count">可更新(%d</string>
<string name="update_notification_more">+%1$d 更多…</string>
<string name="notification_summary_more">+%1$d 更多…</string>
<string name="send_via_bluetooth">用蓝牙发送</string>
<string name="bad_fingerprint">指纹错误</string>

View File

@ -41,8 +41,6 @@
<string name="overwrite">覆寫</string>
<string name="tab_available_apps">可安裝</string>
<string name="tab_updates">更新</string>
<string name="one_update_available">您有 1 個更新。</string>
<string name="many_updates_available">您有 %d 個更新。</string>
<string name="fdroid_updates_available">有尚未安裝的 F-Droid 更新</string>
<string name="bluetooth_activity_not_found">請選擇一個藍牙傳送的方式!</string>
<string name="choose_bt_send">選擇藍牙的傳送方式</string>
@ -228,7 +226,7 @@
<string name="install_confirm_update_system_no_perms">您想更新此應用程式嗎?
您不會失去現有的數據,
已更新的程式亦不需要任何特別的存取權。</string>
<string name="update_notification_more">還有 +%1$d 個…</string>
<string name="notification_summary_more">還有 +%1$d 個…</string>
<string name="next">下一步</string>
<string name="minsdk_up_to_maxsdk">%1$s 至 %2$s</string>
<string name="SignatureMismatch">應用程式的新版本使用了不同的鑰匙簽署。若要安裝新版本,您必須先將舊版本卸載,然後再嘗試安裝。(注意:卸載將會把應用程式內的資料刪除)</string>

View File

@ -64,8 +64,6 @@
<string name="tab_updates">軟體更新</string>
<string name="tab_installed_apps_count">已安裝 (%d)</string>
<string name="tab_updates_count">軟體更新 (%d)</string>
<string name="one_update_available">有 1 個軟體更新。</string>
<string name="many_updates_available">有 %d 個軟體更新。</string>
<string name="fdroid_updates_available">有可用的 F-Droid 更新</string>
<string name="send_via_bluetooth">透過藍牙傳送</string>
@ -257,7 +255,7 @@
<string name="local_repo_https">使用私密連線</string>
<string name="repo_error_empty_username">空的使用者名稱,憑證未改變</string>
<string name="update_notification_more">+%1$d 更多…</string>
<string name="notification_summary_more">+%1$d 更多…</string>
<string name="repo_exists_enable">此應用軟體倉庫已經設立。確認您要重新啟用它。</string>
<string name="repo_exists_and_enabled">進入的儲存庫已設立並已啟用。</string>
<string name="malformed_repo_uri">略過異常的應用軟體倉庫 URI%s</string>

View File

@ -88,10 +88,7 @@
<string name="tab_updates">Updates</string>
<string name="tab_installed_apps_count">Installed (%d)</string>
<string name="tab_updates_count">Updates (%d)</string>
<string name="one_update_available">1 update is available.</string>
<string name="many_updates_available">%d updates are available.</string>
<string name="fdroid_updates_available">F-Droid Updates Available</string>
<string name="update_notification_more">+%1$d more…</string>
<string name="bluetooth_activity_not_found">No Bluetooth send method found, choose one!</string>
<string name="choose_bt_send">Choose Bluetooth send method</string>
<string name="send_via_bluetooth">Send via Bluetooth</string>
@ -400,4 +397,30 @@
forcing the application to stop. Would you like to e-mail the
details to help fix the issue?</string>
<string name="crash_dialog_comment_prompt">You can add extra information and comments here:</string>
<!-- notifications -->
<string name="notification_summary_more">+%1$d more…</string>
<string name="notification_title_single_update_available">Update Available</string>
<string name="notification_title_single_ready_to_install">Ready to install</string>
<string name="notification_title_single_ready_to_install_update">Update ready to install</string>
<string name="notification_title_single_install_error">Install Failed</string>
<string name="notification_content_single_downloading">Downloading \"%1$s\"&#8230;</string>
<string name="notification_content_single_downloading_update">Downloading update for \"%1$s\"&#8230;</string>
<string name="notification_content_single_installing">Installing \"%1$s\"&#8230;</string>
<string name="notification_content_single_installed">Successfully installed</string>
<string name="notification_content_single_install_error">Install Failed</string>
<string name="notification_summary_updates">%1$d Updates</string>
<string name="notification_summary_installed">%1$d Apps Installed</string>
<string name="notification_title_summary_update_available">Update available</string>
<string name="notification_title_summary_downloading">Downloading&#8230;</string>
<string name="notification_title_summary_downloading_update">Downloading update&#8230;</string>
<string name="notification_title_summary_ready_to_install">Ready to install</string>
<string name="notification_title_summary_ready_to_install_update">Update ready to install</string>
<string name="notification_title_summary_installing">Installing</string>
<string name="notification_title_summary_installed">Successfully installed</string>
<string name="notification_title_summary_install_error">Install Failed</string>
<string name="notification_action_update">Update</string>
<string name="notification_action_cancel">Cancel</string>
<string name="notification_action_install">Install</string>
</resources>