diff options
| author | 2024-05-08 17:37:51 +0000 | |
|---|---|---|
| committer | 2024-05-08 17:37:51 +0000 | |
| commit | f054d1d24d19a6f62a9078eef4ceda5eb789e668 (patch) | |
| tree | ddc85436c19744b0694e213dff742e430515d341 | |
| parent | e1d3975e4c9e79aaca139361c587666c59c60e46 (diff) | |
| parent | 20c4f7fb3723f3cfe3ebab83693a9333785dfe59 (diff) | |
Merge changes from topic "single-update-owner-dialog" into main
* changes:
[pia v2] Show a user confirmation dialog only when update-ownership isn't changed
Show a user confirmation dialog only when update-ownership isn't changed
[piav2] Cleanup CL to prepare for single user-confirmation dialog change
Cleanup CL to prepare for single user-confirmation dialog change
7 files changed, 137 insertions, 55 deletions
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java index eef21991b845..c96644ca8920 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java @@ -23,23 +23,23 @@ import android.app.DialogFragment; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; -import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.View; - import androidx.annotation.Nullable; /** * Installation failed: Return status code to the caller or display failure UI to user */ public class InstallFailed extends Activity { + private static final String LOG_TAG = InstallFailed.class.getSimpleName(); - /** Label of the app that failed to install */ + /** + * Label of the app that failed to install + */ private CharSequence mLabel; private AlertDialog mDialog; @@ -80,29 +80,29 @@ public class InstallFailed extends Activity { setFinishOnTouchOutside(true); - int statusCode = getIntent().getIntExtra(PackageInstaller.EXTRA_STATUS, - PackageInstaller.STATUS_FAILURE); + Intent intent = getIntent(); + int statusCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, + PackageInstaller.STATUS_FAILURE); + boolean returnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false); - if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) { - int legacyStatus = getIntent().getIntExtra(PackageInstaller.EXTRA_LEGACY_STATUS, - PackageManager.INSTALL_FAILED_INTERNAL_ERROR); + if (returnResult) { + int legacyStatus = intent.getIntExtra(PackageInstaller.EXTRA_LEGACY_STATUS, + PackageManager.INSTALL_FAILED_INTERNAL_ERROR); // Return result if requested Intent result = new Intent(); result.putExtra(Intent.EXTRA_INSTALL_RESULT, legacyStatus); setResult(Activity.RESULT_FIRST_USER, result); finish(); - } else { - Intent intent = getIntent(); - ApplicationInfo appInfo = intent - .getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO); - Uri packageURI = intent.getData(); + } else if (statusCode != PackageInstaller.STATUS_FAILURE_ABORTED) { + // statusCode will be STATUS_FAILURE_ABORTED if the update-owner confirmation dialog was + // dismissed by the user. We don't want to show a InstallFailed dialog in this case. + // If the user denies install permission for normal installs, this dialog will never be + // triggered as the status code is returned from PackageInstallerActivity.java // Set header icon and title - PackageUtil.AppSnippet as; - PackageManager pm = getPackageManager(); - as = intent.getParcelableExtra(PackageInstallerActivity.EXTRA_APP_SNIPPET, - PackageUtil.AppSnippet.class); + PackageUtil.AppSnippet as = intent.getParcelableExtra( + PackageInstallerActivity.EXTRA_APP_SNIPPET, PackageUtil.AppSnippet.class); // Store label for dialog mLabel = as.label; @@ -127,6 +127,8 @@ public class InstallFailed extends Activity { // Get status messages setExplanationFromErrorCode(statusCode); + } else { + finish(); } } @@ -135,6 +137,7 @@ public class InstallFailed extends Activity { * "manage applications" settings page. */ public static class OutOfSpaceDialog extends DialogFragment { + private InstallFailed mActivity; @Override @@ -147,16 +150,16 @@ public class InstallFailed extends Activity { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { return new AlertDialog.Builder(mActivity) - .setTitle(R.string.out_of_space_dlg_title) - .setMessage(getString(R.string.out_of_space_dlg_text, mActivity.mLabel)) - .setPositiveButton(R.string.manage_applications, (dialog, which) -> { - // launch manage applications - Intent intent = new Intent("android.intent.action.MANAGE_PACKAGE_STORAGE"); - startActivity(intent); - mActivity.finish(); - }) - .setNegativeButton(R.string.cancel, (dialog, which) -> mActivity.finish()) - .create(); + .setTitle(R.string.out_of_space_dlg_title) + .setMessage(getString(R.string.out_of_space_dlg_text, mActivity.mLabel)) + .setPositiveButton(R.string.manage_applications, (dialog, which) -> { + // launch manage applications + Intent intent = new Intent("android.intent.action.MANAGE_PACKAGE_STORAGE"); + startActivity(intent); + mActivity.finish(); + }) + .setNegativeButton(R.string.cancel, (dialog, which) -> mActivity.finish()) + .create(); } @Override diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java index 1a6c2bb2ec18..59a511db5b3a 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java @@ -30,6 +30,8 @@ import android.content.pm.PackageManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.view.View; import android.widget.Button; @@ -91,8 +93,11 @@ public class InstallInstalling extends Activity { // ContentResolver.SCHEME_FILE // STAGED_SESSION_ID extra contains an ID of a previously staged install session. final File sourceFile = new File(mPackageURI.getPath()); - PackageUtil.AppSnippet as = getIntent() - .getParcelableExtra(EXTRA_APP_SNIPPET, PackageUtil.AppSnippet.class); + + // Dialogs displayed while changing update-owner have a blank icon. To fix this, + // fetch the appSnippet from the source file again + PackageUtil.AppSnippet as = PackageUtil.getAppSnippet(this, appInfo, sourceFile); + getIntent().putExtra(EXTRA_APP_SNIPPET, as); AlertDialog.Builder builder = new AlertDialog.Builder(this); @@ -244,6 +249,14 @@ public class InstallInstalling extends Activity { super.onDestroy(); } + @Override + public void finish() { + if (mDialog != null) { + mDialog.dismiss(); + } + super.finish(); + } + /** * Launch the appropriate finish activity (success or failed) for the installation result. * @@ -299,7 +312,11 @@ public class InstallInstalling extends Activity { PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); try { - session.commit(pendingIntent.getIntentSender()); + // Delay committing the session by 100ms to fix a UI glitch while displaying the + // Update-Owner change dialog on top of the Installing dialog + new Handler(Looper.getMainLooper()).postDelayed(() -> { + session.commit(pendingIntent.getIntentSender()); + }, 100); } catch (Exception e) { Log.e(LOG_TAG, "Cannot install package: ", e); launchFailure(PackageInstaller.STATUS_FAILURE, diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java index cf2f85ed5356..13251d8da109 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java @@ -165,7 +165,9 @@ public class InstallStaging extends Activity { if (mStagingTask != null) { mStagingTask.cancel(true); } - + if (mDialog != null) { + mDialog.dismiss(); + } super.onDestroy(); } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java index a4c6ac7d95c7..3fea5996e3ef 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java @@ -193,6 +193,7 @@ public class InstallStart extends Activity { if (isSessionInstall) { nextActivity.setClass(this, PackageInstallerActivity.class); + nextActivity.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); } else { Uri packageUri = intent.getData(); diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java index 8bed945af32c..e0398aa49dc9 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java @@ -176,11 +176,14 @@ public class PackageInstallerActivity extends Activity { } private CharSequence getExistingUpdateOwnerLabel() { + return getApplicationLabel(getExistingUpdateOwner()); + } + + private String getExistingUpdateOwner() { try { final String packageName = mPkgInfo.packageName; final InstallSourceInfo sourceInfo = mPm.getInstallSourceInfo(packageName); - final String existingUpdateOwner = sourceInfo.getUpdateOwnerPackageName(); - return getApplicationLabel(existingUpdateOwner); + return sourceInfo.getUpdateOwnerPackageName(); } catch (NameNotFoundException e) { return null; } @@ -299,6 +302,18 @@ public class PackageInstallerActivity extends Activity { } private void initiateInstall() { + final String existingUpdateOwner = getExistingUpdateOwner(); + if (mSessionId == SessionInfo.INVALID_ID && + !TextUtils.isEmpty(existingUpdateOwner) && + !TextUtils.equals(existingUpdateOwner, mOriginatingPackage)) { + // Since update ownership is being changed, the system will request another + // user confirmation shortly. Thus, we don't need to ask the user to confirm + // installation here. + startInstall(); + return; + } + + // Proceed with user confirmation as we are not changing the update-owner in this install. 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. @@ -465,10 +480,13 @@ public class PackageInstallerActivity extends Activity { @Override protected void onDestroy() { - super.onDestroy(); while (!mActiveUnknownSourcesListeners.isEmpty()) { unregister(mActiveUnknownSourcesListeners.get(0)); } + if (mDialog != null) { + mDialog.dismiss(); + } + super.onDestroy(); } private void bindUi() { diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt index f7752ffad899..d969d1cf0a80 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt @@ -420,25 +420,48 @@ class InstallRepository(private val context: Context) { * * If AppOP is granted and user action is required to proceed with install * * If AppOp grant is to be requested from the user */ - fun requestUserConfirmation(): InstallStage { + fun requestUserConfirmation(): InstallStage? { return if (isTrustedSource) { if (localLogv) { Log.i(LOG_TAG, "Install allowed") } - // Returns InstallUserActionRequired stage if install details could be successfully - // computed, else it returns InstallAborted. - generateConfirmationSnippet() + maybeDeferUserConfirmation() } else { val unknownSourceStage = handleUnknownSources(appOpRequestInfo) if (unknownSourceStage.stageCode == InstallStage.STAGE_READY) { // Source app already has appOp granted. - generateConfirmationSnippet() + maybeDeferUserConfirmation() } else { unknownSourceStage } } } + /** + * If the update-owner for the incoming app is being changed, defer confirming with the + * user and directly proceed with the install. The system will request another + * user confirmation shortly. + */ + private fun maybeDeferUserConfirmation(): InstallStage? { + // Returns InstallUserActionRequired stage if install details could be successfully + // computed, else it returns InstallAborted. + val confirmationSnippet: InstallStage = generateConfirmationSnippet() + + val existingUpdateOwner: CharSequence? = getExistingUpdateOwner(newPackageInfo!!) + return if (sessionId == SessionInfo.INVALID_ID && + !TextUtils.isEmpty(existingUpdateOwner) && + !TextUtils.equals(existingUpdateOwner, callingPackage) + ) { + // Since update ownership is being changed, the system will request another + // user confirmation shortly. Thus, we don't need to ask the user to confirm + // installation here. + initiateInstall() + null + } else { + confirmationSnippet + } + } + private fun generateConfirmationSnippet(): InstallStage { val packageSource: Any? val pendingUserActionReason: Int @@ -639,11 +662,14 @@ class InstallRepository(private val context: Context) { } private fun getExistingUpdateOwnerLabel(pkgInfo: PackageInfo): CharSequence? { + return getApplicationLabel(getExistingUpdateOwner(pkgInfo)) + } + + private fun getExistingUpdateOwner(pkgInfo: PackageInfo): String? { return try { val packageName = pkgInfo.packageName val sourceInfo = packageManager.getInstallSourceInfo(packageName) - val existingUpdateOwner = sourceInfo.updateOwnerPackageName - getApplicationLabel(existingUpdateOwner) + sourceInfo.updateOwnerPackageName } catch (e: PackageManager.NameNotFoundException) { null } @@ -861,7 +887,12 @@ class InstallRepository(private val context: Context) { } _installResult.setValue(InstallSuccess(appSnippet, shouldReturnResult, resultIntent)) } else { - _installResult.setValue(InstallFailed(appSnippet, statusCode, legacyStatus, message)) + if (statusCode != PackageInstaller.STATUS_FAILURE_ABORTED) { + _installResult.setValue(InstallFailed(appSnippet, statusCode, legacyStatus, message)) + } else { + _installResult.setValue(InstallAborted(ABORT_REASON_INTERNAL_ERROR)) + } + } } @@ -889,8 +920,8 @@ class InstallRepository(private val context: Context) { * When the identity of the install source could not be determined, user can skip checking the * source and directly proceed with the install. */ - fun forcedSkipSourceCheck(): InstallStage { - return generateConfirmationSnippet() + fun forcedSkipSourceCheck(): InstallStage? { + return maybeDeferUserConfirmation() } val stagingProgress: LiveData<Int> diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt index 072fb2d34928..388e03f023a1 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.distinctUntilChanged import com.android.packageinstaller.v2.model.InstallRepository import com.android.packageinstaller.v2.model.InstallStage import com.android.packageinstaller.v2.model.InstallStaging @@ -37,6 +38,19 @@ class InstallViewModel(application: Application, val repository: InstallReposito val currentInstallStage: MutableLiveData<InstallStage> get() = _currentInstallStage + init { + // Since installing is an async operation, we may get the install result later in time. + // Result of the installation will be set in InstallRepository#installResult. + // As such, currentInstallStage will need to add another MutableLiveData as a data source + _currentInstallStage.addSource( + repository.installResult.distinctUntilChanged() + ) { installStage: InstallStage? -> + if (installStage != null) { + _currentInstallStage.value = installStage + } + } + } + fun preprocessIntent(intent: Intent, callerInfo: InstallRepository.CallerInfo) { val stage = repository.performPreInstallChecks(intent, callerInfo) if (stage.stageCode == InstallStage.STAGE_ABORTED) { @@ -62,12 +76,16 @@ class InstallViewModel(application: Application, val repository: InstallReposito private fun checkIfAllowedAndInitiateInstall() { val stage = repository.requestUserConfirmation() - _currentInstallStage.value = stage + if (stage != null) { + _currentInstallStage.value = stage + } } fun forcedSkipSourceCheck() { val stage = repository.forcedSkipSourceCheck() - _currentInstallStage.value = stage + if (stage != null) { + _currentInstallStage.value = stage + } } fun cleanupInstall() { @@ -80,15 +98,7 @@ class InstallViewModel(application: Application, val repository: InstallReposito } fun initiateInstall() { - // Since installing is an async operation, we will get the install result later in time. - // Result of the installation will be set in InstallRepository#mInstallResult. - // As such, mCurrentInstallStage will need to add another MutableLiveData as a data source repository.initiateInstall() - _currentInstallStage.addSource(repository.installResult) { installStage: InstallStage? -> - if (installStage != null) { - _currentInstallStage.value = installStage - } - } } val stagedSessionId: Int |