Display install errors as notify/dialog

This commit is contained in:
Dominik Schürmann 2016-05-28 22:21:48 +03:00
parent de1d310499
commit 4e8e148029
8 changed files with 112 additions and 136 deletions

View File

@ -562,55 +562,28 @@ public class AppDetails extends AppCompatActivity {
} }
case Installer.ACTION_INSTALL_COMPLETE: { case Installer.ACTION_INSTALL_COMPLETE: {
headerFragment.removeProgress(); headerFragment.removeProgress();
localBroadcastManager.unregisterReceiver(this); localBroadcastManager.unregisterReceiver(this);
PackageManagerCompat.setInstaller(packageManager, app.packageName);
onAppChanged();
break; break;
} }
case Installer.ACTION_INSTALL_INTERRUPTED: { case Installer.ACTION_INSTALL_INTERRUPTED: {
headerFragment.removeProgress(); headerFragment.removeProgress();
onAppChanged();
String errorMessage =
intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
if (!TextUtils.isEmpty(errorMessage)) {
Log.e(TAG, "Installer aborted with errorMessage: " + errorMessage);
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
alertBuilder.setTitle(R.string.install_error_notify_title);
alertBuilder.setMessage(errorMessage);
alertBuilder.setNeutralButton(android.R.string.ok, null);
alertBuilder.create().show();
}
localBroadcastManager.unregisterReceiver(this); localBroadcastManager.unregisterReceiver(this);
// TODO: old error handling code:
// if (errorCode == InstallerCallback.ERROR_CODE_CANCELED) {
// return;
// }
// final int title, body;
// if (operation == InstallerCallback.OPERATION_INSTALL) {
// title = R.string.install_error_title;
// switch (errorCode) {
// case ERROR_CODE_CANNOT_PARSE:
// body = R.string.install_error_cannot_parse;
// break;
// default: // ERROR_CODE_OTHER
// body = R.string.install_error_unknown;
// break;
// }
// } else { // InstallerCallback.OPERATION_DELETE
// title = R.string.uninstall_error_title;
// switch (errorCode) {
// default: // ERROR_CODE_OTHER
// body = R.string.uninstall_error_unknown;
// break;
// }
// }
// runOnUiThread(new Runnable() {
// @Override
// public void run() {
// onAppChanged();
//
// Log.e(TAG, "Installer aborted with errorCode: " + errorCode);
//
// AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
// alertBuilder.setTitle(title);
// alertBuilder.setMessage(body);
// alertBuilder.setNeutralButton(android.R.string.ok, null);
// alertBuilder.create().show();
// }
// });
break; break;
} }
case Installer.ACTION_INSTALL_USER_INTERACTION: { case Installer.ACTION_INSTALL_USER_INTERACTION: {
@ -643,52 +616,28 @@ public class AppDetails extends AppCompatActivity {
} }
case Installer.ACTION_UNINSTALL_COMPLETE: { case Installer.ACTION_UNINSTALL_COMPLETE: {
headerFragment.removeProgress(); headerFragment.removeProgress();
localBroadcastManager.unregisterReceiver(this);
onAppChanged(); onAppChanged();
localBroadcastManager.unregisterReceiver(this);
break; break;
} }
case Installer.ACTION_UNINSTALL_INTERRUPTED: { case Installer.ACTION_UNINSTALL_INTERRUPTED: {
headerFragment.removeProgress(); headerFragment.removeProgress();
localBroadcastManager.unregisterReceiver(this);
// TODO: old error handling code: String errorMessage =
// if (errorCode == InstallerCallback.ERROR_CODE_CANCELED) { intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
// return;
// } if (!TextUtils.isEmpty(errorMessage)) {
// final int title, body; Log.e(TAG, "Installer aborted with errorMessage: " + errorMessage);
// if (operation == InstallerCallback.OPERATION_INSTALL) {
// title = R.string.install_error_title; AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
// switch (errorCode) { alertBuilder.setTitle(R.string.uninstall_error_notify_title);
// case ERROR_CODE_CANNOT_PARSE: alertBuilder.setMessage(errorMessage);
// body = R.string.install_error_cannot_parse; alertBuilder.setNeutralButton(android.R.string.ok, null);
// break; alertBuilder.create().show();
// default: // ERROR_CODE_OTHER }
// body = R.string.install_error_unknown;
// break; localBroadcastManager.unregisterReceiver(this);
// }
// } else { // InstallerCallback.OPERATION_DELETE
// title = R.string.uninstall_error_title;
// switch (errorCode) {
// default: // ERROR_CODE_OTHER
// body = R.string.uninstall_error_unknown;
// break;
// }
// }
// runOnUiThread(new Runnable() {
// @Override
// public void run() {
// onAppChanged();
//
// Log.e(TAG, "Installer aborted with errorCode: " + errorCode);
//
// AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
// alertBuilder.setTitle(title);
// alertBuilder.setMessage(body);
// alertBuilder.setNeutralButton(android.R.string.ok, null);
// alertBuilder.create().show();
// }
// });
break; break;
} }
case Installer.ACTION_UNINSTALL_USER_INTERACTION: { case Installer.ACTION_UNINSTALL_USER_INTERACTION: {

View File

@ -25,9 +25,7 @@ import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity;
import java.io.File; import java.io.File;
@ -43,36 +41,22 @@ public class DefaultInstaller extends Installer {
protected void installPackage(Uri uri, Uri originatingUri, String packageName) { protected void installPackage(Uri uri, Uri originatingUri, String packageName) {
sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED); sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED);
Utils.debugLog(TAG, "ACTION_INSTALL uri: " + uri + " file: " + new File(uri.getPath())); Utils.debugLog(TAG, "DefaultInstaller uri: " + uri + " file: " + new File(uri.getPath()));
Uri sanitizedUri; Uri sanitizedUri;
try { try {
sanitizedUri = Installer.prepareApkFile(mContext, uri, packageName); sanitizedUri = Installer.prepareApkFile(mContext, uri, packageName);
} catch (Installer.InstallFailedException e) { } catch (Installer.InstallFailedException e) {
Log.e(TAG, "prepareApkFile failed", e); Log.e(TAG, "prepareApkFile failed", e);
sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
e.getMessage());
return; return;
} }
Intent installIntent; Intent installIntent = new Intent(mContext, DefaultInstallerActivity.class);
// special case: F-Droid Privileged Extension installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE);
if (packageName != null && packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) { installIntent.putExtra(DefaultInstallerActivity.EXTRA_ORIGINATING_URI, originatingUri);
installIntent.setData(sanitizedUri);
// extension must be signed with the same public key as main F-Droid
// NOTE: Disabled for debug builds to be able to use official extension from repo
ApkSignatureVerifier signatureVerifier = new ApkSignatureVerifier(mContext);
if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(new File(sanitizedUri.getPath()))) {
throw new RuntimeException("APK signature of extension not correct!");
}
installIntent = new Intent(mContext, InstallExtensionDialogActivity.class);
installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL);
installIntent.setData(sanitizedUri);
} else {
installIntent = new Intent(mContext, DefaultInstallerActivity.class);
installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE);
installIntent.putExtra(DefaultInstallerActivity.EXTRA_ORIGINATING_URI, originatingUri);
installIntent.setData(sanitizedUri);
}
PendingIntent installPendingIntent = PendingIntent.getActivity( PendingIntent installPendingIntent = PendingIntent.getActivity(
mContext.getApplicationContext(), mContext.getApplicationContext(),
@ -88,17 +72,10 @@ public class DefaultInstaller extends Installer {
protected void uninstallPackage(String packageName) { protected void uninstallPackage(String packageName) {
sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED); sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED);
Intent uninstallIntent; Intent uninstallIntent = new Intent(mContext, DefaultInstallerActivity.class);
// special case: F-Droid Privileged Extension uninstallIntent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE);
if (packageName != null && packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) { uninstallIntent.putExtra(
uninstallIntent = new Intent(mContext, InstallExtensionDialogActivity.class); DefaultInstallerActivity.EXTRA_UNINSTALL_PACKAGE_NAME, packageName);
uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL);
} else {
uninstallIntent = new Intent(mContext, DefaultInstallerActivity.class);
uninstallIntent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE);
uninstallIntent.putExtra(
DefaultInstallerActivity.EXTRA_UNINSTALL_PACKAGE_NAME, packageName);
}
PendingIntent uninstallPendingIntent = PendingIntent.getActivity( PendingIntent uninstallPendingIntent = PendingIntent.getActivity(
mContext.getApplicationContext(), mContext.getApplicationContext(),
packageName.hashCode(), packageName.hashCode(),

View File

@ -30,6 +30,7 @@ import android.os.Bundle;
import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentActivity;
import android.util.Log; import android.util.Log;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
/** /**
@ -207,9 +208,10 @@ public class DefaultInstallerActivity extends FragmentActivity {
} }
default: default:
case Activity.RESULT_FIRST_USER: { case Activity.RESULT_FIRST_USER: {
// AOSP actually returns Activity.RESULT_FIRST_USER if something breaks // AOSP returns Activity.RESULT_FIRST_USER on error
installer.sendBroadcastInstall(mInstallUri, mInstallOriginatingUri, installer.sendBroadcastInstall(mInstallUri, mInstallOriginatingUri,
Installer.ACTION_INSTALL_INTERRUPTED, "error"); Installer.ACTION_INSTALL_INTERRUPTED,
getString(R.string.install_error_unknown));
break; break;
} }
} }
@ -237,11 +239,10 @@ public class DefaultInstallerActivity extends FragmentActivity {
} }
default: default:
case Activity.RESULT_FIRST_USER: { case Activity.RESULT_FIRST_USER: {
// AOSP UninstallAppProgress actually returns // AOSP UninstallAppProgress returns RESULT_FIRST_USER on error
// Activity.RESULT_FIRST_USER if something breaks
installer.sendBroadcastUninstall(mUninstallPackageName, installer.sendBroadcastUninstall(mUninstallPackageName,
Installer.ACTION_UNINSTALL_INTERRUPTED, Installer.ACTION_UNINSTALL_INTERRUPTED,
"error"); getString(R.string.uninstall_error_unknown));
break; break;
} }
} }

View File

@ -43,13 +43,13 @@ public class ExtensionInstaller extends Installer {
@Override @Override
protected void installPackage(Uri uri, Uri originatingUri, String packageName) { protected void installPackage(Uri uri, Uri originatingUri, String packageName) {
sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED);
Uri sanitizedUri; Uri sanitizedUri;
try { try {
sanitizedUri = Installer.prepareApkFile(mContext, uri, packageName); sanitizedUri = Installer.prepareApkFile(mContext, uri, packageName);
} catch (InstallFailedException e) { } catch (InstallFailedException e) {
Log.e(TAG, "prepareApkFile failed", e); Log.e(TAG, "prepareApkFile failed", e);
sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
e.getMessage());
return; return;
} }
@ -57,10 +57,10 @@ public class ExtensionInstaller extends Installer {
// NOTE: Disabled for debug builds to be able to use official extension from repo // NOTE: Disabled for debug builds to be able to use official extension from repo
ApkSignatureVerifier signatureVerifier = new ApkSignatureVerifier(mContext); ApkSignatureVerifier signatureVerifier = new ApkSignatureVerifier(mContext);
if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(new File(sanitizedUri.getPath()))) { if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(new File(sanitizedUri.getPath()))) {
throw new RuntimeException("APK signature of extension not correct!"); sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
"APK signature of extension not correct!");
} }
Intent installIntent; Intent installIntent = new Intent(mContext, InstallExtensionDialogActivity.class);
installIntent = new Intent(mContext, InstallExtensionDialogActivity.class);
installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL); installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL);
installIntent.setData(sanitizedUri); installIntent.setData(sanitizedUri);
@ -72,14 +72,16 @@ public class ExtensionInstaller extends Installer {
sendBroadcastInstall(uri, originatingUri, sendBroadcastInstall(uri, originatingUri,
Installer.ACTION_INSTALL_USER_INTERACTION, installPendingIntent); Installer.ACTION_INSTALL_USER_INTERACTION, installPendingIntent);
// don't use broadcasts for the rest of this special installer
sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_COMPLETE);
} }
@Override @Override
protected void uninstallPackage(String packageName) { protected void uninstallPackage(String packageName) {
sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED); sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED);
Intent uninstallIntent; Intent uninstallIntent = new Intent(mContext, InstallExtensionDialogActivity.class);
uninstallIntent = new Intent(mContext, InstallExtensionDialogActivity.class);
uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL); uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL);
PendingIntent uninstallPendingIntent = PendingIntent.getActivity( PendingIntent uninstallPendingIntent = PendingIntent.getActivity(
@ -90,5 +92,8 @@ public class ExtensionInstaller extends Installer {
sendBroadcastUninstall(packageName, sendBroadcastUninstall(packageName,
Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent); Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent);
// don't use broadcasts for the rest of this special installer
sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_COMPLETE);
} }
} }

View File

@ -19,6 +19,7 @@ import android.text.TextUtils;
import org.fdroid.fdroid.AppDetails; import org.fdroid.fdroid.AppDetails;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.PackageManagerCompat;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.Downloader;
@ -274,15 +275,26 @@ public class InstallManagerService extends Service {
Uri originatingUri = Uri originatingUri =
intent.getParcelableExtra(Installer.EXTRA_ORIGINATING_URI); intent.getParcelableExtra(Installer.EXTRA_ORIGINATING_URI);
String urlString = originatingUri.toString(); String urlString = originatingUri.toString();
removeFromActive(urlString); Apk apk = removeFromActive(urlString);
PackageManagerCompat.setInstaller(getPackageManager(), apk.packageName);
localBroadcastManager.unregisterReceiver(this); localBroadcastManager.unregisterReceiver(this);
break; break;
} }
case Installer.ACTION_INSTALL_INTERRUPTED: { case Installer.ACTION_INSTALL_INTERRUPTED: {
localBroadcastManager.unregisterReceiver(this); Uri originatingUri =
intent.getParcelableExtra(Installer.EXTRA_ORIGINATING_URI);
String urlString = originatingUri.toString();
String errorMessage =
intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
if (!TextUtils.isEmpty(errorMessage)) {
App app = getAppFromActive(urlString);
notifyError(app, urlString, errorMessage, false);
}
localBroadcastManager.unregisterReceiver(this);
break; break;
} }
case Installer.ACTION_INSTALL_USER_INTERACTION: { case Installer.ACTION_INSTALL_USER_INTERACTION: {
@ -292,7 +304,7 @@ public class InstallManagerService extends Service {
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI); intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
Utils.debugLog(TAG, "originatingUri: " + originatingUri); Utils.debugLog(TAG, "originatingUri: " + originatingUri);
Apk apk = getFromActive(originatingUri.toString()); Apk apk = getApkFromActive(originatingUri.toString());
// show notification if app details is not visible // show notification if app details is not visible
if (AppDetails.isAppVisible(apk.packageName)) { if (AppDetails.isAppVisible(apk.packageName)) {
cancelNotification(originatingUri.toString()); cancelNotification(originatingUri.toString());
@ -379,6 +391,25 @@ public class InstallManagerService extends Service {
notificationManager.notify(downloadUrlId, notification); notificationManager.notify(downloadUrlId, notification);
} }
private void notifyError(App app, String urlString, String text, boolean uninstall) {
String title;
if (uninstall) {
title = String.format(getString(R.string.uninstall_error_notify_title), app.name);
} else {
title = String.format(getString(R.string.install_error_notify_title), app.name);
}
int downloadUrlId = urlString.hashCode();
NotificationCompat.Builder builder =
new NotificationCompat.Builder(this)
.setAutoCancel(true)
.setContentTitle(title)
.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 * Cancel the {@link Notification} tied to {@code urlString}, which is the
* unique ID used to represent a given APK file. {@link String#hashCode()} * unique ID used to represent a given APK file. {@link String#hashCode()}
@ -393,7 +424,7 @@ public class InstallManagerService extends Service {
ACTIVE_APPS.put(app.packageName, app); ACTIVE_APPS.put(app.packageName, app);
} }
private static Apk getFromActive(String urlString) { private static Apk getApkFromActive(String urlString) {
return ACTIVE_APKS.get(urlString); return ACTIVE_APKS.get(urlString);
} }
@ -404,6 +435,10 @@ public class InstallManagerService extends Service {
* {@link BroadcastReceiver}s, in which case {@code urlString} would not * {@link BroadcastReceiver}s, in which case {@code urlString} would not
* find anything in the active maps. * find anything in the active maps.
*/ */
private static App getAppFromActive(String urlString) {
return ACTIVE_APPS.get(getApkFromActive(urlString).packageName);
}
private static Apk removeFromActive(String urlString) { private static Apk removeFromActive(String urlString) {
Apk apk = ACTIVE_APKS.remove(urlString); Apk apk = ACTIVE_APKS.remove(urlString);
if (apk != null) { if (apk != null) {

View File

@ -160,6 +160,9 @@ public abstract class Installer {
// if (count < 0) { // if (count < 0) {
// mCallback.onError(InstallerCallback.OPERATION_INSTALL, // mCallback.onError(InstallerCallback.OPERATION_INSTALL,
// InstallerCallback.ERROR_CODE_CANNOT_PARSE); // InstallerCallback.ERROR_CODE_CANNOT_PARSE);
// install_error_cannot_parse
// return; // return;
// } // }
// if (count > 0) { // if (count > 0) {

View File

@ -151,6 +151,8 @@ public class PrivilegedInstaller extends Installer {
sanitizedUri = Installer.prepareApkFile(mContext, uri, packageName); sanitizedUri = Installer.prepareApkFile(mContext, uri, packageName);
} catch (Installer.InstallFailedException e) { } catch (Installer.InstallFailedException e) {
Log.e(TAG, "prepareApkFile failed", e); Log.e(TAG, "prepareApkFile failed", e);
sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
e.getMessage());
return; return;
} }
@ -299,6 +301,9 @@ public class PrivilegedInstaller extends Installer {
// } else if (resultCode == InstallConfirmActivity.RESULT_CANNOT_PARSE) { // } else if (resultCode == InstallConfirmActivity.RESULT_CANNOT_PARSE) {
// mCallback.onError(InstallerCallback.OPERATION_INSTALL, // mCallback.onError(InstallerCallback.OPERATION_INSTALL,
// InstallerCallback.ERROR_CODE_CANNOT_PARSE); // InstallerCallback.ERROR_CODE_CANNOT_PARSE);
// install_error_cannot_parse
// } else { // Activity.RESULT_CANCELED // } else { // Activity.RESULT_CANCELED
// mCallback.onError(InstallerCallback.OPERATION_INSTALL, // mCallback.onError(InstallerCallback.OPERATION_INSTALL,
// InstallerCallback.ERROR_CODE_CANCELED); // InstallerCallback.ERROR_CODE_CANCELED);

View File

@ -267,10 +267,8 @@
<string name="requesting_root_access_body">Requesting root access…</string> <string name="requesting_root_access_body">Requesting root access…</string>
<string name="root_access_denied_title">Root access denied</string> <string name="root_access_denied_title">Root access denied</string>
<string name="root_access_denied_body">Either your Android device is not rooted or you have denied root access for F-Droid.</string> <string name="root_access_denied_body">Either your Android device is not rooted or you have denied root access for F-Droid.</string>
<string name="install_error_title">Install error</string>
<string name="install_error_unknown">Failed to install due to an unknown error</string> <string name="install_error_unknown">Failed to install due to an unknown error</string>
<string name="install_error_cannot_parse">An error occurred while parsing the package</string> <string name="install_error_cannot_parse">An error occurred while parsing the package</string>
<string name="uninstall_error_title">Uninstall error</string>
<string name="uninstall_error_unknown">Failed to uninstall due to an unknown error</string> <string name="uninstall_error_unknown">Failed to uninstall due to an unknown error</string>
<string name="system_install_denied_title">F-Droid Privileged Extension is not available</string> <string name="system_install_denied_title">F-Droid Privileged Extension is not available</string>
<string name="system_install_denied_body">This option is only available when F-Droid Privileged Extension is installed.</string> <string name="system_install_denied_body">This option is only available when F-Droid Privileged Extension is installed.</string>
@ -365,6 +363,9 @@
<string name="tap_to_install">Download completed, tap to install</string> <string name="tap_to_install">Download completed, tap to install</string>
<string name="download_error">Download unsuccessful</string> <string name="download_error">Download unsuccessful</string>
<string name="download_pending">Waiting to start download…</string> <string name="download_pending">Waiting to start download…</string>
<string name="install_error_notify_title">Error installing %s</string>
<string name="uninstall_error_notify_title">Error uninstalling %s</string>
<string name="perms_new_perm_prefix">New: </string> <string name="perms_new_perm_prefix">New: </string>
<string name="perms_description_app">Provided by %1$s.</string> <string name="perms_description_app">Provided by %1$s.</string>