diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 85f2b0310..9fb2fe575 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -316,12 +316,18 @@ + + + + + @@ -440,6 +454,9 @@ + diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails.java b/app/src/main/java/org/fdroid/fdroid/AppDetails.java index 70d5f5454..5eb446118 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppDetails.java +++ b/app/src/main/java/org/fdroid/fdroid/AppDetails.java @@ -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 diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index df5e614c1..95e0ea708 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -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); } diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index c93a6e466..73b81d805 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -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 toArrayList() { + ArrayList out = new ArrayList<>(); + for (String element : this) { + out.add(element); + } + return out; + } + + public String[] toArray() { + ArrayList list = toArrayList(); + return list.toArray(new String[list.size()]); + } + public boolean contains(String v) { for (final String s : this) { if (s.equals(v)) { diff --git a/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java b/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java index f1629c657..e1395f92d 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java @@ -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) { diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ApkSignatureVerifier.java b/app/src/main/java/org/fdroid/fdroid/installer/ApkSignatureVerifier.java index 27b6aeb3f..50af70798 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ApkSignatureVerifier.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkSignatureVerifier.java @@ -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) { diff --git a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java index a6fa2b9be..c6d73654f 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Dominik Schürmann + * Copyright (C) 2016 Dominik Schürmann * * 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. + *

+ * 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; } } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java new file mode 100644 index 000000000..6e1e64ff4 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstallerActivity.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2014-2016 Dominik Schürmann + * + * 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(); + } + +} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/DefaultSdk14Installer.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultSdk14Installer.java deleted file mode 100644 index 6ca757dc6..000000000 --- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultSdk14Installer.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2014 Dominik Schürmann - * - * 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; - } - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ErrorDialogActivity.java b/app/src/main/java/org/fdroid/fdroid/installer/ErrorDialogActivity.java new file mode 100644 index 000000000..9e38794e1 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/installer/ErrorDialogActivity.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016 Dominik Schürmann + * + * 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(); + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java new file mode 100644 index 000000000..970b8f99f --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2016 Dominik Schürmann + * + * 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. + *

+ * 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; + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index 237a2584b..9903ad188 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -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 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. - *

- * TODO delete me once InstallerService exists - */ - private static final HashMap 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 */ - 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) { diff --git a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java index f9800f74f..b1ed0ff13 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Dominik Schürmann + * Copyright (C) 2016 Dominik Schürmann * * 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 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); } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java new file mode 100644 index 000000000..f2ecd7b6a --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerFactory.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016 Dominik Schürmann + * + * 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(); + } + +} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java new file mode 100644 index 000000000..1af566fa3 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2016 Dominik Schürmann + * + * 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. + *

+ * 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); + } + +} diff --git a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java index 324ed7767..24a4001ff 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Dominik Schürmann + * Copyright (C) 2014-2016 Dominik Schürmann * Copyright (C) 2015 Daniel Martí * * 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 - *

    - *
  • android.permission.INSTALL_PACKAGES
  • - *
  • android.permission.DELETE_PACKAGES
  • - *
- * + * Installer that only works if the "F-Droid Privileged + * Extension" is installed as a privileged app. + *

+ * "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: - *

    - *
  • On all Android versions if F-Droid is pre-deployed as a - * system-application with the Rom
  • - *
  • On Android < 4.4 also when moved into /system/app/
  • - *
  • On Android >= 4.4 also when moved into /system/priv-app/
  • - *
- * + * system|signature) and cannot be used directly by F-Droid. + *

+ * 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. + *

+ * This installer makes unattended installs/uninstalls possible. + * Thus no PendingIntents are returned. + *

* 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 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 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; - } diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java b/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java index 5305483d2..02d4cf6be 100644 --- a/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java @@ -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"); } diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/views/AppDiff.java b/app/src/main/java/org/fdroid/fdroid/privileged/views/AppDiff.java index 096f0290b..03bb77592 100644 --- a/app/src/main/java/org/fdroid/fdroid/privileged/views/AppDiff.java +++ b/app/src/main/java/org/fdroid/fdroid/privileged/views/AppDiff.java @@ -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 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; diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/views/AppSecurityPermissions.java b/app/src/main/java/org/fdroid/fdroid/privileged/views/AppSecurityPermissions.java index b48108682..75981bbc5 100644 --- a/app/src/main/java/org/fdroid/fdroid/privileged/views/AppSecurityPermissions.java +++ b/app/src/main/java/org/fdroid/fdroid/privileged/views/AppSecurityPermissions.java @@ -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); } diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/views/InstallConfirmActivity.java b/app/src/main/java/org/fdroid/fdroid/privileged/views/InstallConfirmActivity.java index ceddb8f43..e6a45238b 100644 --- a/app/src/main/java/org/fdroid/fdroid/privileged/views/InstallConfirmActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/privileged/views/InstallConfirmActivity.java @@ -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(); } diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/views/UninstallDialogActivity.java b/app/src/main/java/org/fdroid/fdroid/privileged/views/UninstallDialogActivity.java new file mode 100644 index 000000000..f0d2bd04c --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/privileged/views/UninstallDialogActivity.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2016 Dominik Schürmann + * + * 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(); + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java b/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java index 18baad9e1..ff29171f6 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java +++ b/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java @@ -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"); } diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index d789ac7d0..f6a81b9c8 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -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!"); + } + } + }; + } diff --git a/app/src/main/res/layout-v11/install_confirm.xml b/app/src/main/res/layout-v11/install_confirm.xml index 7676b2b75..e0d978ff1 100644 --- a/app/src/main/res/layout-v11/install_confirm.xml +++ b/app/src/main/res/layout-v11/install_confirm.xml @@ -21,37 +21,35 @@ user before it is installed. --> - + + 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" /> + 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:visibility="gone"> + android:layout_height="match_parent" + android:orientation="vertical"> - - + + + + android:layout_gravity="center" + android:orientation="horizontal" /> @@ -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:layout_weight="1" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:divider="?android:attr/dividerHorizontal" + android:orientation="vertical" + android:showDividers="beginning"> + + + android:visibility="gone" /> - +