diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eae2723ad..70cecf9cb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -109,6 +109,15 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/apk_file_provider" /> + + + + diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java index f433ae184..fd7030dff 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java @@ -862,7 +862,7 @@ public class AppDetails extends AppCompatActivity { return true; case UNINSTALL: - uninstallApk(app.packageName); + uninstallApk(); return true; case IGNOREALL: @@ -942,7 +942,7 @@ public class AppDetails extends AppCompatActivity { private void initiateInstall(Apk apk) { Installer installer = InstallerFactory.create(this, apk); - Intent intent = installer.getPermissionScreen(apk); + Intent intent = installer.getPermissionScreen(); if (intent != null) { // permission screen required Utils.debugLog(TAG, "permission screen required"); @@ -959,9 +959,13 @@ public class AppDetails extends AppCompatActivity { InstallManagerService.queue(this, app, apk); } - private void uninstallApk(String packageName) { - Installer installer = InstallerFactory.create(this, null); - Intent intent = installer.getUninstallScreen(packageName); + /** + * Queue for uninstall based on the instance variable {@link #app} + */ + private void uninstallApk() { + Apk apk = app.installedApk; + Installer installer = InstallerFactory.create(this, apk); + Intent intent = installer.getUninstallScreen(); if (intent != null) { // uninstall screen required Utils.debugLog(TAG, "screen screen required"); @@ -975,7 +979,7 @@ public class AppDetails extends AppCompatActivity { private void startUninstall() { localBroadcastManager.registerReceiver(uninstallReceiver, Installer.getUninstallIntentFilter(app.packageName)); - InstallerService.uninstall(context, app.packageName); + InstallerService.uninstall(context, app.installedApk); } private void launchApk(String packageName) { @@ -1630,7 +1634,7 @@ public class AppDetails extends AppCompatActivity { // If "launchable", launch activity.launchApk(app.packageName); } else { - activity.uninstallApk(app.packageName); + activity.uninstallApk(); } } else if (app.suggestedVersionCode > 0) { // If not installed, install @@ -1658,10 +1662,6 @@ public class AppDetails extends AppCompatActivity { appDetails = (AppDetails) activity; } - void remove() { - appDetails.uninstallApk(appDetails.getApp().packageName); - } - @Override public void onViewCreated(View view, Bundle savedInstanceState) { // A bit of a hack, but we can't add the header view in setupSummaryHeader(), @@ -1689,7 +1689,7 @@ public class AppDetails extends AppCompatActivity { App app = appDetails.getApp(); final Apk apk = appDetails.getApks().getItem(position - l.getHeaderViewsCount()); if (app.installedVersionCode == apk.versionCode) { - remove(); + appDetails.uninstallApk(); } else if (app.installedVersionCode > apk.versionCode) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setMessage(R.string.installDowngrade); diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 610779db5..c4a9369c2 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -56,6 +56,7 @@ import org.fdroid.fdroid.compat.PRNGFixes; import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.InstalledAppProviderService; import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.installer.InstallHistoryService; import org.fdroid.fdroid.net.IconDownloader; import org.fdroid.fdroid.net.WifiStateChangeService; @@ -299,6 +300,21 @@ public class FDroidApp extends Application { }); configureTor(Preferences.get().isTorEnabled()); + + if (Preferences.get().isKeepingInstallHistory()) { + InstallHistoryService.register(this); + } + + String packageName = getString(R.string.install_history_reader_packageName); + String unset = getString(R.string.install_history_reader_packageName_UNSET); + if (!TextUtils.equals(packageName, unset)) { + int modeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + if (Build.VERSION.SDK_INT >= 19) { + modeFlags |= Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION; + } + grantUriPermission(packageName, InstallHistoryService.LOG_URI, modeFlags); + } } @TargetApi(18) diff --git a/app/src/main/java/org/fdroid/fdroid/Preferences.java b/app/src/main/java/org/fdroid/fdroid/Preferences.java index 1973d4dd0..7a5b91bed 100644 --- a/app/src/main/java/org/fdroid/fdroid/Preferences.java +++ b/app/src/main/java/org/fdroid/fdroid/Preferences.java @@ -55,6 +55,7 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh public static final String PREF_IGN_TOUCH = "ignoreTouchscreen"; public static final String PREF_KEEP_CACHE_TIME = "keepCacheFor"; public static final String PREF_UNSTABLE_UPDATES = "unstableUpdates"; + public static final String PREF_KEEP_INSTALL_HISTORY = "keepInstallHistory"; public static final String PREF_EXPERT = "expert"; public static final String PREF_PRIVILEGED_INSTALLER = "privilegedInstaller"; public static final String PREF_UNINSTALL_PRIVILEGED_APP = "uninstallPrivilegedApp"; @@ -75,6 +76,7 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh //private static final boolean DEFAULT_LOCAL_REPO_BONJOUR = true; private static final long DEFAULT_KEEP_CACHE_TIME = TimeUnit.DAYS.toMillis(1); private static final boolean DEFAULT_UNSTABLE_UPDATES = false; + private static final boolean DEFAULT_KEEP_INSTALL_HISTORY = false; //private static final boolean DEFAULT_LOCAL_REPO_HTTPS = false; private static final boolean DEFAULT_INCOMP_VER = false; private static final boolean DEFAULT_EXPERT = false; @@ -184,6 +186,10 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh return preferences.getBoolean(PREF_UNSTABLE_UPDATES, DEFAULT_UNSTABLE_UPDATES); } + public boolean isKeepingInstallHistory() { + return preferences.getBoolean(PREF_KEEP_INSTALL_HISTORY, DEFAULT_KEEP_INSTALL_HISTORY); + } + public boolean showIncompatibleVersions() { return preferences.getBoolean(PREF_INCOMP_VER, DEFAULT_INCOMP_VER); } diff --git a/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java b/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java index 9d30651e1..c4acc9a76 100644 --- a/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java +++ b/app/src/main/java/org/fdroid/fdroid/RepoUpdater.java @@ -483,7 +483,9 @@ public class RepoUpdater { } if (repoPushRequest.versionCode == null || repoPushRequest.versionCode == packageInfo.versionCode) { - InstallerService.uninstall(context, packageName); + Apk apk = ApkProvider.Helper.find(context, repoPushRequest.packageName, + packageInfo.versionCode); + InstallerService.uninstall(context, apk); } else { Utils.debugLog(TAG, "ignoring request based on versionCode:" + repoPushRequest); } diff --git a/app/src/main/java/org/fdroid/fdroid/compat/FileCompat.java b/app/src/main/java/org/fdroid/fdroid/compat/FileCompat.java index 5612d6d05..6ad56b4c3 100644 --- a/app/src/main/java/org/fdroid/fdroid/compat/FileCompat.java +++ b/app/src/main/java/org/fdroid/fdroid/compat/FileCompat.java @@ -66,7 +66,8 @@ public class FileCompat { dest.getAbsolutePath(), }; try { - Utils.debugLog(TAG, "Executing command: " + commands[0] + " " + commands[1] + " " + commands[2]); + Utils.debugLog(TAG, "Executing command: " + commands[0] + " " + commands[1] + + " " + commands[2] + " " + commands[3]); Process proc = Runtime.getRuntime().exec(commands); Utils.consumeStream(proc.getInputStream()); Utils.consumeStream(proc.getErrorStream()); diff --git a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java index cf440ae96..e35ad48ae 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2016 Dominik Schürmann + * Copyright (C) 2016 Blue Jay Wireless * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -25,11 +26,8 @@ import android.content.Intent; import android.net.Uri; import android.os.Build; -import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Apk; -import java.io.File; - /** * The default installer of F-Droid. It uses the normal Intents APIs of Android * to install apks. Its main inner workings are encapsulated in DefaultInstallerActivity. @@ -39,21 +37,19 @@ import java.io.File; */ public class DefaultInstaller extends Installer { - private static final String TAG = "DefaultInstaller"; + public static final String TAG = "DefaultInstaller"; - DefaultInstaller(Context context) { - super(context); + DefaultInstaller(Context context, Apk apk) { + super(context, apk); } @Override - protected void installPackageInternal(Uri localApkUri, Uri downloadUri, Apk apk) { - sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_STARTED); - - Utils.debugLog(TAG, "DefaultInstaller uri: " + localApkUri + " file: " + new File(localApkUri.getPath())); + protected void installPackageInternal(Uri localApkUri, Uri downloadUri) { Intent installIntent = new Intent(context, DefaultInstallerActivity.class); installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE); installIntent.putExtra(Installer.EXTRA_DOWNLOAD_URI, downloadUri); + installIntent.putExtra(Installer.EXTRA_APK, apk); installIntent.setData(localApkUri); PendingIntent installPendingIntent = PendingIntent.getActivity( @@ -67,21 +63,19 @@ public class DefaultInstaller extends Installer { } @Override - protected void uninstallPackage(String packageName) { - sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED); + protected void uninstallPackage() { + sendBroadcastUninstall(Installer.ACTION_UNINSTALL_STARTED); Intent uninstallIntent = new Intent(context, DefaultInstallerActivity.class); uninstallIntent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE); - uninstallIntent.putExtra( - DefaultInstallerActivity.EXTRA_UNINSTALL_PACKAGE_NAME, packageName); + uninstallIntent.putExtra(Installer.EXTRA_APK, apk); PendingIntent uninstallPendingIntent = PendingIntent.getActivity( context.getApplicationContext(), - packageName.hashCode(), + apk.packageName.hashCode(), uninstallIntent, PendingIntent.FLAG_UPDATE_CURRENT); - sendBroadcastUninstall(packageName, - Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent); + sendBroadcastUninstall(Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent); } @Override diff --git a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java index 8c7769f41..9c53800f2 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2014-2016 Dominik Schürmann + * Copyright (C) 2016 Blue Jay Wireless * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -31,23 +32,21 @@ import android.support.v4.app.FragmentActivity; import android.util.Log; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.Apk; /** * A transparent activity as a wrapper around Android's PackageInstaller Intents */ public class DefaultInstallerActivity extends FragmentActivity { - private static final String TAG = "AndroidInstallerAct"; + private static final String TAG = "DefaultInstallerActivit"; static final String ACTION_INSTALL_PACKAGE = "org.fdroid.fdroid.installer.DefaultInstaller.action.INSTALL_PACKAGE"; static final String ACTION_UNINSTALL_PACKAGE = "org.fdroid.fdroid.installer.DefaultInstaller.action.UNINSTALL_PACKAGE"; - static final String EXTRA_UNINSTALL_PACKAGE_NAME = "org.fdroid.fdroid.installer.DefaultInstaller.extra.UNINSTALL_PACKAGE_NAME"; - private static final int REQUEST_CODE_INSTALL = 0; private static final int REQUEST_CODE_UNINSTALL = 1; private Uri downloadUri; - private String uninstallPackageName; // for the broadcasts private DefaultInstaller installer; @@ -56,18 +55,16 @@ public class DefaultInstallerActivity extends FragmentActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - installer = new DefaultInstaller(this); - Intent intent = getIntent(); String action = intent.getAction(); + Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK); + installer = new DefaultInstaller(this, apk); if (ACTION_INSTALL_PACKAGE.equals(action)) { Uri localApkUri = intent.getData(); downloadUri = intent.getParcelableExtra(Installer.EXTRA_DOWNLOAD_URI); installPackage(localApkUri); } else if (ACTION_UNINSTALL_PACKAGE.equals(action)) { - uninstallPackageName = intent.getStringExtra(EXTRA_UNINSTALL_PACKAGE_NAME); - - uninstallPackage(uninstallPackageName); + uninstallPackage(apk.packageName); } else { throw new IllegalStateException("Intent action not specified!"); } @@ -125,7 +122,6 @@ public class DefaultInstallerActivity extends FragmentActivity { "This Android rom does not support ACTION_INSTALL_PACKAGE!"); finish(); } - installer.sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_STARTED); } private void uninstallPackage(String packageName) { @@ -134,7 +130,7 @@ public class DefaultInstallerActivity extends FragmentActivity { getPackageManager().getPackageInfo(packageName, 0); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "NameNotFoundException", e); - installer.sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_INTERRUPTED, + installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_INTERRUPTED, "Package that is scheduled for uninstall is not installed!"); finish(); return; @@ -155,7 +151,7 @@ public class DefaultInstallerActivity extends FragmentActivity { startActivityForResult(intent, REQUEST_CODE_UNINSTALL); } catch (ActivityNotFoundException e) { Log.e(TAG, "ActivityNotFoundException", e); - installer.sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_INTERRUPTED, + installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_INTERRUPTED, "This Android rom does not support ACTION_UNINSTALL_PACKAGE!"); finish(); } @@ -197,25 +193,21 @@ public class DefaultInstallerActivity extends FragmentActivity { case REQUEST_CODE_UNINSTALL: // resultCode is always 0 on Android < 4.0. if (Build.VERSION.SDK_INT < 14) { - installer.sendBroadcastUninstall(uninstallPackageName, - Installer.ACTION_UNINSTALL_COMPLETE); + installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_COMPLETE); break; } switch (resultCode) { case Activity.RESULT_OK: - installer.sendBroadcastUninstall(uninstallPackageName, - Installer.ACTION_UNINSTALL_COMPLETE); + installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_COMPLETE); break; case Activity.RESULT_CANCELED: - installer.sendBroadcastUninstall(uninstallPackageName, - Installer.ACTION_UNINSTALL_INTERRUPTED); + installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_INTERRUPTED); break; case Activity.RESULT_FIRST_USER: default: // AOSP UninstallAppProgress returns RESULT_FIRST_USER on error - installer.sendBroadcastUninstall(uninstallPackageName, - Installer.ACTION_UNINSTALL_INTERRUPTED, + installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_INTERRUPTED, getString(R.string.uninstall_error_unknown)); break; } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java index d40137c2d..3d0b11623 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java @@ -1,4 +1,5 @@ /* + * Copyright (C) 2016 Blue Jay Wireless * Copyright (C) 2016 Dominik Schürmann * * This program is free software; you can redistribute it and/or @@ -39,12 +40,12 @@ import java.io.File; */ public class ExtensionInstaller extends Installer { - ExtensionInstaller(Context context) { - super(context); + ExtensionInstaller(Context context, Apk apk) { + super(context, apk); } @Override - protected void installPackageInternal(Uri localApkUri, Uri downloadUri, Apk apk) { + protected void installPackageInternal(Uri localApkUri, Uri downloadUri) { // extension must be signed with the same public key as main F-Droid // NOTE: Disabled for debug builds to be able to test official extension from repo ApkSignatureVerifier signatureVerifier = new ApkSignatureVerifier(context); @@ -71,23 +72,22 @@ public class ExtensionInstaller extends Installer { } @Override - protected void uninstallPackage(String packageName) { - sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED); + protected void uninstallPackage() { + sendBroadcastUninstall(Installer.ACTION_UNINSTALL_STARTED); Intent uninstallIntent = new Intent(context, InstallExtensionDialogActivity.class); uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL); PendingIntent uninstallPendingIntent = PendingIntent.getActivity( context.getApplicationContext(), - packageName.hashCode(), + apk.packageName.hashCode(), uninstallIntent, PendingIntent.FLAG_UPDATE_CURRENT); - sendBroadcastUninstall(packageName, - Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent); + sendBroadcastUninstall(Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent); // don't use broadcasts for the rest of this special installer - sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_COMPLETE); + sendBroadcastUninstall(Installer.ACTION_UNINSTALL_COMPLETE); } @Override diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallHistoryService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallHistoryService.java new file mode 100644 index 000000000..6d930c850 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallHistoryService.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2016 Blue Jay Wireless + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +package org.fdroid.fdroid.installer; + +import android.app.IntentService; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.Process; +import android.support.v4.content.LocalBroadcastManager; +import android.text.TextUtils; + +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.Apk; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Saves all activity of installs and uninstalls to the database for later use, like + * displaying in some kind of history viewer or reporting to a "popularity contest" + * app tracker. + */ +public class InstallHistoryService extends IntentService { + public static final String TAG = "InstallHistoryService"; + + public static final Uri LOG_URI = Uri.parse("content://org.fdroid.fdroid.installer/install_history/all"); + + private static BroadcastReceiver broadcastReceiver; + + public static void register(Context context) { + if (broadcastReceiver != null) { + return; // already registered + } + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addDataScheme("http"); + intentFilter.addDataScheme("https"); + intentFilter.addDataScheme("package"); + intentFilter.addAction(Installer.ACTION_INSTALL_STARTED); + intentFilter.addAction(Installer.ACTION_INSTALL_COMPLETE); + intentFilter.addAction(Installer.ACTION_INSTALL_INTERRUPTED); + intentFilter.addAction(Installer.ACTION_INSTALL_USER_INTERACTION); + intentFilter.addAction(Installer.ACTION_UNINSTALL_STARTED); + intentFilter.addAction(Installer.ACTION_UNINSTALL_COMPLETE); + intentFilter.addAction(Installer.ACTION_UNINSTALL_INTERRUPTED); + intentFilter.addAction(Installer.ACTION_UNINSTALL_USER_INTERACTION); + + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + queue(context, intent); + } + }; + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context); + localBroadcastManager.registerReceiver(broadcastReceiver, intentFilter); + } + + public static void unregister(Context context) { + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context); + localBroadcastManager.unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + + public static void queue(Context context, Intent intent) { + Utils.debugLog(TAG, "queue " + intent); + intent.setClass(context, InstallHistoryService.class); + context.startService(intent); + } + + public InstallHistoryService() { + super("InstallHistoryService"); + } + + @Override + protected void onHandleIntent(Intent intent) { + Utils.debugLog(TAG, "onHandleIntent " + intent); + if (intent == null) { + return; + } + + Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); + long timestamp = System.currentTimeMillis(); + Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK); + String packageName = apk.packageName; + int versionCode = apk.versionCode; + + List values = new ArrayList<>(4); + values.add(String.valueOf(timestamp)); + values.add(packageName); + values.add(String.valueOf(versionCode)); + values.add(intent.getAction()); + + File installHistoryDir = new File(getCacheDir(), "install_history"); + installHistoryDir.mkdir(); + File logFile = new File(installHistoryDir, "all"); + FileWriter fw = null; + PrintWriter out = null; + try { + fw = new FileWriter(logFile, true); + out = new PrintWriter(fw); + out.println(TextUtils.join(",", values)); + } catch (IOException e) { + Utils.debugLog(TAG, e.getMessage()); + } finally { + Utils.closeQuietly(out); + Utils.closeQuietly(fw); + } + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index 1a21bd9db..2e3564507 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -5,6 +5,7 @@ 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; @@ -22,6 +23,7 @@ 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.net.Downloader; import org.fdroid.fdroid.net.DownloaderService; @@ -235,6 +237,7 @@ public class InstallManagerService extends Service { @Override public void onReceive(Context context, Intent intent) { String downloadUrl = intent.getDataString(); + Apk apk; switch (intent.getAction()) { case Installer.ACTION_INSTALL_STARTED: // nothing to do @@ -247,12 +250,17 @@ public class InstallManagerService extends Service { 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.findByPackageName(resolver, apk.packageName); + } // show notification if app details is not visible if (app != null && AppDetails.isAppVisible(app.packageName)) { cancelNotification(downloadUrl); @@ -264,15 +272,15 @@ public class InstallManagerService extends Service { 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); - Apk apkUserInteraction = getApkFromActive(downloadUrl); // show notification if app details is not visible - if (AppDetails.isAppVisible(apkUserInteraction.packageName)) { + if (AppDetails.isAppVisible(apk.packageName)) { cancelNotification(downloadUrl); } else { - notifyDownloadComplete(apkUserInteraction, downloadUrl, installPendingIntent); + notifyDownloadComplete(apk, downloadUrl, installPendingIntent); } break; @@ -335,10 +343,14 @@ public class InstallManagerService extends Service { title = String.format(getString(R.string.tap_to_update_format), pm.getApplicationLabel(pm.getApplicationInfo(apk.packageName, 0))); } catch (PackageManager.NameNotFoundException e) { - // TODO use packageName to fetch App instance from database if not cached String name = getAppName(apk); if (TextUtils.isEmpty(name) || name.equals(new App().name)) { - return; // do not have a name to display, so leave notification as is + ContentResolver resolver = getContentResolver(); + App app = AppProvider.Helper.findByPackageName(resolver, apk.packageName); + 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); } @@ -450,10 +462,13 @@ public class InstallManagerService extends Service { */ public static void queue(Context context, App app, Apk apk) { String urlString = apk.getUrl(); + Uri downloadUri = Uri.parse(urlString); + Installer.sendBroadcastInstall(context, downloadUri, Installer.ACTION_INSTALL_STARTED, apk, + null, null); Utils.debugLog(TAG, "queue " + app.packageName + " " + apk.versionCode + " from " + urlString); Intent intent = new Intent(context, InstallManagerService.class); intent.setAction(ACTION_INSTALL); - intent.setData(Uri.parse(urlString)); + intent.setData(downloadUri); intent.putExtra(EXTRA_APP, app); intent.putExtra(EXTRA_APK, apk); context.startService(intent); diff --git a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java index 8df65cd4c..ce1216a0b 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java @@ -1,4 +1,5 @@ /* + * Copyright (C) 2016 Blue Jay Wireless * Copyright (C) 2016 Dominik Schürmann * * This program is free software; you can redistribute it and/or @@ -42,11 +43,11 @@ import java.io.IOException; * Handles the actual install process. Subclasses implement the details. */ public abstract class Installer { - final Context context; - private final LocalBroadcastManager localBroadcastManager; - private static final String TAG = "Installer"; + final Context context; + final Apk apk; + public static final String ACTION_INSTALL_STARTED = "org.fdroid.fdroid.installer.Installer.action.INSTALL_STARTED"; public static final String ACTION_INSTALL_COMPLETE = "org.fdroid.fdroid.installer.Installer.action.INSTALL_COMPLETE"; public static final String ACTION_INSTALL_INTERRUPTED = "org.fdroid.fdroid.installer.Installer.action.INSTALL_INTERRUPTED"; @@ -67,29 +68,31 @@ public abstract class Installer { */ static final String EXTRA_DOWNLOAD_URI = "org.fdroid.fdroid.installer.Installer.extra.DOWNLOAD_URI"; public static final String EXTRA_APK = "org.fdroid.fdroid.installer.Installer.extra.APK"; - public static final String EXTRA_PACKAGE_NAME = "org.fdroid.fdroid.installer.Installer.extra.PACKAGE_NAME"; public static final String EXTRA_USER_INTERACTION_PI = "org.fdroid.fdroid.installer.Installer.extra.USER_INTERACTION_PI"; public static final String EXTRA_ERROR_MESSAGE = "org.fdroid.fdroid.net.installer.Installer.extra.ERROR_MESSAGE"; - Installer(Context context) { + /** + * @param apk must be included so that all the phases of the install process + * can get all the data about the app, even after F-Droid was killed + */ + Installer(Context context, Apk apk) { this.context = context; - localBroadcastManager = LocalBroadcastManager.getInstance(context); + this.apk = apk; } /** * Returns permission screen for given apk. * - * @param apk instance of Apk * @return Intent with Activity to show required permissions. * Returns null if Installer handles that on itself, e.g., with DefaultInstaller, * or if no new permissions have been introduced during an update */ - public Intent getPermissionScreen(Apk apk) { + public Intent getPermissionScreen() { if (!isUnattended()) { return null; } - int count = newPermissionCount(apk); + int count = newPermissionCount(); if (count == 0) { // no permission screen needed! return null; @@ -101,7 +104,7 @@ public abstract class Installer { return intent; } - private int newPermissionCount(Apk apk) { + private int newPermissionCount() { boolean supportsRuntimePermissions = apk.targetSdkVersion >= 23; if (supportsRuntimePermissions) { return 0; @@ -125,69 +128,69 @@ public abstract class Installer { * Returns an Intent to start a dialog wrapped in an activity * for uninstall confirmation. * - * @param packageName packageName of app to uninstall * @return Intent with activity for uninstall confirmation * Returns null if Installer handles that on itself, e.g., * with DefaultInstaller. */ - public Intent getUninstallScreen(String packageName) { + public Intent getUninstallScreen() { if (!isUnattended()) { return null; } Intent intent = new Intent(context, UninstallDialogActivity.class); - intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(Installer.EXTRA_APK, apk); return intent; } void sendBroadcastInstall(Uri downloadUri, String action, PendingIntent pendingIntent) { - sendBroadcastInstall(downloadUri, action, pendingIntent, null); + sendBroadcastInstall(context, downloadUri, action, apk, pendingIntent, null); } void sendBroadcastInstall(Uri downloadUri, String action) { - sendBroadcastInstall(downloadUri, action, null, null); + sendBroadcastInstall(context, downloadUri, action, apk, null, null); } void sendBroadcastInstall(Uri downloadUri, String action, String errorMessage) { - sendBroadcastInstall(downloadUri, action, null, errorMessage); + sendBroadcastInstall(context, downloadUri, action, apk, null, errorMessage); } - void sendBroadcastInstall(Uri downloadUri, String action, - PendingIntent pendingIntent, String errorMessage) { + static void sendBroadcastInstall(Context context, + Uri downloadUri, String action, Apk apk, + PendingIntent pendingIntent, String errorMessage) { Intent intent = new Intent(action); intent.setData(downloadUri); intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent); + intent.putExtra(Installer.EXTRA_APK, apk); if (!TextUtils.isEmpty(errorMessage)) { intent.putExtra(Installer.EXTRA_ERROR_MESSAGE, errorMessage); } - localBroadcastManager.sendBroadcast(intent); + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); } - void sendBroadcastUninstall(String packageName, String action, String errorMessage) { - sendBroadcastUninstall(packageName, action, null, errorMessage); + void sendBroadcastUninstall(String action, String errorMessage) { + sendBroadcastUninstall(action, null, errorMessage); } - void sendBroadcastUninstall(String packageName, String action) { - sendBroadcastUninstall(packageName, action, null, null); + void sendBroadcastUninstall(String action) { + sendBroadcastUninstall(action, null, null); } - void sendBroadcastUninstall(String packageName, String action, PendingIntent pendingIntent) { - sendBroadcastUninstall(packageName, action, pendingIntent, null); + void sendBroadcastUninstall(String action, PendingIntent pendingIntent) { + sendBroadcastUninstall(action, pendingIntent, null); } - void sendBroadcastUninstall(String packageName, String action, - PendingIntent pendingIntent, String errorMessage) { - Uri uri = Uri.fromParts("package", packageName, null); + void sendBroadcastUninstall(String action, PendingIntent pendingIntent, String errorMessage) { + Uri uri = Uri.fromParts("package", apk.packageName, null); Intent intent = new Intent(action); intent.setData(uri); // for broadcast filtering - intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(Installer.EXTRA_APK, apk); intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent); if (!TextUtils.isEmpty(errorMessage)) { intent.putExtra(Installer.EXTRA_ERROR_MESSAGE, errorMessage); } - localBroadcastManager.sendBroadcast(intent); + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); } /** @@ -223,9 +226,8 @@ public abstract class Installer { * @param localApkUri points to the local copy of the APK to be installed * @param downloadUri serves as the unique ID for all actions related to the * installation of that specific APK - * @param apk apk object of the app that should be installed */ - public void installPackage(Uri localApkUri, Uri downloadUri, Apk apk) { + public void installPackage(Uri localApkUri, Uri downloadUri) { try { // verify that permissions of the apk file match the ones from the apk object ApkVerifier apkVerifier = new ApkVerifier(context, localApkUri, apk); @@ -242,8 +244,8 @@ public abstract class Installer { if (isUnattended()) { Log.e(TAG, e.getMessage(), e); Log.e(TAG, "Falling back to AOSP DefaultInstaller!"); - DefaultInstaller defaultInstaller = new DefaultInstaller(context); - defaultInstaller.installPackageInternal(localApkUri, downloadUri, apk); + DefaultInstaller defaultInstaller = new DefaultInstaller(context, apk); + defaultInstaller.installPackageInternal(localApkUri, downloadUri); return; } } @@ -260,17 +262,16 @@ public abstract class Installer { return; } - installPackageInternal(sanitizedUri, downloadUri, apk); + installPackageInternal(sanitizedUri, downloadUri); } - protected abstract void installPackageInternal(Uri localApkUri, Uri downloadUri, Apk apk); + protected abstract void installPackageInternal(Uri localApkUri, Uri downloadUri); /** - * Uninstall app - * - * @param packageName package name of the app that should be uninstalled + * Uninstall app as defined by {@link Installer#apk} in + * {@link Installer#Installer(Context, Apk)} */ - protected abstract void uninstallPackage(String packageName); + protected abstract void uninstallPackage(); /** * This {@link Installer} instance is capable of "unattended" install and diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java index 3c19ae42c..47c9bc74c 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java @@ -1,4 +1,5 @@ /* + * Copyright (C) 2016 Blue Jay Wireless * Copyright (C) 2016 Dominik Schürmann * * This program is free software; you can redistribute it and/or @@ -20,6 +21,7 @@ package org.fdroid.fdroid.installer; import android.content.Context; +import android.text.TextUtils; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Apk; @@ -34,22 +36,23 @@ public class InstallerFactory { * case to install the "F-Droid Privileged Extension" ExtensionInstaller. * * @param context current {@link Context} - * @param apk apk to be installed. Required to select the ExtensionInstaller. - * If this is null, the ExtensionInstaller will never be returned. + * @param apk to be installed, always required. * @return instance of an Installer */ public static Installer create(Context context, Apk apk) { - Installer installer; + if (apk == null || TextUtils.isEmpty(apk.packageName)) { + throw new IllegalArgumentException("packageName must not be empty!"); + } - if (apk != null - && apk.packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) { + Installer installer; + if (apk.packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) { // special case for "F-Droid Privileged Extension" - installer = new ExtensionInstaller(context); + installer = new ExtensionInstaller(context, apk); } else if (PrivilegedInstaller.isDefault(context)) { Utils.debugLog(TAG, "privileged extension correctly installed -> PrivilegedInstaller"); - installer = new PrivilegedInstaller(context); + installer = new PrivilegedInstaller(context, apk); } else { - installer = new DefaultInstaller(context); + installer = new DefaultInstaller(context, apk); } return installer; diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java index 4e3b3ad9f..3784c788c 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java @@ -1,4 +1,5 @@ /* + * Copyright (C) 2016 Blue Jay Wireless * Copyright (C) 2016 Dominik Schürmann * * This program is free software; you can redistribute it and/or @@ -63,10 +64,9 @@ public class InstallerService extends IntentService { if (ACTION_INSTALL.equals(intent.getAction())) { Uri uri = intent.getData(); Uri downloadUri = intent.getParcelableExtra(Installer.EXTRA_DOWNLOAD_URI); - installer.installPackage(uri, downloadUri, apk); + installer.installPackage(uri, downloadUri); } else if (ACTION_UNINSTALL.equals(intent.getAction())) { - String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME); - installer.uninstallPackage(packageName); + installer.uninstallPackage(); } } @@ -90,13 +90,13 @@ public class InstallerService extends IntentService { /** * Uninstall an app * - * @param context this app's {@link Context} - * @param packageName package name of the app that will be uninstalled + * @param context this app's {@link Context} + * @param apk {@link Apk} instance of the app that will be uninstalled */ - public static void uninstall(Context context, String packageName) { + public static void uninstall(Context context, Apk apk) { Intent intent = new Intent(context, InstallerService.class); intent.setAction(ACTION_UNINSTALL); - intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(Installer.EXTRA_APK, apk); context.startService(intent); } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java index 8649f729b..1ecd62789 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java @@ -1,4 +1,5 @@ /* + * Copyright (C) 2016 Blue Jay Wireless * Copyright (C) 2014-2016 Dominik Schürmann * Copyright (C) 2015 Daniel Martí * @@ -255,8 +256,8 @@ public class PrivilegedInstaller extends Installer { "device owner has marked the package as uninstallable."); } - public PrivilegedInstaller(Context context) { - super(context); + public PrivilegedInstaller(Context context, Apk apk) { + super(context, apk); } public static boolean isExtensionInstalled(Context context) { @@ -306,9 +307,7 @@ public class PrivilegedInstaller extends Installer { } @Override - protected void installPackageInternal(final Uri localApkUri, final Uri downloadUri, Apk apk) { - sendBroadcastInstall(downloadUri, Installer.ACTION_INSTALL_STARTED); - + protected void installPackageInternal(final Uri localApkUri, final Uri downloadUri) { ServiceConnection mServiceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName name, IBinder service) { IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service); @@ -354,8 +353,8 @@ public class PrivilegedInstaller extends Installer { } @Override - protected void uninstallPackage(final String packageName) { - sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED); + protected void uninstallPackage() { + sendBroadcastUninstall(Installer.ACTION_UNINSTALL_STARTED); ServiceConnection mServiceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName name, IBinder service) { @@ -365,9 +364,9 @@ public class PrivilegedInstaller extends Installer { @Override public void handleResult(String packageName, int returnCode) throws RemoteException { if (returnCode == DELETE_SUCCEEDED) { - sendBroadcastUninstall(packageName, ACTION_UNINSTALL_COMPLETE); + sendBroadcastUninstall(ACTION_UNINSTALL_COMPLETE); } else { - sendBroadcastUninstall(packageName, ACTION_UNINSTALL_INTERRUPTED, + sendBroadcastUninstall(ACTION_UNINSTALL_INTERRUPTED, "Error " + returnCode + ": " + UNINSTALL_RETURN_CODES.get(returnCode)); } @@ -377,15 +376,15 @@ public class PrivilegedInstaller extends Installer { try { boolean hasPermissions = privService.hasPrivilegedPermissions(); if (!hasPermissions) { - sendBroadcastUninstall(packageName, ACTION_UNINSTALL_INTERRUPTED, + sendBroadcastUninstall(ACTION_UNINSTALL_INTERRUPTED, context.getString(R.string.system_install_denied_permissions)); return; } - privService.deletePackage(packageName, 0, callback); + privService.deletePackage(apk.packageName, 0, callback); } catch (RemoteException e) { Log.e(TAG, "RemoteException", e); - sendBroadcastUninstall(packageName, ACTION_UNINSTALL_INTERRUPTED, + sendBroadcastUninstall(ACTION_UNINSTALL_INTERRUPTED, "connecting to privileged service failed"); } } diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/views/UninstallDialogActivity.java b/app/src/main/java/org/fdroid/fdroid/privileged/views/UninstallDialogActivity.java index fa2380719..2538cefb8 100644 --- a/app/src/main/java/org/fdroid/fdroid/privileged/views/UninstallDialogActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/privileged/views/UninstallDialogActivity.java @@ -32,6 +32,7 @@ import android.view.ContextThemeWrapper; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.installer.Installer; /** @@ -48,14 +49,15 @@ public class UninstallDialogActivity extends FragmentActivity { super.onCreate(savedInstanceState); final Intent intent = getIntent(); - final String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME); + final Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK); PackageManager pm = getPackageManager(); ApplicationInfo appInfo; try { //noinspection WrongConstant (lint is actually wrong here!) - appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES); + appInfo = pm.getApplicationInfo(apk.packageName, + PackageManager.GET_UNINSTALLED_PACKAGES); } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException("Failed to get ApplicationInfo for uninstalling"); } @@ -86,7 +88,7 @@ public class UninstallDialogActivity extends FragmentActivity { @Override public void onClick(DialogInterface dialog, int which) { Intent data = new Intent(); - data.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName); + data.putExtra(Installer.EXTRA_APK, apk); setResult(Activity.RESULT_OK, intent); finish(); } diff --git a/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java b/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java index 4304b87c6..c1355b5ad 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java +++ b/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java @@ -19,6 +19,7 @@ import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.PreferencesActivity; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.installer.InstallHistoryService; import org.fdroid.fdroid.installer.PrivilegedInstaller; import info.guardianproject.netcipher.NetCipher; @@ -193,6 +194,14 @@ public class PreferencesFragment extends PreferenceFragment } break; + case Preferences.PREF_KEEP_INSTALL_HISTORY: + CheckBoxPreference p = (CheckBoxPreference) findPreference(key); + if (p.isChecked()) { + InstallHistoryService.register(getContext()); + } else { + InstallHistoryService.unregister(getContext()); + } + break; } } diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 00e6b1e2d..cb5f3c688 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -7,7 +7,7 @@ https://gitlab.com/fdroid/fdroidclient team@f-droid.org GNU General Public License version\u00A03 or later - + transition_app_item_icon https:// @@ -15,6 +15,11 @@ %1$s on F-Droid + + 1-THIS MEANS NO APP IS GRANTED ACCESS! + @string/install_history_reader_packageName_UNSET + 0 1 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a3a30b88f..d23295a97 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,6 +14,8 @@ Updates Unstable updates Suggest updates to unstable versions + Keep install history + Store a log of all installs and uninstalls inside F-Droid Other Automatic update interval diff --git a/app/src/main/res/xml/install_history_file_provider.xml b/app/src/main/res/xml/install_history_file_provider.xml new file mode 100644 index 000000000..06e42c762 --- /dev/null +++ b/app/src/main/res/xml/install_history_file_provider.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index aa80594bd..96ae03aaa 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -91,6 +91,12 @@ android:summary="@string/unstable_updates_summary" android:defaultValue="false" android:dependency="expert" /> +