diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6f5d2d622..b4ae7cd49 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -316,12 +316,18 @@ + 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/DefaultInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java index d43f6d040..169c2a07c 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/DefaultInstaller.java @@ -85,4 +85,9 @@ public class DefaultInstaller extends Installer { sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_USER_INTERACTION, uninstallPendingIntent); } + + @Override + protected boolean isUnattended() { + return false; + } } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java index c778a6897..8782085bf 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ExtensionInstaller.java @@ -96,4 +96,9 @@ public class ExtensionInstaller extends Installer { // 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/Installer.java b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java index 3663ecd5c..1a8e1de9a 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/Installer.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/Installer.java @@ -37,6 +37,8 @@ import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.SanitizedFile; 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; @@ -153,48 +155,55 @@ public abstract class Installer { return Uri.fromFile(sanitizedApkFile); } - public PendingIntent getPermissionScreen(Apk apk) { - // old code: -// Uri packageUri = Uri.fromFile(apkFile); -// int count = newPermissionCount(packageUri); -// if (count < 0) { -// mCallback.onError(InstallerCallback.OPERATION_INSTALL, -// InstallerCallback.ERROR_CODE_CANNOT_PARSE); + public Intent getPermissionScreen(Apk apk) { + if (!isUnattended()) { + return null; + } -// install_error_cannot_parse + int count = newPermissionCount(apk); + if (count > 0) { + Uri uri = ApkProvider.getContentUri(apk); + Intent intent = new Intent(mContext, InstallConfirmActivity.class); + intent.setData(uri); -// 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); -// } -// } - return null; + return intent; + } else { + // no permission screen needed! + return null; + } } + 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; +// } - private int newPermissionCount(Uri packageUri) { - AppDiff appDiff = new AppDiff(mContext.getPackageManager(), packageUri); + AppDiff appDiff = new AppDiff(mContext.getPackageManager(), apk); if (appDiff.mPkgInfo == null) { // could not get diff because we couldn't parse the package - return -1; + throw new RuntimeException("cannot parse!"); } 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; + // new app install + return perms.getPermissionCount(AppSecurityPermissions.WHICH_ALL); + } + + public Intent getUninstallScreen(String packageName) { + if (!isUnattended()) { + return null; + } + + Intent intent = new Intent(mContext, UninstallDialogActivity.class); + intent.putExtra(Installer.EXTRA_PACKAGE_NAME, packageName); + + return intent; } /** @@ -287,4 +296,6 @@ public abstract class Installer { protected abstract void uninstallPackage(String packageName); + protected abstract boolean isUnattended(); + } 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 8c3dfcdb4..420adc0b5 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/PrivilegedInstaller.java @@ -238,7 +238,7 @@ public class PrivilegedInstaller extends Installer { private static final HashMap sUninstallReturnCodes; static { - // Descriptions extrgacted from the source code comments in AOSP + // Descriptions extracted from the source code comments in AOSP sUninstallReturnCodes = new HashMap<>(); sUninstallReturnCodes.put(DELETE_SUCCEEDED, "Success"); @@ -324,6 +324,7 @@ public class PrivilegedInstaller extends Installer { @Override protected void installPackage(final Uri uri, final Uri originatingUri, String packageName) { + sendBroadcastInstall(uri, originatingUri, Installer.ACTION_INSTALL_STARTED); final Uri sanitizedUri; try { @@ -374,6 +375,8 @@ public class PrivilegedInstaller extends Installer { @Override protected void uninstallPackage(final String packageName) { + sendBroadcastUninstall(packageName, Installer.ACTION_UNINSTALL_STARTED); + ApplicationInfo appInfo; try { //noinspection WrongConstant (lint is actually wrong here!) @@ -466,33 +469,9 @@ public class PrivilegedInstaller extends Installer { 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); - -// install_error_cannot_parse - -// } else { // Activity.RESULT_CANCELED -// mCallback.onError(InstallerCallback.OPERATION_INSTALL, -// InstallerCallback.ERROR_CODE_CANCELED); -// } -// return true; -// default: -// return false; -// } -// } - + @Override + protected boolean isUnattended() { + return true; + } } 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..46829a162 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,29 @@ public class AppDiff { public ApplicationInfo mInstalledAppInfo; + /** + * Constructor based on F-Droids Apk object + */ + public AppDiff(PackageManager mPm, Apk apk) { + this.mPm = mPm; + + if (apk.permissions == null) { + throw new RuntimeException("apk.permissions is null"); + } + mPkgInfo = new PackageInfo(); + mPkgInfo.packageName = apk.packageName; + mPkgInfo.applicationInfo = new ApplicationInfo(); + + // 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 +85,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..3d983633b 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,16 @@ 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,15 +38,23 @@ 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; @@ -67,16 +75,27 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener 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); @@ -136,7 +155,7 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener : 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); @@ -171,20 +190,28 @@ public class InstallConfirmActivity extends Activity implements OnCancelListener protected void onCreate(Bundle icicle) { super.onCreate(icicle); - ((FDroidApp) getApplication()).applyTheme(this); + ((FDroidApp) getApplication()).applyDialogTheme(this); mPm = getPackageManager(); 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); + mAppDiff = new AppDiff(mPm, apk); if (mAppDiff.mPkgInfo == null) { setResult(RESULT_CANNOT_PARSE, intent); finish(); } setContentView(R.layout.install_start); + + // 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); + mInstallConfirm = findViewById(R.id.install_confirm_panel); mInstallConfirm.setVisibility(View.INVISIBLE); 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/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" /> - +