diff options
40 files changed, 870 insertions, 216 deletions
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java index 762e2af09cd3..0750c8dfea74 100644 --- a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java +++ b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java @@ -145,7 +145,9 @@ public class UserLifecycleTests { Intent.ACTION_USER_STARTED, Intent.ACTION_MEDIA_MOUNTED, Intent.ACTION_USER_UNLOCKED, - Intent.ACTION_USER_STOPPED); + Intent.ACTION_USER_STOPPED, + Intent.ACTION_PROFILE_AVAILABLE, + Intent.ACTION_PROFILE_UNAVAILABLE); mUserSwitchWaiter = new UserSwitchWaiter(TAG, TIMEOUT_IN_SECOND); removeAnyPreviousTestUsers(); if (mAm.getCurrentUser() != UserHandle.USER_SYSTEM) { @@ -1230,6 +1232,255 @@ public class UserLifecycleTests { } } + /** Tests creating a private profile. */ + @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) + public void createPrivateProfile() throws RemoteException { + while (mRunner.keepRunning()) { + Log.i(TAG, "Starting timer"); + + final int userId = createPrivateProfileUser(); + + mRunner.pauseTiming(); + Log.i(TAG, "Stopping timer"); + removeUser(userId); + waitCoolDownPeriod(); + mRunner.resumeTimingForNextIteration(); + } + } + + /** + * Tests creating and starting a newly created private profile. This test waits for the + * {@link Intent.ACTION_USER_STARTED} to be received. + */ + @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) + public void createAndStartPrivateProfile_realistic() throws RemoteException { + while (mRunner.keepRunning()) { + Log.i(TAG, "Starting timer"); + final int userId = createPrivateProfileUser(); + + // Don't use this.startUserInBackgroundAndWaitForUnlock() since only waiting until + // ACTION_USER_STARTED. + runThenWaitForBroadcasts(userId, () -> { + mIam.startUserInBackground(userId); + }, Intent.ACTION_USER_STARTED); + + mRunner.pauseTiming(); + Log.i(TAG, "Stopping timer"); + removeUser(userId); + waitCoolDownPeriod(); + mRunner.resumeTimingForNextIteration(); + } + } + + /** Tests for stopping the private profile. */ + @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) + public void privateProfileStopped() throws RemoteException { + while (mRunner.keepRunning()) { + mRunner.pauseTiming(); + final int userId = createPrivateProfileUser(); + runThenWaitForBroadcasts(userId, () -> { + startUserInBackgroundAndWaitForUnlock(userId); + }, Intent.ACTION_MEDIA_MOUNTED); + + mRunner.resumeTiming(); + Log.i(TAG, "Starting timer"); + + stopUser(userId); + + mRunner.pauseTiming(); + Log.i(TAG, "Stopping timer"); + removeUser(userId); + mRunner.resumeTimingForNextIteration(); + } + } + + /** + * Tests unlocking a newly-created private profile using the + * {@link UserManager#requestQuietModeEnabled} api. + */ + @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) + public void privateProfileUnlock() throws RemoteException { + + while (mRunner.keepRunning()) { + mRunner.pauseTiming(); + final int userId = createPrivateProfileUser(); + startUserInBackgroundAndWaitForUnlock(userId); + mUm.requestQuietModeEnabled(true, UserHandle.of(userId)); + waitCoolDownPeriod(); + mRunner.resumeTiming(); + Log.i(TAG, "Starting timer"); + + mUm.requestQuietModeEnabled(false, UserHandle.of(userId)); + + mRunner.pauseTiming(); + Log.i(TAG, "Stopping timer"); + removeUser(userId); + mRunner.resumeTimingForNextIteration(); + } + } + + /** + * Tests unlocking a newly-created private profile using the + * {@link UserManager#requestQuietModeEnabled} api and waiting for the + * {@link Intent.ACTION_PROFILE_AVAILABLE} to be received. + */ + @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) + public void privateProfileUnlock_realistic() throws RemoteException { + + while (mRunner.keepRunning()) { + mRunner.pauseTiming(); + final int userId = createPrivateProfileUser(); + startUserInBackgroundAndWaitForUnlock(userId); + mUm.requestQuietModeEnabled(true, UserHandle.of(userId)); + waitCoolDownPeriod(); + mRunner.resumeTiming(); + Log.i(TAG, "Starting timer"); + + runThenWaitForBroadcasts(userId, () -> { + mUm.requestQuietModeEnabled(false, UserHandle.of(userId)); + }, Intent.ACTION_PROFILE_AVAILABLE); + + mRunner.pauseTiming(); + Log.i(TAG, "Stopping timer"); + removeUser(userId); + mRunner.resumeTimingForNextIteration(); + } + } + + /** + * Tests locking a newly-created private profile using the + * {@link UserManager#requestQuietModeEnabled} api. + */ + @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) + public void privateProfileLock() throws RemoteException { + + while (mRunner.keepRunning()) { + mRunner.pauseTiming(); + final int userId = createPrivateProfileUser(); + startUserInBackgroundAndWaitForUnlock(userId); + mRunner.resumeTiming(); + Log.i(TAG, "Starting timer"); + + mUm.requestQuietModeEnabled(true, UserHandle.of(userId)); + + mRunner.pauseTiming(); + Log.i(TAG, "Stopping timer"); + removeUser(userId); + mRunner.resumeTimingForNextIteration(); + } + } + + /** + * Tests locking a newly-created private profile using the + * {@link UserManager#requestQuietModeEnabled} api and waiting for the + * {@link Intent.ACTION_PROFILE_UNAVAILABLE} to be received. + */ + @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) + public void privateProfileLock_realistic() throws RemoteException { + + while (mRunner.keepRunning()) { + mRunner.pauseTiming(); + final int userId = createPrivateProfileUser(); + startUserInBackgroundAndWaitForUnlock(userId); + mRunner.resumeTiming(); + Log.i(TAG, "Starting timer"); + + runThenWaitForBroadcasts(userId, () -> { + mUm.requestQuietModeEnabled(true, UserHandle.of(userId)); + }, Intent.ACTION_PROFILE_UNAVAILABLE); + + + mRunner.pauseTiming(); + Log.i(TAG, "Stopping timer"); + removeUser(userId); + mRunner.resumeTimingForNextIteration(); + } + } + + /** Tests removing a newly-created private profile */ + @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) + public void privateProfileRemove() throws RemoteException { + while (mRunner.keepRunning()) { + mRunner.pauseTiming(); + final int userId = createPrivateProfileUser(); + mRunner.resumeTiming(); + Log.i(TAG, "Starting timer"); + removeUser(userId); + mRunner.pauseTiming(); + Log.i(TAG, "Stopping timer"); + mRunner.resumeTimingForNextIteration(); + } + } + + /** Tests installing a pre-existing app in a private profile. */ + @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) + public void privateProfileInstall() throws RemoteException { + while (mRunner.keepRunning()) { + mRunner.pauseTiming(); + final int userId = createPrivateProfileUser(); + startUserInBackgroundAndWaitForUnlock(userId); + mRunner.resumeTiming(); + Log.i(TAG, "Starting timer"); + + installPreexistingApp(userId, DUMMY_PACKAGE_NAME); + + mRunner.pauseTiming(); + Log.i(TAG, "Stopping timer"); + removeUser(userId); + waitCoolDownPeriod(); + mRunner.resumeTimingForNextIteration(); + } + } + + /** + * Tests creating a new private profile, starting it, installing an app, and launching that app + * in it. + */ + @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) + public void privateProfileCreateStartInstallAndLaunchApp() throws RemoteException { + while (mRunner.keepRunning()) { + mRunner.pauseTiming(); + WindowManagerGlobal.getWindowManagerService().dismissKeyguard(null, null); + mRunner.resumeTiming(); + Log.i(TAG, "Starting timer"); + + final int userId = createPrivateProfileUser(); + startUserInBackgroundAndWaitForUnlock(userId); + installPreexistingApp(userId, DUMMY_PACKAGE_NAME); + startApp(userId, DUMMY_PACKAGE_NAME); + + mRunner.pauseTiming(); + Log.i(TAG, "Stopping timer"); + removeUser(userId); + waitCoolDownPeriod(); + mRunner.resumeTimingForNextIteration(); + } + } + + /** + * Tests starting & launching an already-installed app in a private profile. + */ + @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) + public void privateProfileStartAndLaunchApp() throws RemoteException { + while (mRunner.keepRunning()) { + mRunner.pauseTiming(); + final int userId = createPrivateProfileUser(); + WindowManagerGlobal.getWindowManagerService().dismissKeyguard(null, null); + installPreexistingApp(userId, DUMMY_PACKAGE_NAME); + mRunner.resumeTiming(); + Log.i(TAG, "Starting timer"); + + startUserInBackgroundAndWaitForUnlock(userId); + startApp(userId, DUMMY_PACKAGE_NAME); + + mRunner.pauseTiming(); + Log.i(TAG, "Stopping timer"); + removeUser(userId); + waitCoolDownPeriod(); + mRunner.resumeTimingForNextIteration(); + } + } + /** Creates a new user, returning its userId. */ private int createUserNoFlags() { return createUserWithFlags(/* flags= */ 0); @@ -1252,6 +1503,18 @@ public class UserLifecycleTests { return userInfo.id; } + /** Creates a private profile under the current user, returning its userId. */ + private int createPrivateProfileUser() { + final UserInfo userInfo = mUm.createProfileForUser(TEST_USER_NAME, + UserManager.USER_TYPE_PROFILE_PRIVATE, /* flags */ 0, mAm.getCurrentUser()); + attestFalse( + "Creating a private profile failed. Most likely there is already a pre-existing " + + "private profile on the device.", + userInfo == null); + mUsersToRemove.add(userInfo.id); + return userInfo.id; + } + /** * Start user in background and wait for it to unlock by waiting for * UserState.mUnlockProgress.finish(). diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index e029e520f1b1..5ef597d7104a 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -213,3 +213,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "location_bypass_privacy_dashboard_enabled" + is_exported: true + namespace: "permissions" + description: "Show access entry of location bypass permission in the Privacy Dashboard" + bug: "325536053" +} diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 2948129ae956..6b0d301c3baf 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -1405,7 +1405,8 @@ public class DreamService extends Service implements Window.Callback { convertToComponentName( rawMetadata.getString( com.android.internal.R.styleable.Dream_settingsActivity), - serviceInfo), + serviceInfo, + packageManager), rawMetadata.getDrawable( com.android.internal.R.styleable.Dream_previewImage), rawMetadata.getBoolean(R.styleable.Dream_showClockAndComplications, @@ -1420,26 +1421,38 @@ public class DreamService extends Service implements Window.Callback { } @Nullable - private static ComponentName convertToComponentName(@Nullable String flattenedString, - ServiceInfo serviceInfo) { + private static ComponentName convertToComponentName( + @Nullable String flattenedString, + ServiceInfo serviceInfo, + PackageManager packageManager) { if (flattenedString == null) { return null; } - if (!flattenedString.contains("/")) { - return new ComponentName(serviceInfo.packageName, flattenedString); + final ComponentName cn = + flattenedString.contains("/") + ? ComponentName.unflattenFromString(flattenedString) + : new ComponentName(serviceInfo.packageName, flattenedString); + + if (cn == null) { + return null; } // Ensure that the component is from the same package as the dream service. If not, // treat the component as invalid and return null instead. - final ComponentName cn = ComponentName.unflattenFromString(flattenedString); - if (cn == null) return null; if (!cn.getPackageName().equals(serviceInfo.packageName)) { Log.w(TAG, "Inconsistent package name in component: " + cn.getPackageName() + ", should be: " + serviceInfo.packageName); return null; } + + // Ensure that the activity exists. If not, treat the component as invalid and return null. + if (new Intent().setComponent(cn).resolveActivityInfo(packageManager, 0) == null) { + Log.w(TAG, "Dream settings activity not found: " + cn); + return null; + } + return cn; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index dc449d1aaf74..a8346a9b3b48 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -17,6 +17,7 @@ package com.android.wm.shell.pip; import static android.util.RotationUtils.rotateBounds; +import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; @@ -625,6 +626,14 @@ public class PipAnimationController { } } else { adjustedSourceRectHint.set(sourceRectHint); + if (isInPipDirection(direction) + && rotationDelta == ROTATION_0 + && taskInfo.displayCutoutInsets != null) { + // TODO: this is to special case the issues on Foldable device + // with display cutout. This aligns with what's in SwipePipToHomeAnimator. + adjustedSourceRectHint.offset(taskInfo.displayCutoutInsets.left, + taskInfo.displayCutoutInsets.top); + } } final Rect sourceHintRectInsets = new Rect(); if (!adjustedSourceRectHint.isEmpty()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java index 6325c686a682..ea8c0eb30061 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java @@ -390,7 +390,8 @@ public class SplashScreenExitAnimationUtils { SurfaceControl firstWindowSurface, ViewGroup splashScreenView, TransactionPool transactionPool, Rect firstWindowFrame, int mainWindowShiftLength, float roundedCornerRadius) { - mFromYDelta = fromYDelta - Math.max(firstWindowFrame.top, roundedCornerRadius); + mFromYDelta = firstWindowFrame.top + - Math.max(firstWindowFrame.top, roundedCornerRadius); mToYDelta = toYDelta; mOccludeHoleView = occludeHoleView; mApplier = new SyncRtSurfaceTransactionApplier(occludeHoleView); 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 a23df64f3582..c260426d47f5 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt @@ -95,9 +95,22 @@ class InstallRepository(private val context: Context) { */ var stagedSessionId = SessionInfo.INVALID_ID private set + + /** + * UID of the last caller of Pia. This can point to a 3P installer if it uses intents to install + * an APK, or receives a + * [STATUS_PENDING_USER_ACTION][PackageInstaller.STATUS_PENDING_USER_ACTION] status code. + * It may point to Pia, when it receives the STATUS_PENDING_USER_ACTION status code in case of + * an update-ownership change. + */ private var callingUid = Process.INVALID_UID + + /** + * UID of the origin of the installation. This UID is used to fetch the app-label of the + * source of the install, and also check whether the source app has the AppOp to install other + * apps. + */ private var originatingUid = Process.INVALID_UID - private var originatingUidFromSessionInfo = Process.INVALID_UID private var callingPackage: String? = null private var sessionStager: SessionStager? = null private lateinit var intent: Intent @@ -136,68 +149,60 @@ class InstallRepository(private val context: Context) { stagedSessionId = intent.getIntExtra(EXTRA_STAGED_SESSION_ID, SessionInfo.INVALID_ID) callingPackage = callerInfo.packageName - - // Uid of the source package, coming from ActivityManager callingUid = callerInfo.uid - if (callingUid == Process.INVALID_UID) { - Log.e(LOG_TAG, "Could not determine the launching uid.") + + val sourceInfo: ApplicationInfo? = getSourceInfo(callingPackage) + if (callingUid == Process.INVALID_UID && sourceInfo == null) { + // Caller's identity could not be determined. Abort the install + Log.e(LOG_TAG, "Cannot determine caller since UID is invalid and sourceInfo is null") + return InstallAborted(ABORT_REASON_INTERNAL_ERROR) } - originatingUidFromSessionInfo = callingUid + originatingUid = callingUid val sessionInfo: SessionInfo? = if (sessionId != SessionInfo.INVALID_ID) packageInstaller.getSessionInfo(sessionId) else null if (sessionInfo != null) { - callingPackage = sessionInfo.installerPackageName callingAttributionTag = sessionInfo.installerAttributionTag if (sessionInfo.originatingUid != Process.INVALID_UID) { - originatingUidFromSessionInfo = sessionInfo.originatingUid + originatingUid = sessionInfo.originatingUid } } - val sourceInfo: ApplicationInfo? = getSourceInfo(callingPackage) - // Uid of the source package, with a preference to uid from ApplicationInfo - originatingUid = sourceInfo?.uid ?: callingUid appOpRequestInfo = AppOpRequestInfo( getPackageNameForUid(context, originatingUid, callingPackage), - originatingUid, callingAttributionTag + originatingUid, + callingAttributionTag ) - if(localLogv) { - Log.i(LOG_TAG, "Intent: $intent\n" + - "sessionId: $sessionId\n" + - "staged sessionId: $stagedSessionId\n" + - "calling package: $callingPackage\n" + - "callingUid: $callingUid\n" + - "originatingUid: $originatingUid") - } - - if (callingUid == Process.INVALID_UID && sourceInfo == null) { - // Caller's identity could not be determined. Abort the install - Log.e(LOG_TAG, "Cannot determine caller since UID is invalid and sourceInfo is null") - return InstallAborted(ABORT_REASON_INTERNAL_ERROR) + if (localLogv) { + Log.i( + LOG_TAG, "Intent: $intent\n" + + "sessionId: $sessionId\n" + + "staged sessionId: $stagedSessionId\n" + + "calling package: $callingPackage\n" + + "callingUid: $callingUid\n" + + "originatingUid: $originatingUid\n" + + "sourceInfo: $sourceInfo" + ) } if ((sessionId != SessionInfo.INVALID_ID - && !isCallerSessionOwner(packageInstaller, originatingUid, sessionId)) + && !isCallerSessionOwner(packageInstaller, callingUid, sessionId)) || (stagedSessionId != SessionInfo.INVALID_ID && !isCallerSessionOwner(packageInstaller, Process.myUid(), stagedSessionId)) ) { - Log.e(LOG_TAG, "UID is not the owner of the session:\n" + - "CallingUid: $originatingUid | SessionId: $sessionId\n" + - "My UID: ${Process.myUid()} | StagedSessionId: $stagedSessionId") + Log.e( + LOG_TAG, "UID is not the owner of the session:\n" + + "CallingUid: $callingUid | SessionId: $sessionId\n" + + "My UID: ${Process.myUid()} | StagedSessionId: $stagedSessionId" + ) return InstallAborted(ABORT_REASON_INTERNAL_ERROR) } - isTrustedSource = isInstallRequestFromTrustedSource(sourceInfo, this.intent, originatingUid) - if (!isInstallPermissionGrantedOrRequested( - context, callingUid, originatingUid, isTrustedSource - ) - ) { - Log.e(LOG_TAG, "UID $originatingUid needs to declare " + - Manifest.permission.REQUEST_INSTALL_PACKAGES - ) + isTrustedSource = isInstallRequestFromTrustedSource(sourceInfo, this.intent, callingUid) + if (!isInstallPermissionGrantedOrRequested(context, callingUid, isTrustedSource)) { return InstallAborted(ABORT_REASON_INTERNAL_ERROR) } @@ -205,7 +210,7 @@ class InstallRepository(private val context: Context) { if (restriction != null) { val adminSupportDetailsIntent = devicePolicyManager!!.createAdminSupportIntent(restriction) - Log.e(LOG_TAG, "$restriction set in place. Cannot install." ) + Log.e(LOG_TAG, "$restriction set in place. Cannot install.") return InstallAborted( ABORT_REASON_POLICY, message = restriction, resultIntent = adminSupportDetailsIntent ) @@ -230,12 +235,12 @@ class InstallRepository(private val context: Context) { private fun isInstallRequestFromTrustedSource( sourceInfo: ApplicationInfo?, intent: Intent, - originatingUid: Int, + callingUid: Int, ): Boolean { val isNotUnknownSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false) return (sourceInfo != null && sourceInfo.isPrivilegedApp && (isNotUnknownSource - || isPermissionGranted(context, Manifest.permission.INSTALL_PACKAGES, originatingUid))) + || isPermissionGranted(context, Manifest.permission.INSTALL_PACKAGES, callingUid))) } private fun getDevicePolicyRestrictions(): String? { @@ -320,7 +325,7 @@ class InstallRepository(private val context: Context) { Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_FAILED_INVALID_APK ), activityResultCode = Activity.RESULT_FIRST_USER, - errorDialogType = if (e is IOException) DLG_PACKAGE_ERROR else DLG_NONE + errorDialogType = if (e is IOException) DLG_PACKAGE_ERROR else DLG_NONE ) return } @@ -401,8 +406,9 @@ class InstallRepository(private val context: Context) { Log.e(LOG_TAG, "Cannot parse package $debugPathName. Assuming defaults.", e) params.setSize(pfd.statSize) } catch (e: IOException) { - Log.e(LOG_TAG, "Cannot calculate installed size $debugPathName. " + - "Try only apk size.", e + Log.e( + LOG_TAG, "Cannot calculate installed size $debugPathName. " + + "Try only apk size.", e ) } } else { @@ -661,10 +667,9 @@ class InstallRepository(private val context: Context) { if (isAppUpdating(pkgInfo)) { val existingUpdateOwnerLabel = getExistingUpdateOwnerLabel(pkgInfo) - val originatingPackageNameFromSessionInfo = - getPackageNameForUid(context, originatingUidFromSessionInfo, callingPackage) - val requestedUpdateOwnerLabel = - getApplicationLabel(originatingPackageNameFromSessionInfo) + val originatingPackageName = + getPackageNameForUid(context, originatingUid, callingPackage) + val requestedUpdateOwnerLabel = getApplicationLabel(originatingPackageName) if (!TextUtils.isEmpty(existingUpdateOwnerLabel) && userActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP @@ -768,14 +773,14 @@ class InstallRepository(private val context: Context) { } private fun handleUnknownSources(requestInfo: AppOpRequestInfo): InstallStage { - if (requestInfo.callingPackage == null) { + if (requestInfo.originatingPackage == null) { Log.i(LOG_TAG, "No source found for package " + newPackageInfo?.packageName) return InstallUserActionRequired(USER_ACTION_REASON_ANONYMOUS_SOURCE) } // Shouldn't use static constant directly, see b/65534401. val appOpStr = AppOpsManager.permissionToOp(Manifest.permission.REQUEST_INSTALL_PACKAGES) val appOpMode = appOpsManager!!.noteOpNoThrow( - appOpStr!!, requestInfo.originatingUid, requestInfo.callingPackage, + appOpStr!!, requestInfo.originatingUid, requestInfo.originatingPackage, requestInfo.attributionTag, "Started package installation activity" ) if (localLogv) { @@ -786,20 +791,20 @@ class InstallRepository(private val context: Context) { AppOpsManager.MODE_DEFAULT, AppOpsManager.MODE_ERRORED -> { if (appOpMode == AppOpsManager.MODE_DEFAULT) { appOpsManager.setMode( - appOpStr, requestInfo.originatingUid, requestInfo.callingPackage, + appOpStr, requestInfo.originatingUid, requestInfo.originatingPackage, AppOpsManager.MODE_ERRORED ) } try { val sourceInfo = - packageManager.getApplicationInfo(requestInfo.callingPackage, 0) + packageManager.getApplicationInfo(requestInfo.originatingPackage, 0) val sourceAppSnippet = getAppSnippet(context, sourceInfo) InstallUserActionRequired( USER_ACTION_REASON_UNKNOWN_SOURCE, appSnippet = sourceAppSnippet, - dialogMessage = requestInfo.callingPackage + sourceApp = requestInfo.originatingPackage ) } catch (e: PackageManager.NameNotFoundException) { - Log.e(LOG_TAG, "Did not find appInfo for " + requestInfo.callingPackage) + Log.e(LOG_TAG, "Did not find appInfo for " + requestInfo.originatingPackage) InstallAborted(ABORT_REASON_INTERNAL_ERROR) } } @@ -841,8 +846,10 @@ class InstallRepository(private val context: Context) { setStageBasedOnResult(PackageInstaller.STATUS_SUCCESS, -1, null) } catch (e: PackageManager.NameNotFoundException) { setStageBasedOnResult( - PackageInstaller.STATUS_FAILURE, PackageManager.INSTALL_FAILED_INTERNAL_ERROR, - null) + PackageInstaller.STATUS_FAILURE, + PackageManager.INSTALL_FAILED_INTERNAL_ERROR, + null + ) } return } @@ -862,7 +869,8 @@ class InstallRepository(private val context: Context) { } } catch (e: OutOfIdsException) { setStageBasedOnResult( - PackageInstaller.STATUS_FAILURE, PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null) + PackageInstaller.STATUS_FAILURE, PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null + ) return } val broadcastIntent = Intent(BROADCAST_ACTION) @@ -880,7 +888,8 @@ class InstallRepository(private val context: Context) { Log.e(LOG_TAG, "Session $stagedSessionId could not be opened.", e) packageInstaller.abandonSession(stagedSessionId) setStageBasedOnResult( - PackageInstaller.STATUS_FAILURE, PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null) + PackageInstaller.STATUS_FAILURE, PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null + ) } } @@ -890,9 +899,11 @@ class InstallRepository(private val context: Context) { message: String?, ) { if (localLogv) { - Log.i(LOG_TAG, "Status code: $statusCode\n" + - "legacy status: $legacyStatus\n" + - "message: $message") + Log.i( + LOG_TAG, "Status code: $statusCode\n" + + "legacy status: $legacyStatus\n" + + "message: $message" + ) } if (statusCode == PackageInstaller.STATUS_SUCCESS) { val shouldReturnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false) @@ -905,11 +916,12 @@ class InstallRepository(private val context: Context) { _installResult.setValue(InstallSuccess(appSnippet, shouldReturnResult, resultIntent)) } else { if (statusCode != PackageInstaller.STATUS_FAILURE_ABORTED) { - _installResult.setValue(InstallFailed(appSnippet, statusCode, legacyStatus, message)) + _installResult.setValue( + InstallFailed(appSnippet, statusCode, legacyStatus, message) + ) } else { _installResult.setValue(InstallAborted(ABORT_REASON_INTERNAL_ERROR)) } - } } @@ -953,7 +965,7 @@ class InstallRepository(private val context: Context) { data class CallerInfo(val packageName: String?, val uid: Int) data class AppOpRequestInfo( - val callingPackage: String?, + val originatingPackage: String?, val originatingUid: Int, val attributionTag: String?, ) diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt index bbb9bca6db51..5dd4d2905f47 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt @@ -43,7 +43,10 @@ data class InstallUserActionRequired( val actionReason: Int, private val appSnippet: PackageUtil.AppSnippet? = null, val isAppUpdating: Boolean = false, - val dialogMessage: String? = null, + /** + * This holds either a package name or the app label of the install source. + */ + val sourceApp: String? = null, ) : InstallStage(STAGE_USER_ACTION_REQUIRED) { val appIcon: Drawable? diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt index bae6f6876580..85728528a25c 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt @@ -128,18 +128,19 @@ object PackageUtil { /** * @param context the [Context] object - * @param callingUid the UID of the caller who's permission is being checked - * @param originatingUid the UID from where install is being originated. This could be same as - * callingUid or it will be the UID of the package performing a session based install - * @param isTrustedSource whether install request is coming from a privileged app or an app that - * has [Manifest.permission.INSTALL_PACKAGES] permission granted - * @return `true` if the package is granted the said permission + * @param callingUid the UID of the caller of Pia + * @param isTrustedSource indicates whether install request is coming from a privileged app + * that has passed EXTRA_NOT_UNKNOWN_SOURCE as `true` in the installation intent, or that has + * the [INSTALL_PACKAGES][Manifest.permission.INSTALL_PACKAGES] permission granted. + * + * @return `true` if the package is either a system downloads provider, a document manager, + * a trusted source, or has declared the + * [REQUEST_INSTALL_PACKAGES][Manifest.permission.REQUEST_INSTALL_PACKAGES] in its manifest. */ @JvmStatic fun isInstallPermissionGrantedOrRequested( context: Context, callingUid: Int, - originatingUid: Int, isTrustedSource: Boolean, ): Boolean { val isDocumentsManager = @@ -148,19 +149,18 @@ object PackageUtil { getSystemDownloadsProviderInfo(context.packageManager, callingUid) != null if (!isTrustedSource && !isSystemDownloadsProvider && !isDocumentsManager) { - val targetSdkVersion = getMaxTargetSdkVersionForUid(context, originatingUid) + val targetSdkVersion = getMaxTargetSdkVersionForUid(context, callingUid) if (targetSdkVersion < 0) { - // Invalid originating uid supplied. Abort install. - Log.w(LOG_TAG, "Cannot get target sdk version for uid $originatingUid") + // Invalid calling uid supplied. Abort install. + Log.e(LOG_TAG, "Cannot get target SDK version for uid $callingUid") return false } else if (targetSdkVersion >= Build.VERSION_CODES.O && !isUidRequestingPermission( - context.packageManager, originatingUid, - Manifest.permission.REQUEST_INSTALL_PACKAGES + context.packageManager, callingUid, Manifest.permission.REQUEST_INSTALL_PACKAGES ) ) { Log.e( - LOG_TAG, "Requesting uid " + originatingUid + " needs to declare permission " + LOG_TAG, "Requesting uid " + callingUid + " needs to declare permission " + Manifest.permission.REQUEST_INSTALL_PACKAGES ) return false @@ -204,13 +204,13 @@ object PackageUtil { * @return `true` if the caller is the session owner */ @JvmStatic - fun isCallerSessionOwner(pi: PackageInstaller, originatingUid: Int, sessionId: Int): Boolean { - if (originatingUid == Process.ROOT_UID) { + fun isCallerSessionOwner(pi: PackageInstaller, callingUid: Int, sessionId: Int): Boolean { + if (callingUid == Process.ROOT_UID) { return true } val sessionInfo = pi.getSessionInfo(sessionId) ?: return false val installerUid = sessionInfo.getInstallerUid() - return originatingUid == installerUid + return callingUid == installerUid } /** @@ -362,8 +362,8 @@ object PackageUtil { * @return the packageName corresponding to a UID. */ @JvmStatic - fun getPackageNameForUid(context: Context, sourceUid: Int, callingPackage: String?): String? { - if (sourceUid == Process.INVALID_UID) { + fun getPackageNameForUid(context: Context, uid: Int, preferredPkgName: String?): String? { + if (uid == Process.INVALID_UID) { return null } // If the sourceUid belongs to the system downloads provider, we explicitly return the @@ -371,20 +371,21 @@ object PackageUtil { // packages, resulting in uncertainty about which package will end up first in the list // of packages associated with this UID val pm = context.packageManager - val systemDownloadProviderInfo = getSystemDownloadsProviderInfo(pm, sourceUid) + val systemDownloadProviderInfo = getSystemDownloadsProviderInfo(pm, uid) if (systemDownloadProviderInfo != null) { return systemDownloadProviderInfo.packageName } - val packagesForUid = pm.getPackagesForUid(sourceUid) ?: return null + + val packagesForUid = pm.getPackagesForUid(uid) ?: return null if (packagesForUid.size > 1) { - if (callingPackage != null) { + Log.i(LOG_TAG, "Multiple packages found for source uid $uid") + if (preferredPkgName != null) { for (packageName in packagesForUid) { - if (packageName == callingPackage) { + if (packageName == preferredPkgName) { return packageName } } } - Log.i(LOG_TAG, "Multiple packages found for source uid $sourceUid") } return packagesForUid[0] } @@ -439,7 +440,7 @@ object PackageUtil { */ data class AppSnippet(var label: CharSequence?, var icon: Drawable?) { override fun toString(): String { - return "AppSnippet[label = ${label}, hasIcon = ${icon != null}]" + return "AppSnippet[label = $label, hasIcon = ${icon != null}]" } } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java index a95137d57a32..343a213780b3 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java @@ -63,7 +63,7 @@ public class ExternalSourcesBlockedFragment extends DialogFragment { .setMessage(R.string.untrusted_external_source_warning) .setPositiveButton(R.string.external_sources_settings, (dialog, which) -> mInstallActionListener.sendUnknownAppsIntent( - mDialogData.getDialogMessage())) + mDialogData.getSourceApp())) .setNegativeButton(R.string.cancel, (dialog, which) -> mInstallActionListener.onNegativeResponse( mDialogData.getStageCode())) diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java index 99b1eec9cd9e..e186590fa5e2 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java @@ -64,7 +64,7 @@ public class InstallConfirmationFragment extends DialogFragment { int positiveBtnTextRes; if (mDialogData.isAppUpdating()) { - if (mDialogData.getDialogMessage() != null) { + if (mDialogData.getSourceApp() != null) { positiveBtnTextRes = R.string.update_anyway; } else { positiveBtnTextRes = R.string.update; @@ -88,9 +88,10 @@ public class InstallConfirmationFragment extends DialogFragment { TextView viewToEnable; if (mDialogData.isAppUpdating()) { viewToEnable = dialogView.requireViewById(R.id.install_confirm_question_update); - String dialogMessage = mDialogData.getDialogMessage(); - if (dialogMessage != null) { - viewToEnable.setText(Html.fromHtml(dialogMessage, Html.FROM_HTML_MODE_LEGACY)); + String sourcePackageName = mDialogData.getSourceApp(); + if (sourcePackageName != null) { + // Show the update-ownership change message + viewToEnable.setText(Html.fromHtml(sourcePackageName, Html.FROM_HTML_MODE_LEGACY)); } } else { viewToEnable = dialogView.requireViewById(R.id.install_confirm_question); diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index 097840ead0d3..5ddf005d9468 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -45,7 +45,7 @@ <integer name="def_location_mode">3</integer> <!-- 0 == off, 1 == on--> <integer name="def_paired_device_location_mode">1</integer> - <bool name="assisted_gps_enabled">true</bool> + <bool name="assisted_gps_enabled">false</bool> <bool name="def_netstats_enabled">true</bool> <bool name="def_usb_mass_storage_enabled">true</bool> <bool name="def_wifi_on">false</bool> diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 46726f1c867c..83b74557ea07 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -146,6 +146,16 @@ flag { } flag { + name: "notification_transparent_header_fix" + namespace: "systemui" + description: "fix the transparent group header issue for async header inflation." + bug: "340161724" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "pss_app_selector_abrupt_exit_fix" namespace: "systemui" description: "Fixes the app selector abruptly disappearing without an animation, when the" diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt index 418df5c7778d..0b9669410b8e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt @@ -89,7 +89,7 @@ fun AlertDialogContent( // Content. val contentColor = LocalAndroidColorScheme.current.onSurfaceVariant - Box(Modifier.defaultMinSize(minHeight = 48.dp)) { + Box { CompositionLocalProvider(LocalContentColor provides contentColor) { ProvideTextStyle( MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.Center) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 2eea2f0565c0..a568824473cb 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -19,13 +19,19 @@ package com.android.systemui.notifications.ui.composable import android.util.Log import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.animateScrollBy +import androidx.compose.foundation.gestures.rememberScrollableState import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -41,9 +47,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -52,6 +60,7 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.boundsInWindow @@ -59,10 +68,12 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -71,6 +82,7 @@ import com.android.compose.animation.scene.LowestZIndexScenePicker import com.android.compose.animation.scene.NestedScrollBehavior import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.thenIf +import com.android.internal.policy.SystemBarUtils import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius import com.android.systemui.res.R @@ -137,6 +149,87 @@ fun SceneScope.HeadsUpNotificationSpace( ) } +/** + * A version of [HeadsUpNotificationSpace] that can be swiped up off the top edge of the screen by + * the user. When swiped up, the heads up notification is snoozed. + */ +@Composable +fun SceneScope.SnoozeableHeadsUpNotificationSpace( + stackScrollView: NotificationScrollView, + viewModel: NotificationsPlaceholderViewModel, +) { + val context = LocalContext.current + val density = LocalDensity.current + val statusBarHeight = SystemBarUtils.getStatusBarHeight(context) + val headsUpPadding = + with(density) { dimensionResource(id = R.dimen.heads_up_status_bar_padding).roundToPx() } + + val isHeadsUp by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false) + + var scrollOffset by remember { mutableFloatStateOf(0f) } + val minScrollOffset = -(statusBarHeight + headsUpPadding.toFloat()) + val maxScrollOffset = 0f + + val scrollableState = rememberScrollableState { delta -> + consumeDeltaWithinRange( + current = scrollOffset, + setCurrent = { scrollOffset = it }, + min = minScrollOffset, + max = maxScrollOffset, + delta + ) + } + + val nestedScrollConnection = + object : NestedScrollConnection { + override suspend fun onPreFling(available: Velocity): Velocity { + if ( + velocityOrPositionalThresholdReached(scrollOffset, minScrollOffset, available.y) + ) { + scrollableState.animateScrollBy(minScrollOffset, tween()) + } else { + scrollableState.animateScrollBy(-minScrollOffset, tween()) + } + return available + } + } + + LaunchedEffect(isHeadsUp) { scrollOffset = 0f } + + LaunchedEffect(scrollableState.isScrollInProgress) { + if (!scrollableState.isScrollInProgress && scrollOffset <= minScrollOffset) { + viewModel.snoozeHun() + } + } + + HeadsUpNotificationSpace( + stackScrollView = stackScrollView, + viewModel = viewModel, + modifier = + Modifier.absoluteOffset { + IntOffset( + x = 0, + y = + calculateHeadsUpPlaceholderYOffset( + scrollOffset.roundToInt(), + minScrollOffset.roundToInt(), + stackScrollView.topHeadsUpHeight + ) + ) + } + .thenIf(isHeadsUp) { + Modifier.verticalNestedScrollToScene( + bottomBehavior = NestedScrollBehavior.EdgeAlways + ) + .nestedScroll(nestedScrollConnection) + .scrollable( + orientation = Orientation.Vertical, + state = scrollableState, + ) + } + ) +} + /** Adds the space where notification stack should appear in the scene. */ @Composable fun SceneScope.ConstrainedNotificationStack( @@ -480,6 +573,47 @@ private fun calculateCornerRadius( } } +private fun calculateHeadsUpPlaceholderYOffset( + scrollOffset: Int, + minScrollOffset: Int, + topHeadsUpHeight: Int, +): Int { + return -minScrollOffset + + (scrollOffset * (-minScrollOffset + topHeadsUpHeight) / -minScrollOffset) +} + +private fun velocityOrPositionalThresholdReached( + scrollOffset: Float, + minScrollOffset: Float, + availableVelocityY: Float, +): Boolean { + return availableVelocityY < HUN_SNOOZE_VELOCITY_THRESHOLD || + (availableVelocityY <= 0f && + scrollOffset < minScrollOffset * HUN_SNOOZE_POSITIONAL_THRESHOLD_FRACTION) +} + +/** + * Takes a range, current value, and delta, and updates the current value by the delta, coercing the + * result within the given range. Returns how much of the delta was consumed. + */ +private fun consumeDeltaWithinRange( + current: Float, + setCurrent: (Float) -> Unit, + min: Float, + max: Float, + delta: Float +): Float { + return if (delta < 0 && current > min) { + val remainder = (current + delta - min).coerceAtMost(0f) + setCurrent((current + delta).coerceAtLeast(min)) + delta - remainder + } else if (delta > 0 && current < max) { + val remainder = (current + delta).coerceAtLeast(0f) + setCurrent((current + delta).coerceAtMost(max)) + delta - remainder + } else 0f +} + private inline fun debugLog( viewModel: NotificationsPlaceholderViewModel, msg: () -> Any, @@ -514,3 +648,5 @@ private const val TAG = "FlexiNotifs" private val DEBUG_STACK_COLOR = Color(1f, 0f, 0f, 0.2f) private val DEBUG_HUN_COLOR = Color(0f, 0f, 1f, 0.2f) private val DEBUG_BOX_COLOR = Color(0f, 1f, 0f, 0.2f) +private const val HUN_SNOOZE_POSITIONAL_THRESHOLD_FRACTION = 0.25f +private const val HUN_SNOOZE_VELOCITY_THRESHOLD = -70f diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt index 4e334c274657..a44041a6ebf7 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt @@ -17,26 +17,19 @@ package com.android.systemui.scene.ui.composable import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.unit.IntOffset import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateSceneDpAsState import com.android.compose.animation.scene.animateSceneFloatAsState -import com.android.internal.policy.SystemBarUtils import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace +import com.android.systemui.notifications.ui.composable.SnoozeableHeadsUpNotificationSpace import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaLandscapeTopOffset import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaOffset.Default -import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.viewmodel.GoneSceneViewModel import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView @@ -72,28 +65,9 @@ constructor( ) animateSceneDpAsState(value = Default, key = MediaLandscapeTopOffset, canOverflow = false) Spacer(modifier.fillMaxSize()) - HeadsUpNotificationStack( + SnoozeableHeadsUpNotificationSpace( stackScrollView = notificationStackScrolLView.get(), viewModel = notificationsPlaceholderViewModel ) } } - -@Composable -private fun SceneScope.HeadsUpNotificationStack( - stackScrollView: NotificationScrollView, - viewModel: NotificationsPlaceholderViewModel, -) { - val context = LocalContext.current - val density = LocalDensity.current - val statusBarHeight = SystemBarUtils.getStatusBarHeight(context) - val headsUpPadding = - with(density) { dimensionResource(id = R.dimen.heads_up_status_bar_padding).roundToPx() } - - HeadsUpNotificationSpace( - stackScrollView = stackScrollView, - viewModel = viewModel, - modifier = - Modifier.absoluteOffset { IntOffset(x = 0, y = statusBarHeight + headsUpPadding) } - ) -} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index fbf91b702fb9..a23bb67215b5 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -48,6 +48,7 @@ import com.android.compose.PlatformSlider import com.android.compose.PlatformSliderColors import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState @Composable @@ -62,7 +63,7 @@ fun VolumeSlider( val value by valueState(state) PlatformSlider( modifier = - modifier.clearAndSetSemantics { + modifier.sysuiResTag(state.label).clearAndSetSemantics { if (state.isEnabled) { contentDescription = state.label state.a11yClickDescription?.let { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt index ab14911ab425..9da2a1b06f30 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt @@ -25,20 +25,25 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.paneTitle import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.res.R import com.android.systemui.volume.panel.ui.layout.ComponentsLayout import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel +/** Same as android.platform.systemui_tapl.ui.VolumePanel#VolumePanelTestTag */ +private const val VolumePanelTestTag = "VolumePanel" private val padding = 24.dp @Composable +@OptIn(ExperimentalComposeUiApi::class) fun VolumePanelRoot( viewModel: VolumePanelViewModel, modifier: Modifier = Modifier, @@ -52,6 +57,7 @@ fun VolumePanelRoot( Components( componentsState, modifier + .sysuiResTag(VolumePanelTestTag) .semantics { paneTitle = accessibilityTitle } .padding( start = padding, diff --git a/packages/SystemUI/res/layout/alert_dialog_systemui.xml b/packages/SystemUI/res/layout/alert_dialog_systemui.xml index fd0623812745..14751ef901c8 100644 --- a/packages/SystemUI/res/layout/alert_dialog_systemui.xml +++ b/packages/SystemUI/res/layout/alert_dialog_systemui.xml @@ -31,7 +31,6 @@ android:id="@*android:id/contentPanel" android:layout_width="match_parent" android:layout_height="wrap_content" - android:minHeight="48dp" android:paddingStart="@dimen/dialog_side_padding" android:paddingEnd="@dimen/dialog_side_padding" > diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt index e9306a5bd209..069ae9381012 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt @@ -41,4 +41,7 @@ interface HeadsUpRepository { val activeHeadsUpRows: Flow<Set<HeadsUpRowRepository>> fun setHeadsUpAnimatingAway(animatingAway: Boolean) + + /** Snooze the currently pinned HUN. */ + fun snooze() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt index 4a6553f724c2..cdab108c0254 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt @@ -101,10 +101,17 @@ constructor( fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowInteractor = HeadsUpRowInteractor(key as HeadsUpRowRepository) + fun elementKeyFor(key: HeadsUpRowKey) = (key as HeadsUpRowRepository).elementKey + fun setHeadsUpAnimatingAway(animatingAway: Boolean) { headsUpRepository.setHeadsUpAnimatingAway(animatingAway) } + + /** Snooze the currently pinned HUN. */ + fun snooze() { + headsUpRepository.snooze() + } } class HeadsUpRowInteractor(repository: HeadsUpRowRepository) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 4a043d3617c3..9e943ee597f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -104,6 +104,7 @@ import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderVi import com.android.systemui.statusbar.notification.row.wrapper.NotificationCompactMessagingTemplateViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization; +import com.android.systemui.statusbar.notification.shared.TransparentHeaderFix; import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; @@ -1584,6 +1585,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /* headerView= */ headerView, /* onClickListener= */ mExpandClickListener ); + if (TransparentHeaderFix.isEnabled()) { + updateBackgroundForGroupState(); + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/TransparentHeaderFix.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/TransparentHeaderFix.kt new file mode 100644 index 000000000000..0b01510baec4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/TransparentHeaderFix.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 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.systemui.statusbar.notification.shared + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the notification transparent header fix flag state. */ +@Suppress("NOTHING_TO_INLINE") +object TransparentHeaderFix { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_NOTIFICATION_TRANSPARENT_HEADER_FIX + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the flag enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.notificationTransparentHeaderFix() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 715c6e65d1de..cb0333414e41 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -3499,6 +3499,13 @@ public class NotificationStackScrollLayout setIsBeingDragged(true); } + // Only when scene container is enabled, mark that we are being dragged so that we start + // dispatching the rest of the gesture to scene container. + void startDraggingOnHun() { + SceneContainerFlag.isUnexpectedlyInLegacyMode(); + setIsBeingDragged(true); + } + @Override public boolean onGenericMotionEvent(MotionEvent event) { if (!isScrollingEnabled() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 12f8f6996cb6..58b72e52dd7b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -59,6 +59,7 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.statusbar.IStatusBarService; import com.android.internal.view.OneShotPreDrawListener; import com.android.systemui.Dumpable; import com.android.systemui.ExpandHelper; @@ -126,6 +127,7 @@ import com.android.systemui.statusbar.notification.row.NotificationSnooze; import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor; import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; +import com.android.systemui.statusbar.phone.HeadsUpNotificationViewControllerEmptyImpl; import com.android.systemui.statusbar.phone.HeadsUpTouchHelper; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -168,6 +170,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final NotificationVisibilityProvider mVisibilityProvider; private final NotificationWakeUpCoordinator mWakeUpCoordinator; private final HeadsUpManager mHeadsUpManager; + private HeadsUpTouchHelper mHeadsUpTouchHelper; private final NotificationRoundnessManager mNotificationRoundnessManager; private final TunerService mTunerService; private final DeviceProvisionedController mDeviceProvisionedController; @@ -706,6 +709,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { NotificationVisibilityProvider visibilityProvider, NotificationWakeUpCoordinator wakeUpCoordinator, HeadsUpManager headsUpManager, + Provider<IStatusBarService> statusBarService, NotificationRoundnessManager notificationRoundnessManager, TunerService tunerService, DeviceProvisionedController deviceProvisionedController, @@ -758,6 +762,14 @@ public class NotificationStackScrollLayoutController implements Dumpable { mVisibilityProvider = visibilityProvider; mWakeUpCoordinator = wakeUpCoordinator; mHeadsUpManager = headsUpManager; + if (SceneContainerFlag.isEnabled()) { + mHeadsUpTouchHelper = new HeadsUpTouchHelper( + mHeadsUpManager, + statusBarService.get(), + getHeadsUpCallback(), + new HeadsUpNotificationViewControllerEmptyImpl() + ); + } mNotificationRoundnessManager = notificationRoundnessManager; mTunerService = tunerService; mDeviceProvisionedController = deviceProvisionedController; @@ -2000,6 +2012,13 @@ public class NotificationStackScrollLayoutController implements Dumpable { && !mView.isExpandingNotification()) { scrollWantsIt = mView.onInterceptTouchEventScroll(ev); } + boolean hunWantsIt = false; + if (shouldHeadsUpHandleTouch()) { + hunWantsIt = mHeadsUpTouchHelper.onInterceptTouchEvent(ev); + if (hunWantsIt) { + mView.startDraggingOnHun(); + } + } boolean swipeWantsIt = false; if (mLongPressedView == null && !mView.isBeingDragged() && !mView.isExpandingNotification() @@ -2029,7 +2048,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { && ev.getActionMasked() != MotionEvent.ACTION_DOWN) { mJankMonitor.begin(mView, CUJ_NOTIFICATION_SHADE_SCROLL_FLING); } - return swipeWantsIt || scrollWantsIt || expandWantsIt || longPressWantsIt; + return swipeWantsIt || scrollWantsIt || expandWantsIt || longPressWantsIt || hunWantsIt; } @Override @@ -2081,6 +2100,10 @@ public class NotificationStackScrollLayoutController implements Dumpable { && !expandingNotification && !mView.getDisallowScrollingInThisMotion()) { scrollerWantsIt = mView.onScrollTouch(ev); } + boolean hunWantsIt = false; + if (shouldHeadsUpHandleTouch()) { + hunWantsIt = mHeadsUpTouchHelper.onTouchEvent(ev); + } // Check if we need to clear any snooze leavebehinds if (guts != null && !NotificationSwipeHelper.isTouchInView(ev, guts) @@ -2101,7 +2124,8 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.setCheckForLeaveBehind(true); } traceJankOnTouchEvent(ev.getActionMasked(), scrollerWantsIt); - return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || longPressWantsIt; + return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || longPressWantsIt + || hunWantsIt; } private void traceJankOnTouchEvent(int action, boolean scrollerWantsIt) { @@ -2128,6 +2152,11 @@ public class NotificationStackScrollLayoutController implements Dumpable { break; } } + + private boolean shouldHeadsUpHandleTouch() { + return SceneContainerFlag.isEnabled() && mLongPressedView == null + && !mSwipeHelper.isSwiping(); + } } private class NotifStackControllerImpl implements NotifStackController { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index 08e81d556216..eb1f7784f3aa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -23,6 +23,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel +import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimRounding @@ -43,6 +44,7 @@ constructor( private val interactor: NotificationStackAppearanceInteractor, shadeInteractor: ShadeInteractor, private val shadeSceneViewModel: ShadeSceneViewModel, + private val headsUpNotificationInteractor: HeadsUpNotificationInteractor, featureFlags: FeatureFlagsClassic, ) : FlowDumperImpl(dumpManager) { /** DEBUG: whether the placeholder should be made slightly visible for positional debugging. */ @@ -74,6 +76,10 @@ constructor( /** Whether or not the notification scrim should be clickable. */ val isClickable: StateFlow<Boolean> = shadeSceneViewModel.isClickable + /** True when a HUN is pinned or animating away. */ + val isHeadsUpOrAnimatingAway: Flow<Boolean> = + headsUpNotificationInteractor.isHeadsUpOrAnimatingAway + /** Corner rounding of the stack */ val shadeScrimRounding: Flow<ShadeScrimRounding> = interactor.shadeScrimRounding.dumpWhileCollecting("shadeScrimRounding") @@ -103,6 +109,11 @@ constructor( fun setScrolledToTop(scrolledToTop: Boolean) { interactor.setScrolledToTop(scrolledToTop) } + + /** Snooze the currently pinned HUN. */ + fun snoozeHun() { + headsUpNotificationInteractor.snooze() + } } // Expansion fraction thresholds (between 0-1f) at which the corresponding value should be diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpNotificationViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpNotificationViewControllerEmptyImpl.kt new file mode 100644 index 000000000000..9f76429bd6a5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpNotificationViewControllerEmptyImpl.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 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.systemui.statusbar.phone + +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.phone.HeadsUpTouchHelper.HeadsUpNotificationViewController + +/** Empty impl of [HeadsUpNotificationViewController] for use with Scene Container */ +class HeadsUpNotificationViewControllerEmptyImpl : HeadsUpNotificationViewController { + override fun setHeadsUpDraggingStartingHeight(startHeight: Int) {} + + override fun setTrackedHeadsUp(expandableNotificationRow: ExpandableNotificationRow?) {} + + override fun startExpand( + newX: Float, + newY: Float, + startTracking: Boolean, + expandedHeight: Float + ) {} +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java index 198272e9b162..7f16e18716c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java @@ -23,6 +23,7 @@ import android.view.ViewConfiguration; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.Gefingerpoken; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; @@ -122,9 +123,14 @@ public class HeadsUpTouchHelper implements Gefingerpoken { + mPickedChild.getTranslationY()); mPanel.setHeadsUpDraggingStartingHeight(startHeight); mPanel.startExpand(x, y, true /* startTracking */, startHeight); - // This call needs to be after the expansion start otherwise we will get a - // flicker of one frame as it's not expanded yet. - mHeadsUpManager.unpinAll(true); + + // TODO(b/340514839): Figure out where to move this side effect in flexiglass + if (!SceneContainerFlag.isEnabled()) { + // This call needs to be after the expansion start otherwise we will get a + // flicker of one frame as it's not expanded yet. + mHeadsUpManager.unpinAll(true); + } + clearNotificationEffects(); endMotion(); return true; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index ce2491bb098e..1060b62f071f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -53,6 +53,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto; +import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.ExpandHelper; import com.android.systemui.SysuiTestCase; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; @@ -99,6 +100,7 @@ import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent; import com.android.systemui.statusbar.notification.stack.NotificationSwipeHelper.NotificationCallback; import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder; +import com.android.systemui.statusbar.phone.HeadsUpTouchHelper; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; @@ -135,6 +137,8 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private NotificationVisibilityProvider mVisibilityProvider; @Mock private NotificationWakeUpCoordinator mNotificationWakeUpCoordinator; @Mock private HeadsUpManager mHeadsUpManager; + @Mock private HeadsUpTouchHelper.Callback mHeadsUpCallback; + @Mock private Provider<IStatusBarService> mStatusBarService; @Mock private NotificationRoundnessManager mNotificationRoundnessManager; @Mock private TunerService mTunerService; @Mock private DeviceProvisionedController mDeviceProvisionedController; @@ -973,6 +977,8 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { when(mNotificationStackScrollLayout.getViewTreeObserver()) .thenReturn(viewTreeObserver); when(mNotificationStackScrollLayout.getContext()).thenReturn(getContext()); + when(mNotificationStackScrollLayout.getHeadsUpCallback()).thenReturn(mHeadsUpCallback); + when(mHeadsUpCallback.getContext()).thenReturn(getContext()); mController = new NotificationStackScrollLayoutController( mNotificationStackScrollLayout, true, @@ -981,6 +987,7 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mVisibilityProvider, mNotificationWakeUpCoordinator, mHeadsUpManager, + mStatusBarService, mNotificationRoundnessManager, mTunerService, mDeviceProvisionedController, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt index 7bf77e5199fe..492e87bbcac4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt @@ -30,7 +30,12 @@ class FakeHeadsUpNotificationRepository : HeadsUpRepository { override val topHeadsUpRow: Flow<HeadsUpRowRepository?> = MutableStateFlow(null) override val activeHeadsUpRows: MutableStateFlow<Set<HeadsUpRowRepository>> = MutableStateFlow(emptySet()) + override fun setHeadsUpAnimatingAway(animatingAway: Boolean) { isHeadsUpAnimatingAway.value = animatingAway } + + override fun snooze() { + // do nothing + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt index a0e9303890ba..afb8acb3d819 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt @@ -22,6 +22,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.ui.viewmodel.shadeSceneViewModel +import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor val Kosmos.notificationsPlaceholderViewModel by Fixture { @@ -30,6 +31,7 @@ val Kosmos.notificationsPlaceholderViewModel by Fixture { interactor = notificationStackAppearanceInteractor, shadeInteractor = shadeInteractor, shadeSceneViewModel = shadeSceneViewModel, + headsUpNotificationInteractor = headsUpNotificationInteractor, featureFlags = featureFlagsClassic, ) } diff --git a/services/core/java/com/android/server/appop/DiscreteRegistry.java b/services/core/java/com/android/server/appop/DiscreteRegistry.java index b1a12f7338da..5d83ad67d535 100644 --- a/services/core/java/com/android/server/appop/DiscreteRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteRegistry.java @@ -26,6 +26,7 @@ import static android.app.AppOpsManager.FILTER_BY_PACKAGE_NAME; import static android.app.AppOpsManager.FILTER_BY_UID; import static android.app.AppOpsManager.OP_CAMERA; import static android.app.AppOpsManager.OP_COARSE_LOCATION; +import static android.app.AppOpsManager.OP_EMERGENCY_LOCATION; import static android.app.AppOpsManager.OP_FINE_LOCATION; import static android.app.AppOpsManager.OP_FLAGS_ALL; import static android.app.AppOpsManager.OP_FLAG_SELF; @@ -135,9 +136,10 @@ final class DiscreteRegistry { private static final String PROPERTY_DISCRETE_FLAGS = "discrete_history_op_flags"; private static final String PROPERTY_DISCRETE_OPS_LIST = "discrete_history_ops_cslist"; private static final String DEFAULT_DISCRETE_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION - + "," + OP_CAMERA + "," + OP_RECORD_AUDIO + "," + OP_PHONE_CALL_MICROPHONE + "," - + OP_PHONE_CALL_CAMERA + "," + OP_RECEIVE_AMBIENT_TRIGGER_AUDIO + "," - + OP_RECEIVE_SANDBOX_TRIGGER_AUDIO + "," + OP_RESERVED_FOR_TESTING; + + "," + OP_EMERGENCY_LOCATION + "," + OP_CAMERA + "," + OP_RECORD_AUDIO + "," + + OP_PHONE_CALL_MICROPHONE + "," + OP_PHONE_CALL_CAMERA + "," + + OP_RECEIVE_AMBIENT_TRIGGER_AUDIO + "," + OP_RECEIVE_SANDBOX_TRIGGER_AUDIO + + "," + OP_RESERVED_FOR_TESTING; private static final long DEFAULT_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(7).toMillis(); private static final long MAXIMUM_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(30).toMillis(); private static final long DEFAULT_DISCRETE_HISTORY_QUANTIZATION = diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 5228be48371c..07a7c5536352 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -3038,7 +3038,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // getCurrentInputMethodSubtype. subtypeId = NOT_A_SUBTYPE_ID; // TODO(b/347083680): The method below has questionable behaviors. - newSubtype = getCurrentInputMethodSubtypeLocked(); + newSubtype = getCurrentInputMethodSubtypeLocked(userId); if (newSubtype != null) { for (int i = 0; i < subtypeCount; ++i) { if (Objects.equals(newSubtype, info.getSubtypeAt(i))) { @@ -5486,7 +5486,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. newSubtypeHashcode = INVALID_SUBTYPE_HASHCODE; // If the subtype is not specified, choose the most applicable one // TODO(b/347083680): The method below has questionable behaviors. - newSubtype = getCurrentInputMethodSubtypeLocked(); + newSubtype = getCurrentInputMethodSubtypeLocked(userId); } } settings.putSelectedSubtype(newSubtypeHashcode); @@ -5541,37 +5541,25 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); } synchronized (ImfLock.class) { - if (mCurrentUserId == userId) { - // TODO(b/347083680): The method below has questionable behaviors. - return getCurrentInputMethodSubtypeLocked(); - } - - return InputMethodSettingsRepository.get(userId) - .getCurrentInputMethodSubtypeForNonCurrentUsers(); + // TODO(b/347083680): The method below has questionable behaviors. + return getCurrentInputMethodSubtypeLocked(userId); } } /** * Returns the current {@link InputMethodSubtype} for the current user. * - * <p>CAVEATS: You must also update - * {@link InputMethodSettings#getCurrentInputMethodSubtypeForNonCurrentUsers()} - * when you update the algorithm of this method.</p> - * - * <p>TODO: Address code duplication between this and - * {@link InputMethodSettings#getCurrentInputMethodSubtypeForNonCurrentUsers()}.</p> - * * <p>Also this method has had questionable behaviors:</p> * <ul> - * <li>Calling this method can update {@link #mCurrentSubtype}.</li> - * <li>This method may return {@link #mCurrentSubtype} as-is, even if it does not belong - * to the current IME.</li> + * <li>Calling this method can update {@link InputMethodBindingController#mCurrentSubtype}. + * </li> + * <li>This method may return {@link InputMethodBindingController#mCurrentSubtype} as-is, + * even if it does not belong to the current IME.</li> * </ul> * <p>TODO(b/347083680): Address above issues.</p> */ @GuardedBy("ImfLock.class") - InputMethodSubtype getCurrentInputMethodSubtypeLocked() { - final int userId = mCurrentUserId; + InputMethodSubtype getCurrentInputMethodSubtypeLocked(@UserIdInt int userId) { final var selectedMethodId = getInputMethodBindingController(userId).getSelectedMethodId(); if (selectedMethodId == null) { return null; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java index 154ee1d9fde8..a5d963bd5be3 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java @@ -85,7 +85,7 @@ final class InputMethodMenuController { if (preferredInputMethodSubtypeId == NOT_A_SUBTYPE_ID) { final InputMethodSubtype currentSubtype = - mService.getCurrentInputMethodSubtypeLocked(); + mService.getCurrentInputMethodSubtypeLocked(userId); if (currentSubtype != null) { final String curMethodId = bindingController.getSelectedMethodId(); final InputMethodInfo currentImi = diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java index 5569e1e5211e..0152158cbb7a 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java @@ -571,57 +571,6 @@ final class InputMethodSettings { } } - /** - * A variant of {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()} for - * non-current users. - * - * <p>TODO: Address code duplication between this and - * {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()}.</p> - * - * @return {@link InputMethodSubtype} if exists. {@code null} otherwise. - */ - @Nullable - InputMethodSubtype getCurrentInputMethodSubtypeForNonCurrentUsers() { - final String selectedMethodId = getSelectedInputMethod(); - if (selectedMethodId == null) { - return null; - } - final InputMethodInfo imi = mMethodMap.get(selectedMethodId); - if (imi == null || imi.getSubtypeCount() == 0) { - return null; - } - - final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode(); - if (subtypeHashCode != INVALID_SUBTYPE_HASHCODE) { - final int subtypeIndex = SubtypeUtils.getSubtypeIdFromHashCode(imi, - subtypeHashCode); - if (subtypeIndex >= 0) { - return imi.getSubtypeAt(subtypeIndex); - } - } - - // If there are no selected subtypes, the framework will try to find the most applicable - // subtype from explicitly or implicitly enabled subtypes. - final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes = - getEnabledInputMethodSubtypeList(imi, true); - // If there is only one explicitly or implicitly enabled subtype, just returns it. - if (explicitlyOrImplicitlyEnabledSubtypes.isEmpty()) { - return null; - } - if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) { - return explicitlyOrImplicitlyEnabledSubtypes.get(0); - } - final String locale = SystemLocaleWrapper.get(mUserId).get(0).toString(); - final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtype( - explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD, - locale, true); - if (subtype != null) { - return subtype; - } - return SubtypeUtils.findLastResortApplicableSubtype( - explicitlyOrImplicitlyEnabledSubtypes, null, locale, true); - } - @NonNull AdditionalSubtypeMap getNewAdditionalSubtypeMap(@NonNull String imeId, @NonNull ArrayList<InputMethodSubtype> subtypes, diff --git a/services/tests/dreamservicetests/AndroidManifest.xml b/services/tests/dreamservicetests/AndroidManifest.xml index 6092ef6f9427..449521b05135 100644 --- a/services/tests/dreamservicetests/AndroidManifest.xml +++ b/services/tests/dreamservicetests/AndroidManifest.xml @@ -53,6 +53,35 @@ android:name="android.service.dream" android:resource="@xml/test_dream_metadata_invalid" /> </service> + + <service + android:name="com.android.server.dreams.TestDreamServiceWithNonexistentSettings" + android:exported="false" + android:label="Test Dream" > + <intent-filter> + <action android:name="android.service.dreams.DreamService" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + <meta-data + android:name="android.service.dream" + android:resource="@xml/test_dream_metadata_nonexistent_settings" /> + </service> + + <service + android:name="com.android.server.dreams.TestDreamServiceNoPackageNonexistentSettings" + android:exported="false" + android:label="Test Dream" > + <intent-filter> + <action android:name="android.service.dreams.DreamService" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + <meta-data + android:name="android.service.dream" + android:resource="@xml/test_dream_metadata_nopackage_nonexistent_settings" /> + </service> + + <activity android:name=".TestDreamSettingsActivity"> + </activity> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" diff --git a/services/tests/dreamservicetests/res/xml/test_dream_metadata_nonexistent_settings.xml b/services/tests/dreamservicetests/res/xml/test_dream_metadata_nonexistent_settings.xml new file mode 100644 index 000000000000..f91f0586ec6c --- /dev/null +++ b/services/tests/dreamservicetests/res/xml/test_dream_metadata_nonexistent_settings.xml @@ -0,0 +1,20 @@ +<!-- + ~ Copyright (C) 2024 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. + --> + +<!-- The settings activity does not exist, which is invalid. --> +<dream xmlns:android="http://schemas.android.com/apk/res/android" + android:settingsActivity="com.android.frameworks.dreamservicetests/.TestDreamSettingsActivityNonexistent" + android:showClockAndComplications="false"/> diff --git a/services/tests/dreamservicetests/res/xml/test_dream_metadata_nopackage_nonexistent_settings.xml b/services/tests/dreamservicetests/res/xml/test_dream_metadata_nopackage_nonexistent_settings.xml new file mode 100644 index 000000000000..24032c9efcea --- /dev/null +++ b/services/tests/dreamservicetests/res/xml/test_dream_metadata_nopackage_nonexistent_settings.xml @@ -0,0 +1,20 @@ +<!-- + ~ Copyright (C) 2024 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. + --> + +<!-- The settings activity's ComponentName is invalid. --> +<dream xmlns:android="http://schemas.android.com/apk/res/android" + android:settingsActivity="com.android.frameworks.dreamservicetests.TestDreamSettingsActivityNonexistentNoPackage" + android:showClockAndComplications="false"/> diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamServiceTest.java b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamServiceTest.java index b98af6bc7dd0..b4e1abff3bf7 100644 --- a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamServiceTest.java +++ b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamServiceTest.java @@ -91,6 +91,28 @@ public class DreamServiceTest { } @Test + public void testMetadataParsing_nonexistentSettingsActivity() + throws PackageManager.NameNotFoundException { + final String testDreamClassName = + "com.android.server.dreams.TestDreamServiceWithNonexistentSettings"; + final DreamService.DreamMetadata metadata = getDreamMetadata(testDreamClassName); + + assertThat(metadata.settingsActivity).isNull(); + assertThat(metadata.dreamCategory).isEqualTo(DreamService.DREAM_CATEGORY_DEFAULT); + } + + @Test + public void testMetadataParsing_noPackage_nonexistentSettingsActivity() + throws PackageManager.NameNotFoundException { + final String testDreamClassName = + "com.android.server.dreams.TestDreamServiceNoPackageNonexistentSettings"; + final DreamService.DreamMetadata metadata = getDreamMetadata(testDreamClassName); + + assertThat(metadata.settingsActivity).isNull(); + assertThat(metadata.dreamCategory).isEqualTo(DreamService.DREAM_CATEGORY_DEFAULT); + } + + @Test public void testMetadataParsing_exceptionReading() { final PackageManager packageManager = Mockito.mock(PackageManager.class); final ServiceInfo serviceInfo = Mockito.mock(ServiceInfo.class); diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamSettingsActivity.java b/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamSettingsActivity.java new file mode 100644 index 000000000000..bb8db8a9b655 --- /dev/null +++ b/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamSettingsActivity.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 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.server.dreams; + +public class TestDreamSettingsActivity { +} diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java index 7aea993b1105..0db0d954d14d 100644 --- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java +++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java @@ -95,7 +95,6 @@ public final class ConcurrentMultiUserTest { } } - @Ignore("b/345557347") @Test public void driverShowImeNotAffectPassenger() { assertDriverImeHidden(); |