diff options
| author | 2023-11-15 15:14:35 +0000 | |
|---|---|---|
| committer | 2023-11-21 19:59:27 +0000 | |
| commit | 83dd0af261c99fb4fe895240f773d1e2e0760593 (patch) | |
| tree | c4c40e02f36e62a93cb3a4f3d7be7d57a9a91ade | |
| parent | cb62207ec5003ff693e8099d8f537461da1978ae (diff) | |
Add confirmation dialog for unarchival if app only possesses weak
permissions.
Test: PackageInstallerArchiveTest
Bug: 305902395
Change-Id: I9f3bb5bf1ba6c0ed5164ac8be644287ee95251d9
7 files changed, 301 insertions, 28 deletions
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml index 6e47689d585c..0d1c9b07bf55 100644 --- a/packages/PackageInstaller/AndroidManifest.xml +++ b/packages/PackageInstaller/AndroidManifest.xml @@ -181,6 +181,18 @@ <receiver android:name="androidx.profileinstaller.ProfileInstallReceiver" tools:node="remove" /> + + <activity android:name=".UnarchiveActivity" + android:configChanges="orientation|keyboardHidden|screenSize" + android:theme="@style/Theme.AlertDialogActivity.NoActionBar" + android:excludeFromRecents="true" + android:noHistory="true" + android:exported="true"> + <intent-filter android:priority="1"> + <action android:name="android.intent.action.UNARCHIVE_DIALOG" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> </application> </manifest> diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml index 4eaa39bcd3a4..0a2e8809a17c 100644 --- a/packages/PackageInstaller/res/values/strings.xml +++ b/packages/PackageInstaller/res/values/strings.xml @@ -257,4 +257,14 @@ <!-- Notification shown in status bar when an application is successfully installed. [CHAR LIMIT=50] --> <string name="notification_installation_success_status">Successfully installed \u201c<xliff:g id="appname" example="Package Installer">%1$s</xliff:g>\u201d</string> + + <!-- The title of a dialog which asks the user to restore (i.e. re-install, re-download) an app + after parts of the app have been previously moved into the cloud for temporary storage. + "installername" is the app that will facilitate the download of the app. [CHAR LIMIT=50] --> + <string name="unarchive_application_title">Restore <xliff:g id="appname" example="Bird Game">%1$s</xliff:g> from <xliff:g id="installername" example="App Store">%1$s</xliff:g>?</string> + <!-- After the user confirms the dialog, a download will start. [CHAR LIMIT=none] --> + <string name="unarchive_body_text">This app will begin to download in the background</string> + <!-- The action to restore (i.e. re-install, re-download) an app after parts of the app have been previously moved + into the cloud for temporary storage. [CHAR LIMIT=15] --> + <string name="restore">Restore</string> </resources> diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java new file mode 100644 index 000000000000..754437e64e78 --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.packageinstaller; + +import static android.Manifest.permission; +import static android.content.pm.PackageManager.GET_PERMISSIONS; +import static android.content.pm.PackageManager.MATCH_ARCHIVED_PACKAGES; + +import android.app.Activity; +import android.app.DialogFragment; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.content.IntentSender; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.Process; +import android.util.Log; + +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +public class UnarchiveActivity extends Activity { + + public static final String EXTRA_UNARCHIVE_INTENT_SENDER = + "android.content.pm.extra.UNARCHIVE_INTENT_SENDER"; + static final String APP_TITLE = "com.android.packageinstaller.unarchive.app_title"; + static final String INSTALLER_TITLE = "com.android.packageinstaller.unarchive.installer_title"; + + private static final String TAG = "UnarchiveActivity"; + + private String mPackageName; + private IntentSender mIntentSender; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(null); + + int callingUid = getLaunchedFromUid(); + if (callingUid == Process.INVALID_UID) { + // Cannot reach Package/ActivityManager. Aborting uninstall. + Log.e(TAG, "Could not determine the launching uid."); + + setResult(Activity.RESULT_FIRST_USER); + finish(); + return; + } + + String callingPackage = getPackageNameForUid(callingUid); + if (callingPackage == null) { + Log.e(TAG, "Package not found for originating uid " + callingUid); + setResult(Activity.RESULT_FIRST_USER); + finish(); + return; + } + + // We don't check the AppOpsManager here for REQUEST_INSTALL_PACKAGES because the requester + // is not the source of the installation. + boolean hasRequestInstallPermission = Arrays.asList(getRequestedPermissions(callingPackage)) + .contains(permission.REQUEST_INSTALL_PACKAGES); + boolean hasInstallPermission = getBaseContext().checkPermission(permission.INSTALL_PACKAGES, + 0 /* random value for pid */, callingUid) != PackageManager.PERMISSION_GRANTED; + if (!hasRequestInstallPermission && !hasInstallPermission) { + Log.e(TAG, "Uid " + callingUid + " does not have " + + permission.REQUEST_INSTALL_PACKAGES + " or " + + permission.INSTALL_PACKAGES); + setResult(Activity.RESULT_FIRST_USER); + finish(); + return; + } + + Bundle extras = getIntent().getExtras(); + mPackageName = extras.getString(PackageInstaller.EXTRA_PACKAGE_NAME); + mIntentSender = extras.getParcelable(EXTRA_UNARCHIVE_INTENT_SENDER, IntentSender.class); + Objects.requireNonNull(mPackageName); + Objects.requireNonNull(mIntentSender); + + PackageManager pm = getPackageManager(); + try { + String appTitle = pm.getApplicationInfo(mPackageName, + PackageManager.ApplicationInfoFlags.of( + MATCH_ARCHIVED_PACKAGES)).loadLabel(pm).toString(); + // TODO(ag/25387215) Get the real installer title here after fixing getInstallSource for + // archived apps. + showDialogFragment(appTitle, "installerTitle"); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Invalid packageName: " + e.getMessage()); + } + } + + @Nullable + private String[] getRequestedPermissions(String callingPackage) { + String[] requestedPermissions = null; + try { + requestedPermissions = getPackageManager() + .getPackageInfo(callingPackage, GET_PERMISSIONS).requestedPermissions; + } catch (PackageManager.NameNotFoundException e) { + // Should be unreachable because we've just fetched the packageName above. + Log.e(TAG, "Package not found for " + callingPackage); + } + return requestedPermissions; + } + + void startUnarchive() { + try { + getPackageManager().getPackageInstaller().requestUnarchive(mPackageName, mIntentSender); + } catch (PackageManager.NameNotFoundException | IOException e) { + Log.e(TAG, "RequestUnarchive failed with %s." + e.getMessage()); + } + } + + private void showDialogFragment(String appTitle, String installerAppTitle) { + FragmentTransaction ft = getFragmentManager().beginTransaction(); + Fragment prev = getFragmentManager().findFragmentByTag("dialog"); + if (prev != null) { + ft.remove(prev); + } + + Bundle args = new Bundle(); + args.putString(APP_TITLE, appTitle); + args.putString(INSTALLER_TITLE, installerAppTitle); + DialogFragment fragment = new UnarchiveFragment(); + fragment.setArguments(args); + fragment.show(ft, "dialog"); + } + + private String getPackageNameForUid(int sourceUid) { + String[] packagesForUid = getPackageManager().getPackagesForUid(sourceUid); + if (packagesForUid == null) { + return null; + } + return packagesForUid[0]; + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java new file mode 100644 index 000000000000..6ccbc4cb5e6b --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.packageinstaller; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.os.Bundle; + +public class UnarchiveFragment extends DialogFragment implements + DialogInterface.OnClickListener { + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + String appTitle = getArguments().getString(UnarchiveActivity.APP_TITLE); + + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity()); + + dialogBuilder.setTitle( + String.format(getContext().getString(R.string.unarchive_application_title), + appTitle)); + dialogBuilder.setMessage(R.string.unarchive_body_text); + + dialogBuilder.setPositiveButton(R.string.restore, this); + dialogBuilder.setNegativeButton(android.R.string.cancel, this); + + return dialogBuilder.create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == Dialog.BUTTON_POSITIVE) { + ((UnarchiveActivity) getActivity()).startUnarchive(); + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if (isAdded()) { + getActivity().finish(); + } + } +} diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index d2a4c2713097..968be5c2cf1c 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -95,6 +95,9 @@ public class PackageArchiver { private static final String TAG = "PackageArchiverService"; + public static final String EXTRA_UNARCHIVE_INTENT_SENDER = + "android.content.pm.extra.UNARCHIVE_INTENT_SENDER"; + /** * The maximum time granted for an app store to start a foreground service when unarchival * is requested. @@ -104,6 +107,8 @@ public class PackageArchiver { private static final String ARCHIVE_ICONS_DIR = "package_archiver"; + private static final String ACTION_UNARCHIVE_DIALOG = "android.intent.action.UNARCHIVE_DIALOG"; + private final Context mContext; private final PackageManagerService mPm; @@ -403,11 +408,12 @@ public class PackageArchiver { } snapshot.enforceCrossUserPermission(binderUid, userId, true, true, "unarchiveApp"); - verifyInstallPermissions(); PackageStateInternal ps; + PackageStateInternal callerPs; try { ps = getPackageState(packageName, snapshot, binderUid, userId); + callerPs = getPackageState(callerPackageName, snapshot, binderUid, userId); verifyArchived(ps, userId); } catch (PackageManager.NameNotFoundException e) { throw new ParcelableException(e); @@ -420,12 +426,32 @@ public class PackageArchiver { packageName))); } - // TODO(b/305902395) Introduce a confirmation dialog if the requestor only holds - // REQUEST_INSTALL permission. + boolean hasInstallPackages = mContext.checkCallingOrSelfPermission( + Manifest.permission.INSTALL_PACKAGES) + == PackageManager.PERMISSION_GRANTED; + // We don't check the AppOpsManager here for REQUEST_INSTALL_PACKAGES because the requester + // is not the source of the installation. + boolean hasRequestInstallPackages = callerPs.getAndroidPackage().getRequestedPermissions() + .contains(android.Manifest.permission.REQUEST_INSTALL_PACKAGES); + if (!hasInstallPackages && !hasRequestInstallPackages) { + throw new SecurityException("You need the com.android.permission.INSTALL_PACKAGES " + + "or com.android.permission.REQUEST_INSTALL_PACKAGES permission to request " + + "an unarchival."); + } + + if (!hasInstallPackages) { + requestUnarchiveConfirmation(packageName, statusReceiver); + return; + } + + // TODO(b/311709794) Check that the responsible installer has INSTALL_PACKAGES or + // OPSTR_REQUEST_INSTALL_PACKAGES too. Edge case: In reality this should always be the case, + // unless a user has disabled the permission after archiving an app. + int draftSessionId; try { - draftSessionId = createDraftSession(packageName, installerPackage, statusReceiver, - userId); + draftSessionId = Binder.withCleanCallingIdentity(() -> + createDraftSession(packageName, installerPackage, statusReceiver, userId)); } catch (RuntimeException e) { if (e.getCause() instanceof IOException) { throw ExceptionUtils.wrap((IOException) e.getCause()); @@ -438,15 +464,17 @@ public class PackageArchiver { () -> unarchiveInternal(packageName, userHandle, installerPackage, draftSessionId)); } - private void verifyInstallPermissions() { - if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES) - != PackageManager.PERMISSION_GRANTED && mContext.checkCallingOrSelfPermission( - Manifest.permission.REQUEST_INSTALL_PACKAGES) - != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("You need the com.android.permission.INSTALL_PACKAGES " - + "or com.android.permission.REQUEST_INSTALL_PACKAGES permission to request " - + "an unarchival."); - } + private void requestUnarchiveConfirmation(String packageName, IntentSender statusReceiver) { + final Intent dialogIntent = new Intent(ACTION_UNARCHIVE_DIALOG); + dialogIntent.putExtra(EXTRA_UNARCHIVE_INTENT_SENDER, statusReceiver); + dialogIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); + + final Intent broadcastIntent = new Intent(); + broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); + broadcastIntent.putExtra(PackageInstaller.EXTRA_UNARCHIVE_STATUS, + PackageInstaller.STATUS_PENDING_USER_ACTION); + broadcastIntent.putExtra(Intent.EXTRA_INTENT, dialogIntent); + sendIntent(statusReceiver, packageName, /* message= */ "", broadcastIntent); } private void verifyUninstallPermissions() { @@ -461,7 +489,7 @@ public class PackageArchiver { } private int createDraftSession(String packageName, String installerPackage, - IntentSender statusReceiver, int userId) { + IntentSender statusReceiver, int userId) throws IOException { PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams( PackageInstaller.SessionParams.MODE_FULL_INSTALL); sessionParams.setAppPackageName(packageName); @@ -477,12 +505,11 @@ public class PackageArchiver { return existingSessionId; } - int sessionId = Binder.withCleanCallingIdentity( - () -> mPm.mInstallerService.createSessionInternal( - sessionParams, - installerPackage, mContext.getAttributionTag(), - installerUid, - userId)); + int sessionId = mPm.mInstallerService.createSessionInternal( + sessionParams, + installerPackage, mContext.getAttributionTag(), + installerUid, + userId); // TODO(b/297358628) Also cleanup sessions upon device restart. mPm.mHandler.postDelayed(() -> mPm.mInstallerService.cleanupDraftIfUnclaimed(sessionId), getUnarchiveForegroundTimeout()); @@ -692,20 +719,25 @@ public class PackageArchiver { String message) { Slog.d(TAG, TextUtils.formatSimple("Failed to archive %s with message %s", packageName, message)); - final Intent fillIn = new Intent(); - fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); - fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); - fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, message); + final Intent intent = new Intent(); + intent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); + intent.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, message); + sendIntent(statusReceiver, packageName, message, intent); + } + + private void sendIntent(IntentSender statusReceiver, String packageName, String message, + Intent intent) { try { final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setPendingIntentBackgroundActivityStartMode( MODE_BACKGROUND_ACTIVITY_START_DENIED); - statusReceiver.sendIntent(mContext, 0, fillIn, /* onFinished= */ null, + statusReceiver.sendIntent(mContext, 0, intent, /* onFinished= */ null, /* handler= */ null, /* requiredPermission= */ null, options.toBundle()); } catch (IntentSender.SendIntentException e) { Slog.e( TAG, - TextUtils.formatSimple("Failed to send failure status for %s with message %s", + TextUtils.formatSimple("Failed to send status for %s with message %s", packageName, message), e); } diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index f992bd83a8de..fc662038d5d5 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -4693,7 +4693,7 @@ class PackageManagerShellCommand extends ShellCommand { try { mInterface.getPackageInstaller().requestUnarchive(packageName, - /* callerPackageName= */ "", receiver.getIntentSender(), + mContext.getPackageName(), receiver.getIntentSender(), new UserHandle(translatedUserId)); } catch (Exception e) { pw.println("Failure [" + e.getMessage() + "]"); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java index 18a2accf071d..733a43329478 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java @@ -65,6 +65,7 @@ import android.text.TextUtils; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.ArchiveState; import com.android.server.pm.pkg.PackageStateInternal; import com.android.server.pm.pkg.PackageUserStateImpl; @@ -81,6 +82,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Set; @SmallTest @Presubmit @@ -114,6 +116,8 @@ public class PackageArchiverTest { @Mock private PackageStateInternal mPackageState; @Mock + private PackageStateInternal mCallerPackageState; + @Mock private Bitmap mIcon; private final InstallSource mInstallSource = @@ -155,6 +159,11 @@ public class PackageArchiverTest { mPackageState); when(mComputer.getPackageStateFiltered(eq(INSTALLER_PACKAGE), anyInt(), anyInt())).thenReturn(mock(PackageStateInternal.class)); + when(mComputer.getPackageStateFiltered(eq(CALLER_PACKAGE), anyInt(), anyInt())).thenReturn( + mCallerPackageState); + AndroidPackage androidPackage = mock(AndroidPackage.class); + when(mCallerPackageState.getAndroidPackage()).thenReturn(androidPackage); + when(androidPackage.getRequestedPermissions()).thenReturn(Set.of()); when(mPackageState.getPackageName()).thenReturn(PACKAGE); when(mPackageState.getInstallSource()).thenReturn(mInstallSource); mPackageSetting = createBasicPackageSetting(); |