Loading packages/PackageInstaller/AndroidManifest.xml +12 −0 Original line number Original line Diff line number Diff line Loading @@ -181,6 +181,18 @@ <receiver android:name="androidx.profileinstaller.ProfileInstallReceiver" <receiver android:name="androidx.profileinstaller.ProfileInstallReceiver" tools:node="remove" /> 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> </application> </manifest> </manifest> packages/PackageInstaller/res/values/strings.xml +10 −0 Original line number Original line Diff line number Diff line Loading @@ -257,4 +257,14 @@ <!-- Notification shown in status bar when an application is successfully installed. <!-- Notification shown in status bar when an application is successfully installed. [CHAR LIMIT=50] --> [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> <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> </resources> packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java 0 → 100644 +151 −0 Original line number Original line Diff line number Diff line /* * 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]; } } packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java 0 → 100644 +59 −0 Original line number Original line Diff line number Diff line /* * 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(); } } } services/core/java/com/android/server/pm/PackageArchiver.java +59 −27 Original line number Original line Diff line number Diff line Loading @@ -95,6 +95,9 @@ public class PackageArchiver { private static final String TAG = "PackageArchiverService"; 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 * The maximum time granted for an app store to start a foreground service when unarchival * is requested. * is requested. Loading @@ -104,6 +107,8 @@ public class PackageArchiver { private static final String ARCHIVE_ICONS_DIR = "package_archiver"; 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 Context mContext; private final PackageManagerService mPm; private final PackageManagerService mPm; Loading Loading @@ -403,11 +408,12 @@ public class PackageArchiver { } } snapshot.enforceCrossUserPermission(binderUid, userId, true, true, snapshot.enforceCrossUserPermission(binderUid, userId, true, true, "unarchiveApp"); "unarchiveApp"); verifyInstallPermissions(); PackageStateInternal ps; PackageStateInternal ps; PackageStateInternal callerPs; try { try { ps = getPackageState(packageName, snapshot, binderUid, userId); ps = getPackageState(packageName, snapshot, binderUid, userId); callerPs = getPackageState(callerPackageName, snapshot, binderUid, userId); verifyArchived(ps, userId); verifyArchived(ps, userId); } catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) { throw new ParcelableException(e); throw new ParcelableException(e); Loading @@ -420,12 +426,32 @@ public class PackageArchiver { packageName))); packageName))); } } // TODO(b/305902395) Introduce a confirmation dialog if the requestor only holds boolean hasInstallPackages = mContext.checkCallingOrSelfPermission( // REQUEST_INSTALL permission. 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; int draftSessionId; try { try { draftSessionId = createDraftSession(packageName, installerPackage, statusReceiver, draftSessionId = Binder.withCleanCallingIdentity(() -> userId); createDraftSession(packageName, installerPackage, statusReceiver, userId)); } catch (RuntimeException e) { } catch (RuntimeException e) { if (e.getCause() instanceof IOException) { if (e.getCause() instanceof IOException) { throw ExceptionUtils.wrap((IOException) e.getCause()); throw ExceptionUtils.wrap((IOException) e.getCause()); Loading @@ -438,15 +464,17 @@ public class PackageArchiver { () -> unarchiveInternal(packageName, userHandle, installerPackage, draftSessionId)); () -> unarchiveInternal(packageName, userHandle, installerPackage, draftSessionId)); } } private void verifyInstallPermissions() { private void requestUnarchiveConfirmation(String packageName, IntentSender statusReceiver) { if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES) final Intent dialogIntent = new Intent(ACTION_UNARCHIVE_DIALOG); != PackageManager.PERMISSION_GRANTED && mContext.checkCallingOrSelfPermission( dialogIntent.putExtra(EXTRA_UNARCHIVE_INTENT_SENDER, statusReceiver); Manifest.permission.REQUEST_INSTALL_PACKAGES) dialogIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("You need the com.android.permission.INSTALL_PACKAGES " final Intent broadcastIntent = new Intent(); + "or com.android.permission.REQUEST_INSTALL_PACKAGES permission to request " broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); + "an unarchival."); 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() { private void verifyUninstallPermissions() { Loading @@ -461,7 +489,7 @@ public class PackageArchiver { } } private int createDraftSession(String packageName, String installerPackage, 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 sessionParams = new PackageInstaller.SessionParams( PackageInstaller.SessionParams.MODE_FULL_INSTALL); PackageInstaller.SessionParams.MODE_FULL_INSTALL); sessionParams.setAppPackageName(packageName); sessionParams.setAppPackageName(packageName); Loading @@ -477,12 +505,11 @@ public class PackageArchiver { return existingSessionId; return existingSessionId; } } int sessionId = Binder.withCleanCallingIdentity( int sessionId = mPm.mInstallerService.createSessionInternal( () -> mPm.mInstallerService.createSessionInternal( sessionParams, sessionParams, installerPackage, mContext.getAttributionTag(), installerPackage, mContext.getAttributionTag(), installerUid, installerUid, userId)); userId); // TODO(b/297358628) Also cleanup sessions upon device restart. // TODO(b/297358628) Also cleanup sessions upon device restart. mPm.mHandler.postDelayed(() -> mPm.mInstallerService.cleanupDraftIfUnclaimed(sessionId), mPm.mHandler.postDelayed(() -> mPm.mInstallerService.cleanupDraftIfUnclaimed(sessionId), getUnarchiveForegroundTimeout()); getUnarchiveForegroundTimeout()); Loading Loading @@ -692,20 +719,25 @@ public class PackageArchiver { String message) { String message) { Slog.d(TAG, TextUtils.formatSimple("Failed to archive %s with message %s", packageName, Slog.d(TAG, TextUtils.formatSimple("Failed to archive %s with message %s", packageName, message)); message)); final Intent fillIn = new Intent(); final Intent intent = new Intent(); fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); intent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); intent.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, message); intent.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, message); sendIntent(statusReceiver, packageName, message, intent); } private void sendIntent(IntentSender statusReceiver, String packageName, String message, Intent intent) { try { try { final BroadcastOptions options = BroadcastOptions.makeBasic(); final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setPendingIntentBackgroundActivityStartMode( options.setPendingIntentBackgroundActivityStartMode( MODE_BACKGROUND_ACTIVITY_START_DENIED); 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()); /* handler= */ null, /* requiredPermission= */ null, options.toBundle()); } catch (IntentSender.SendIntentException e) { } catch (IntentSender.SendIntentException e) { Slog.e( Slog.e( TAG, 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), packageName, message), e); e); } } Loading Loading
packages/PackageInstaller/AndroidManifest.xml +12 −0 Original line number Original line Diff line number Diff line Loading @@ -181,6 +181,18 @@ <receiver android:name="androidx.profileinstaller.ProfileInstallReceiver" <receiver android:name="androidx.profileinstaller.ProfileInstallReceiver" tools:node="remove" /> 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> </application> </manifest> </manifest>
packages/PackageInstaller/res/values/strings.xml +10 −0 Original line number Original line Diff line number Diff line Loading @@ -257,4 +257,14 @@ <!-- Notification shown in status bar when an application is successfully installed. <!-- Notification shown in status bar when an application is successfully installed. [CHAR LIMIT=50] --> [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> <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> </resources>
packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java 0 → 100644 +151 −0 Original line number Original line Diff line number Diff line /* * 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]; } }
packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java 0 → 100644 +59 −0 Original line number Original line Diff line number Diff line /* * 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(); } } }
services/core/java/com/android/server/pm/PackageArchiver.java +59 −27 Original line number Original line Diff line number Diff line Loading @@ -95,6 +95,9 @@ public class PackageArchiver { private static final String TAG = "PackageArchiverService"; 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 * The maximum time granted for an app store to start a foreground service when unarchival * is requested. * is requested. Loading @@ -104,6 +107,8 @@ public class PackageArchiver { private static final String ARCHIVE_ICONS_DIR = "package_archiver"; 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 Context mContext; private final PackageManagerService mPm; private final PackageManagerService mPm; Loading Loading @@ -403,11 +408,12 @@ public class PackageArchiver { } } snapshot.enforceCrossUserPermission(binderUid, userId, true, true, snapshot.enforceCrossUserPermission(binderUid, userId, true, true, "unarchiveApp"); "unarchiveApp"); verifyInstallPermissions(); PackageStateInternal ps; PackageStateInternal ps; PackageStateInternal callerPs; try { try { ps = getPackageState(packageName, snapshot, binderUid, userId); ps = getPackageState(packageName, snapshot, binderUid, userId); callerPs = getPackageState(callerPackageName, snapshot, binderUid, userId); verifyArchived(ps, userId); verifyArchived(ps, userId); } catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) { throw new ParcelableException(e); throw new ParcelableException(e); Loading @@ -420,12 +426,32 @@ public class PackageArchiver { packageName))); packageName))); } } // TODO(b/305902395) Introduce a confirmation dialog if the requestor only holds boolean hasInstallPackages = mContext.checkCallingOrSelfPermission( // REQUEST_INSTALL permission. 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; int draftSessionId; try { try { draftSessionId = createDraftSession(packageName, installerPackage, statusReceiver, draftSessionId = Binder.withCleanCallingIdentity(() -> userId); createDraftSession(packageName, installerPackage, statusReceiver, userId)); } catch (RuntimeException e) { } catch (RuntimeException e) { if (e.getCause() instanceof IOException) { if (e.getCause() instanceof IOException) { throw ExceptionUtils.wrap((IOException) e.getCause()); throw ExceptionUtils.wrap((IOException) e.getCause()); Loading @@ -438,15 +464,17 @@ public class PackageArchiver { () -> unarchiveInternal(packageName, userHandle, installerPackage, draftSessionId)); () -> unarchiveInternal(packageName, userHandle, installerPackage, draftSessionId)); } } private void verifyInstallPermissions() { private void requestUnarchiveConfirmation(String packageName, IntentSender statusReceiver) { if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES) final Intent dialogIntent = new Intent(ACTION_UNARCHIVE_DIALOG); != PackageManager.PERMISSION_GRANTED && mContext.checkCallingOrSelfPermission( dialogIntent.putExtra(EXTRA_UNARCHIVE_INTENT_SENDER, statusReceiver); Manifest.permission.REQUEST_INSTALL_PACKAGES) dialogIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("You need the com.android.permission.INSTALL_PACKAGES " final Intent broadcastIntent = new Intent(); + "or com.android.permission.REQUEST_INSTALL_PACKAGES permission to request " broadcastIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); + "an unarchival."); 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() { private void verifyUninstallPermissions() { Loading @@ -461,7 +489,7 @@ public class PackageArchiver { } } private int createDraftSession(String packageName, String installerPackage, 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 sessionParams = new PackageInstaller.SessionParams( PackageInstaller.SessionParams.MODE_FULL_INSTALL); PackageInstaller.SessionParams.MODE_FULL_INSTALL); sessionParams.setAppPackageName(packageName); sessionParams.setAppPackageName(packageName); Loading @@ -477,12 +505,11 @@ public class PackageArchiver { return existingSessionId; return existingSessionId; } } int sessionId = Binder.withCleanCallingIdentity( int sessionId = mPm.mInstallerService.createSessionInternal( () -> mPm.mInstallerService.createSessionInternal( sessionParams, sessionParams, installerPackage, mContext.getAttributionTag(), installerPackage, mContext.getAttributionTag(), installerUid, installerUid, userId)); userId); // TODO(b/297358628) Also cleanup sessions upon device restart. // TODO(b/297358628) Also cleanup sessions upon device restart. mPm.mHandler.postDelayed(() -> mPm.mInstallerService.cleanupDraftIfUnclaimed(sessionId), mPm.mHandler.postDelayed(() -> mPm.mInstallerService.cleanupDraftIfUnclaimed(sessionId), getUnarchiveForegroundTimeout()); getUnarchiveForegroundTimeout()); Loading Loading @@ -692,20 +719,25 @@ public class PackageArchiver { String message) { String message) { Slog.d(TAG, TextUtils.formatSimple("Failed to archive %s with message %s", packageName, Slog.d(TAG, TextUtils.formatSimple("Failed to archive %s with message %s", packageName, message)); message)); final Intent fillIn = new Intent(); final Intent intent = new Intent(); fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); intent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName); fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); intent.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, message); intent.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, message); sendIntent(statusReceiver, packageName, message, intent); } private void sendIntent(IntentSender statusReceiver, String packageName, String message, Intent intent) { try { try { final BroadcastOptions options = BroadcastOptions.makeBasic(); final BroadcastOptions options = BroadcastOptions.makeBasic(); options.setPendingIntentBackgroundActivityStartMode( options.setPendingIntentBackgroundActivityStartMode( MODE_BACKGROUND_ACTIVITY_START_DENIED); 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()); /* handler= */ null, /* requiredPermission= */ null, options.toBundle()); } catch (IntentSender.SendIntentException e) { } catch (IntentSender.SendIntentException e) { Slog.e( Slog.e( TAG, 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), packageName, message), e); e); } } Loading