Merge branch 'installerservice-wip' into 'master'
InstallerService This merge request mainly introduces the ``InstallerService``. Many files have been touched and reworked in this merge request, due to the following changes: * After download of an apk in ``InstallManagerService``, the ``InstallerService``is started an kicks off the installation process. For unattended installers this directly runs through without any user interaction, for the default installer a new PendingIntent containing ``DefaultActivityInstaller`` is returned that is either stuffed into the notification or directly started from ``AppDetails`` * Using local broadcasts, ``InstallManagerService`` and ``AppDetails`` are informed of state changes in the installation process * ``DefaultActivityInstaller`` is a wrapper around the default installation APIs of Android * If the unattended ``PrivilegedInstaller`` is available, a permission screen is shown before download * Actual error codes and messages are displayed in notification or dialog on fail, especially interesting when using the ``PrivilegedInstaller`` * The process for installing the Privileged Extension has been moved into an own installer for logic seperation, called ``ExtensionInstaller`` Some design considerations: * I try to use Uris where ever possible. At some points this clashes with the usage of ``urlString`` in ``InstallManagerService``. This could be fixed in a later merge request Some other TODOs are left, but I would like to do them after this merge request has been merged if it's okay, as this one is already too huge: * Check if apk permissions are the same as announced in the permission screen for ``PrivilegedInstaller`` * In ``Installer.newPermissionCount()``, I need the target SDK before download to check if it's targetting Android M, which does not require the permission screen * Introduce FileProvider for Android N * Redesign layout of ``InstallConfirmActivity`` * Remove "cancel" icon for installing progress in AppDetails See merge request !300
This commit is contained in:
		
						commit
						9c1b917604
					
				@ -316,12 +316,18 @@
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".privileged.views.InstallConfirmActivity"
 | 
			
		||||
            android:label="@string/menu_install"
 | 
			
		||||
            android:theme="@style/MinWithDialogBaseThemeLight"
 | 
			
		||||
            android:excludeFromRecents="true"
 | 
			
		||||
            android:parentActivityName=".FDroid"
 | 
			
		||||
            android:configChanges="layoutDirection|locale" >
 | 
			
		||||
            <meta-data
 | 
			
		||||
                android:name="android.support.PARENT_ACTIVITY"
 | 
			
		||||
                android:value=".FDroid" />
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".privileged.views.UninstallDialogActivity"
 | 
			
		||||
            android:excludeFromRecents="true"
 | 
			
		||||
            android:theme="@style/AppThemeTransparent" />
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".views.ManageReposActivity"
 | 
			
		||||
            android:label="@string/app_name"
 | 
			
		||||
@ -401,6 +407,14 @@
 | 
			
		||||
                <action android:name="android.intent.action.BOOT_COMPLETED" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </receiver>
 | 
			
		||||
        <!-- Note: AppThemeTransparent, this activity shows dialogs only -->
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".installer.DefaultInstallerActivity"
 | 
			
		||||
            android:theme="@style/AppThemeTransparent" />
 | 
			
		||||
        <!-- Note: AppThemeTransparent, this activity shows dialogs only -->
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".installer.ErrorDialogActivity"
 | 
			
		||||
            android:theme="@style/AppThemeTransparent" />
 | 
			
		||||
 | 
			
		||||
        <receiver android:name=".receiver.StartupReceiver" >
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
@ -440,6 +454,9 @@
 | 
			
		||||
        <service
 | 
			
		||||
            android:name=".net.DownloaderService"
 | 
			
		||||
            android:exported="false" />
 | 
			
		||||
        <service
 | 
			
		||||
            android:name=".installer.InstallerService"
 | 
			
		||||
            android:exported="false" />
 | 
			
		||||
        <service
 | 
			
		||||
            android:name=".CleanCacheService"
 | 
			
		||||
            android:exported="false" />
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@
 | 
			
		||||
package org.fdroid.fdroid;
 | 
			
		||||
 | 
			
		||||
import android.app.Activity;
 | 
			
		||||
import android.app.PendingIntent;
 | 
			
		||||
import android.bluetooth.BluetoothAdapter;
 | 
			
		||||
import android.content.ActivityNotFoundException;
 | 
			
		||||
import android.content.BroadcastReceiver;
 | 
			
		||||
@ -78,17 +79,16 @@ import com.nostra13.universalimageloader.core.ImageLoader;
 | 
			
		||||
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
 | 
			
		||||
 | 
			
		||||
import org.fdroid.fdroid.Utils.CommaSeparatedList;
 | 
			
		||||
import org.fdroid.fdroid.compat.PackageManagerCompat;
 | 
			
		||||
import org.fdroid.fdroid.data.Apk;
 | 
			
		||||
import org.fdroid.fdroid.data.ApkProvider;
 | 
			
		||||
import org.fdroid.fdroid.data.App;
 | 
			
		||||
import org.fdroid.fdroid.data.AppProvider;
 | 
			
		||||
import org.fdroid.fdroid.data.InstalledAppProvider;
 | 
			
		||||
import org.fdroid.fdroid.data.RepoProvider;
 | 
			
		||||
import org.fdroid.fdroid.installer.InstallManagerService;
 | 
			
		||||
import org.fdroid.fdroid.installer.Installer;
 | 
			
		||||
import org.fdroid.fdroid.installer.Installer.InstallFailedException;
 | 
			
		||||
import org.fdroid.fdroid.installer.Installer.InstallerCallback;
 | 
			
		||||
import org.fdroid.fdroid.installer.InstallManagerService;
 | 
			
		||||
import org.fdroid.fdroid.installer.InstallerFactory;
 | 
			
		||||
import org.fdroid.fdroid.installer.InstallerService;
 | 
			
		||||
import org.fdroid.fdroid.net.Downloader;
 | 
			
		||||
import org.fdroid.fdroid.net.DownloaderService;
 | 
			
		||||
 | 
			
		||||
@ -101,6 +101,8 @@ public class AppDetails extends AppCompatActivity {
 | 
			
		||||
    private static final String TAG = "AppDetails";
 | 
			
		||||
 | 
			
		||||
    private static final int REQUEST_ENABLE_BLUETOOTH = 2;
 | 
			
		||||
    private static final int REQUEST_PERMISSION_DIALOG = 3;
 | 
			
		||||
    private static final int REQUEST_UNINSTALL_DIALOG = 4;
 | 
			
		||||
 | 
			
		||||
    public static final String EXTRA_APPID = "appid";
 | 
			
		||||
    public static final String EXTRA_FROM = "from";
 | 
			
		||||
@ -319,7 +321,6 @@ public class AppDetails extends AppCompatActivity {
 | 
			
		||||
    private int startingIgnoreThis;
 | 
			
		||||
 | 
			
		||||
    private final Context context = this;
 | 
			
		||||
    private Installer installer;
 | 
			
		||||
 | 
			
		||||
    private AppDetailsHeaderFragment headerFragment;
 | 
			
		||||
 | 
			
		||||
@ -375,8 +376,6 @@ public class AppDetails extends AppCompatActivity {
 | 
			
		||||
 | 
			
		||||
        packageManager = getPackageManager();
 | 
			
		||||
 | 
			
		||||
        installer = Installer.getActivityInstaller(this, packageManager, myInstallerCallback);
 | 
			
		||||
 | 
			
		||||
        // Get the preferences we're going to use in this Activity...
 | 
			
		||||
        ConfigurationChangeHelper previousData = (ConfigurationChangeHelper) getLastCustomNonConfigurationInstance();
 | 
			
		||||
        if (previousData != null) {
 | 
			
		||||
@ -530,13 +529,12 @@ public class AppDetails extends AppCompatActivity {
 | 
			
		||||
    private final BroadcastReceiver completeReceiver = new BroadcastReceiver() {
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onReceive(Context context, Intent intent) {
 | 
			
		||||
            File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
 | 
			
		||||
            try {
 | 
			
		||||
                installer.installPackage(localFile, app.packageName, intent.getDataString());
 | 
			
		||||
            } catch (InstallFailedException e) {
 | 
			
		||||
                Log.e(TAG, "Android not compatible with this Installer!", e);
 | 
			
		||||
            }
 | 
			
		||||
            cleanUpFinishedDownload();
 | 
			
		||||
 | 
			
		||||
            Uri localUri =
 | 
			
		||||
                    Uri.fromFile(new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH)));
 | 
			
		||||
            localBroadcastManager.registerReceiver(installReceiver,
 | 
			
		||||
                    Installer.getInstallIntentFilter(localUri));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@ -555,6 +553,108 @@ public class AppDetails extends AppCompatActivity {
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private final BroadcastReceiver installReceiver = new BroadcastReceiver() {
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onReceive(Context context, Intent intent) {
 | 
			
		||||
            switch (intent.getAction()) {
 | 
			
		||||
                case Installer.ACTION_INSTALL_STARTED:
 | 
			
		||||
                    headerFragment.startProgress();
 | 
			
		||||
                    headerFragment.showIndeterminateProgress(getString(R.string.installing));
 | 
			
		||||
                    break;
 | 
			
		||||
                case Installer.ACTION_INSTALL_COMPLETE:
 | 
			
		||||
                    headerFragment.removeProgress();
 | 
			
		||||
 | 
			
		||||
                    localBroadcastManager.unregisterReceiver(this);
 | 
			
		||||
                    break;
 | 
			
		||||
                case Installer.ACTION_INSTALL_INTERRUPTED:
 | 
			
		||||
                    headerFragment.removeProgress();
 | 
			
		||||
                    onAppChanged();
 | 
			
		||||
 | 
			
		||||
                    String errorMessage =
 | 
			
		||||
                            intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
 | 
			
		||||
 | 
			
		||||
                    if (!TextUtils.isEmpty(errorMessage)) {
 | 
			
		||||
                        Log.e(TAG, "install aborted with errorMessage: " + errorMessage);
 | 
			
		||||
 | 
			
		||||
                        String title = String.format(
 | 
			
		||||
                                getString(R.string.install_error_notify_title),
 | 
			
		||||
                                app.name);
 | 
			
		||||
 | 
			
		||||
                        AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
 | 
			
		||||
                        alertBuilder.setTitle(title);
 | 
			
		||||
                        alertBuilder.setMessage(errorMessage);
 | 
			
		||||
                        alertBuilder.setNeutralButton(android.R.string.ok, null);
 | 
			
		||||
                        alertBuilder.create().show();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    localBroadcastManager.unregisterReceiver(this);
 | 
			
		||||
                    break;
 | 
			
		||||
                case Installer.ACTION_INSTALL_USER_INTERACTION:
 | 
			
		||||
                    PendingIntent installPendingIntent =
 | 
			
		||||
                            intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
 | 
			
		||||
 | 
			
		||||
                    try {
 | 
			
		||||
                        installPendingIntent.send();
 | 
			
		||||
                    } catch (PendingIntent.CanceledException e) {
 | 
			
		||||
                        Log.e(TAG, "PI canceled", e);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    throw new RuntimeException("intent action not handled!");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private final BroadcastReceiver uninstallReceiver = new BroadcastReceiver() {
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onReceive(Context context, Intent intent) {
 | 
			
		||||
            switch (intent.getAction()) {
 | 
			
		||||
                case Installer.ACTION_UNINSTALL_STARTED:
 | 
			
		||||
                    headerFragment.startProgress();
 | 
			
		||||
                    headerFragment.showIndeterminateProgress(getString(R.string.uninstalling));
 | 
			
		||||
                    break;
 | 
			
		||||
                case Installer.ACTION_UNINSTALL_COMPLETE:
 | 
			
		||||
                    headerFragment.removeProgress();
 | 
			
		||||
                    onAppChanged();
 | 
			
		||||
 | 
			
		||||
                    localBroadcastManager.unregisterReceiver(this);
 | 
			
		||||
                    break;
 | 
			
		||||
                case Installer.ACTION_UNINSTALL_INTERRUPTED:
 | 
			
		||||
                    headerFragment.removeProgress();
 | 
			
		||||
 | 
			
		||||
                    String errorMessage =
 | 
			
		||||
                            intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
 | 
			
		||||
 | 
			
		||||
                    if (!TextUtils.isEmpty(errorMessage)) {
 | 
			
		||||
                        Log.e(TAG, "uninstall aborted with errorMessage: " + errorMessage);
 | 
			
		||||
 | 
			
		||||
                        AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
 | 
			
		||||
                        alertBuilder.setTitle(R.string.uninstall_error_notify_title);
 | 
			
		||||
                        alertBuilder.setMessage(errorMessage);
 | 
			
		||||
                        alertBuilder.setNeutralButton(android.R.string.ok, null);
 | 
			
		||||
                        alertBuilder.create().show();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    localBroadcastManager.unregisterReceiver(this);
 | 
			
		||||
                    break;
 | 
			
		||||
                case Installer.ACTION_UNINSTALL_USER_INTERACTION:
 | 
			
		||||
                    PendingIntent uninstallPendingIntent =
 | 
			
		||||
                            intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
 | 
			
		||||
 | 
			
		||||
                    try {
 | 
			
		||||
                        uninstallPendingIntent.send();
 | 
			
		||||
                    } catch (PendingIntent.CanceledException e) {
 | 
			
		||||
                        Log.e(TAG, "PI canceled", e);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    throw new RuntimeException("intent action not handled!");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private void onAppChanged() {
 | 
			
		||||
        if (!reset(app.packageName)) {
 | 
			
		||||
            this.finish();
 | 
			
		||||
@ -796,7 +896,7 @@ public class AppDetails extends AppCompatActivity {
 | 
			
		||||
                return true;
 | 
			
		||||
 | 
			
		||||
            case UNINSTALL:
 | 
			
		||||
                removeApk(app.packageName);
 | 
			
		||||
                uninstallApk(app.packageName);
 | 
			
		||||
                return true;
 | 
			
		||||
 | 
			
		||||
            case IGNOREALL:
 | 
			
		||||
@ -875,76 +975,43 @@ public class AppDetails extends AppCompatActivity {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void initiateInstall(Apk apk) {
 | 
			
		||||
        Installer installer = InstallerFactory.create(this, apk.packageName);
 | 
			
		||||
        Intent intent = installer.getPermissionScreen(apk);
 | 
			
		||||
        if (intent != null) {
 | 
			
		||||
            // permission screen required
 | 
			
		||||
            Utils.debugLog(TAG, "permission screen required");
 | 
			
		||||
            startActivityForResult(intent, REQUEST_PERMISSION_DIALOG);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        startInstall(apk);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void startInstall(Apk apk) {
 | 
			
		||||
        activeDownloadUrlString = apk.getUrl();
 | 
			
		||||
        registerDownloaderReceivers();
 | 
			
		||||
        headerFragment.startProgress();
 | 
			
		||||
        InstallManagerService.queue(this, app, apk);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void removeApk(String packageName) {
 | 
			
		||||
        try {
 | 
			
		||||
            installer.deletePackage(packageName);
 | 
			
		||||
        } catch (InstallFailedException e) {
 | 
			
		||||
            Log.e(TAG, "Android not compatible with this Installer!", e);
 | 
			
		||||
    private void uninstallApk(String packageName) {
 | 
			
		||||
        Installer installer = InstallerFactory.create(this, packageName);
 | 
			
		||||
        Intent intent = installer.getUninstallScreen(packageName);
 | 
			
		||||
        if (intent != null) {
 | 
			
		||||
            // uninstall screen required
 | 
			
		||||
            Utils.debugLog(TAG, "screen screen required");
 | 
			
		||||
            startActivityForResult(intent, REQUEST_UNINSTALL_DIALOG);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        startUninstall();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private final Installer.InstallerCallback myInstallerCallback = new Installer.InstallerCallback() {
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onSuccess(final int operation) {
 | 
			
		||||
            runOnUiThread(new Runnable() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void run() {
 | 
			
		||||
                    if (operation == Installer.InstallerCallback.OPERATION_INSTALL) {
 | 
			
		||||
                        PackageManagerCompat.setInstaller(packageManager, app.packageName);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    onAppChanged();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onError(int operation, final int errorCode) {
 | 
			
		||||
            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();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    private void startUninstall() {
 | 
			
		||||
        localBroadcastManager.registerReceiver(uninstallReceiver,
 | 
			
		||||
                Installer.getUninstallIntentFilter(app.packageName));
 | 
			
		||||
        InstallerService.uninstall(context, app.packageName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void launchApk(String packageName) {
 | 
			
		||||
        Intent intent = packageManager.getLaunchIntentForPackage(packageName);
 | 
			
		||||
@ -963,15 +1030,22 @@ public class AppDetails extends AppCompatActivity {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
 | 
			
		||||
        // handle cases for install manager first
 | 
			
		||||
        if (installer.handleOnActivityResult(requestCode, resultCode, data)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        switch (requestCode) {
 | 
			
		||||
            case REQUEST_ENABLE_BLUETOOTH:
 | 
			
		||||
                fdroidApp.sendViaBluetooth(this, resultCode, app.packageName);
 | 
			
		||||
                break;
 | 
			
		||||
            case REQUEST_PERMISSION_DIALOG:
 | 
			
		||||
                if (resultCode == Activity.RESULT_OK) {
 | 
			
		||||
                    Uri uri = data.getData();
 | 
			
		||||
                    Apk apk = ApkProvider.Helper.find(this, uri, ApkProvider.DataColumns.ALL);
 | 
			
		||||
                    startInstall(apk);
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            case REQUEST_UNINSTALL_DIALOG:
 | 
			
		||||
                if (resultCode == Activity.RESULT_OK) {
 | 
			
		||||
                    startUninstall();
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1606,7 +1680,7 @@ public class AppDetails extends AppCompatActivity {
 | 
			
		||||
                        // If "launchable", launch
 | 
			
		||||
                        activity.launchApk(app.packageName);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        activity.removeApk(app.packageName);
 | 
			
		||||
                        activity.uninstallApk(app.packageName);
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (app.suggestedVersionCode > 0) {
 | 
			
		||||
                    // If not installed, install
 | 
			
		||||
@ -1635,7 +1709,7 @@ public class AppDetails extends AppCompatActivity {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        void remove() {
 | 
			
		||||
            appDetails.removeApk(appDetails.getApp().packageName);
 | 
			
		||||
            appDetails.uninstallApk(appDetails.getApp().packageName);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
 | 
			
		||||
@ -125,6 +125,23 @@ public class FDroidApp extends Application {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void applyDialogTheme(Activity activity) {
 | 
			
		||||
        activity.setTheme(getCurDialogThemeResId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static int getCurDialogThemeResId() {
 | 
			
		||||
        switch (curTheme) {
 | 
			
		||||
            case light:
 | 
			
		||||
                return R.style.MinWithDialogBaseThemeLight;
 | 
			
		||||
            case dark:
 | 
			
		||||
                return R.style.MinWithDialogBaseThemeDark;
 | 
			
		||||
            case night:
 | 
			
		||||
                return R.style.MinWithDialogBaseThemeDark;
 | 
			
		||||
            default:
 | 
			
		||||
                return R.style.MinWithDialogBaseThemeLight;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void enableSpongyCastle() {
 | 
			
		||||
        Security.addProvider(SPONGYCASTLE_PROVIDER);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -57,6 +57,7 @@ import java.security.cert.CertificateEncodingException;
 | 
			
		||||
import java.text.DateFormat;
 | 
			
		||||
import java.text.ParseException;
 | 
			
		||||
import java.text.SimpleDateFormat;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Date;
 | 
			
		||||
import java.util.Formatter;
 | 
			
		||||
import java.util.Iterator;
 | 
			
		||||
@ -270,7 +271,7 @@ public final class Utils {
 | 
			
		||||
     * This location is only for caching, do not install directly from this location
 | 
			
		||||
     * because if the file is on the External Storage, any other app could swap out
 | 
			
		||||
     * the APK while the install was in process, allowing malware to install things.
 | 
			
		||||
     * Using {@link org.fdroid.fdroid.installer.Installer#installPackage(File, String, String)}
 | 
			
		||||
     * Using {@link Installer#installPackage(File, String, String)}
 | 
			
		||||
     * is fine since that does the right thing.
 | 
			
		||||
     */
 | 
			
		||||
    public static File getApkCacheDir(Context context) {
 | 
			
		||||
@ -457,6 +458,19 @@ public final class Utils {
 | 
			
		||||
            return splitter.iterator();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public ArrayList<String> toArrayList() {
 | 
			
		||||
            ArrayList<String> out = new ArrayList<>();
 | 
			
		||||
            for (String element : this) {
 | 
			
		||||
                out.add(element);
 | 
			
		||||
            }
 | 
			
		||||
            return out;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public String[] toArray() {
 | 
			
		||||
            ArrayList<String> list = toArrayList();
 | 
			
		||||
            return list.toArray(new String[list.size()]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public boolean contains(String v) {
 | 
			
		||||
            for (final String s : this) {
 | 
			
		||||
                if (s.equals(v)) {
 | 
			
		||||
 | 
			
		||||
@ -107,8 +107,12 @@ public class ApkProvider extends FDroidProvider {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static Apk find(Context context, String packageName, int versionCode, String[] projection) {
 | 
			
		||||
            ContentResolver resolver = context.getContentResolver();
 | 
			
		||||
            final Uri uri = getContentUri(packageName, versionCode);
 | 
			
		||||
            return find(context, uri, projection);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static Apk find(Context context, Uri uri, String[] projection) {
 | 
			
		||||
            ContentResolver resolver = context.getContentResolver();
 | 
			
		||||
            Cursor cursor = resolver.query(uri, projection, null, null, null);
 | 
			
		||||
            Apk apk = null;
 | 
			
		||||
            if (cursor != null) {
 | 
			
		||||
 | 
			
		||||
@ -42,12 +42,12 @@ public class ApkSignatureVerifier {
 | 
			
		||||
 | 
			
		||||
    private static final String TAG = "ApkSignatureVerifier";
 | 
			
		||||
 | 
			
		||||
    private final Context mContext;
 | 
			
		||||
    private final PackageManager mPm;
 | 
			
		||||
    private final Context context;
 | 
			
		||||
    private final PackageManager pm;
 | 
			
		||||
 | 
			
		||||
    ApkSignatureVerifier(Context context) {
 | 
			
		||||
        mContext = context;
 | 
			
		||||
        mPm = context.getPackageManager();
 | 
			
		||||
        this.context = context;
 | 
			
		||||
        pm = context.getPackageManager();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean hasFDroidSignature(File apkFile) {
 | 
			
		||||
@ -66,7 +66,7 @@ public class ApkSignatureVerifier {
 | 
			
		||||
 | 
			
		||||
    private byte[] getApkSignature(File apkFile) {
 | 
			
		||||
        final String pkgPath = apkFile.getAbsolutePath();
 | 
			
		||||
        PackageInfo pkgInfo = mPm.getPackageArchiveInfo(pkgPath, PackageManager.GET_SIGNATURES);
 | 
			
		||||
        PackageInfo pkgInfo = pm.getPackageArchiveInfo(pkgPath, PackageManager.GET_SIGNATURES);
 | 
			
		||||
        return signatureToBytes(pkgInfo.signatures);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -74,7 +74,7 @@ public class ApkSignatureVerifier {
 | 
			
		||||
        try {
 | 
			
		||||
            // we do check the byte array of *all* signatures
 | 
			
		||||
            @SuppressLint("PackageManagerGetSignatures")
 | 
			
		||||
            PackageInfo pkgInfo = mPm.getPackageInfo(mContext.getPackageName(),
 | 
			
		||||
            PackageInfo pkgInfo = pm.getPackageInfo(context.getPackageName(),
 | 
			
		||||
                    PackageManager.GET_SIGNATURES);
 | 
			
		||||
            return signatureToBytes(pkgInfo.signatures);
 | 
			
		||||
        } catch (PackageManager.NameNotFoundException e) {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
 | 
			
		||||
 * Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software; you can redistribute it and/or
 | 
			
		||||
 * modify it under the terms of the GNU General Public License
 | 
			
		||||
@ -19,82 +19,82 @@
 | 
			
		||||
 | 
			
		||||
package org.fdroid.fdroid.installer;
 | 
			
		||||
 | 
			
		||||
import android.app.Activity;
 | 
			
		||||
import android.content.ActivityNotFoundException;
 | 
			
		||||
import android.app.PendingIntent;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.content.pm.PackageInfo;
 | 
			
		||||
import android.content.pm.PackageManager;
 | 
			
		||||
import android.net.Uri;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import org.fdroid.fdroid.Utils;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * For Android < 4: Default Installer using the public PackageManager API of
 | 
			
		||||
 * Android to install/delete packages. This starts a Activity from the Android
 | 
			
		||||
 * OS showing all permissions/changed permissions. The the user needs to
 | 
			
		||||
 * manually press an install button, this Installer cannot be used for
 | 
			
		||||
 * unattended installations.
 | 
			
		||||
 * 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.
 | 
			
		||||
 * <p/>
 | 
			
		||||
 * This is installer requires user interaction and thus install/uninstall directly
 | 
			
		||||
 * return PendingIntents.
 | 
			
		||||
 */
 | 
			
		||||
public class DefaultInstaller extends Installer {
 | 
			
		||||
    private final Activity mActivity;
 | 
			
		||||
 | 
			
		||||
    public DefaultInstaller(Activity activity, PackageManager pm, InstallerCallback callback)
 | 
			
		||||
            throws InstallFailedException {
 | 
			
		||||
        super(activity, pm, callback);
 | 
			
		||||
        this.mActivity = activity;
 | 
			
		||||
    private static final String TAG = "DefaultInstaller";
 | 
			
		||||
 | 
			
		||||
    DefaultInstaller(Context context) {
 | 
			
		||||
        super(context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final int REQUEST_CODE_INSTALL = 0;
 | 
			
		||||
    private static final int REQUEST_CODE_DELETE = 1;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void installPackageInternal(File apkFile) throws InstallFailedException {
 | 
			
		||||
        Intent intent = new Intent();
 | 
			
		||||
        intent.setAction(Intent.ACTION_VIEW);
 | 
			
		||||
        intent.setDataAndType(Uri.fromFile(apkFile),
 | 
			
		||||
                "application/vnd.android.package-archive");
 | 
			
		||||
    protected void installPackage(Uri uri, Uri originatingUri, String packageName) {
 | 
			
		||||
        sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED);
 | 
			
		||||
 | 
			
		||||
        Utils.debugLog(TAG, "DefaultInstaller uri: " + uri + " file: " + new File(uri.getPath()));
 | 
			
		||||
 | 
			
		||||
        Uri sanitizedUri;
 | 
			
		||||
        try {
 | 
			
		||||
            mActivity.startActivityForResult(intent, REQUEST_CODE_INSTALL);
 | 
			
		||||
        } catch (ActivityNotFoundException e) {
 | 
			
		||||
            throw new InstallFailedException(e);
 | 
			
		||||
            sanitizedUri = Installer.prepareApkFile(context, uri, packageName);
 | 
			
		||||
        } catch (Installer.InstallFailedException e) {
 | 
			
		||||
            Log.e(TAG, "prepareApkFile failed", e);
 | 
			
		||||
            sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
 | 
			
		||||
                    e.getMessage());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Intent installIntent = new Intent(context, DefaultInstallerActivity.class);
 | 
			
		||||
        installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE);
 | 
			
		||||
        installIntent.putExtra(DefaultInstallerActivity.EXTRA_ORIGINATING_URI, originatingUri);
 | 
			
		||||
        installIntent.setData(sanitizedUri);
 | 
			
		||||
 | 
			
		||||
        PendingIntent installPendingIntent = PendingIntent.getActivity(
 | 
			
		||||
                context.getApplicationContext(),
 | 
			
		||||
                uri.hashCode(),
 | 
			
		||||
                installIntent,
 | 
			
		||||
                PendingIntent.FLAG_UPDATE_CURRENT);
 | 
			
		||||
 | 
			
		||||
        sendBroadcastInstall(uri, originatingUri,
 | 
			
		||||
                Installer.ACTION_INSTALL_USER_INTERACTION, installPendingIntent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void deletePackageInternal(String packageName) throws InstallFailedException {
 | 
			
		||||
        try {
 | 
			
		||||
            PackageInfo pkgInfo = mPm.getPackageInfo(packageName, 0);
 | 
			
		||||
    protected void uninstallPackage(String packageName) {
 | 
			
		||||
        sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED);
 | 
			
		||||
 | 
			
		||||
            Uri uri = Uri.fromParts("package", pkgInfo.packageName, null);
 | 
			
		||||
            Intent intent = new Intent(Intent.ACTION_DELETE, uri);
 | 
			
		||||
            try {
 | 
			
		||||
                mActivity.startActivityForResult(intent, REQUEST_CODE_DELETE);
 | 
			
		||||
            } catch (ActivityNotFoundException e) {
 | 
			
		||||
                throw new InstallFailedException(e);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (PackageManager.NameNotFoundException e) {
 | 
			
		||||
            // already checked in super class
 | 
			
		||||
        }
 | 
			
		||||
        Intent uninstallIntent = new Intent(context, DefaultInstallerActivity.class);
 | 
			
		||||
        uninstallIntent.setAction(DefaultInstallerActivity.ACTION_UNINSTALL_PACKAGE);
 | 
			
		||||
        uninstallIntent.putExtra(
 | 
			
		||||
                DefaultInstallerActivity.EXTRA_UNINSTALL_PACKAGE_NAME, packageName);
 | 
			
		||||
        PendingIntent uninstallPendingIntent = PendingIntent.getActivity(
 | 
			
		||||
                context.getApplicationContext(),
 | 
			
		||||
                packageName.hashCode(),
 | 
			
		||||
                uninstallIntent,
 | 
			
		||||
                PendingIntent.FLAG_UPDATE_CURRENT);
 | 
			
		||||
 | 
			
		||||
        sendBroadcastUninstall(packageName,
 | 
			
		||||
                Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) {
 | 
			
		||||
        /**
 | 
			
		||||
         * resultCode is always 0 on Android < 4.0. See
 | 
			
		||||
         * com.android.packageinstaller.PackageInstallerActivity: setResult is
 | 
			
		||||
         * never executed on Androids before 4.0
 | 
			
		||||
         */
 | 
			
		||||
        switch (requestCode) {
 | 
			
		||||
            case REQUEST_CODE_INSTALL:
 | 
			
		||||
                mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL);
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            case REQUEST_CODE_DELETE:
 | 
			
		||||
                mCallback.onSuccess(InstallerCallback.OPERATION_DELETE);
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            default:
 | 
			
		||||
                return false;
 | 
			
		||||
        }
 | 
			
		||||
    protected boolean isUnattended() {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,241 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2014-2016 Dominik Schürmann <dominik@dominikschuermann.de>
 | 
			
		||||
 *
 | 
			
		||||
 * 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.annotation.SuppressLint;
 | 
			
		||||
import android.app.Activity;
 | 
			
		||||
import android.content.ActivityNotFoundException;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.content.pm.PackageManager;
 | 
			
		||||
import android.net.Uri;
 | 
			
		||||
import android.os.Build;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.support.v4.app.FragmentActivity;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import org.fdroid.fdroid.R;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A transparent activity as a wrapper around Android's PackageInstaller Intents
 | 
			
		||||
 */
 | 
			
		||||
public class DefaultInstallerActivity extends FragmentActivity {
 | 
			
		||||
    public static final String TAG = "AndroidInstallerAct";
 | 
			
		||||
 | 
			
		||||
    public static final String ACTION_INSTALL_PACKAGE = "org.fdroid.fdroid.INSTALL_PACKAGE";
 | 
			
		||||
    public static final String ACTION_UNINSTALL_PACKAGE = "org.fdroid.fdroid.UNINSTALL_PACKAGE";
 | 
			
		||||
 | 
			
		||||
    public static final String EXTRA_UNINSTALL_PACKAGE_NAME = "uninstallPackageName";
 | 
			
		||||
    public static final String EXTRA_ORIGINATING_URI = "originatingUri";
 | 
			
		||||
 | 
			
		||||
    private static final int REQUEST_CODE_INSTALL = 0;
 | 
			
		||||
    private static final int REQUEST_CODE_UNINSTALL = 1;
 | 
			
		||||
 | 
			
		||||
    private Uri installOriginatingUri;
 | 
			
		||||
    private Uri installUri;
 | 
			
		||||
 | 
			
		||||
    private String uninstallPackageName;
 | 
			
		||||
 | 
			
		||||
    // for the broadcasts
 | 
			
		||||
    private DefaultInstaller installer;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
        super.onCreate(savedInstanceState);
 | 
			
		||||
 | 
			
		||||
        installer = new DefaultInstaller(this);
 | 
			
		||||
 | 
			
		||||
        Intent intent = getIntent();
 | 
			
		||||
        String action = intent.getAction();
 | 
			
		||||
        if (ACTION_INSTALL_PACKAGE.equals(action)) {
 | 
			
		||||
            installUri = intent.getData();
 | 
			
		||||
            installOriginatingUri = intent.getParcelableExtra(EXTRA_ORIGINATING_URI);
 | 
			
		||||
 | 
			
		||||
            installPackage(installUri, installOriginatingUri);
 | 
			
		||||
        } else if (ACTION_UNINSTALL_PACKAGE.equals(action)) {
 | 
			
		||||
            uninstallPackageName = intent.getStringExtra(EXTRA_UNINSTALL_PACKAGE_NAME);
 | 
			
		||||
 | 
			
		||||
            uninstallPackage(uninstallPackageName);
 | 
			
		||||
        } else {
 | 
			
		||||
            throw new IllegalStateException("Intent action not specified!");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SuppressLint("InlinedApi")
 | 
			
		||||
    private void installPackage(Uri uri, Uri originatingUri) {
 | 
			
		||||
        if (uri == null) {
 | 
			
		||||
            throw new RuntimeException("Set the data uri to point to an apk location!");
 | 
			
		||||
        }
 | 
			
		||||
        // https://code.google.com/p/android/issues/detail?id=205827
 | 
			
		||||
        if ((Build.VERSION.SDK_INT <= Build.VERSION_CODES.M)
 | 
			
		||||
                && (!uri.getScheme().equals("file"))) {
 | 
			
		||||
            throw new RuntimeException("PackageInstaller <= Android 6 only supports file scheme!");
 | 
			
		||||
        }
 | 
			
		||||
        if (("N".equals(Build.VERSION.CODENAME))
 | 
			
		||||
                && (!uri.getScheme().equals("content"))) {
 | 
			
		||||
            throw new RuntimeException("PackageInstaller >= Android N only supports content scheme!");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Intent intent = new Intent();
 | 
			
		||||
        intent.setData(uri);
 | 
			
		||||
 | 
			
		||||
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
 | 
			
		||||
            intent.setAction(Intent.ACTION_VIEW);
 | 
			
		||||
            intent.setType("application/vnd.android.package-archive");
 | 
			
		||||
        } else {
 | 
			
		||||
            intent.setAction(Intent.ACTION_INSTALL_PACKAGE);
 | 
			
		||||
 | 
			
		||||
            // EXTRA_RETURN_RESULT throws a RuntimeException on N
 | 
			
		||||
            // https://gitlab.com/fdroid/fdroidclient/issues/631
 | 
			
		||||
            if (!"N".equals(Build.VERSION.CODENAME)) {
 | 
			
		||||
                intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // following extras only work when being installed as system-app
 | 
			
		||||
            // https://code.google.com/p/android/issues/detail?id=42253
 | 
			
		||||
            intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
 | 
			
		||||
 | 
			
		||||
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
 | 
			
		||||
                // deprecated in Android 4.1
 | 
			
		||||
                intent.putExtra(Intent.EXTRA_ALLOW_REPLACE, true);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            startActivityForResult(intent, REQUEST_CODE_INSTALL);
 | 
			
		||||
        } catch (ActivityNotFoundException e) {
 | 
			
		||||
            Log.e(TAG, "ActivityNotFoundException", e);
 | 
			
		||||
            installer.sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
 | 
			
		||||
                    "This Android rom does not support ACTION_INSTALL_PACKAGE!");
 | 
			
		||||
            finish();
 | 
			
		||||
        }
 | 
			
		||||
        installer.sendBroadcastInstall(installUri, installOriginatingUri,
 | 
			
		||||
                Installer.ACTION_INSTALL_STARTED);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void uninstallPackage(String packageName) {
 | 
			
		||||
        // check that the package is installed
 | 
			
		||||
        try {
 | 
			
		||||
            getPackageManager().getPackageInfo(packageName, 0);
 | 
			
		||||
        } catch (PackageManager.NameNotFoundException e) {
 | 
			
		||||
            Log.e(TAG, "NameNotFoundException", e);
 | 
			
		||||
            installer.sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_INTERRUPTED,
 | 
			
		||||
                    "Package that is scheduled for uninstall is not installed!");
 | 
			
		||||
            finish();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Uri uri = Uri.fromParts("package", packageName, null);
 | 
			
		||||
        Intent intent = new Intent();
 | 
			
		||||
        intent.setData(uri);
 | 
			
		||||
 | 
			
		||||
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
 | 
			
		||||
            intent.setAction(Intent.ACTION_DELETE);
 | 
			
		||||
        } else {
 | 
			
		||||
            intent.setAction(Intent.ACTION_UNINSTALL_PACKAGE);
 | 
			
		||||
            intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            startActivityForResult(intent, REQUEST_CODE_UNINSTALL);
 | 
			
		||||
        } catch (ActivityNotFoundException e) {
 | 
			
		||||
            Log.e(TAG, "ActivityNotFoundException", e);
 | 
			
		||||
            installer.sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_INTERRUPTED,
 | 
			
		||||
                    "This Android rom does not support ACTION_UNINSTALL_PACKAGE!");
 | 
			
		||||
            finish();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
 | 
			
		||||
        switch (requestCode) {
 | 
			
		||||
            case REQUEST_CODE_INSTALL:
 | 
			
		||||
                /**
 | 
			
		||||
                 * resultCode is always 0 on Android < 4.0. See
 | 
			
		||||
                 * com.android.packageinstaller.PackageInstallerActivity: setResult is
 | 
			
		||||
                 * never executed on Androids < 4.0
 | 
			
		||||
                 */
 | 
			
		||||
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
 | 
			
		||||
                    installer.sendBroadcastInstall(installUri, installOriginatingUri,
 | 
			
		||||
                            Installer.ACTION_INSTALL_COMPLETE);
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Fallback on N for https://gitlab.com/fdroid/fdroidclient/issues/631
 | 
			
		||||
                if ("N".equals(Build.VERSION.CODENAME)) {
 | 
			
		||||
                    installer.sendBroadcastInstall(installUri, installOriginatingUri,
 | 
			
		||||
                            Installer.ACTION_INSTALL_COMPLETE);
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                switch (resultCode) {
 | 
			
		||||
                    case Activity.RESULT_OK:
 | 
			
		||||
                        installer.sendBroadcastInstall(installUri, installOriginatingUri,
 | 
			
		||||
                                Installer.ACTION_INSTALL_COMPLETE);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case Activity.RESULT_CANCELED:
 | 
			
		||||
                        installer.sendBroadcastInstall(installUri, installOriginatingUri,
 | 
			
		||||
                                Installer.ACTION_INSTALL_INTERRUPTED);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case Activity.RESULT_FIRST_USER:
 | 
			
		||||
                    default:
 | 
			
		||||
                        // AOSP returns Activity.RESULT_FIRST_USER on error
 | 
			
		||||
                        installer.sendBroadcastInstall(installUri, installOriginatingUri,
 | 
			
		||||
                                Installer.ACTION_INSTALL_INTERRUPTED,
 | 
			
		||||
                                getString(R.string.install_error_unknown));
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                break;
 | 
			
		||||
            case REQUEST_CODE_UNINSTALL:
 | 
			
		||||
                // resultCode is always 0 on Android < 4.0.
 | 
			
		||||
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
 | 
			
		||||
                    installer.sendBroadcastUninstall(uninstallPackageName,
 | 
			
		||||
                            Installer.ACTION_UNINSTALL_COMPLETE);
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                switch (resultCode) {
 | 
			
		||||
                    case Activity.RESULT_OK:
 | 
			
		||||
                        installer.sendBroadcastUninstall(uninstallPackageName,
 | 
			
		||||
                                Installer.ACTION_UNINSTALL_COMPLETE);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case Activity.RESULT_CANCELED:
 | 
			
		||||
                        installer.sendBroadcastUninstall(uninstallPackageName,
 | 
			
		||||
                                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,
 | 
			
		||||
                                getString(R.string.uninstall_error_unknown));
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                throw new RuntimeException("Invalid request code!");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // after doing the broadcasts, finish this transparent wrapper activity
 | 
			
		||||
        finish();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,134 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
 | 
			
		||||
 *
 | 
			
		||||
 * 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.annotation.TargetApi;
 | 
			
		||||
import android.app.Activity;
 | 
			
		||||
import android.content.ActivityNotFoundException;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.content.pm.PackageInfo;
 | 
			
		||||
import android.content.pm.PackageManager;
 | 
			
		||||
import android.net.Uri;
 | 
			
		||||
import android.os.Build;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * For Android >= 4.0: Default Installer using the public PackageManager API of
 | 
			
		||||
 * Android to install/delete packages. This starts a Activity from the Android
 | 
			
		||||
 * OS showing all permissions/changed permissions. The the user needs to
 | 
			
		||||
 * manually press an install button, this Installer cannot be used for
 | 
			
		||||
 * unattended installations.
 | 
			
		||||
 */
 | 
			
		||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
 | 
			
		||||
public class DefaultSdk14Installer extends Installer {
 | 
			
		||||
    private final Activity mActivity;
 | 
			
		||||
 | 
			
		||||
    public DefaultSdk14Installer(Activity activity, PackageManager pm, InstallerCallback callback)
 | 
			
		||||
            throws InstallFailedException {
 | 
			
		||||
        super(activity, pm, callback);
 | 
			
		||||
        this.mActivity = activity;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final int REQUEST_CODE_INSTALL = 0;
 | 
			
		||||
    private static final int REQUEST_CODE_DELETE = 1;
 | 
			
		||||
 | 
			
		||||
    @SuppressWarnings("deprecation")
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void installPackageInternal(File apkFile) throws InstallFailedException {
 | 
			
		||||
        Intent intent = new Intent();
 | 
			
		||||
        intent.setAction(Intent.ACTION_INSTALL_PACKAGE);
 | 
			
		||||
        intent.setData(Uri.fromFile(apkFile));
 | 
			
		||||
        // EXTRA_RETURN_RESULT throws a RuntimeException on N
 | 
			
		||||
        // https://gitlab.com/fdroid/fdroidclient/issues/631
 | 
			
		||||
        if (!"N".equals(Build.VERSION.CODENAME)) {
 | 
			
		||||
            intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // following extras only work when being installed as system-app
 | 
			
		||||
        // https://code.google.com/p/android/issues/detail?id=42253
 | 
			
		||||
        intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true);
 | 
			
		||||
        if (Build.VERSION.SDK_INT < 16) {
 | 
			
		||||
            // deprecated in Android 4.1
 | 
			
		||||
            intent.putExtra(Intent.EXTRA_ALLOW_REPLACE, true);
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
            mActivity.startActivityForResult(intent, REQUEST_CODE_INSTALL);
 | 
			
		||||
        } catch (ActivityNotFoundException e) {
 | 
			
		||||
            throw new InstallFailedException(e);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void deletePackageInternal(String packageName) throws InstallFailedException {
 | 
			
		||||
        try {
 | 
			
		||||
            PackageInfo pkgInfo = mPm.getPackageInfo(packageName, 0);
 | 
			
		||||
 | 
			
		||||
            Uri uri = Uri.fromParts("package", pkgInfo.packageName, null);
 | 
			
		||||
            Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, uri);
 | 
			
		||||
            intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
 | 
			
		||||
            try {
 | 
			
		||||
                mActivity.startActivityForResult(intent, REQUEST_CODE_DELETE);
 | 
			
		||||
            } catch (ActivityNotFoundException e) {
 | 
			
		||||
                throw new InstallFailedException(e);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (PackageManager.NameNotFoundException e) {
 | 
			
		||||
            // already checked in super class
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) {
 | 
			
		||||
        switch (requestCode) {
 | 
			
		||||
            case REQUEST_CODE_INSTALL:
 | 
			
		||||
                if (resultCode == Activity.RESULT_OK) {
 | 
			
		||||
                    mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL);
 | 
			
		||||
                } else if (resultCode == Activity.RESULT_CANCELED) {
 | 
			
		||||
                    mCallback.onError(InstallerCallback.OPERATION_INSTALL,
 | 
			
		||||
                            InstallerCallback.ERROR_CODE_CANCELED);
 | 
			
		||||
                } else {
 | 
			
		||||
                    mCallback.onError(InstallerCallback.OPERATION_INSTALL,
 | 
			
		||||
                            InstallerCallback.ERROR_CODE_OTHER);
 | 
			
		||||
                }
 | 
			
		||||
                // Fallback on N for https://gitlab.com/fdroid/fdroidclient/issues/631
 | 
			
		||||
                if ("N".equals(Build.VERSION.CODENAME)) {
 | 
			
		||||
                    mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            case REQUEST_CODE_DELETE:
 | 
			
		||||
                if (resultCode == Activity.RESULT_OK) {
 | 
			
		||||
                    mCallback.onSuccess(InstallerCallback.OPERATION_DELETE);
 | 
			
		||||
                } else if (resultCode == Activity.RESULT_CANCELED) {
 | 
			
		||||
                    mCallback.onError(InstallerCallback.OPERATION_DELETE,
 | 
			
		||||
                            InstallerCallback.ERROR_CODE_CANCELED);
 | 
			
		||||
                } else {
 | 
			
		||||
                    // UninstallAppProgress actually returns
 | 
			
		||||
                    // Activity.RESULT_FIRST_USER if something breaks
 | 
			
		||||
                    mCallback.onError(InstallerCallback.OPERATION_DELETE,
 | 
			
		||||
                            InstallerCallback.ERROR_CODE_OTHER);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            default:
 | 
			
		||||
                return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,70 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
 | 
			
		||||
 *
 | 
			
		||||
 * 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.Activity;
 | 
			
		||||
import android.content.DialogInterface;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.support.annotation.Nullable;
 | 
			
		||||
import android.support.v4.app.FragmentActivity;
 | 
			
		||||
import android.support.v7.app.AlertDialog;
 | 
			
		||||
import android.view.ContextThemeWrapper;
 | 
			
		||||
 | 
			
		||||
import org.fdroid.fdroid.FDroidApp;
 | 
			
		||||
 | 
			
		||||
public class ErrorDialogActivity extends FragmentActivity {
 | 
			
		||||
 | 
			
		||||
    public static final String EXTRA_TITLE = "title";
 | 
			
		||||
    public static final String EXTRA_MESSAGE = "message";
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onCreate(@Nullable Bundle savedInstanceState) {
 | 
			
		||||
        super.onCreate(savedInstanceState);
 | 
			
		||||
 | 
			
		||||
        final Intent intent = getIntent();
 | 
			
		||||
        final String title = intent.getStringExtra(EXTRA_TITLE);
 | 
			
		||||
        final String message = intent.getStringExtra(EXTRA_MESSAGE);
 | 
			
		||||
 | 
			
		||||
        // hack to get theme applied (which is not automatically applied due to activity's Theme.NoDisplay
 | 
			
		||||
        ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId());
 | 
			
		||||
 | 
			
		||||
        final AlertDialog.Builder builder = new AlertDialog.Builder(theme);
 | 
			
		||||
        builder.setTitle(title);
 | 
			
		||||
        builder.setNeutralButton(android.R.string.ok,
 | 
			
		||||
                new DialogInterface.OnClickListener() {
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onClick(DialogInterface dialog, int which) {
 | 
			
		||||
                        setResult(Activity.RESULT_OK);
 | 
			
		||||
                        finish();
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
        builder.setOnCancelListener(
 | 
			
		||||
                new DialogInterface.OnCancelListener() {
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onCancel(DialogInterface dialog) {
 | 
			
		||||
                        setResult(Activity.RESULT_CANCELED);
 | 
			
		||||
                        finish();
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
        builder.setMessage(message);
 | 
			
		||||
        builder.create().show();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,108 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
 | 
			
		||||
 *
 | 
			
		||||
 * 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.PendingIntent;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.net.Uri;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import org.fdroid.fdroid.BuildConfig;
 | 
			
		||||
import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Special Installer that is only useful to install the Privileged Extension apk
 | 
			
		||||
 * as a privileged app into the system partition of Android.
 | 
			
		||||
 * <p/>
 | 
			
		||||
 * This is installer requires user interaction and thus install/uninstall directly
 | 
			
		||||
 * return PendingIntents.
 | 
			
		||||
 */
 | 
			
		||||
public class ExtensionInstaller extends Installer {
 | 
			
		||||
 | 
			
		||||
    private static final String TAG = "ExtensionInstaller";
 | 
			
		||||
 | 
			
		||||
    ExtensionInstaller(Context context) {
 | 
			
		||||
        super(context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void installPackage(Uri uri, Uri originatingUri, String packageName) {
 | 
			
		||||
        Uri sanitizedUri;
 | 
			
		||||
        try {
 | 
			
		||||
            sanitizedUri = Installer.prepareApkFile(context, uri, packageName);
 | 
			
		||||
        } catch (InstallFailedException e) {
 | 
			
		||||
            Log.e(TAG, "prepareApkFile failed", e);
 | 
			
		||||
            sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
 | 
			
		||||
                    e.getMessage());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 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(context);
 | 
			
		||||
        if (!BuildConfig.DEBUG && !signatureVerifier.hasFDroidSignature(new File(sanitizedUri.getPath()))) {
 | 
			
		||||
            sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
 | 
			
		||||
                    "APK signature of extension not correct!");
 | 
			
		||||
        }
 | 
			
		||||
        Intent installIntent = new Intent(context, InstallExtensionDialogActivity.class);
 | 
			
		||||
        installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL);
 | 
			
		||||
        installIntent.setData(sanitizedUri);
 | 
			
		||||
 | 
			
		||||
        PendingIntent installPendingIntent = PendingIntent.getActivity(
 | 
			
		||||
                context.getApplicationContext(),
 | 
			
		||||
                uri.hashCode(),
 | 
			
		||||
                installIntent,
 | 
			
		||||
                PendingIntent.FLAG_UPDATE_CURRENT);
 | 
			
		||||
 | 
			
		||||
        sendBroadcastInstall(uri, originatingUri,
 | 
			
		||||
                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
 | 
			
		||||
    protected void uninstallPackage(String packageName) {
 | 
			
		||||
        sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED);
 | 
			
		||||
 | 
			
		||||
        Intent uninstallIntent = new Intent(context, InstallExtensionDialogActivity.class);
 | 
			
		||||
        uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL);
 | 
			
		||||
 | 
			
		||||
        PendingIntent uninstallPendingIntent = PendingIntent.getActivity(
 | 
			
		||||
                context.getApplicationContext(),
 | 
			
		||||
                packageName.hashCode(),
 | 
			
		||||
                uninstallIntent,
 | 
			
		||||
                PendingIntent.FLAG_UPDATE_CURRENT);
 | 
			
		||||
 | 
			
		||||
        sendBroadcastUninstall(packageName,
 | 
			
		||||
                Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent);
 | 
			
		||||
 | 
			
		||||
        // don't use broadcasts for the rest of this special installer
 | 
			
		||||
        sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_COMPLETE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected boolean isUnattended() {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -19,6 +19,7 @@ import android.text.TextUtils;
 | 
			
		||||
import org.fdroid.fdroid.AppDetails;
 | 
			
		||||
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.net.Downloader;
 | 
			
		||||
@ -85,18 +86,6 @@ public class InstallManagerService extends Service {
 | 
			
		||||
     */
 | 
			
		||||
    private final HashMap<String, BroadcastReceiver[]> receivers = new HashMap<>(3);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the app name based on a {@code urlString} key. The app name needs
 | 
			
		||||
     * to be kept around for the final notification update, but {@link App}
 | 
			
		||||
     * and {@link Apk} instances have already removed by the time that final
 | 
			
		||||
     * notification update comes around.  Once there is a proper
 | 
			
		||||
     * {@code InstallerService} and its integrated here, this must go away,
 | 
			
		||||
     * since the {@link App} and {@link Apk} instances will be available.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * TODO <b>delete me once InstallerService exists</b>
 | 
			
		||||
     */
 | 
			
		||||
    private static final HashMap<String, String> TEMP_HACK_APP_NAMES = new HashMap<>(3);
 | 
			
		||||
 | 
			
		||||
    private LocalBroadcastManager localBroadcastManager;
 | 
			
		||||
    private NotificationManager notificationManager;
 | 
			
		||||
 | 
			
		||||
@ -180,7 +169,7 @@ public class InstallManagerService extends Service {
 | 
			
		||||
            sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath);
 | 
			
		||||
            sendBroadcast(intent.getData(), Downloader.ACTION_COMPLETE, apkFilePath);
 | 
			
		||||
        } else {
 | 
			
		||||
            Utils.debugLog(TAG, " delete and download again " + urlString + " " + apkFilePath);
 | 
			
		||||
            Utils.debugLog(TAG, "delete and download again " + urlString + " " + apkFilePath);
 | 
			
		||||
            apkFilePath.delete();
 | 
			
		||||
            DownloaderService.queue(this, urlString);
 | 
			
		||||
        }
 | 
			
		||||
@ -234,22 +223,26 @@ public class InstallManagerService extends Service {
 | 
			
		||||
        BroadcastReceiver completeReceiver = new BroadcastReceiver() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onReceive(Context context, Intent intent) {
 | 
			
		||||
                String urlString = intent.getDataString();
 | 
			
		||||
                // TODO these need to be removed based on whether they are fed to InstallerService or not
 | 
			
		||||
                Apk apk = removeFromActive(urlString);
 | 
			
		||||
                if (AppDetails.isAppVisible(apk.packageName)) {
 | 
			
		||||
                    cancelNotification(urlString);
 | 
			
		||||
                } else {
 | 
			
		||||
                    notifyDownloadComplete(urlString, apk);
 | 
			
		||||
                }
 | 
			
		||||
                unregisterDownloaderReceivers(urlString);
 | 
			
		||||
                // elsewhere called urlString
 | 
			
		||||
                Uri originatingUri = intent.getData();
 | 
			
		||||
                File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
 | 
			
		||||
                Uri localUri = Uri.fromFile(localFile);
 | 
			
		||||
 | 
			
		||||
                Utils.debugLog(TAG, "download completed of " + originatingUri
 | 
			
		||||
                        + " to " + localUri);
 | 
			
		||||
 | 
			
		||||
                unregisterDownloaderReceivers(intent.getDataString());
 | 
			
		||||
 | 
			
		||||
                registerInstallerReceivers(localUri);
 | 
			
		||||
                Apk apk = ACTIVE_APKS.get(originatingUri.toString());
 | 
			
		||||
                InstallerService.install(context, localUri, originatingUri, apk.packageName);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        BroadcastReceiver interruptedReceiver = new BroadcastReceiver() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onReceive(Context context, Intent intent) {
 | 
			
		||||
                String urlString = intent.getDataString();
 | 
			
		||||
                Apk apk = removeFromActive(urlString);
 | 
			
		||||
                removeFromActive(urlString);
 | 
			
		||||
                unregisterDownloaderReceivers(urlString);
 | 
			
		||||
                cancelNotification(urlString);
 | 
			
		||||
            }
 | 
			
		||||
@ -265,6 +258,70 @@ public class InstallManagerService extends Service {
 | 
			
		||||
        receivers.put(urlString, new BroadcastReceiver[]{
 | 
			
		||||
                startedReceiver, progressReceiver, completeReceiver, interruptedReceiver,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void registerInstallerReceivers(Uri uri) {
 | 
			
		||||
 | 
			
		||||
        BroadcastReceiver installReceiver = new BroadcastReceiver() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onReceive(Context context, Intent intent) {
 | 
			
		||||
                Uri originatingUri = intent.getParcelableExtra(Installer.EXTRA_ORIGINATING_URI);
 | 
			
		||||
 | 
			
		||||
                switch (intent.getAction()) {
 | 
			
		||||
                    case Installer.ACTION_INSTALL_STARTED:
 | 
			
		||||
                        // nothing to do
 | 
			
		||||
                        break;
 | 
			
		||||
                    case Installer.ACTION_INSTALL_COMPLETE:
 | 
			
		||||
                        Apk apkComplete = removeFromActive(originatingUri.toString());
 | 
			
		||||
 | 
			
		||||
                        PackageManagerCompat.setInstaller(getPackageManager(), apkComplete.packageName);
 | 
			
		||||
 | 
			
		||||
                        localBroadcastManager.unregisterReceiver(this);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case Installer.ACTION_INSTALL_INTERRUPTED:
 | 
			
		||||
                        String errorMessage =
 | 
			
		||||
                                intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
 | 
			
		||||
 | 
			
		||||
                        // show notification if app details is not visible
 | 
			
		||||
                        if (!TextUtils.isEmpty(errorMessage)) {
 | 
			
		||||
                            App app = getAppFromActive(originatingUri.toString());
 | 
			
		||||
                            String title = String.format(
 | 
			
		||||
                                    getString(R.string.install_error_notify_title),
 | 
			
		||||
                                    app.name);
 | 
			
		||||
 | 
			
		||||
                            // show notification if app details is not visible
 | 
			
		||||
                            if (AppDetails.isAppVisible(app.packageName)) {
 | 
			
		||||
                                cancelNotification(originatingUri.toString());
 | 
			
		||||
                            } else {
 | 
			
		||||
                                notifyError(originatingUri.toString(), title, errorMessage);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        localBroadcastManager.unregisterReceiver(this);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case Installer.ACTION_INSTALL_USER_INTERACTION:
 | 
			
		||||
                        PendingIntent installPendingIntent =
 | 
			
		||||
                                intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
 | 
			
		||||
 | 
			
		||||
                        Apk apkUserInteraction = getApkFromActive(originatingUri.toString());
 | 
			
		||||
                        // show notification if app details is not visible
 | 
			
		||||
                        if (AppDetails.isAppVisible(apkUserInteraction.packageName)) {
 | 
			
		||||
                            cancelNotification(originatingUri.toString());
 | 
			
		||||
                        } else {
 | 
			
		||||
                            notifyDownloadComplete(apkUserInteraction, originatingUri.toString(), installPendingIntent);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    default:
 | 
			
		||||
                        throw new RuntimeException("intent action not handled!");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        localBroadcastManager.registerReceiver(installReceiver,
 | 
			
		||||
                Installer.getInstallIntentFilter(uri));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private NotificationCompat.Builder createNotificationBuilder(String urlString, Apk apk) {
 | 
			
		||||
@ -273,7 +330,7 @@ public class InstallManagerService extends Service {
 | 
			
		||||
                .setAutoCancel(false)
 | 
			
		||||
                .setOngoing(true)
 | 
			
		||||
                .setContentIntent(getAppDetailsIntent(downloadUrlId, apk))
 | 
			
		||||
                .setContentTitle(getString(R.string.downloading_apk, getAppName(urlString, apk)))
 | 
			
		||||
                .setContentTitle(getString(R.string.downloading_apk, getAppName(apk)))
 | 
			
		||||
                .addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel),
 | 
			
		||||
                        DownloaderService.getCancelPendingIntent(this, urlString))
 | 
			
		||||
                .setSmallIcon(android.R.drawable.stat_sys_download)
 | 
			
		||||
@ -281,18 +338,8 @@ public class InstallManagerService extends Service {
 | 
			
		||||
                .setProgress(100, 0, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private String getAppName(String urlString, Apk apk) {
 | 
			
		||||
        App app = ACTIVE_APPS.get(apk.packageName);
 | 
			
		||||
        if (app == null || TextUtils.isEmpty(app.name)) {
 | 
			
		||||
            if (TEMP_HACK_APP_NAMES.containsKey(urlString)) {
 | 
			
		||||
                return TEMP_HACK_APP_NAMES.get(urlString);
 | 
			
		||||
            } else {
 | 
			
		||||
                // this is ugly, but its better than nothing as a failsafe
 | 
			
		||||
                return urlString;
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            return app.name;
 | 
			
		||||
        }
 | 
			
		||||
    private String getAppName(Apk apk) {
 | 
			
		||||
        return ACTIVE_APPS.get(apk.packageName).name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -319,14 +366,14 @@ public class InstallManagerService extends Service {
 | 
			
		||||
     * Removing the progress bar from a notification should cause the notification's content
 | 
			
		||||
     * text to return to normal size</a>
 | 
			
		||||
     */
 | 
			
		||||
    private void notifyDownloadComplete(String urlString, Apk apk) {
 | 
			
		||||
    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) {
 | 
			
		||||
            title = String.format(getString(R.string.tap_to_install_format), getAppName(urlString, apk));
 | 
			
		||||
            title = String.format(getString(R.string.tap_to_install_format), getAppName(apk));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        int downloadUrlId = urlString.hashCode();
 | 
			
		||||
@ -335,13 +382,38 @@ public class InstallManagerService extends Service {
 | 
			
		||||
                .setAutoCancel(true)
 | 
			
		||||
                .setOngoing(false)
 | 
			
		||||
                .setContentTitle(title)
 | 
			
		||||
                .setContentIntent(getAppDetailsIntent(downloadUrlId, apk))
 | 
			
		||||
                .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, String title, String text) {
 | 
			
		||||
        int downloadUrlId = urlString.hashCode();
 | 
			
		||||
 | 
			
		||||
        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()}
 | 
			
		||||
@ -354,7 +426,10 @@ public class InstallManagerService extends Service {
 | 
			
		||||
    private static void addToActive(String urlString, App app, Apk apk) {
 | 
			
		||||
        ACTIVE_APKS.put(urlString, apk);
 | 
			
		||||
        ACTIVE_APPS.put(app.packageName, app);
 | 
			
		||||
        TEMP_HACK_APP_NAMES.put(urlString, app.name);  // TODO delete me once InstallerService exists
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Apk getApkFromActive(String urlString) {
 | 
			
		||||
        return ACTIVE_APKS.get(urlString);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -364,6 +439,10 @@ public class InstallManagerService extends Service {
 | 
			
		||||
     * {@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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Apk removeFromActive(String urlString) {
 | 
			
		||||
        Apk apk = ACTIVE_APKS.remove(urlString);
 | 
			
		||||
        if (apk != null) {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
 | 
			
		||||
 * Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software; you can redistribute it and/or
 | 
			
		||||
 * modify it under the terms of the GNU General Public License
 | 
			
		||||
@ -19,24 +19,26 @@
 | 
			
		||||
 | 
			
		||||
package org.fdroid.fdroid.installer;
 | 
			
		||||
 | 
			
		||||
import android.app.Activity;
 | 
			
		||||
import android.app.NotificationManager;
 | 
			
		||||
import android.app.PendingIntent;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.content.IntentFilter;
 | 
			
		||||
import android.content.pm.PackageManager;
 | 
			
		||||
import android.net.Uri;
 | 
			
		||||
import android.os.PatternMatcher;
 | 
			
		||||
import android.support.v4.content.LocalBroadcastManager;
 | 
			
		||||
import android.text.TextUtils;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import org.apache.commons.io.FileUtils;
 | 
			
		||||
import org.fdroid.fdroid.AndroidXMLDecompress;
 | 
			
		||||
import org.fdroid.fdroid.BuildConfig;
 | 
			
		||||
import org.fdroid.fdroid.Hasher;
 | 
			
		||||
import org.fdroid.fdroid.Preferences;
 | 
			
		||||
import org.fdroid.fdroid.Utils;
 | 
			
		||||
import org.fdroid.fdroid.data.Apk;
 | 
			
		||||
import org.fdroid.fdroid.data.ApkProvider;
 | 
			
		||||
import org.fdroid.fdroid.data.SanitizedFile;
 | 
			
		||||
import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity;
 | 
			
		||||
import org.fdroid.fdroid.privileged.views.AppDiff;
 | 
			
		||||
import org.fdroid.fdroid.privileged.views.AppSecurityPermissions;
 | 
			
		||||
import org.fdroid.fdroid.privileged.views.InstallConfirmActivity;
 | 
			
		||||
import org.fdroid.fdroid.privileged.views.UninstallDialogActivity;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
@ -44,22 +46,32 @@ import java.security.NoSuchAlgorithmException;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Abstract Installer class. Also provides static methods to automatically
 | 
			
		||||
 * instantiate a working Installer based on F-Droids granted permissions.
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
public abstract class Installer {
 | 
			
		||||
    final Context mContext;
 | 
			
		||||
    final PackageManager mPm;
 | 
			
		||||
    final InstallerCallback mCallback;
 | 
			
		||||
    final Context context;
 | 
			
		||||
    final PackageManager pm;
 | 
			
		||||
    final LocalBroadcastManager localBroadcastManager;
 | 
			
		||||
 | 
			
		||||
    private static final String TAG = "Installer";
 | 
			
		||||
    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";
 | 
			
		||||
    public static final String ACTION_INSTALL_USER_INTERACTION = "org.fdroid.fdroid.installer.Installer.action.INSTALL_USER_INTERACTION";
 | 
			
		||||
 | 
			
		||||
    public static final String ACTION_UNINSTALL_STARTED = "org.fdroid.fdroid.installer.Installer.action.UNINSTALL_STARTED";
 | 
			
		||||
    public static final String ACTION_UNINSTALL_COMPLETE = "org.fdroid.fdroid.installer.Installer.action.UNINSTALL_COMPLETE";
 | 
			
		||||
    public static final String ACTION_UNINSTALL_INTERRUPTED = "org.fdroid.fdroid.installer.Installer.action.UNINSTALL_INTERRUPTED";
 | 
			
		||||
    public static final String ACTION_UNINSTALL_USER_INTERACTION = "org.fdroid.fdroid.installer.Installer.action.UNINSTALL_USER_INTERACTION";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This is thrown when an Installer is not compatible with the Android OS it
 | 
			
		||||
     * is running on. This could be due to a broken superuser in case of
 | 
			
		||||
     * RootInstaller or due to an incompatible Android version in case of
 | 
			
		||||
     * SystemPermissionInstaller
 | 
			
		||||
     * Same as http://developer.android.com/reference/android/content/Intent.html#EXTRA_ORIGINATING_URI
 | 
			
		||||
     * In InstallManagerService often called urlString
 | 
			
		||||
     */
 | 
			
		||||
    public static final String EXTRA_ORIGINATING_URI = "org.fdroid.fdroid.installer.Installer.extra.ORIGINATING_URI";
 | 
			
		||||
    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";
 | 
			
		||||
 | 
			
		||||
    public static class InstallFailedException extends Exception {
 | 
			
		||||
 | 
			
		||||
        private static final long serialVersionUID = -8343133906463328027L;
 | 
			
		||||
@ -73,116 +85,31 @@ public abstract class Installer {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Callback from Installer. NOTE: This callback can be in a different thread
 | 
			
		||||
     * than the UI thread
 | 
			
		||||
     */
 | 
			
		||||
    public interface InstallerCallback {
 | 
			
		||||
 | 
			
		||||
        int OPERATION_INSTALL = 1;
 | 
			
		||||
        int OPERATION_DELETE  = 2;
 | 
			
		||||
 | 
			
		||||
        // Avoid using [-1,1] as they may conflict with Activity.RESULT_*
 | 
			
		||||
        int ERROR_CODE_CANCELED     = 2;
 | 
			
		||||
        int ERROR_CODE_OTHER        = 3;
 | 
			
		||||
        int ERROR_CODE_CANNOT_PARSE = 4;
 | 
			
		||||
 | 
			
		||||
        void onSuccess(int operation);
 | 
			
		||||
 | 
			
		||||
        void onError(int operation, int errorCode);
 | 
			
		||||
    Installer(Context context) {
 | 
			
		||||
        this.context = context;
 | 
			
		||||
        this.pm = context.getPackageManager();
 | 
			
		||||
        localBroadcastManager = LocalBroadcastManager.getInstance(context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Installer(Context context, PackageManager pm, InstallerCallback callback)
 | 
			
		||||
    public static Uri prepareApkFile(Context context, Uri uri, String packageName)
 | 
			
		||||
            throws InstallFailedException {
 | 
			
		||||
        this.mContext = context;
 | 
			
		||||
        this.mPm = pm;
 | 
			
		||||
        this.mCallback = callback;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Installer getActivityInstaller(Activity activity, InstallerCallback callback) {
 | 
			
		||||
        return getActivityInstaller(activity, activity.getPackageManager(), callback);
 | 
			
		||||
    }
 | 
			
		||||
        File apkFile = new File(uri.getPath());
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new Installer for installing/deleting processes starting from
 | 
			
		||||
     * an Activity
 | 
			
		||||
     */
 | 
			
		||||
    public static Installer getActivityInstaller(Activity activity, PackageManager pm,
 | 
			
		||||
            InstallerCallback callback) {
 | 
			
		||||
 | 
			
		||||
        // system permissions and pref enabled -> SystemInstaller
 | 
			
		||||
        boolean isSystemInstallerEnabled = Preferences.get().isPrivilegedInstallerEnabled();
 | 
			
		||||
        if (isSystemInstallerEnabled) {
 | 
			
		||||
            if (PrivilegedInstaller.isExtensionInstalledCorrectly(activity)
 | 
			
		||||
                    == PrivilegedInstaller.IS_EXTENSION_INSTALLED_YES) {
 | 
			
		||||
                Utils.debugLog(TAG, "system permissions -> SystemInstaller");
 | 
			
		||||
 | 
			
		||||
                try {
 | 
			
		||||
                    return new PrivilegedInstaller(activity, pm, callback);
 | 
			
		||||
                } catch (InstallFailedException e) {
 | 
			
		||||
                    Log.e(TAG, "Android not compatible with SystemInstaller!", e);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                Log.e(TAG, "SystemInstaller is enabled in prefs, but system-perms are not granted!");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // else -> DefaultInstaller
 | 
			
		||||
        if (android.os.Build.VERSION.SDK_INT >= 14) {
 | 
			
		||||
            // Default installer on Android >= 4.0
 | 
			
		||||
            try {
 | 
			
		||||
                Utils.debugLog(TAG, "try default installer for android >= 14");
 | 
			
		||||
 | 
			
		||||
                return new DefaultSdk14Installer(activity, pm, callback);
 | 
			
		||||
            } catch (InstallFailedException e) {
 | 
			
		||||
                Log.e(TAG, "Android not compatible with DefaultInstallerSdk14!", e);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // Default installer on Android < 4.0 (android-14)
 | 
			
		||||
            try {
 | 
			
		||||
                Utils.debugLog(TAG, "try default installer for android < 14");
 | 
			
		||||
 | 
			
		||||
                return new DefaultInstaller(activity, pm, callback);
 | 
			
		||||
            } catch (InstallFailedException e) {
 | 
			
		||||
                Log.e(TAG, "Android not compatible with DefaultInstaller!", e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // this should not happen!
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks the APK file against the provided hash, returning whether it is a match.
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean verifyApkFile(File apkFile, String hash, String hashType)
 | 
			
		||||
            throws NoSuchAlgorithmException {
 | 
			
		||||
        if (!apkFile.exists()) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        Hasher hasher = new Hasher(hashType, apkFile);
 | 
			
		||||
        return hasher.match(hash);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This is the safe, single point of entry for submitting an APK file to be installed.
 | 
			
		||||
     */
 | 
			
		||||
    public void installPackage(File apkFile, String packageName, String urlString)
 | 
			
		||||
            throws InstallFailedException {
 | 
			
		||||
        SanitizedFile apkToInstall = null;
 | 
			
		||||
        SanitizedFile sanitizedApkFile = null;
 | 
			
		||||
        try {
 | 
			
		||||
            Map<String, Object> attributes = AndroidXMLDecompress.getManifestHeaderAttributes(apkFile.getAbsolutePath());
 | 
			
		||||
 | 
			
		||||
            /* This isn't really needed, but might as well since we have the data already */
 | 
			
		||||
            if (attributes.containsKey("packageName") && !TextUtils.equals(packageName, (String) attributes.get("packageName"))) {
 | 
			
		||||
                throw new InstallFailedException(apkFile + " has packageName that clashes with " + packageName);
 | 
			
		||||
                throw new InstallFailedException(uri + " has packageName that clashes with " + packageName);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!attributes.containsKey("versionCode")) {
 | 
			
		||||
                throw new InstallFailedException(apkFile + " is missing versionCode!");
 | 
			
		||||
                throw new InstallFailedException(uri + " is missing versionCode!");
 | 
			
		||||
            }
 | 
			
		||||
            int versionCode = (Integer) attributes.get("versionCode");
 | 
			
		||||
            Apk apk = ApkProvider.Helper.find(mContext, packageName, versionCode, new String[]{
 | 
			
		||||
            Apk apk = ApkProvider.Helper.find(context, packageName, versionCode, new String[]{
 | 
			
		||||
                    ApkProvider.DataColumns.HASH,
 | 
			
		||||
                    ApkProvider.DataColumns.HASH_TYPE,
 | 
			
		||||
            });
 | 
			
		||||
@ -190,50 +117,29 @@ public abstract class Installer {
 | 
			
		||||
             * of the app to prevent attacks based on other apps swapping the file
 | 
			
		||||
             * out during the install process. Most likely, apkFile was just downloaded,
 | 
			
		||||
             * so it should still be in the RAM disk cache */
 | 
			
		||||
            apkToInstall = SanitizedFile.knownSanitized(File.createTempFile("install-", ".apk", mContext.getFilesDir()));
 | 
			
		||||
            FileUtils.copyFile(apkFile, apkToInstall);
 | 
			
		||||
            if (!verifyApkFile(apkToInstall, apk.hash, apk.hashType)) {
 | 
			
		||||
            sanitizedApkFile = SanitizedFile.knownSanitized(File.createTempFile("install-", ".apk",
 | 
			
		||||
                    context.getFilesDir()));
 | 
			
		||||
            FileUtils.copyFile(apkFile, sanitizedApkFile);
 | 
			
		||||
            if (!verifyApkFile(sanitizedApkFile, apk.hash, apk.hashType)) {
 | 
			
		||||
                FileUtils.deleteQuietly(apkFile);
 | 
			
		||||
                throw new InstallFailedException(apkFile + " failed to verify!");
 | 
			
		||||
            }
 | 
			
		||||
            apkFile = null; // ensure this is not used now that its copied to apkToInstall
 | 
			
		||||
 | 
			
		||||
            // special case: F-Droid Privileged Extension
 | 
			
		||||
            if (packageName != null && packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) {
 | 
			
		||||
 | 
			
		||||
                // 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(apkToInstall)) {
 | 
			
		||||
                    throw new InstallFailedException("APK signature of extension not correct!");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Activity activity = (Activity) mContext;
 | 
			
		||||
                Intent installIntent = new Intent(activity, InstallExtensionDialogActivity.class);
 | 
			
		||||
                installIntent.setAction(InstallExtensionDialogActivity.ACTION_INSTALL);
 | 
			
		||||
                installIntent.putExtra(InstallExtensionDialogActivity.EXTRA_INSTALL_APK, apkToInstall.getAbsolutePath());
 | 
			
		||||
                activity.startActivity(installIntent);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Need the apk to be world readable, so that the installer is able to read it.
 | 
			
		||||
            // Note that saving it into external storage for the purpose of letting the installer
 | 
			
		||||
            // have access is insecure, because apps with permission to write to the external
 | 
			
		||||
            // storage can overwrite the app between F-Droid asking for it to be installed and
 | 
			
		||||
            // the installer actually installing it.
 | 
			
		||||
            apkToInstall.setReadable(true, false);
 | 
			
		||||
            installPackageInternal(apkToInstall);
 | 
			
		||||
            sanitizedApkFile.setReadable(true, false);
 | 
			
		||||
 | 
			
		||||
            NotificationManager nm = (NotificationManager)
 | 
			
		||||
                    mContext.getSystemService(Context.NOTIFICATION_SERVICE);
 | 
			
		||||
            nm.cancel(urlString.hashCode());
 | 
			
		||||
        } catch (NumberFormatException | NoSuchAlgorithmException | IOException e) {
 | 
			
		||||
        } catch (NumberFormatException | IOException | NoSuchAlgorithmException e) {
 | 
			
		||||
            throw new InstallFailedException(e);
 | 
			
		||||
        } catch (ClassCastException e) {
 | 
			
		||||
            throw new InstallFailedException("F-Droid Privileged can only be updated using an activity!");
 | 
			
		||||
        } finally {
 | 
			
		||||
            // 20 minutes the start of the install process, delete the file
 | 
			
		||||
            final File apkToDelete = apkToInstall;
 | 
			
		||||
            final File apkToDelete = sanitizedApkFile;
 | 
			
		||||
            new Thread() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void run() {
 | 
			
		||||
@ -248,41 +154,168 @@ public abstract class Installer {
 | 
			
		||||
                }
 | 
			
		||||
            }.start();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Uri.fromFile(sanitizedApkFile);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void deletePackage(String packageName) throws InstallFailedException {
 | 
			
		||||
        // check if package exists before proceeding...
 | 
			
		||||
        try {
 | 
			
		||||
            mPm.getPackageInfo(packageName, 0);
 | 
			
		||||
        } catch (PackageManager.NameNotFoundException e) {
 | 
			
		||||
            Log.e(TAG, "Couldn't find package " + packageName + " to delete.");
 | 
			
		||||
            return;
 | 
			
		||||
    /**
 | 
			
		||||
     * 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) {
 | 
			
		||||
        if (!isUnattended()) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // special case: F-Droid Privileged Extension
 | 
			
		||||
        if (packageName != null && packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) {
 | 
			
		||||
            Activity activity;
 | 
			
		||||
            try {
 | 
			
		||||
                activity = (Activity) mContext;
 | 
			
		||||
            } catch (ClassCastException e) {
 | 
			
		||||
                Utils.debugLog(TAG, "F-Droid Privileged can only be uninstalled using an activity!");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        int count = newPermissionCount(apk);
 | 
			
		||||
        if (count > 0) {
 | 
			
		||||
            Uri uri = ApkProvider.getContentUri(apk);
 | 
			
		||||
            Intent intent = new Intent(context, InstallConfirmActivity.class);
 | 
			
		||||
            intent.setData(uri);
 | 
			
		||||
 | 
			
		||||
            Intent uninstallIntent = new Intent(activity, InstallExtensionDialogActivity.class);
 | 
			
		||||
            uninstallIntent.setAction(InstallExtensionDialogActivity.ACTION_UNINSTALL);
 | 
			
		||||
            activity.startActivity(uninstallIntent);
 | 
			
		||||
            return;
 | 
			
		||||
            return intent;
 | 
			
		||||
        } else {
 | 
			
		||||
            // no permission screen needed!
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        deletePackageInternal(packageName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract void installPackageInternal(File apkFile)
 | 
			
		||||
            throws InstallFailedException;
 | 
			
		||||
    private int newPermissionCount(Apk apk) {
 | 
			
		||||
        // TODO: requires targetSdk in Apk class/database
 | 
			
		||||
        //boolean supportsRuntimePermissions = mPkgInfo.applicationInfo.targetSdkVersion
 | 
			
		||||
        //        >= Build.VERSION_CODES.M;
 | 
			
		||||
        //if (supportsRuntimePermissions) {
 | 
			
		||||
        //    return 0;
 | 
			
		||||
        //}
 | 
			
		||||
 | 
			
		||||
    protected abstract void deletePackageInternal(String packageName)
 | 
			
		||||
            throws InstallFailedException;
 | 
			
		||||
        AppDiff appDiff = new AppDiff(context.getPackageManager(), apk);
 | 
			
		||||
        if (appDiff.mPkgInfo == null) {
 | 
			
		||||
            // could not get diff because we couldn't parse the package
 | 
			
		||||
            throw new RuntimeException("cannot parse!");
 | 
			
		||||
        }
 | 
			
		||||
        AppSecurityPermissions perms = new AppSecurityPermissions(context, appDiff.mPkgInfo);
 | 
			
		||||
        if (appDiff.mInstalledAppInfo != null) {
 | 
			
		||||
            // update to an existing app
 | 
			
		||||
            return perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW);
 | 
			
		||||
        }
 | 
			
		||||
        // new app install
 | 
			
		||||
        return perms.getPermissionCount(AppSecurityPermissions.WHICH_ALL);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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) {
 | 
			
		||||
        if (!isUnattended()) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Intent intent = new Intent(context, UninstallDialogActivity.class);
 | 
			
		||||
        intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
 | 
			
		||||
 | 
			
		||||
        return intent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks the APK file against the provided hash, returning whether it is a match.
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean verifyApkFile(File apkFile, String hash, String hashType)
 | 
			
		||||
            throws NoSuchAlgorithmException {
 | 
			
		||||
        if (!apkFile.exists()) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        Hasher hasher = new Hasher(hashType, apkFile);
 | 
			
		||||
        return hasher.match(hash);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void sendBroadcastInstall(Uri uri, Uri originatingUri, String action,
 | 
			
		||||
                                     PendingIntent pendingIntent) {
 | 
			
		||||
        sendBroadcastInstall(uri, originatingUri, action, pendingIntent, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void sendBroadcastInstall(Uri uri, Uri originatingUri, String action) {
 | 
			
		||||
        sendBroadcastInstall(uri, originatingUri, action, null, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void sendBroadcastInstall(Uri uri, Uri originatingUri, String action, String errorMessage) {
 | 
			
		||||
        sendBroadcastInstall(uri, originatingUri, action, null, errorMessage);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void sendBroadcastInstall(Uri uri, Uri originatingUri, String action,
 | 
			
		||||
                                     PendingIntent pendingIntent, String errorMessage) {
 | 
			
		||||
        Intent intent = new Intent(action);
 | 
			
		||||
        intent.setData(uri);
 | 
			
		||||
        intent.putExtra(Installer.EXTRA_ORIGINATING_URI, originatingUri);
 | 
			
		||||
        intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent);
 | 
			
		||||
        if (!TextUtils.isEmpty(errorMessage)) {
 | 
			
		||||
            intent.putExtra(Installer.EXTRA_ERROR_MESSAGE, errorMessage);
 | 
			
		||||
        }
 | 
			
		||||
        localBroadcastManager.sendBroadcast(intent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void sendBroadcastUninstall(String packageName, String action, String errorMessage) {
 | 
			
		||||
        sendBroadcastUninstall(packageName, action, null, errorMessage);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void sendBroadcastUninstall(String packageName, String action) {
 | 
			
		||||
        sendBroadcastUninstall(packageName, action, null, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void sendBroadcastUninstall(String packageName, String action,
 | 
			
		||||
                                       PendingIntent pendingIntent) {
 | 
			
		||||
        sendBroadcastUninstall(packageName, action, pendingIntent, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void sendBroadcastUninstall(String packageName, String action,
 | 
			
		||||
                                       PendingIntent pendingIntent, String errorMessage) {
 | 
			
		||||
        Uri uri = Uri.fromParts("package", packageName, null);
 | 
			
		||||
 | 
			
		||||
        Intent intent = new Intent(action);
 | 
			
		||||
        intent.setData(uri); // for broadcast filtering
 | 
			
		||||
        intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
 | 
			
		||||
        intent.putExtra(Installer.EXTRA_USER_INTERACTION_PI, pendingIntent);
 | 
			
		||||
        if (!TextUtils.isEmpty(errorMessage)) {
 | 
			
		||||
            intent.putExtra(Installer.EXTRA_ERROR_MESSAGE, errorMessage);
 | 
			
		||||
        }
 | 
			
		||||
        localBroadcastManager.sendBroadcast(intent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static IntentFilter getInstallIntentFilter(Uri uri) {
 | 
			
		||||
        IntentFilter intentFilter = new IntentFilter();
 | 
			
		||||
        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.addDataScheme(uri.getScheme());
 | 
			
		||||
        intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL);
 | 
			
		||||
        return intentFilter;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static IntentFilter getUninstallIntentFilter(String packageName) {
 | 
			
		||||
        IntentFilter intentFilter = new IntentFilter();
 | 
			
		||||
        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);
 | 
			
		||||
        intentFilter.addDataScheme("package");
 | 
			
		||||
        intentFilter.addDataPath(packageName, PatternMatcher.PATTERN_LITERAL);
 | 
			
		||||
        return intentFilter;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract void installPackage(Uri uri, Uri originatingUri, String packageName);
 | 
			
		||||
 | 
			
		||||
    protected abstract void uninstallPackage(String packageName);
 | 
			
		||||
 | 
			
		||||
    protected abstract boolean isUnattended();
 | 
			
		||||
 | 
			
		||||
    public abstract boolean handleOnActivityResult(int requestCode, int resultCode, Intent data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,77 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
 | 
			
		||||
 *
 | 
			
		||||
 * 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.content.Context;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import org.fdroid.fdroid.Preferences;
 | 
			
		||||
import org.fdroid.fdroid.Utils;
 | 
			
		||||
 | 
			
		||||
public class InstallerFactory {
 | 
			
		||||
 | 
			
		||||
    private static final String TAG = "InstallerFactory";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an instance of an appropriate installer.
 | 
			
		||||
     * Either DefaultInstaller, PrivilegedInstaller, or in the special
 | 
			
		||||
     * case to install the "F-Droid Privileged Extension" ExtensionInstaller.
 | 
			
		||||
     *
 | 
			
		||||
     * @param context     current {@link Context}
 | 
			
		||||
     * @param packageName package name of apk to be installed. Required to select
 | 
			
		||||
     *                    the ExtensionInstaller.
 | 
			
		||||
     *                    If this is null, the ExtensionInstaller will never be returned.
 | 
			
		||||
     * @return instance of an Installer
 | 
			
		||||
     */
 | 
			
		||||
    public static Installer create(Context context, String packageName) {
 | 
			
		||||
        Installer installer;
 | 
			
		||||
 | 
			
		||||
        if (packageName != null
 | 
			
		||||
                && packageName.equals(PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME)) {
 | 
			
		||||
            // special case for "F-Droid Privileged Extension"
 | 
			
		||||
            installer = new ExtensionInstaller(context);
 | 
			
		||||
        } else if (isPrivilegedInstallerEnabled()) {
 | 
			
		||||
            if (PrivilegedInstaller.isExtensionInstalledCorrectly(context)
 | 
			
		||||
                    == PrivilegedInstaller.IS_EXTENSION_INSTALLED_YES) {
 | 
			
		||||
                Utils.debugLog(TAG, "privileged extension correctly installed -> PrivilegedInstaller");
 | 
			
		||||
 | 
			
		||||
                installer = new PrivilegedInstaller(context);
 | 
			
		||||
            } else {
 | 
			
		||||
                Log.e(TAG, "PrivilegedInstaller is enabled in prefs, but permissions are not granted!");
 | 
			
		||||
                // TODO: better error handling?
 | 
			
		||||
 | 
			
		||||
                // fallback to default installer
 | 
			
		||||
                installer = new DefaultInstaller(context);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            installer = new DefaultInstaller(context);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return installer;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Extension has privileged permissions and preference is enabled?
 | 
			
		||||
     */
 | 
			
		||||
    private static boolean isPrivilegedInstallerEnabled() {
 | 
			
		||||
        return Preferences.get().isPrivilegedInstallerEnabled();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,91 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
 | 
			
		||||
 *
 | 
			
		||||
 * 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.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.net.Uri;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This service handles the install process of apk files and
 | 
			
		||||
 * uninstall process of apps.
 | 
			
		||||
 * <p/>
 | 
			
		||||
 * This service is based on an IntentService because:
 | 
			
		||||
 * - no parallel installs/uninstalls should be allowed,
 | 
			
		||||
 * i.e., runs sequentially
 | 
			
		||||
 * - no cancel operation is needed. Cancelling an installation
 | 
			
		||||
 * would be the same as starting uninstall afterwards
 | 
			
		||||
 */
 | 
			
		||||
public class InstallerService extends IntentService {
 | 
			
		||||
 | 
			
		||||
    private static final String ACTION_INSTALL = "org.fdroid.fdroid.installer.InstallerService.action.INSTALL";
 | 
			
		||||
    private static final String ACTION_UNINSTALL = "org.fdroid.fdroid.installer.InstallerService.action.UNINSTALL";
 | 
			
		||||
 | 
			
		||||
    public InstallerService() {
 | 
			
		||||
        super("InstallerService");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onHandleIntent(Intent intent) {
 | 
			
		||||
        String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME);
 | 
			
		||||
        Installer installer = InstallerFactory.create(this, packageName);
 | 
			
		||||
 | 
			
		||||
        if (ACTION_INSTALL.equals(intent.getAction())) {
 | 
			
		||||
            Uri uri = intent.getData();
 | 
			
		||||
            Uri originatingUri = intent.getParcelableExtra(Installer.EXTRA_ORIGINATING_URI);
 | 
			
		||||
 | 
			
		||||
            installer.installPackage(uri, originatingUri, packageName);
 | 
			
		||||
        } else if (ACTION_UNINSTALL.equals(intent.getAction())) {
 | 
			
		||||
            installer.uninstallPackage(packageName);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Install an apk from {@link Uri}
 | 
			
		||||
     *
 | 
			
		||||
     * @param context        this app's {@link Context}
 | 
			
		||||
     * @param uri            {@link Uri} pointing to (downloaded) local apk file
 | 
			
		||||
     * @param originatingUri {@link Uri} where the apk has been downloaded from
 | 
			
		||||
     * @param packageName    package name of the app that should be installed
 | 
			
		||||
     */
 | 
			
		||||
    public static void install(Context context, Uri uri, Uri originatingUri, String packageName) {
 | 
			
		||||
        Intent intent = new Intent(context, InstallerService.class);
 | 
			
		||||
        intent.setAction(ACTION_INSTALL);
 | 
			
		||||
        intent.setData(uri);
 | 
			
		||||
        intent.putExtra(Installer.EXTRA_ORIGINATING_URI, originatingUri);
 | 
			
		||||
        intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
 | 
			
		||||
        context.startService(intent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Uninstall an app
 | 
			
		||||
     *
 | 
			
		||||
     * @param context     this app's {@link Context}
 | 
			
		||||
     * @param packageName package name of the app that will be uninstalled
 | 
			
		||||
     */
 | 
			
		||||
    public static void uninstall(Context context, String packageName) {
 | 
			
		||||
        Intent intent = new Intent(context, InstallerService.class);
 | 
			
		||||
        intent.setAction(ACTION_UNINSTALL);
 | 
			
		||||
        intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
 | 
			
		||||
        context.startService(intent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
 | 
			
		||||
 * Copyright (C) 2014-2016 Dominik Schürmann <dominik@dominikschuermann.de>
 | 
			
		||||
 * Copyright (C) 2015 Daniel Martí <mvdan@mvdan.cc>
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software; you can redistribute it and/or
 | 
			
		||||
@ -20,49 +20,39 @@
 | 
			
		||||
 | 
			
		||||
package org.fdroid.fdroid.installer;
 | 
			
		||||
 | 
			
		||||
import android.app.Activity;
 | 
			
		||||
import android.content.ComponentName;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.DialogInterface;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.content.ServiceConnection;
 | 
			
		||||
import android.content.pm.ApplicationInfo;
 | 
			
		||||
import android.content.pm.PackageManager;
 | 
			
		||||
import android.net.Uri;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.os.IBinder;
 | 
			
		||||
import android.os.RemoteException;
 | 
			
		||||
import android.support.v7.app.AlertDialog;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import org.fdroid.fdroid.R;
 | 
			
		||||
import org.fdroid.fdroid.Utils;
 | 
			
		||||
import org.fdroid.fdroid.privileged.IPrivilegedCallback;
 | 
			
		||||
import org.fdroid.fdroid.privileged.IPrivilegedService;
 | 
			
		||||
import org.fdroid.fdroid.privileged.views.AppDiff;
 | 
			
		||||
import org.fdroid.fdroid.privileged.views.AppSecurityPermissions;
 | 
			
		||||
import org.fdroid.fdroid.privileged.views.InstallConfirmActivity;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Installer based on using internal hidden APIs of the Android OS, which are
 | 
			
		||||
 * protected by the permissions
 | 
			
		||||
 * <ul>
 | 
			
		||||
 * <li>android.permission.INSTALL_PACKAGES</li>
 | 
			
		||||
 * <li>android.permission.DELETE_PACKAGES</li>
 | 
			
		||||
 * </ul>
 | 
			
		||||
 *
 | 
			
		||||
 * Installer that only works if the "F-Droid Privileged
 | 
			
		||||
 * Extension" is installed as a privileged app.
 | 
			
		||||
 * <p/>
 | 
			
		||||
 * "F-Droid Privileged Extension" provides a service that exposes
 | 
			
		||||
 * internal Android APIs for install/uninstall which are protected
 | 
			
		||||
 * by INSTALL_PACKAGES, DELETE_PACKAGES permissions.
 | 
			
		||||
 * Both permissions are protected by systemOrSignature (in newer versions:
 | 
			
		||||
 * system|signature) and only granted on F-Droid's install in the following
 | 
			
		||||
 * cases:
 | 
			
		||||
 * <ul>
 | 
			
		||||
 * <li>On all Android versions if F-Droid is pre-deployed as a
 | 
			
		||||
 * system-application with the Rom</li>
 | 
			
		||||
 * <li>On Android < 4.4 also when moved into /system/app/</li>
 | 
			
		||||
 * <li>On Android >= 4.4 also when moved into /system/priv-app/</li>
 | 
			
		||||
 * </ul>
 | 
			
		||||
 *
 | 
			
		||||
 * system|signature) and cannot be used directly by F-Droid.
 | 
			
		||||
 * <p/>
 | 
			
		||||
 * Instead, this installer binds to the service of
 | 
			
		||||
 * "F-Droid Privileged Extension" and then executes the appropriate methods
 | 
			
		||||
 * inside the privileged context of the privileged extension.
 | 
			
		||||
 * <p/>
 | 
			
		||||
 * This installer makes unattended installs/uninstalls possible.
 | 
			
		||||
 * Thus no PendingIntents are returned.
 | 
			
		||||
 * <p/>
 | 
			
		||||
 * Sources for Android 4.4 change:
 | 
			
		||||
 * https://groups.google.com/forum/#!msg/android-
 | 
			
		||||
 * security-discuss/r7uL_OEMU5c/LijNHvxeV80J
 | 
			
		||||
@ -76,19 +66,195 @@ public class PrivilegedInstaller extends Installer {
 | 
			
		||||
    private static final String PRIVILEGED_EXTENSION_SERVICE_INTENT = "org.fdroid.fdroid.privileged.IPrivilegedService";
 | 
			
		||||
    public static final String PRIVILEGED_EXTENSION_PACKAGE_NAME = "org.fdroid.fdroid.privileged";
 | 
			
		||||
 | 
			
		||||
    private final Activity mActivity;
 | 
			
		||||
 | 
			
		||||
    private static final int REQUEST_CONFIRM_PERMS = 0;
 | 
			
		||||
 | 
			
		||||
    public static final int IS_EXTENSION_INSTALLED_NO = 0;
 | 
			
		||||
    public static final int IS_EXTENSION_INSTALLED_YES = 1;
 | 
			
		||||
    public static final int IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM = 2;
 | 
			
		||||
    public static final int IS_EXTENSION_INSTALLED_PERMISSIONS_PROBLEM = 3;
 | 
			
		||||
 | 
			
		||||
    public PrivilegedInstaller(Activity activity, PackageManager pm,
 | 
			
		||||
                               InstallerCallback callback) throws InstallFailedException {
 | 
			
		||||
        super(activity, pm, callback);
 | 
			
		||||
        this.mActivity = activity;
 | 
			
		||||
    // From AOSP source code
 | 
			
		||||
    public static final int ACTION_INSTALL_REPLACE_EXISTING = 2;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Following return codes are copied from AOSP 5.1 source code
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_SUCCEEDED = 1;
 | 
			
		||||
    public static final int INSTALL_FAILED_ALREADY_EXISTS = -1;
 | 
			
		||||
    public static final int INSTALL_FAILED_INVALID_APK = -2;
 | 
			
		||||
    public static final int INSTALL_FAILED_INVALID_URI = -3;
 | 
			
		||||
    public static final int INSTALL_FAILED_INSUFFICIENT_STORAGE = -4;
 | 
			
		||||
    public static final int INSTALL_FAILED_DUPLICATE_PACKAGE = -5;
 | 
			
		||||
    public static final int INSTALL_FAILED_NO_SHARED_USER = -6;
 | 
			
		||||
    public static final int INSTALL_FAILED_UPDATE_INCOMPATIBLE = -7;
 | 
			
		||||
    public static final int INSTALL_FAILED_SHARED_USER_INCOMPATIBLE = -8;
 | 
			
		||||
    public static final int INSTALL_FAILED_MISSING_SHARED_LIBRARY = -9;
 | 
			
		||||
    public static final int INSTALL_FAILED_REPLACE_COULDNT_DELETE = -10;
 | 
			
		||||
    public static final int INSTALL_FAILED_DEXOPT = -11;
 | 
			
		||||
    public static final int INSTALL_FAILED_OLDER_SDK = -12;
 | 
			
		||||
    public static final int INSTALL_FAILED_CONFLICTING_PROVIDER = -13;
 | 
			
		||||
    public static final int INSTALL_FAILED_NEWER_SDK = -14;
 | 
			
		||||
    public static final int INSTALL_FAILED_TEST_ONLY = -15;
 | 
			
		||||
    public static final int INSTALL_FAILED_CPU_ABI_INCOMPATIBLE = -16;
 | 
			
		||||
    public static final int INSTALL_FAILED_MISSING_FEATURE = -17;
 | 
			
		||||
    public static final int INSTALL_FAILED_CONTAINER_ERROR = -18;
 | 
			
		||||
    public static final int INSTALL_FAILED_INVALID_INSTALL_LOCATION = -19;
 | 
			
		||||
    public static final int INSTALL_FAILED_MEDIA_UNAVAILABLE = -20;
 | 
			
		||||
    public static final int INSTALL_FAILED_VERIFICATION_TIMEOUT = -21;
 | 
			
		||||
    public static final int INSTALL_FAILED_VERIFICATION_FAILURE = -22;
 | 
			
		||||
    public static final int INSTALL_FAILED_PACKAGE_CHANGED = -23;
 | 
			
		||||
    public static final int INSTALL_FAILED_UID_CHANGED = -24;
 | 
			
		||||
    public static final int INSTALL_FAILED_VERSION_DOWNGRADE = -25;
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_NOT_APK = -100;
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_BAD_MANIFEST = -101;
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION = -102;
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_NO_CERTIFICATES = -103;
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES = -104;
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING = -105;
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME = -106;
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID = -107;
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_MANIFEST_MALFORMED = -108;
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_MANIFEST_EMPTY = -109;
 | 
			
		||||
    public static final int INSTALL_FAILED_INTERNAL_ERROR = -110;
 | 
			
		||||
    public static final int INSTALL_FAILED_USER_RESTRICTED = -111;
 | 
			
		||||
    public static final int INSTALL_FAILED_DUPLICATE_PERMISSION = -112;
 | 
			
		||||
    public static final int INSTALL_FAILED_NO_MATCHING_ABIS = -113;
 | 
			
		||||
    /**
 | 
			
		||||
     * Internal return code for NativeLibraryHelper methods to indicate that the package
 | 
			
		||||
     * being processed did not contain any native code. This is placed here only so that
 | 
			
		||||
     * it can belong to the same value space as the other install failure codes.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int NO_NATIVE_LIBRARIES = -114;
 | 
			
		||||
    public static final int INSTALL_FAILED_ABORTED = -115;
 | 
			
		||||
 | 
			
		||||
    private static final HashMap<Integer, String> INSTALL_RETURN_CODES;
 | 
			
		||||
 | 
			
		||||
    static {
 | 
			
		||||
        // Descriptions extracted from the source code comments in AOSP
 | 
			
		||||
        INSTALL_RETURN_CODES = new HashMap<>();
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_SUCCEEDED,
 | 
			
		||||
                "Success");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_ALREADY_EXISTS,
 | 
			
		||||
                "Package is already installed.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_INVALID_APK,
 | 
			
		||||
                "The package archive file is invalid.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_INVALID_URI,
 | 
			
		||||
                "The URI passed in is invalid.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_INSUFFICIENT_STORAGE,
 | 
			
		||||
                "The package manager service found that the device didn't have enough " +
 | 
			
		||||
                        "storage space to install the app.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_DUPLICATE_PACKAGE,
 | 
			
		||||
                "A package is already installed with the same name.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_NO_SHARED_USER,
 | 
			
		||||
                "The requested shared user does not exist.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
 | 
			
		||||
                "A previously installed package of the same name has a different signature than " +
 | 
			
		||||
                        "the new package (and the old package's data was not removed).");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_SHARED_USER_INCOMPATIBLE,
 | 
			
		||||
                "The new package is requested a shared user which is already installed on " +
 | 
			
		||||
                        "the device and does not have matching signature.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_MISSING_SHARED_LIBRARY,
 | 
			
		||||
                "The new package uses a shared library that is not available.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_REPLACE_COULDNT_DELETE,
 | 
			
		||||
                "Unknown"); // wrong comment in source
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_DEXOPT,
 | 
			
		||||
                "The package failed while optimizing and validating its dex files, either " +
 | 
			
		||||
                        "because there was not enough storage or the validation failed.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_OLDER_SDK,
 | 
			
		||||
                "The new package failed because the current SDK version is older than that " +
 | 
			
		||||
                        "required by the package.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_CONFLICTING_PROVIDER,
 | 
			
		||||
                "The new package failed because it contains a content provider with the same " +
 | 
			
		||||
                        "authority as a provider already installed in the system.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_NEWER_SDK,
 | 
			
		||||
                "The new package failed because the current SDK version is newer than that " +
 | 
			
		||||
                        "required by the package.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_TEST_ONLY,
 | 
			
		||||
                "The new package failed because it has specified that it is a test-only package " +
 | 
			
		||||
                        "and the caller has not supplied the {@link #INSTALL_ALLOW_TEST} flag.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_CPU_ABI_INCOMPATIBLE,
 | 
			
		||||
                "The package being installed contains native code, but none that is compatible " +
 | 
			
		||||
                        "with the device's CPU_ABI.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_MISSING_FEATURE,
 | 
			
		||||
                "The new package uses a feature that is not available.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_CONTAINER_ERROR,
 | 
			
		||||
                "A secure container mount point couldn't be accessed on external media.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_INVALID_INSTALL_LOCATION,
 | 
			
		||||
                "The new package couldn't be installed in the specified install location.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_MEDIA_UNAVAILABLE,
 | 
			
		||||
                "The new package couldn't be installed in the specified install location " +
 | 
			
		||||
                        "because the media is not available.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_VERIFICATION_TIMEOUT,
 | 
			
		||||
                "The new package couldn't be installed because the verification timed out.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_VERIFICATION_FAILURE,
 | 
			
		||||
                "The new package couldn't be installed because the verification did not succeed.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_PACKAGE_CHANGED,
 | 
			
		||||
                "The package changed from what the calling program expected.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_UID_CHANGED,
 | 
			
		||||
                "The new package is assigned a different UID than it previously held.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_VERSION_DOWNGRADE,
 | 
			
		||||
                "The new package has an older version code than the currently installed package.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_NOT_APK,
 | 
			
		||||
                "The parser was given a path that is not a file, or does not end with the " +
 | 
			
		||||
                        "expected '.apk' extension.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_BAD_MANIFEST,
 | 
			
		||||
                "the parser was unable to retrieve the AndroidManifest.xml file.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
 | 
			
		||||
                "The parser encountered an unexpected exception.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
 | 
			
		||||
                "The parser did not find any certificates in the .apk.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
 | 
			
		||||
                "The parser found inconsistent certificates on the files in the .apk.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
 | 
			
		||||
                "The parser encountered a CertificateEncodingException in one of the files in " +
 | 
			
		||||
                        "the .apk.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME,
 | 
			
		||||
                "The parser encountered a bad or missing package name in the manifest.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID,
 | 
			
		||||
                "The parser encountered a bad shared user id name in the manifest.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,
 | 
			
		||||
                "The parser encountered some structural problem in the manifest.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_PARSE_FAILED_MANIFEST_EMPTY,
 | 
			
		||||
                "The parser did not find any actionable tags (instrumentation or application) " +
 | 
			
		||||
                        "in the manifest.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_INTERNAL_ERROR,
 | 
			
		||||
                "The system failed to install the package because of system issues.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_USER_RESTRICTED,
 | 
			
		||||
                "The system failed to install the package because the user is restricted from " +
 | 
			
		||||
                        "installing apps.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_DUPLICATE_PERMISSION,
 | 
			
		||||
                "The system failed to install the package because it is attempting to define a " +
 | 
			
		||||
                        "permission that is already defined by some existing package.");
 | 
			
		||||
        INSTALL_RETURN_CODES.put(INSTALL_FAILED_NO_MATCHING_ABIS,
 | 
			
		||||
                "The system failed to install the package because its packaged native code did " +
 | 
			
		||||
                        "not match any of the ABIs supported by the system.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static final int DELETE_SUCCEEDED = 1;
 | 
			
		||||
    public static final int DELETE_FAILED_INTERNAL_ERROR = -1;
 | 
			
		||||
    public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2;
 | 
			
		||||
    public static final int DELETE_FAILED_USER_RESTRICTED = -3;
 | 
			
		||||
    public static final int DELETE_FAILED_OWNER_BLOCKED = -4;
 | 
			
		||||
    public static final int DELETE_FAILED_ABORTED = -5;
 | 
			
		||||
 | 
			
		||||
    private static final HashMap<Integer, String> UNINSTALL_RETURN_CODES;
 | 
			
		||||
 | 
			
		||||
    static {
 | 
			
		||||
        // Descriptions extracted from the source code comments in AOSP
 | 
			
		||||
        UNINSTALL_RETURN_CODES = new HashMap<>();
 | 
			
		||||
        UNINSTALL_RETURN_CODES.put(DELETE_SUCCEEDED,
 | 
			
		||||
                "Success");
 | 
			
		||||
        UNINSTALL_RETURN_CODES.put(DELETE_FAILED_INTERNAL_ERROR,
 | 
			
		||||
                " the system failed to delete the package for an unspecified reason.");
 | 
			
		||||
        UNINSTALL_RETURN_CODES.put(DELETE_FAILED_DEVICE_POLICY_MANAGER,
 | 
			
		||||
                "the system failed to delete the package because it is the active " +
 | 
			
		||||
                        "DevicePolicy manager.");
 | 
			
		||||
        UNINSTALL_RETURN_CODES.put(DELETE_FAILED_USER_RESTRICTED,
 | 
			
		||||
                "the system failed to delete the package since the user is restricted.");
 | 
			
		||||
        UNINSTALL_RETURN_CODES.put(DELETE_FAILED_OWNER_BLOCKED,
 | 
			
		||||
                "the system failed to delete the package because a profile or " +
 | 
			
		||||
                        "device owner has marked the package as uninstallable.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public PrivilegedInstaller(Context context) {
 | 
			
		||||
        super(context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static boolean isExtensionInstalled(Context context) {
 | 
			
		||||
@ -102,29 +268,14 @@ public class PrivilegedInstaller extends Installer {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static int isExtensionInstalledCorrectly(Context context) {
 | 
			
		||||
 | 
			
		||||
        // check if installed
 | 
			
		||||
        if (!isExtensionInstalled(context)) {
 | 
			
		||||
            Log.e(TAG, "IS_EXTENSION_INSTALLED_NO");
 | 
			
		||||
            return IS_EXTENSION_INSTALLED_NO;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // check if it has the privileged permissions granted
 | 
			
		||||
        final Object mutex = new Object();
 | 
			
		||||
        final Bundle returnBundle = new Bundle();
 | 
			
		||||
        ServiceConnection mServiceConnection = new ServiceConnection() {
 | 
			
		||||
        ServiceConnection serviceConnection = new ServiceConnection() {
 | 
			
		||||
            public void onServiceConnected(ComponentName name, IBinder service) {
 | 
			
		||||
                IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
 | 
			
		||||
 | 
			
		||||
                try {
 | 
			
		||||
                    boolean hasPermissions = privService.hasPrivilegedPermissions();
 | 
			
		||||
                    returnBundle.putBoolean("has_permissions", hasPermissions);
 | 
			
		||||
                } catch (RemoteException e) {
 | 
			
		||||
                    Log.e(TAG, "RemoteException", e);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                synchronized (mutex) {
 | 
			
		||||
                    mutex.notify();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public void onServiceDisconnected(ComponentName name) {
 | 
			
		||||
@ -133,68 +284,32 @@ public class PrivilegedInstaller extends Installer {
 | 
			
		||||
        Intent serviceIntent = new Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT);
 | 
			
		||||
        serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME);
 | 
			
		||||
 | 
			
		||||
        // try to connect to check for signature
 | 
			
		||||
        try {
 | 
			
		||||
            context.getApplicationContext().bindService(serviceIntent, mServiceConnection,
 | 
			
		||||
            context.getApplicationContext().bindService(serviceIntent, serviceConnection,
 | 
			
		||||
                    Context.BIND_AUTO_CREATE);
 | 
			
		||||
        } catch (SecurityException e) {
 | 
			
		||||
            Log.e(TAG, "IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM", e);
 | 
			
		||||
            return IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        synchronized (mutex) {
 | 
			
		||||
            try {
 | 
			
		||||
                mutex.wait(3000);
 | 
			
		||||
            } catch (InterruptedException e) {
 | 
			
		||||
                // don't care
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        boolean hasPermissions = returnBundle.getBoolean("has_permissions", false);
 | 
			
		||||
        if (!hasPermissions) {
 | 
			
		||||
            return IS_EXTENSION_INSTALLED_PERMISSIONS_PROBLEM;
 | 
			
		||||
        }
 | 
			
		||||
        return IS_EXTENSION_INSTALLED_YES;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void installPackageInternal(File apkFile) throws InstallFailedException {
 | 
			
		||||
        Uri packageUri = Uri.fromFile(apkFile);
 | 
			
		||||
        int count = newPermissionCount(packageUri);
 | 
			
		||||
        if (count < 0) {
 | 
			
		||||
            mCallback.onError(InstallerCallback.OPERATION_INSTALL,
 | 
			
		||||
                    InstallerCallback.ERROR_CODE_CANNOT_PARSE);
 | 
			
		||||
    protected void installPackage(final Uri uri, final Uri originatingUri, String packageName) {
 | 
			
		||||
        sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED);
 | 
			
		||||
 | 
			
		||||
        final Uri sanitizedUri;
 | 
			
		||||
        try {
 | 
			
		||||
            sanitizedUri = Installer.prepareApkFile(context, uri, packageName);
 | 
			
		||||
        } catch (Installer.InstallFailedException e) {
 | 
			
		||||
            Log.e(TAG, "prepareApkFile failed", e);
 | 
			
		||||
            sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_INTERRUPTED,
 | 
			
		||||
                    e.getMessage());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (count > 0) {
 | 
			
		||||
            Intent intent = new Intent(mContext, InstallConfirmActivity.class);
 | 
			
		||||
            intent.setData(packageUri);
 | 
			
		||||
            mActivity.startActivityForResult(intent, REQUEST_CONFIRM_PERMS);
 | 
			
		||||
        } else {
 | 
			
		||||
            try {
 | 
			
		||||
                doInstallPackageInternal(packageUri);
 | 
			
		||||
            } catch (InstallFailedException e) {
 | 
			
		||||
                mCallback.onError(InstallerCallback.OPERATION_INSTALL,
 | 
			
		||||
                        InstallerCallback.ERROR_CODE_OTHER);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private int newPermissionCount(Uri packageUri) {
 | 
			
		||||
        AppDiff appDiff = new AppDiff(mContext.getPackageManager(), packageUri);
 | 
			
		||||
        if (appDiff.mPkgInfo == null) {
 | 
			
		||||
            // could not get diff because we couldn't parse the package
 | 
			
		||||
            return -1;
 | 
			
		||||
        }
 | 
			
		||||
        AppSecurityPermissions perms = new AppSecurityPermissions(mContext, appDiff.mPkgInfo);
 | 
			
		||||
        if (appDiff.mInstalledAppInfo != null) {
 | 
			
		||||
            // update to an existing app
 | 
			
		||||
            return perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW);
 | 
			
		||||
        }
 | 
			
		||||
        // default: even if there aren't any permissions, we want to make the
 | 
			
		||||
        // user always confirm installing new apps
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void doInstallPackageInternal(final Uri packageURI) throws InstallFailedException {
 | 
			
		||||
        ServiceConnection mServiceConnection = new ServiceConnection() {
 | 
			
		||||
            public void onServiceConnected(ComponentName name, IBinder service) {
 | 
			
		||||
                IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
 | 
			
		||||
@ -202,22 +317,30 @@ public class PrivilegedInstaller extends Installer {
 | 
			
		||||
                IPrivilegedCallback callback = new IPrivilegedCallback.Stub() {
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void handleResult(String packageName, int returnCode) throws RemoteException {
 | 
			
		||||
                        // TODO: propagate other return codes?
 | 
			
		||||
                        if (returnCode == INSTALL_SUCCEEDED) {
 | 
			
		||||
                            Utils.debugLog(TAG, "Install succeeded");
 | 
			
		||||
                            mCallback.onSuccess(InstallerCallback.OPERATION_INSTALL);
 | 
			
		||||
                            sendBroadcastInstall(uri, originatingUri, ACTION_INSTALL_COMPLETE);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Log.e(TAG, "Install failed with returnCode " + returnCode);
 | 
			
		||||
                            mCallback.onError(InstallerCallback.OPERATION_INSTALL,
 | 
			
		||||
                                    InstallerCallback.ERROR_CODE_OTHER);
 | 
			
		||||
                            sendBroadcastInstall(uri, originatingUri, ACTION_INSTALL_INTERRUPTED,
 | 
			
		||||
                                    "Error " + returnCode + ": "
 | 
			
		||||
                                            + INSTALL_RETURN_CODES.get(returnCode));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                try {
 | 
			
		||||
                    privService.installPackage(packageURI, INSTALL_REPLACE_EXISTING, null, callback);
 | 
			
		||||
                    boolean hasPermissions = privService.hasPrivilegedPermissions();
 | 
			
		||||
                    if (!hasPermissions) {
 | 
			
		||||
                        sendBroadcastInstall(uri, originatingUri, ACTION_INSTALL_INTERRUPTED,
 | 
			
		||||
                                context.getString(R.string.system_install_denied_permissions));
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    privService.installPackage(sanitizedUri, ACTION_INSTALL_REPLACE_EXISTING,
 | 
			
		||||
                            null, callback);
 | 
			
		||||
                } catch (RemoteException e) {
 | 
			
		||||
                    Log.e(TAG, "RemoteException", e);
 | 
			
		||||
                    sendBroadcastInstall(uri, originatingUri, ACTION_INSTALL_INTERRUPTED,
 | 
			
		||||
                            "connecting to privileged service failed");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -227,69 +350,14 @@ public class PrivilegedInstaller extends Installer {
 | 
			
		||||
 | 
			
		||||
        Intent serviceIntent = new Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT);
 | 
			
		||||
        serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME);
 | 
			
		||||
        mContext.getApplicationContext().bindService(serviceIntent, mServiceConnection,
 | 
			
		||||
        context.getApplicationContext().bindService(serviceIntent, mServiceConnection,
 | 
			
		||||
                Context.BIND_AUTO_CREATE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void deletePackageInternal(final String packageName)
 | 
			
		||||
            throws InstallFailedException {
 | 
			
		||||
        ApplicationInfo appInfo;
 | 
			
		||||
        try {
 | 
			
		||||
            //noinspection WrongConstant (lint is actually wrong here!)
 | 
			
		||||
            appInfo = mPm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
 | 
			
		||||
        } catch (PackageManager.NameNotFoundException e) {
 | 
			
		||||
            Log.w(TAG, "Failed to get ApplicationInfo for uninstalling");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    protected void uninstallPackage(final String packageName) {
 | 
			
		||||
        sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED);
 | 
			
		||||
 | 
			
		||||
        final boolean isSystem = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
 | 
			
		||||
        final boolean isUpdate = (appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
 | 
			
		||||
 | 
			
		||||
        if (isSystem && !isUpdate) {
 | 
			
		||||
            // Cannot remove system apps unless we're uninstalling updates
 | 
			
		||||
            mCallback.onError(InstallerCallback.OPERATION_DELETE,
 | 
			
		||||
                    InstallerCallback.ERROR_CODE_OTHER);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        int messageId;
 | 
			
		||||
        if (isUpdate) {
 | 
			
		||||
            messageId = R.string.uninstall_update_confirm;
 | 
			
		||||
        } else {
 | 
			
		||||
            messageId = R.string.uninstall_confirm;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
 | 
			
		||||
        builder.setTitle(appInfo.loadLabel(mPm));
 | 
			
		||||
        builder.setIcon(appInfo.loadIcon(mPm));
 | 
			
		||||
        builder.setPositiveButton(android.R.string.ok,
 | 
			
		||||
                new DialogInterface.OnClickListener() {
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onClick(DialogInterface dialog, int which) {
 | 
			
		||||
                        try {
 | 
			
		||||
                            doDeletePackageInternal(packageName);
 | 
			
		||||
                        } catch (InstallFailedException e) {
 | 
			
		||||
                            mCallback.onError(InstallerCallback.OPERATION_DELETE,
 | 
			
		||||
                                    InstallerCallback.ERROR_CODE_OTHER);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
        builder.setNegativeButton(android.R.string.cancel,
 | 
			
		||||
                new DialogInterface.OnClickListener() {
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onClick(DialogInterface dialog, int which) {
 | 
			
		||||
                        dialog.cancel();
 | 
			
		||||
                        mCallback.onError(InstallerCallback.OPERATION_DELETE,
 | 
			
		||||
                                InstallerCallback.ERROR_CODE_CANCELED);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
        builder.setMessage(messageId);
 | 
			
		||||
        builder.create().show();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void doDeletePackageInternal(final String packageName)
 | 
			
		||||
            throws InstallFailedException {
 | 
			
		||||
        ServiceConnection mServiceConnection = new ServiceConnection() {
 | 
			
		||||
            public void onServiceConnected(ComponentName name, IBinder service) {
 | 
			
		||||
                IPrivilegedService privService = IPrivilegedService.Stub.asInterface(service);
 | 
			
		||||
@ -297,23 +365,29 @@ public class PrivilegedInstaller extends Installer {
 | 
			
		||||
                IPrivilegedCallback callback = new IPrivilegedCallback.Stub() {
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void handleResult(String packageName, int returnCode) throws RemoteException {
 | 
			
		||||
                        // TODO: propagate other return codes?
 | 
			
		||||
                        if (returnCode == DELETE_SUCCEEDED) {
 | 
			
		||||
                            Utils.debugLog(TAG, "Delete succeeded");
 | 
			
		||||
 | 
			
		||||
                            mCallback.onSuccess(InstallerCallback.OPERATION_DELETE);
 | 
			
		||||
                            sendBroadcastUninstall(packageName, ACTION_UNINSTALL_COMPLETE);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Log.e(TAG, "Delete failed with returnCode " + returnCode);
 | 
			
		||||
                            mCallback.onError(InstallerCallback.OPERATION_DELETE,
 | 
			
		||||
                                    InstallerCallback.ERROR_CODE_OTHER);
 | 
			
		||||
                            sendBroadcastUninstall(packageName, ACTION_UNINSTALL_INTERRUPTED,
 | 
			
		||||
                                    "Error " + returnCode + ": "
 | 
			
		||||
                                            + UNINSTALL_RETURN_CODES.get(returnCode));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                try {
 | 
			
		||||
                    boolean hasPermissions = privService.hasPrivilegedPermissions();
 | 
			
		||||
                    if (!hasPermissions) {
 | 
			
		||||
                        sendBroadcastUninstall(packageName, ACTION_UNINSTALL_INTERRUPTED,
 | 
			
		||||
                                context.getString(R.string.system_install_denied_permissions));
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    privService.deletePackage(packageName, 0, callback);
 | 
			
		||||
                } catch (RemoteException e) {
 | 
			
		||||
                    Log.e(TAG, "RemoteException", e);
 | 
			
		||||
                    sendBroadcastUninstall(packageName, ACTION_UNINSTALL_INTERRUPTED,
 | 
			
		||||
                            "connecting to privileged service failed");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -323,389 +397,13 @@ public class PrivilegedInstaller extends Installer {
 | 
			
		||||
 | 
			
		||||
        Intent serviceIntent = new Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT);
 | 
			
		||||
        serviceIntent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME);
 | 
			
		||||
        mContext.getApplicationContext().bindService(serviceIntent, mServiceConnection,
 | 
			
		||||
        context.getApplicationContext().bindService(serviceIntent, mServiceConnection,
 | 
			
		||||
                Context.BIND_AUTO_CREATE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean handleOnActivityResult(int requestCode, int resultCode, Intent data) {
 | 
			
		||||
        switch (requestCode) {
 | 
			
		||||
            case REQUEST_CONFIRM_PERMS:
 | 
			
		||||
                if (resultCode == Activity.RESULT_OK) {
 | 
			
		||||
                    final Uri packageUri = data.getData();
 | 
			
		||||
                    try {
 | 
			
		||||
                        doInstallPackageInternal(packageUri);
 | 
			
		||||
                    } catch (InstallFailedException e) {
 | 
			
		||||
                        mCallback.onError(InstallerCallback.OPERATION_INSTALL,
 | 
			
		||||
                                InstallerCallback.ERROR_CODE_OTHER);
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (resultCode == InstallConfirmActivity.RESULT_CANNOT_PARSE) {
 | 
			
		||||
                    mCallback.onError(InstallerCallback.OPERATION_INSTALL,
 | 
			
		||||
                            InstallerCallback.ERROR_CODE_CANNOT_PARSE);
 | 
			
		||||
                } else { // Activity.RESULT_CANCELED
 | 
			
		||||
                    mCallback.onError(InstallerCallback.OPERATION_INSTALL,
 | 
			
		||||
                            InstallerCallback.ERROR_CODE_CANCELED);
 | 
			
		||||
                }
 | 
			
		||||
                return true;
 | 
			
		||||
            default:
 | 
			
		||||
                return false;
 | 
			
		||||
        }
 | 
			
		||||
    protected boolean isUnattended() {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static final int INSTALL_REPLACE_EXISTING = 2;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Following return codes are copied from Android 5.1 source code
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} on success.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_SUCCEEDED = 1;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the package is
 | 
			
		||||
     * already installed.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_ALREADY_EXISTS = -1;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the package archive
 | 
			
		||||
     * file is invalid.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_INVALID_APK = -2;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the URI passed in
 | 
			
		||||
     * is invalid.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_INVALID_URI = -3;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the package manager
 | 
			
		||||
     * service found that the device didn't have enough storage space to install the app.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_INSUFFICIENT_STORAGE = -4;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if a
 | 
			
		||||
     * package is already installed with the same name.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_DUPLICATE_PACKAGE = -5;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the requested shared user does not exist.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_NO_SHARED_USER = -6;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * a previously installed package of the same name has a different signature
 | 
			
		||||
     * than the new package (and the old package's data was not removed).
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_UPDATE_INCOMPATIBLE = -7;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the new package is requested a shared user which is already installed on the
 | 
			
		||||
     * device and does not have matching signature.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_SHARED_USER_INCOMPATIBLE = -8;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the new package uses a shared library that is not available.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_MISSING_SHARED_LIBRARY = -9;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the new package uses a shared library that is not available.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_REPLACE_COULDNT_DELETE = -10;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the new package failed while optimizing and validating its dex files,
 | 
			
		||||
     * either because there was not enough storage or the validation failed.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_DEXOPT = -11;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the new package failed because the current SDK version is older than
 | 
			
		||||
     * that required by the package.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_OLDER_SDK = -12;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the new package failed because it contains a content provider with the
 | 
			
		||||
     * same authority as a provider already installed in the system.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_CONFLICTING_PROVIDER = -13;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the new package failed because the current SDK version is newer than
 | 
			
		||||
     * that required by the package.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_NEWER_SDK = -14;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the new package failed because it has specified that it is a test-only
 | 
			
		||||
     * package and the caller has not supplied the {@link #INSTALL_ALLOW_TEST}
 | 
			
		||||
     * flag.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_TEST_ONLY = -15;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the package being installed contains native code, but none that is
 | 
			
		||||
     * compatible with the device's CPU_ABI.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_CPU_ABI_INCOMPATIBLE = -16;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the new package uses a feature that is not available.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_MISSING_FEATURE = -17;
 | 
			
		||||
 | 
			
		||||
    // ------ Errors related to sdcard
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * a secure container mount point couldn't be accessed on external media.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_CONTAINER_ERROR = -18;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the new package couldn't be installed in the specified install
 | 
			
		||||
     * location.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_INVALID_INSTALL_LOCATION = -19;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the new package couldn't be installed in the specified install
 | 
			
		||||
     * location because the media is not available.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_MEDIA_UNAVAILABLE = -20;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the new package couldn't be installed because the verification timed out.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_VERIFICATION_TIMEOUT = -21;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the new package couldn't be installed because the verification did not succeed.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_VERIFICATION_FAILURE = -22;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the package changed from what the calling program expected.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_PACKAGE_CHANGED = -23;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the new package is assigned a different UID than it previously held.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_UID_CHANGED = -24;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
 | 
			
		||||
     * the new package has an older version code than the currently installed package.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_VERSION_DOWNGRADE = -25;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
 | 
			
		||||
     * if the parser was given a path that is not a file, or does not end with the expected
 | 
			
		||||
     * '.apk' extension.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_NOT_APK = -100;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
 | 
			
		||||
     * if the parser was unable to retrieve the AndroidManifest.xml file.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_BAD_MANIFEST = -101;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
 | 
			
		||||
     * if the parser encountered an unexpected exception.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION = -102;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
 | 
			
		||||
     * if the parser did not find any certificates in the .apk.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_NO_CERTIFICATES = -103;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
 | 
			
		||||
     * if the parser found inconsistent certificates on the files in the .apk.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES = -104;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
 | 
			
		||||
     * if the parser encountered a CertificateEncodingException in one of the
 | 
			
		||||
     * files in the .apk.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING = -105;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
 | 
			
		||||
     * if the parser encountered a bad or missing package name in the manifest.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME = -106;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
 | 
			
		||||
     * if the parser encountered a bad shared user id name in the manifest.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID = -107;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
 | 
			
		||||
     * if the parser encountered some structural problem in the manifest.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_MANIFEST_MALFORMED = -108;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
 | 
			
		||||
     * if the parser did not find any actionable tags (instrumentation or application)
 | 
			
		||||
     * in the manifest.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_PARSE_FAILED_MANIFEST_EMPTY = -109;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
 | 
			
		||||
     * if the system failed to install the package because of system issues.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_INTERNAL_ERROR = -110;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
 | 
			
		||||
     * if the system failed to install the package because the user is restricted from installing
 | 
			
		||||
     * apps.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_USER_RESTRICTED = -111;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
 | 
			
		||||
     * if the system failed to install the package because it is attempting to define a
 | 
			
		||||
     * permission that is already defined by some existing package.
 | 
			
		||||
     *
 | 
			
		||||
     * The package name of the app which has already defined the permission is passed to
 | 
			
		||||
     * a {@link PackageInstallObserver}, if any, as the {@link #EXTRA_EXISTING_PACKAGE}
 | 
			
		||||
     * string extra; and the name of the permission being redefined is passed in the
 | 
			
		||||
     * {@link #EXTRA_EXISTING_PERMISSION} string extra.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_DUPLICATE_PERMISSION = -112;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by
 | 
			
		||||
     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
 | 
			
		||||
     * if the system failed to install the package because its packaged native code did not
 | 
			
		||||
     * match any of the ABIs supported by the system.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int INSTALL_FAILED_NO_MATCHING_ABIS = -113;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Internal return code for NativeLibraryHelper methods to indicate that the package
 | 
			
		||||
     * being processed did not contain any native code. This is placed here only so that
 | 
			
		||||
     * it can belong to the same value space as the other install failure codes.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int NO_NATIVE_LIBRARIES = -114;
 | 
			
		||||
 | 
			
		||||
    public static final int INSTALL_FAILED_ABORTED = -115;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return code for when package deletion succeeds. This is passed to the
 | 
			
		||||
     * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system
 | 
			
		||||
     * succeeded in deleting the package.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int DELETE_SUCCEEDED = 1;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Deletion failed return code: this is passed to the
 | 
			
		||||
     * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system
 | 
			
		||||
     * failed to delete the package for an unspecified reason.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int DELETE_FAILED_INTERNAL_ERROR = -1;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Deletion failed return code: this is passed to the
 | 
			
		||||
     * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system
 | 
			
		||||
     * failed to delete the package because it is the active DevicePolicy
 | 
			
		||||
     * manager.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Deletion failed return code: this is passed to the
 | 
			
		||||
     * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system
 | 
			
		||||
     * failed to delete the package since the user is restricted.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int DELETE_FAILED_USER_RESTRICTED = -3;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Deletion failed return code: this is passed to the
 | 
			
		||||
     * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system
 | 
			
		||||
     * failed to delete the package because a profile
 | 
			
		||||
     * or device owner has marked the package as uninstallable.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int DELETE_FAILED_OWNER_BLOCKED = -4;
 | 
			
		||||
 | 
			
		||||
    public static final int DELETE_FAILED_ABORTED = -5;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ import android.app.ProgressDialog;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.DialogInterface;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.net.Uri;
 | 
			
		||||
import android.os.AsyncTask;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.support.v4.app.FragmentActivity;
 | 
			
		||||
@ -43,6 +44,8 @@ import org.fdroid.fdroid.Preferences;
 | 
			
		||||
import org.fdroid.fdroid.R;
 | 
			
		||||
import org.fdroid.fdroid.installer.PrivilegedInstaller;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
 | 
			
		||||
import eu.chainfire.libsuperuser.Shell;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -53,13 +56,12 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
 | 
			
		||||
    private static final String TAG = "InstallIntoSystem";
 | 
			
		||||
 | 
			
		||||
    public static final String ACTION_INSTALL = "install";
 | 
			
		||||
    public static final String EXTRA_INSTALL_APK = "apk_file";
 | 
			
		||||
 | 
			
		||||
    public static final String ACTION_UNINSTALL = "uninstall";
 | 
			
		||||
    public static final String ACTION_POST_INSTALL = "post_install";
 | 
			
		||||
    public static final String ACTION_FIRST_TIME = "first_time";
 | 
			
		||||
 | 
			
		||||
    private String apkFile;
 | 
			
		||||
    private String apkPath;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
@ -73,7 +75,11 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        apkFile = getIntent().getStringExtra(EXTRA_INSTALL_APK);
 | 
			
		||||
        Uri dataUri = getIntent().getData();
 | 
			
		||||
        if (dataUri != null) {
 | 
			
		||||
            File apkFile = new File(dataUri.getPath());
 | 
			
		||||
            apkPath = apkFile.getAbsolutePath();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        switch (getIntent().getAction()) {
 | 
			
		||||
            case ACTION_UNINSTALL:
 | 
			
		||||
@ -105,7 +111,6 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
 | 
			
		||||
                    runFirstTime(context);
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                case PrivilegedInstaller.IS_EXTENSION_INSTALLED_PERMISSIONS_PROBLEM:
 | 
			
		||||
                case PrivilegedInstaller.IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM:
 | 
			
		||||
                default:
 | 
			
		||||
                    // do nothing
 | 
			
		||||
@ -334,7 +339,7 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        protected Void doInBackground(Void... voids) {
 | 
			
		||||
            InstallExtension.create(getApplicationContext()).runInstall(apkFile);
 | 
			
		||||
            InstallExtension.create(getApplicationContext()).runInstall(apkPath);
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
@ -369,12 +374,6 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
 | 
			
		||||
                        "\n\n" + getString(R.string.system_install_denied_signature);
 | 
			
		||||
                result = Activity.RESULT_CANCELED;
 | 
			
		||||
                break;
 | 
			
		||||
            case PrivilegedInstaller.IS_EXTENSION_INSTALLED_PERMISSIONS_PROBLEM:
 | 
			
		||||
                title = getString(R.string.system_install_post_fail);
 | 
			
		||||
                message = getString(R.string.system_install_post_fail_message) +
 | 
			
		||||
                        "\n\n" + getString(R.string.system_install_denied_permissions);
 | 
			
		||||
                result = Activity.RESULT_CANCELED;
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                throw new RuntimeException("unhandled return");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -18,11 +18,18 @@
 | 
			
		||||
 | 
			
		||||
package org.fdroid.fdroid.privileged.views;
 | 
			
		||||
 | 
			
		||||
import android.annotation.TargetApi;
 | 
			
		||||
import android.content.pm.ApplicationInfo;
 | 
			
		||||
import android.content.pm.PackageInfo;
 | 
			
		||||
import android.content.pm.PackageManager;
 | 
			
		||||
import android.net.Uri;
 | 
			
		||||
import android.os.Build;
 | 
			
		||||
 | 
			
		||||
import org.fdroid.fdroid.data.Apk;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
 | 
			
		||||
@TargetApi(Build.VERSION_CODES.M)
 | 
			
		||||
public class AppDiff {
 | 
			
		||||
 | 
			
		||||
    private final PackageManager mPm;
 | 
			
		||||
@ -30,6 +37,30 @@ public class AppDiff {
 | 
			
		||||
 | 
			
		||||
    public ApplicationInfo mInstalledAppInfo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Constructor based on F-Droids Apk object
 | 
			
		||||
     */
 | 
			
		||||
    public AppDiff(PackageManager mPm, Apk apk) {
 | 
			
		||||
        this.mPm = mPm;
 | 
			
		||||
 | 
			
		||||
        mPkgInfo = new PackageInfo();
 | 
			
		||||
        mPkgInfo.packageName = apk.packageName;
 | 
			
		||||
        mPkgInfo.applicationInfo = new ApplicationInfo();
 | 
			
		||||
 | 
			
		||||
        if (apk.permissions == null) {
 | 
			
		||||
            mPkgInfo.requestedPermissions = null;
 | 
			
		||||
        } else {
 | 
			
		||||
            // TODO: duplicate code with Permission.fdroidToAndroid
 | 
			
		||||
            ArrayList<String> permissionsFixed = new ArrayList<>();
 | 
			
		||||
            for (String perm : apk.permissions.toArrayList()) {
 | 
			
		||||
                permissionsFixed.add("android.permission." + perm);
 | 
			
		||||
            }
 | 
			
		||||
            mPkgInfo.requestedPermissions = permissionsFixed.toArray(new String[permissionsFixed.size()]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        init();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public AppDiff(PackageManager mPm, Uri mPackageURI) {
 | 
			
		||||
        this.mPm = mPm;
 | 
			
		||||
 | 
			
		||||
@ -55,7 +86,7 @@ public class AppDiff {
 | 
			
		||||
        String pkgName = mPkgInfo.packageName;
 | 
			
		||||
        // Check if there is already a package on the device with this name
 | 
			
		||||
        // but it has been renamed to something else.
 | 
			
		||||
        final String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] {pkgName});
 | 
			
		||||
        final String[] oldName = mPm.canonicalToCurrentPackageNames(new String[]{pkgName});
 | 
			
		||||
        if (oldName != null && oldName.length > 0 && oldName[0] != null) {
 | 
			
		||||
            pkgName = oldName[0];
 | 
			
		||||
            mPkgInfo.packageName = pkgName;
 | 
			
		||||
 | 
			
		||||
@ -235,8 +235,7 @@ public class AppSecurityPermissions {
 | 
			
		||||
            try {
 | 
			
		||||
                installedPkgInfo = mPm.getPackageInfo(info.packageName,
 | 
			
		||||
                        PackageManager.GET_PERMISSIONS);
 | 
			
		||||
            } catch (NameNotFoundException e) {
 | 
			
		||||
                throw new RuntimeException("NameNotFoundException during GET_PERMISSIONS!");
 | 
			
		||||
            } catch (NameNotFoundException ignored) {
 | 
			
		||||
            }
 | 
			
		||||
            extractPerms(info, permSet, installedPkgInfo);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -18,16 +18,15 @@
 | 
			
		||||
 | 
			
		||||
package org.fdroid.fdroid.privileged.views;
 | 
			
		||||
 | 
			
		||||
import android.app.Activity;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.DialogInterface;
 | 
			
		||||
import android.content.DialogInterface.OnCancelListener;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.content.pm.ApplicationInfo;
 | 
			
		||||
import android.content.pm.PackageManager;
 | 
			
		||||
import android.graphics.drawable.Drawable;
 | 
			
		||||
import android.graphics.Bitmap;
 | 
			
		||||
import android.net.Uri;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.support.v4.app.FragmentActivity;
 | 
			
		||||
import android.support.v4.view.ViewPager;
 | 
			
		||||
import android.view.LayoutInflater;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
@ -38,45 +37,62 @@ import android.widget.ImageView;
 | 
			
		||||
import android.widget.TabHost;
 | 
			
		||||
import android.widget.TextView;
 | 
			
		||||
 | 
			
		||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
 | 
			
		||||
import com.nostra13.universalimageloader.core.ImageLoader;
 | 
			
		||||
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
 | 
			
		||||
 | 
			
		||||
import org.fdroid.fdroid.FDroidApp;
 | 
			
		||||
import org.fdroid.fdroid.R;
 | 
			
		||||
import org.fdroid.fdroid.data.Apk;
 | 
			
		||||
import org.fdroid.fdroid.data.ApkProvider;
 | 
			
		||||
import org.fdroid.fdroid.data.App;
 | 
			
		||||
import org.fdroid.fdroid.data.AppProvider;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * NOTES:
 | 
			
		||||
 * Parts are based on AOSP src/com/android/packageinstaller/PackageInstallerActivity.java
 | 
			
		||||
 * latest included commit: c23d802958158d522e7350321ad9ac6d43013883
 | 
			
		||||
 */
 | 
			
		||||
public class InstallConfirmActivity extends Activity implements OnCancelListener, OnClickListener {
 | 
			
		||||
public class InstallConfirmActivity extends FragmentActivity implements OnCancelListener, OnClickListener {
 | 
			
		||||
 | 
			
		||||
    public static final int RESULT_CANNOT_PARSE = RESULT_FIRST_USER + 1;
 | 
			
		||||
 | 
			
		||||
    private Intent intent;
 | 
			
		||||
 | 
			
		||||
    private PackageManager mPm;
 | 
			
		||||
 | 
			
		||||
    private AppDiff mAppDiff;
 | 
			
		||||
    private AppDiff appDiff;
 | 
			
		||||
 | 
			
		||||
    // View for install progress
 | 
			
		||||
    private View mInstallConfirm;
 | 
			
		||||
    private View installConfirm;
 | 
			
		||||
    // Buttons to indicate user acceptance
 | 
			
		||||
    private Button mOk;
 | 
			
		||||
    private Button mCancel;
 | 
			
		||||
    private CaffeinatedScrollView mScrollView;
 | 
			
		||||
    private boolean mOkCanInstall;
 | 
			
		||||
    private Button okButton;
 | 
			
		||||
    private Button cancelButton;
 | 
			
		||||
    private CaffeinatedScrollView scrollView;
 | 
			
		||||
    private boolean okCanInstall;
 | 
			
		||||
 | 
			
		||||
    private static final String TAB_ID_ALL = "all";
 | 
			
		||||
    private static final String TAB_ID_NEW = "new";
 | 
			
		||||
 | 
			
		||||
    private App mApp;
 | 
			
		||||
 | 
			
		||||
    private final DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder()
 | 
			
		||||
            .cacheInMemory(true)
 | 
			
		||||
            .cacheOnDisk(true)
 | 
			
		||||
            .imageScaleType(ImageScaleType.NONE)
 | 
			
		||||
            .showImageOnLoading(R.drawable.ic_repo_app_default)
 | 
			
		||||
            .showImageForEmptyUri(R.drawable.ic_repo_app_default)
 | 
			
		||||
            .bitmapConfig(Bitmap.Config.RGB_565)
 | 
			
		||||
            .build();
 | 
			
		||||
 | 
			
		||||
    private void startInstallConfirm() {
 | 
			
		||||
 | 
			
		||||
        final Drawable appIcon = mAppDiff.mPkgInfo.applicationInfo.loadIcon(mPm);
 | 
			
		||||
        final String appLabel = (String) mAppDiff.mPkgInfo.applicationInfo.loadLabel(mPm);
 | 
			
		||||
 | 
			
		||||
        View appSnippet = findViewById(R.id.app_snippet);
 | 
			
		||||
        ((ImageView) appSnippet.findViewById(R.id.app_icon)).setImageDrawable(appIcon);
 | 
			
		||||
        ((TextView) appSnippet.findViewById(R.id.app_name)).setText(appLabel);
 | 
			
		||||
 | 
			
		||||
        TextView appName = (TextView) appSnippet.findViewById(R.id.app_name);
 | 
			
		||||
        ImageView appIcon = (ImageView) appSnippet.findViewById(R.id.app_icon);
 | 
			
		||||
        TabHost tabHost = (TabHost) findViewById(android.R.id.tabhost);
 | 
			
		||||
 | 
			
		||||
        appName.setText(mApp.name);
 | 
			
		||||
        ImageLoader.getInstance().displayImage(mApp.iconUrlLarge, appIcon,
 | 
			
		||||
                displayImageOptions);
 | 
			
		||||
 | 
			
		||||
        tabHost.setup();
 | 
			
		||||
        ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
 | 
			
		||||
        TabsAdapter adapter = new TabsAdapter(this, tabHost, viewPager);
 | 
			
		||||
@ -87,27 +103,27 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        boolean permVisible = false;
 | 
			
		||||
        mScrollView = null;
 | 
			
		||||
        mOkCanInstall = false;
 | 
			
		||||
        scrollView = null;
 | 
			
		||||
        okCanInstall = false;
 | 
			
		||||
        int msg = 0;
 | 
			
		||||
        AppSecurityPermissions perms = new AppSecurityPermissions(this, mAppDiff.mPkgInfo);
 | 
			
		||||
        if (mAppDiff.mInstalledAppInfo != null) {
 | 
			
		||||
            msg = (mAppDiff.mInstalledAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
 | 
			
		||||
        AppSecurityPermissions perms = new AppSecurityPermissions(this, appDiff.mPkgInfo);
 | 
			
		||||
        if (appDiff.mInstalledAppInfo != null) {
 | 
			
		||||
            msg = (appDiff.mInstalledAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
 | 
			
		||||
                    ? R.string.install_confirm_update_system
 | 
			
		||||
                    : R.string.install_confirm_update;
 | 
			
		||||
            mScrollView = new CaffeinatedScrollView(this);
 | 
			
		||||
            mScrollView.setFillViewport(true);
 | 
			
		||||
            scrollView = new CaffeinatedScrollView(this);
 | 
			
		||||
            scrollView.setFillViewport(true);
 | 
			
		||||
            final boolean newPermissionsFound =
 | 
			
		||||
                    perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW) > 0;
 | 
			
		||||
            if (newPermissionsFound) {
 | 
			
		||||
                permVisible = true;
 | 
			
		||||
                mScrollView.addView(perms.getPermissionsView(
 | 
			
		||||
                scrollView.addView(perms.getPermissionsView(
 | 
			
		||||
                        AppSecurityPermissions.WHICH_NEW));
 | 
			
		||||
            } else {
 | 
			
		||||
                throw new RuntimeException("This should not happen. No new permissions were found but InstallConfirmActivity has been started!");
 | 
			
		||||
            }
 | 
			
		||||
            adapter.addTab(tabHost.newTabSpec(TAB_ID_NEW).setIndicator(
 | 
			
		||||
                    getText(R.string.newPerms)), mScrollView);
 | 
			
		||||
                    getText(R.string.newPerms)), scrollView);
 | 
			
		||||
        } else {
 | 
			
		||||
            findViewById(R.id.tabscontainer).setVisibility(View.GONE);
 | 
			
		||||
            findViewById(R.id.divider).setVisibility(View.VISIBLE);
 | 
			
		||||
@ -118,8 +134,8 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
 | 
			
		||||
            LayoutInflater inflater = (LayoutInflater) getSystemService(
 | 
			
		||||
                    Context.LAYOUT_INFLATER_SERVICE);
 | 
			
		||||
            View root = inflater.inflate(R.layout.permissions_list, null);
 | 
			
		||||
            if (mScrollView == null) {
 | 
			
		||||
                mScrollView = (CaffeinatedScrollView) root.findViewById(R.id.scrollview);
 | 
			
		||||
            if (scrollView == null) {
 | 
			
		||||
                scrollView = (CaffeinatedScrollView) root.findViewById(R.id.scrollview);
 | 
			
		||||
            }
 | 
			
		||||
            final ViewGroup permList = (ViewGroup) root.findViewById(R.id.permission_list);
 | 
			
		||||
            permList.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL));
 | 
			
		||||
@ -128,40 +144,40 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!permVisible) {
 | 
			
		||||
            if (mAppDiff.mInstalledAppInfo != null) {
 | 
			
		||||
            if (appDiff.mInstalledAppInfo != null) {
 | 
			
		||||
                // This is an update to an application, but there are no
 | 
			
		||||
                // permissions at all.
 | 
			
		||||
                msg = (mAppDiff.mInstalledAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
 | 
			
		||||
                msg = (appDiff.mInstalledAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
 | 
			
		||||
                        ? R.string.install_confirm_update_system_no_perms
 | 
			
		||||
                        : R.string.install_confirm_update_no_perms;
 | 
			
		||||
            } else {
 | 
			
		||||
                // This is a new application with no permissions.
 | 
			
		||||
                msg = R.string.install_confirm_no_perms;
 | 
			
		||||
                throw new RuntimeException("no permissions requested. This screen should not appear!");
 | 
			
		||||
            }
 | 
			
		||||
            tabHost.setVisibility(View.GONE);
 | 
			
		||||
            findViewById(R.id.filler).setVisibility(View.VISIBLE);
 | 
			
		||||
            findViewById(R.id.divider).setVisibility(View.GONE);
 | 
			
		||||
            mScrollView = null;
 | 
			
		||||
            scrollView = null;
 | 
			
		||||
        }
 | 
			
		||||
        if (msg != 0) {
 | 
			
		||||
            ((TextView) findViewById(R.id.install_confirm)).setText(msg);
 | 
			
		||||
        }
 | 
			
		||||
        mInstallConfirm.setVisibility(View.VISIBLE);
 | 
			
		||||
        mOk = (Button) findViewById(R.id.ok_button);
 | 
			
		||||
        mCancel = (Button) findViewById(R.id.cancel_button);
 | 
			
		||||
        mOk.setOnClickListener(this);
 | 
			
		||||
        mCancel.setOnClickListener(this);
 | 
			
		||||
        if (mScrollView == null) {
 | 
			
		||||
        installConfirm.setVisibility(View.VISIBLE);
 | 
			
		||||
        okButton = (Button) findViewById(R.id.ok_button);
 | 
			
		||||
        cancelButton = (Button) findViewById(R.id.cancel_button);
 | 
			
		||||
        okButton.setOnClickListener(this);
 | 
			
		||||
        cancelButton.setOnClickListener(this);
 | 
			
		||||
        if (scrollView == null) {
 | 
			
		||||
            // There is nothing to scroll view, so the ok button is immediately
 | 
			
		||||
            // set to install.
 | 
			
		||||
            mOk.setText(R.string.menu_install);
 | 
			
		||||
            mOkCanInstall = true;
 | 
			
		||||
            okButton.setText(R.string.menu_install);
 | 
			
		||||
            okCanInstall = true;
 | 
			
		||||
        } else {
 | 
			
		||||
            mScrollView.setFullScrollAction(new Runnable() {
 | 
			
		||||
            scrollView.setFullScrollAction(new Runnable() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void run() {
 | 
			
		||||
                    mOk.setText(R.string.menu_install);
 | 
			
		||||
                    mOkCanInstall = true;
 | 
			
		||||
                    okButton.setText(R.string.menu_install);
 | 
			
		||||
                    okCanInstall = true;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
@ -171,22 +187,28 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
 | 
			
		||||
    protected void onCreate(Bundle icicle) {
 | 
			
		||||
        super.onCreate(icicle);
 | 
			
		||||
 | 
			
		||||
        ((FDroidApp) getApplication()).applyTheme(this);
 | 
			
		||||
 | 
			
		||||
        mPm = getPackageManager();
 | 
			
		||||
        ((FDroidApp) getApplication()).applyDialogTheme(this);
 | 
			
		||||
 | 
			
		||||
        intent = getIntent();
 | 
			
		||||
        Uri packageURI = intent.getData();
 | 
			
		||||
        Uri uri = intent.getData();
 | 
			
		||||
        Apk apk = ApkProvider.Helper.find(this, uri, ApkProvider.DataColumns.ALL);
 | 
			
		||||
        mApp = AppProvider.Helper.findByPackageName(getContentResolver(), apk.packageName);
 | 
			
		||||
 | 
			
		||||
        mAppDiff = new AppDiff(mPm, packageURI);
 | 
			
		||||
        if (mAppDiff.mPkgInfo == null) {
 | 
			
		||||
        appDiff = new AppDiff(getPackageManager(), apk);
 | 
			
		||||
        if (appDiff.mPkgInfo == null) {
 | 
			
		||||
            setResult(RESULT_CANNOT_PARSE, intent);
 | 
			
		||||
            finish();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setContentView(R.layout.install_start);
 | 
			
		||||
        mInstallConfirm = findViewById(R.id.install_confirm_panel);
 | 
			
		||||
        mInstallConfirm.setVisibility(View.INVISIBLE);
 | 
			
		||||
 | 
			
		||||
        // increase dialog to full width for now
 | 
			
		||||
        // TODO: create a better design and minimum width for tablets
 | 
			
		||||
        getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT,
 | 
			
		||||
                ViewGroup.LayoutParams.WRAP_CONTENT);
 | 
			
		||||
 | 
			
		||||
        installConfirm = findViewById(R.id.install_confirm_panel);
 | 
			
		||||
        installConfirm.setVisibility(View.INVISIBLE);
 | 
			
		||||
 | 
			
		||||
        startInstallConfirm();
 | 
			
		||||
    }
 | 
			
		||||
@ -197,14 +219,14 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void onClick(View v) {
 | 
			
		||||
        if (v == mOk) {
 | 
			
		||||
            if (mOkCanInstall || mScrollView == null) {
 | 
			
		||||
        if (v == okButton) {
 | 
			
		||||
            if (okCanInstall || scrollView == null) {
 | 
			
		||||
                setResult(RESULT_OK, intent);
 | 
			
		||||
                finish();
 | 
			
		||||
            } else {
 | 
			
		||||
                mScrollView.pageScroll(View.FOCUS_DOWN);
 | 
			
		||||
                scrollView.pageScroll(View.FOCUS_DOWN);
 | 
			
		||||
            }
 | 
			
		||||
        } else if (v == mCancel) {
 | 
			
		||||
        } else if (v == cancelButton) {
 | 
			
		||||
            setResult(RESULT_CANCELED, intent);
 | 
			
		||||
            finish();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,106 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
 | 
			
		||||
 *
 | 
			
		||||
 * 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.privileged.views;
 | 
			
		||||
 | 
			
		||||
import android.app.Activity;
 | 
			
		||||
import android.content.DialogInterface;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.content.pm.ApplicationInfo;
 | 
			
		||||
import android.content.pm.PackageManager;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.support.annotation.Nullable;
 | 
			
		||||
import android.support.v4.app.FragmentActivity;
 | 
			
		||||
import android.support.v7.app.AlertDialog;
 | 
			
		||||
import android.view.ContextThemeWrapper;
 | 
			
		||||
 | 
			
		||||
import org.fdroid.fdroid.FDroidApp;
 | 
			
		||||
import org.fdroid.fdroid.R;
 | 
			
		||||
import org.fdroid.fdroid.installer.Installer;
 | 
			
		||||
 | 
			
		||||
public class UninstallDialogActivity extends FragmentActivity {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onCreate(@Nullable Bundle savedInstanceState) {
 | 
			
		||||
        super.onCreate(savedInstanceState);
 | 
			
		||||
 | 
			
		||||
        final Intent intent = getIntent();
 | 
			
		||||
        final String packageName = intent.getStringExtra(Installer.EXTRA_PACKAGE_NAME);
 | 
			
		||||
 | 
			
		||||
        PackageManager pm = getPackageManager();
 | 
			
		||||
 | 
			
		||||
        ApplicationInfo appInfo;
 | 
			
		||||
        try {
 | 
			
		||||
            //noinspection WrongConstant (lint is actually wrong here!)
 | 
			
		||||
            appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
 | 
			
		||||
        } catch (PackageManager.NameNotFoundException e) {
 | 
			
		||||
            throw new RuntimeException("Failed to get ApplicationInfo for uninstalling");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        final boolean isSystem = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
 | 
			
		||||
        final boolean isUpdate = (appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
 | 
			
		||||
 | 
			
		||||
        if (isSystem && !isUpdate) {
 | 
			
		||||
            // Cannot remove system apps unless we're uninstalling updates
 | 
			
		||||
            throw new RuntimeException("Cannot remove system apps unless we're uninstalling updates");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        int messageId;
 | 
			
		||||
        if (isUpdate) {
 | 
			
		||||
            messageId = R.string.uninstall_update_confirm;
 | 
			
		||||
        } else {
 | 
			
		||||
            messageId = R.string.uninstall_confirm;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // hack to get theme applied (which is not automatically applied due to activity's Theme.NoDisplay
 | 
			
		||||
        ContextThemeWrapper theme = new ContextThemeWrapper(this, FDroidApp.getCurThemeResId());
 | 
			
		||||
 | 
			
		||||
        final AlertDialog.Builder builder = new AlertDialog.Builder(theme);
 | 
			
		||||
        builder.setTitle(appInfo.loadLabel(pm));
 | 
			
		||||
        builder.setIcon(appInfo.loadIcon(pm));
 | 
			
		||||
        builder.setPositiveButton(android.R.string.ok,
 | 
			
		||||
                new DialogInterface.OnClickListener() {
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onClick(DialogInterface dialog, int which) {
 | 
			
		||||
                        Intent data = new Intent();
 | 
			
		||||
                        data.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName);
 | 
			
		||||
                        setResult(Activity.RESULT_OK, intent);
 | 
			
		||||
                        finish();
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
        builder.setNegativeButton(android.R.string.cancel,
 | 
			
		||||
                new DialogInterface.OnClickListener() {
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onClick(DialogInterface dialog, int which) {
 | 
			
		||||
                        setResult(Activity.RESULT_CANCELED);
 | 
			
		||||
                        finish();
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
        builder.setOnCancelListener(
 | 
			
		||||
                new DialogInterface.OnCancelListener() {
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onCancel(DialogInterface dialog) {
 | 
			
		||||
                        setResult(Activity.RESULT_CANCELED);
 | 
			
		||||
                        finish();
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
        builder.setMessage(messageId);
 | 
			
		||||
        builder.create().show();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -225,9 +225,6 @@ public class PreferencesFragment extends PreferenceFragment
 | 
			
		||||
                            case PrivilegedInstaller.IS_EXTENSION_INSTALLED_SIGNATURE_PROBLEM:
 | 
			
		||||
                                message = getActivity().getString(R.string.system_install_denied_signature);
 | 
			
		||||
                                break;
 | 
			
		||||
                            case PrivilegedInstaller.IS_EXTENSION_INSTALLED_PERMISSIONS_PROBLEM:
 | 
			
		||||
                                message = getActivity().getString(R.string.system_install_denied_permissions);
 | 
			
		||||
                                break;
 | 
			
		||||
                            default:
 | 
			
		||||
                                throw new RuntimeException("unhandled return");
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
package org.fdroid.fdroid.views.swap;
 | 
			
		||||
 | 
			
		||||
import android.app.Activity;
 | 
			
		||||
import android.app.PendingIntent;
 | 
			
		||||
import android.bluetooth.BluetoothAdapter;
 | 
			
		||||
import android.content.BroadcastReceiver;
 | 
			
		||||
import android.content.ComponentName;
 | 
			
		||||
@ -43,6 +44,7 @@ import org.fdroid.fdroid.data.App;
 | 
			
		||||
import org.fdroid.fdroid.data.NewRepoConfig;
 | 
			
		||||
import org.fdroid.fdroid.installer.InstallManagerService;
 | 
			
		||||
import org.fdroid.fdroid.installer.Installer;
 | 
			
		||||
import org.fdroid.fdroid.installer.InstallerService;
 | 
			
		||||
import org.fdroid.fdroid.localrepo.LocalRepoManager;
 | 
			
		||||
import org.fdroid.fdroid.localrepo.SwapService;
 | 
			
		||||
import org.fdroid.fdroid.localrepo.peers.Peer;
 | 
			
		||||
@ -119,7 +121,6 @@ public class SwapWorkflowActivity extends AppCompatActivity {
 | 
			
		||||
    private PrepareSwapRepo updateSwappableAppsTask;
 | 
			
		||||
    private NewRepoConfig confirmSwapConfig;
 | 
			
		||||
    private LocalBroadcastManager localBroadcastManager;
 | 
			
		||||
    private BroadcastReceiver downloadCompleteReceiver;
 | 
			
		||||
 | 
			
		||||
    @NonNull
 | 
			
		||||
    private final ServiceConnection serviceConnection = new ServiceConnection() {
 | 
			
		||||
@ -773,7 +774,7 @@ public class SwapWorkflowActivity extends AppCompatActivity {
 | 
			
		||||
    public void install(@NonNull final App app) {
 | 
			
		||||
        final Apk apk = ApkProvider.Helper.find(this, app.packageName, app.suggestedVersionCode);
 | 
			
		||||
        String urlString = apk.getUrl();
 | 
			
		||||
        downloadCompleteReceiver = new BroadcastReceiver() {
 | 
			
		||||
        BroadcastReceiver downloadCompleteReceiver = new BroadcastReceiver() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onReceive(Context context, Intent intent) {
 | 
			
		||||
                String path = intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH);
 | 
			
		||||
@ -786,25 +787,44 @@ public class SwapWorkflowActivity extends AppCompatActivity {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void handleDownloadComplete(File apkFile, String packageName, String urlString) {
 | 
			
		||||
        Uri originatingUri = Uri.parse(urlString);
 | 
			
		||||
        Uri localUri = Uri.fromFile(apkFile);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            Installer.getActivityInstaller(this, new Installer.InstallerCallback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSuccess(int operation) {
 | 
			
		||||
                    // TODO: Don't reload the view weely-neely, but rather get the view to listen
 | 
			
		||||
                    // for broadcasts that say the install was complete.
 | 
			
		||||
                    showRelevantView(true);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onError(int operation, int errorCode) {
 | 
			
		||||
                    // TODO: Boo!
 | 
			
		||||
                }
 | 
			
		||||
            }).installPackage(apkFile, packageName, urlString);
 | 
			
		||||
            localBroadcastManager.unregisterReceiver(downloadCompleteReceiver);
 | 
			
		||||
        } catch (Installer.InstallFailedException e) {
 | 
			
		||||
            // TODO: Handle exception properly
 | 
			
		||||
        }
 | 
			
		||||
        localBroadcastManager.registerReceiver(installReceiver,
 | 
			
		||||
                Installer.getInstallIntentFilter(Uri.fromFile(apkFile)));
 | 
			
		||||
        InstallerService.install(this, localUri, originatingUri, packageName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private final BroadcastReceiver installReceiver = new BroadcastReceiver() {
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onReceive(Context context, Intent intent) {
 | 
			
		||||
            switch (intent.getAction()) {
 | 
			
		||||
                case Installer.ACTION_INSTALL_STARTED:
 | 
			
		||||
                    break;
 | 
			
		||||
                case Installer.ACTION_INSTALL_COMPLETE:
 | 
			
		||||
                    localBroadcastManager.unregisterReceiver(this);
 | 
			
		||||
 | 
			
		||||
                    showRelevantView(true);
 | 
			
		||||
                    break;
 | 
			
		||||
                case Installer.ACTION_INSTALL_INTERRUPTED:
 | 
			
		||||
                    localBroadcastManager.unregisterReceiver(this);
 | 
			
		||||
                    // TODO: handle errors!
 | 
			
		||||
                    break;
 | 
			
		||||
                case Installer.ACTION_INSTALL_USER_INTERACTION:
 | 
			
		||||
                    PendingIntent installPendingIntent =
 | 
			
		||||
                            intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
 | 
			
		||||
 | 
			
		||||
                    try {
 | 
			
		||||
                        installPendingIntent.send();
 | 
			
		||||
                    } catch (PendingIntent.CanceledException e) {
 | 
			
		||||
                        Log.e(TAG, "PI canceled", e);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    throw new RuntimeException("intent action not handled!");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -21,37 +21,35 @@
 | 
			
		||||
  user before it is installed.
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<LinearLayout
 | 
			
		||||
        xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
        android:orientation="vertical"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="match_parent">
 | 
			
		||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="match_parent"
 | 
			
		||||
    android:orientation="vertical">
 | 
			
		||||
 | 
			
		||||
    <TextView
 | 
			
		||||
            android:id="@+id/install_confirm"
 | 
			
		||||
            android:layout_width="wrap_content"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:text="@string/install_confirm"
 | 
			
		||||
            android:textAppearance="?android:attr/textAppearanceMedium"
 | 
			
		||||
            android:paddingLeft="16dp"
 | 
			
		||||
            android:paddingRight="16dp"
 | 
			
		||||
            android:paddingTop="4dip" />
 | 
			
		||||
        android:id="@+id/install_confirm"
 | 
			
		||||
        android:layout_width="wrap_content"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:paddingLeft="16dp"
 | 
			
		||||
        android:paddingRight="16dp"
 | 
			
		||||
        android:paddingTop="4dip"
 | 
			
		||||
        android:text="@string/install_confirm"
 | 
			
		||||
        android:textAppearance="?android:attr/textAppearanceMedium" />
 | 
			
		||||
 | 
			
		||||
    <ImageView
 | 
			
		||||
            android:id="@+id/divider"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:layout_marginTop="16dp"
 | 
			
		||||
            android:background="?android:attr/dividerHorizontal"
 | 
			
		||||
            android:visibility="gone" />
 | 
			
		||||
        android:id="@+id/divider"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_marginTop="16dp"
 | 
			
		||||
        android:background="?android:attr/dividerHorizontal"
 | 
			
		||||
        android:visibility="gone" />
 | 
			
		||||
 | 
			
		||||
    <FrameLayout
 | 
			
		||||
        android:id="@+id/filler"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_weight="1"
 | 
			
		||||
        android:visibility="gone">
 | 
			
		||||
    </FrameLayout>
 | 
			
		||||
        android:visibility="gone"></FrameLayout>
 | 
			
		||||
 | 
			
		||||
    <TabHost
 | 
			
		||||
        android:id="@android:id/tabhost"
 | 
			
		||||
@ -60,24 +58,28 @@
 | 
			
		||||
        android:layout_weight="1">
 | 
			
		||||
 | 
			
		||||
        <LinearLayout
 | 
			
		||||
            android:orientation="vertical"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="match_parent">
 | 
			
		||||
            android:layout_height="match_parent"
 | 
			
		||||
            android:orientation="vertical">
 | 
			
		||||
 | 
			
		||||
            <HorizontalScrollView android:id="@+id/tabscontainer"
 | 
			
		||||
            <HorizontalScrollView
 | 
			
		||||
                android:id="@+id/tabscontainer"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:background="@drawable/tab_unselected_holo"
 | 
			
		||||
                android:fillViewport="true"
 | 
			
		||||
                android:scrollbars="none">
 | 
			
		||||
                <FrameLayout android:layout_width="wrap_content"
 | 
			
		||||
                             android:layout_height="wrap_content">
 | 
			
		||||
 | 
			
		||||
                <FrameLayout
 | 
			
		||||
                    android:layout_width="wrap_content"
 | 
			
		||||
                    android:layout_height="wrap_content">
 | 
			
		||||
 | 
			
		||||
                    <TabWidget
 | 
			
		||||
                        android:id="@android:id/tabs"
 | 
			
		||||
                        android:orientation="horizontal"
 | 
			
		||||
                        android:layout_width="wrap_content"
 | 
			
		||||
                        android:layout_height="wrap_content"
 | 
			
		||||
                        android:layout_gravity="center" />
 | 
			
		||||
                        android:layout_gravity="center"
 | 
			
		||||
                        android:orientation="horizontal" />
 | 
			
		||||
                </FrameLayout>
 | 
			
		||||
            </HorizontalScrollView>
 | 
			
		||||
 | 
			
		||||
@ -85,64 +87,68 @@
 | 
			
		||||
                android:id="@android:id/tabcontent"
 | 
			
		||||
                android:layout_width="0dp"
 | 
			
		||||
                android:layout_height="0dp"
 | 
			
		||||
                android:layout_weight="0"/>
 | 
			
		||||
                android:layout_weight="0" />
 | 
			
		||||
 | 
			
		||||
            <android.support.v4.view.ViewPager
 | 
			
		||||
                android:id="@+id/pager"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="0dp"
 | 
			
		||||
                android:layout_weight="1"/>
 | 
			
		||||
                android:layout_weight="1" />
 | 
			
		||||
 | 
			
		||||
        </LinearLayout>
 | 
			
		||||
    </TabHost>
 | 
			
		||||
 | 
			
		||||
    <!-- OK confirm and cancel buttons.  -->
 | 
			
		||||
    <LinearLayout
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:orientation="vertical"
 | 
			
		||||
            android:divider="?android:attr/dividerHorizontal"
 | 
			
		||||
            android:showDividers="beginning">
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:divider="?android:attr/dividerHorizontal"
 | 
			
		||||
        android:orientation="vertical"
 | 
			
		||||
        android:showDividers="beginning">
 | 
			
		||||
 | 
			
		||||
        <LinearLayout
 | 
			
		||||
                style="?android:attr/buttonBarStyle"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
            style="?android:attr/buttonBarStyle"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:measureWithLargestChild="true"
 | 
			
		||||
            android:orientation="horizontal">
 | 
			
		||||
 | 
			
		||||
            <LinearLayout
 | 
			
		||||
                android:id="@+id/leftSpacer"
 | 
			
		||||
                android:layout_width="0dip"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_weight="0.25"
 | 
			
		||||
                android:orientation="horizontal"
 | 
			
		||||
                android:measureWithLargestChild="true">
 | 
			
		||||
                android:visibility="gone" />
 | 
			
		||||
 | 
			
		||||
            <LinearLayout android:id="@+id/leftSpacer"
 | 
			
		||||
                    android:layout_weight="0.25"
 | 
			
		||||
                    android:layout_width="0dip"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:orientation="horizontal"
 | 
			
		||||
                    android:visibility="gone" />
 | 
			
		||||
            <Button
 | 
			
		||||
                android:id="@+id/cancel_button"
 | 
			
		||||
                style="?android:attr/buttonBarButtonStyle"
 | 
			
		||||
                android:layout_width="0dip"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_gravity="start"
 | 
			
		||||
                android:layout_weight="1"
 | 
			
		||||
                android:maxLines="2"
 | 
			
		||||
                android:text="@string/cancel" />
 | 
			
		||||
 | 
			
		||||
            <Button android:id="@+id/cancel_button"
 | 
			
		||||
                    android:layout_width="0dip"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:layout_gravity="start"
 | 
			
		||||
                    android:layout_weight="1"
 | 
			
		||||
                    android:text="@string/cancel"
 | 
			
		||||
                    android:maxLines="2"
 | 
			
		||||
                    style="?android:attr/buttonBarButtonStyle" />
 | 
			
		||||
            <Button
 | 
			
		||||
                android:id="@+id/ok_button"
 | 
			
		||||
                style="?android:attr/buttonBarButtonStyle"
 | 
			
		||||
                android:layout_width="0dip"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_gravity="end"
 | 
			
		||||
                android:layout_weight="1"
 | 
			
		||||
                android:filterTouchesWhenObscured="true"
 | 
			
		||||
                android:maxLines="2"
 | 
			
		||||
                android:text="@string/next" />
 | 
			
		||||
 | 
			
		||||
            <Button android:id="@+id/ok_button"
 | 
			
		||||
                    android:layout_width="0dip"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:layout_gravity="end"
 | 
			
		||||
                    android:layout_weight="1"
 | 
			
		||||
                    android:text="@string/next"
 | 
			
		||||
                    android:maxLines="2"
 | 
			
		||||
                    android:filterTouchesWhenObscured="true"
 | 
			
		||||
                    style="?android:attr/buttonBarButtonStyle" />
 | 
			
		||||
 | 
			
		||||
            <LinearLayout android:id="@+id/rightSpacer"
 | 
			
		||||
                    android:layout_width="0dip"
 | 
			
		||||
                    android:layout_weight="0.25"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:orientation="horizontal"
 | 
			
		||||
                    android:visibility="gone" />
 | 
			
		||||
            <LinearLayout
 | 
			
		||||
                android:id="@+id/rightSpacer"
 | 
			
		||||
                android:layout_width="0dip"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_weight="0.25"
 | 
			
		||||
                android:orientation="horizontal"
 | 
			
		||||
                android:visibility="gone" />
 | 
			
		||||
 | 
			
		||||
        </LinearLayout>
 | 
			
		||||
    </LinearLayout>
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Copyright (C) 2008 The Android Open Source Project
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2008 The Android Open Source Project
 | 
			
		||||
 | 
			
		||||
     Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
     you may not use this file except in compliance with the License.
 | 
			
		||||
@ -17,59 +16,46 @@
 | 
			
		||||
<!--
 | 
			
		||||
Defines the layout of the application snippet that appears on top of the
 | 
			
		||||
installation screens
 | 
			
		||||
-->
 | 
			
		||||
<!-- The snippet about the application - title, icon, description.  -->
 | 
			
		||||
<RelativeLayout
 | 
			
		||||
    xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
--><!-- The snippet about the application - title, icon, description.  -->
 | 
			
		||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:tools="http://schemas.android.com/tools"
 | 
			
		||||
    android:id="@+id/app_snippet"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="wrap_content"
 | 
			
		||||
    android:paddingLeft="16dip"
 | 
			
		||||
    android:paddingStart="16dip"
 | 
			
		||||
    android:paddingRight="16dip"
 | 
			
		||||
    android:paddingEnd="16dip"
 | 
			
		||||
    android:paddingTop="24dip"
 | 
			
		||||
    >
 | 
			
		||||
    <ImageView android:id="@+id/app_icon"
 | 
			
		||||
        android:layout_width="32dip"
 | 
			
		||||
        android:layout_height="32dip"
 | 
			
		||||
    android:paddingLeft="16dip"
 | 
			
		||||
    android:paddingRight="16dip"
 | 
			
		||||
    android:paddingStart="16dip"
 | 
			
		||||
    android:paddingTop="16dip">
 | 
			
		||||
 | 
			
		||||
    <ImageView
 | 
			
		||||
        android:id="@+id/app_icon"
 | 
			
		||||
        android:layout_width="48dip"
 | 
			
		||||
        android:layout_height="48dip"
 | 
			
		||||
        android:layout_marginLeft="8dip"
 | 
			
		||||
        android:layout_marginStart="8dip"
 | 
			
		||||
        android:background="@android:color/transparent"
 | 
			
		||||
        android:layout_alignParentLeft="true"
 | 
			
		||||
        android:layout_alignParentStart="true"
 | 
			
		||||
        android:gravity="start"
 | 
			
		||||
        android:scaleType="centerCrop"/>
 | 
			
		||||
    <TextView android:id="@+id/app_name"
 | 
			
		||||
        android:scaleType="centerCrop"
 | 
			
		||||
        tools:src="@drawable/ic_launcher" />
 | 
			
		||||
 | 
			
		||||
    <TextView
 | 
			
		||||
        android:id="@+id/app_name"
 | 
			
		||||
        android:layout_width="wrap_content"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_gravity="center_vertical"
 | 
			
		||||
        android:ellipsize="end"
 | 
			
		||||
        android:gravity="center"
 | 
			
		||||
        android:textAppearance="?android:attr/textAppearanceLarge"
 | 
			
		||||
        android:textColor="?android:attr/textColorPrimary"
 | 
			
		||||
        android:paddingEnd="16dip"
 | 
			
		||||
        android:paddingLeft="16dip"
 | 
			
		||||
        android:paddingRight="16dip"
 | 
			
		||||
        android:paddingStart="16dip"
 | 
			
		||||
        android:shadowColor="@color/shadow"
 | 
			
		||||
        android:shadowRadius="2"
 | 
			
		||||
        android:layout_toRightOf="@id/app_icon"
 | 
			
		||||
        android:layout_toEndOf="@id/app_icon"
 | 
			
		||||
        android:singleLine="true"
 | 
			
		||||
        android:layout_centerInParent="true"
 | 
			
		||||
        android:paddingRight="16dip"
 | 
			
		||||
        android:paddingEnd="16dip"
 | 
			
		||||
        android:paddingTop="3dip"
 | 
			
		||||
        android:paddingLeft="16dip"
 | 
			
		||||
        android:paddingStart="16dip"
 | 
			
		||||
        android:ellipsize="end"/>
 | 
			
		||||
    <FrameLayout
 | 
			
		||||
        android:id="@+id/top_divider"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:paddingTop="4dip"
 | 
			
		||||
        android:layout_below="@id/app_name">
 | 
			
		||||
        <ProgressBar
 | 
			
		||||
            android:id="@+id/progress_bar"
 | 
			
		||||
            style="?android:attr/progressBarStyleHorizontal"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content" />
 | 
			
		||||
    </FrameLayout>
 | 
			
		||||
        android:textAppearance="?android:attr/textAppearanceLarge"
 | 
			
		||||
        android:textColor="?android:attr/textColorPrimary"
 | 
			
		||||
        tools:text="App Name" />
 | 
			
		||||
 | 
			
		||||
</RelativeLayout>
 | 
			
		||||
</LinearLayout>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Copyright (C) 2008 The Android Open Source Project
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2008 The Android Open Source Project
 | 
			
		||||
 | 
			
		||||
     Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
     you may not use this file except in compliance with the License.
 | 
			
		||||
@ -21,36 +20,34 @@
 | 
			
		||||
  user before it is installed.
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<LinearLayout
 | 
			
		||||
        xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
        android:orientation="vertical"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="match_parent">
 | 
			
		||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="match_parent"
 | 
			
		||||
    android:orientation="vertical">
 | 
			
		||||
 | 
			
		||||
    <TextView
 | 
			
		||||
            android:id="@+id/install_confirm"
 | 
			
		||||
            android:layout_width="wrap_content"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:text="@string/install_confirm"
 | 
			
		||||
            android:textAppearance="?android:attr/textAppearanceMedium"
 | 
			
		||||
            android:paddingLeft="16dp"
 | 
			
		||||
            android:paddingRight="16dp"
 | 
			
		||||
            android:paddingTop="4dip" />
 | 
			
		||||
        android:id="@+id/install_confirm"
 | 
			
		||||
        android:layout_width="wrap_content"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:paddingLeft="16dp"
 | 
			
		||||
        android:paddingRight="16dp"
 | 
			
		||||
        android:paddingTop="4dip"
 | 
			
		||||
        android:text="@string/install_confirm"
 | 
			
		||||
        android:textAppearance="?android:attr/textAppearanceMedium" />
 | 
			
		||||
 | 
			
		||||
    <ImageView
 | 
			
		||||
            android:id="@+id/divider"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:layout_marginTop="16dp"
 | 
			
		||||
            android:visibility="gone" />
 | 
			
		||||
        android:id="@+id/divider"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_marginTop="16dp"
 | 
			
		||||
        android:visibility="gone" />
 | 
			
		||||
 | 
			
		||||
    <FrameLayout
 | 
			
		||||
        android:id="@+id/filler"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_weight="1"
 | 
			
		||||
        android:visibility="gone">
 | 
			
		||||
    </FrameLayout>
 | 
			
		||||
        android:visibility="gone"></FrameLayout>
 | 
			
		||||
 | 
			
		||||
    <TabHost
 | 
			
		||||
        android:id="@android:id/tabhost"
 | 
			
		||||
@ -59,24 +56,28 @@
 | 
			
		||||
        android:layout_weight="1">
 | 
			
		||||
 | 
			
		||||
        <LinearLayout
 | 
			
		||||
            android:orientation="vertical"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="match_parent">
 | 
			
		||||
            android:layout_height="match_parent"
 | 
			
		||||
            android:orientation="vertical">
 | 
			
		||||
 | 
			
		||||
            <HorizontalScrollView android:id="@+id/tabscontainer"
 | 
			
		||||
            <HorizontalScrollView
 | 
			
		||||
                android:id="@+id/tabscontainer"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:background="@drawable/tab_unselected_holo"
 | 
			
		||||
                android:fillViewport="true"
 | 
			
		||||
                android:scrollbars="none">
 | 
			
		||||
                <FrameLayout android:layout_width="wrap_content"
 | 
			
		||||
                             android:layout_height="wrap_content">
 | 
			
		||||
 | 
			
		||||
                <FrameLayout
 | 
			
		||||
                    android:layout_width="wrap_content"
 | 
			
		||||
                    android:layout_height="wrap_content">
 | 
			
		||||
 | 
			
		||||
                    <TabWidget
 | 
			
		||||
                        android:id="@android:id/tabs"
 | 
			
		||||
                        android:orientation="horizontal"
 | 
			
		||||
                        android:layout_width="wrap_content"
 | 
			
		||||
                        android:layout_height="wrap_content"
 | 
			
		||||
                        android:layout_gravity="center" />
 | 
			
		||||
                        android:layout_gravity="center"
 | 
			
		||||
                        android:orientation="horizontal" />
 | 
			
		||||
                </FrameLayout>
 | 
			
		||||
            </HorizontalScrollView>
 | 
			
		||||
 | 
			
		||||
@ -84,63 +85,65 @@
 | 
			
		||||
                android:id="@android:id/tabcontent"
 | 
			
		||||
                android:layout_width="0dp"
 | 
			
		||||
                android:layout_height="0dp"
 | 
			
		||||
                android:layout_weight="0"/>
 | 
			
		||||
                android:layout_weight="0" />
 | 
			
		||||
 | 
			
		||||
            <android.support.v4.view.ViewPager
 | 
			
		||||
                android:id="@+id/pager"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="0dp"
 | 
			
		||||
                android:layout_weight="1"/>
 | 
			
		||||
                android:layout_weight="1" />
 | 
			
		||||
 | 
			
		||||
        </LinearLayout>
 | 
			
		||||
    </TabHost>
 | 
			
		||||
 | 
			
		||||
    <!-- OK confirm and cancel buttons.  -->
 | 
			
		||||
    <LinearLayout
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:orientation="vertical"
 | 
			
		||||
            android:divider="?android:attr/dividerHorizontal"
 | 
			
		||||
            android:showDividers="beginning">
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:divider="?android:attr/dividerHorizontal"
 | 
			
		||||
        android:orientation="vertical"
 | 
			
		||||
        android:showDividers="beginning">
 | 
			
		||||
 | 
			
		||||
        <LinearLayout
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:measureWithLargestChild="true"
 | 
			
		||||
            android:orientation="horizontal">
 | 
			
		||||
 | 
			
		||||
            <LinearLayout
 | 
			
		||||
                android:id="@+id/leftSpacer"
 | 
			
		||||
                android:layout_width="0dip"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_weight="0.25"
 | 
			
		||||
                android:orientation="horizontal"
 | 
			
		||||
                android:measureWithLargestChild="true">
 | 
			
		||||
                android:visibility="gone" />
 | 
			
		||||
 | 
			
		||||
            <LinearLayout android:id="@+id/leftSpacer"
 | 
			
		||||
                    android:layout_weight="0.25"
 | 
			
		||||
                    android:layout_width="0dip"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:orientation="horizontal"
 | 
			
		||||
                    android:visibility="gone" />
 | 
			
		||||
            <Button
 | 
			
		||||
                android:id="@+id/cancel_button"
 | 
			
		||||
                android:layout_width="0dip"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_gravity="start"
 | 
			
		||||
                android:layout_weight="1"
 | 
			
		||||
                android:maxLines="2"
 | 
			
		||||
                android:text="@string/cancel" />
 | 
			
		||||
 | 
			
		||||
            <Button android:id="@+id/cancel_button"
 | 
			
		||||
                    android:layout_width="0dip"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:layout_gravity="start"
 | 
			
		||||
                    android:layout_weight="1"
 | 
			
		||||
                    android:text="@string/cancel"
 | 
			
		||||
                    android:maxLines="2"
 | 
			
		||||
                    />
 | 
			
		||||
            <Button
 | 
			
		||||
                android:id="@+id/ok_button"
 | 
			
		||||
                android:layout_width="0dip"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_gravity="end"
 | 
			
		||||
                android:layout_weight="1"
 | 
			
		||||
                android:filterTouchesWhenObscured="true"
 | 
			
		||||
                android:maxLines="2"
 | 
			
		||||
                android:text="@string/next" />
 | 
			
		||||
 | 
			
		||||
            <Button android:id="@+id/ok_button"
 | 
			
		||||
                    android:layout_width="0dip"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:layout_gravity="end"
 | 
			
		||||
                    android:layout_weight="1"
 | 
			
		||||
                    android:text="@string/next"
 | 
			
		||||
                    android:maxLines="2"
 | 
			
		||||
                    android:filterTouchesWhenObscured="true"
 | 
			
		||||
                    />
 | 
			
		||||
 | 
			
		||||
            <LinearLayout android:id="@+id/rightSpacer"
 | 
			
		||||
                    android:layout_width="0dip"
 | 
			
		||||
                    android:layout_weight="0.25"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:orientation="horizontal"
 | 
			
		||||
                    android:visibility="gone" />
 | 
			
		||||
            <LinearLayout
 | 
			
		||||
                android:id="@+id/rightSpacer"
 | 
			
		||||
                android:layout_width="0dip"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_weight="0.25"
 | 
			
		||||
                android:orientation="horizontal"
 | 
			
		||||
                android:visibility="gone" />
 | 
			
		||||
 | 
			
		||||
        </LinearLayout>
 | 
			
		||||
    </LinearLayout>
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Copyright (C) 2008 The Android Open Source Project
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2008 The Android Open Source Project
 | 
			
		||||
 | 
			
		||||
     Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
     you may not use this file except in compliance with the License.
 | 
			
		||||
@ -14,24 +13,22 @@
 | 
			
		||||
     limitations under the License.
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<RelativeLayout
 | 
			
		||||
    xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="match_parent">
 | 
			
		||||
    android:layout_height="wrap_content">
 | 
			
		||||
 | 
			
		||||
    <include
 | 
			
		||||
        android:id="@+id/app_snippet"
 | 
			
		||||
        layout="@layout/install_app_details"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:id="@+id/app_snippet"/>
 | 
			
		||||
        android:layout_height="wrap_content" />
 | 
			
		||||
 | 
			
		||||
    <include
 | 
			
		||||
        layout="@layout/install_confirm"
 | 
			
		||||
        android:id="@+id/install_confirm_panel"
 | 
			
		||||
        layout="@layout/install_confirm"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_below="@id/app_snippet"
 | 
			
		||||
        android:layout_alignParentBottom="true"/>
 | 
			
		||||
        android:layout_below="@id/app_snippet" />
 | 
			
		||||
</RelativeLayout>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -267,10 +267,7 @@
 | 
			
		||||
    <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_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_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="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>
 | 
			
		||||
@ -341,10 +338,7 @@
 | 
			
		||||
 | 
			
		||||
    <string name="tap_to_install_format">Tap to install %s</string>
 | 
			
		||||
    <string name="tap_to_update_format">Tap to update %s</string>
 | 
			
		||||
    <string name="install_confirm">Do you want to install this application?
 | 
			
		||||
            It will get access to:</string>
 | 
			
		||||
    <string name="install_confirm_no_perms">Do you want to install this application?
 | 
			
		||||
            It does not require any special access.</string>
 | 
			
		||||
    <string name="install_confirm">needs access to</string>
 | 
			
		||||
    <string name="install_confirm_update">Do you want to install an update
 | 
			
		||||
            to this existing application?  Your existing data will not
 | 
			
		||||
            be lost. The updated application will get access to:</string>
 | 
			
		||||
@ -365,11 +359,16 @@
 | 
			
		||||
    <string name="tap_to_install">Download completed, tap to install</string>
 | 
			
		||||
    <string name="download_error">Download unsuccessful</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_description_app">Provided by %1$s.</string>
 | 
			
		||||
    <string name="downloading">Downloading…</string>
 | 
			
		||||
    <string name="downloading_apk">Downloading %1$s</string>
 | 
			
		||||
    <string name="installing">Installing…</string>
 | 
			
		||||
    <string name="uninstalling">Uninstalling…</string>
 | 
			
		||||
 | 
			
		||||
    <string name="interval_never">Never</string>
 | 
			
		||||
    <string name="interval_1h">Hourly</string>
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,20 @@
 | 
			
		||||
        <item name="colorAccent">@color/fdroid_green</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <style name="MinWithDialogBaseThemeDark" parent="Theme.AppCompat.Dialog.MinWidth">
 | 
			
		||||
        <item name="colorAccent">@color/fdroid_green</item>
 | 
			
		||||
 | 
			
		||||
        <item name="windowActionBar">false</item>
 | 
			
		||||
        <item name="windowNoTitle">true</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <style name="MinWithDialogBaseThemeLight" parent="Theme.AppCompat.Light.Dialog.MinWidth">
 | 
			
		||||
        <item name="colorAccent">@color/fdroid_green</item>
 | 
			
		||||
 | 
			
		||||
        <item name="windowActionBar">false</item>
 | 
			
		||||
        <item name="windowNoTitle">true</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <style name="TextViewStyle" parent="android:Widget.TextView">
 | 
			
		||||
        <item name="android:textColor">?android:attr/textColorPrimary</item>
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user