Loading packages/SystemUI/aconfig/systemui.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -1031,6 +1031,16 @@ flag { } } flag { name: "communal_edit_widgets_activity_finish_fix" namespace: "systemui" description: "finish edit widgets activity when stopping" bug: "354725145" metadata { purpose: PURPOSE_BUGFIX } } flag { name: "app_clips_backlinks" namespace: "systemui" Loading packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityControllerTest.kt 0 → 100644 +143 −0 Original line number Diff line number Diff line /* * 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.communal.widgets import android.app.Activity import android.app.Application.ActivityLifecycleCallbacks import android.os.Bundle import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @ExperimentalCoroutinesApi @SmallTest @RunWith(AndroidJUnit4::class) class EditWidgetsActivityControllerTest : SysuiTestCase() { @Test fun activityLifecycle_finishedWhenNotWaitingForResult() { val activity = mock<Activity>() val controller = EditWidgetsActivity.ActivityControllerImpl(activity) val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) controller.setActivityFullyVisible(true) callbackCapture.lastValue.onActivityStopped(activity) verify(activity).finish() } @Test fun activityLifecycle_notFinishedWhenOnStartCalledAfterOnStop() { val activity = mock<Activity>() val controller = EditWidgetsActivity.ActivityControllerImpl(activity) val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) controller.setActivityFullyVisible(false) callbackCapture.lastValue.onActivityStopped(activity) callbackCapture.lastValue.onActivityStarted(activity) verify(activity, never()).finish() } @Test fun activityLifecycle_notFinishedDuringConfigurationChange() { val activity = mock<Activity>() val controller = EditWidgetsActivity.ActivityControllerImpl(activity) val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) controller.setActivityFullyVisible(true) whenever(activity.isChangingConfigurations).thenReturn(true) callbackCapture.lastValue.onActivityStopped(activity) callbackCapture.lastValue.onActivityStarted(activity) verify(activity, never()).finish() } @Test fun activityLifecycle_notFinishedWhenWaitingForResult() { val activity = mock<Activity>() val controller = EditWidgetsActivity.ActivityControllerImpl(activity) val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) controller.onWaitingForResult(true) callbackCapture.lastValue.onActivityStopped(activity) verify(activity, never()).finish() } @Test fun activityLifecycle_finishedAfterResultReturned() { val activity = mock<Activity>() val controller = EditWidgetsActivity.ActivityControllerImpl(activity) val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) controller.onWaitingForResult(true) controller.onWaitingForResult(false) controller.setActivityFullyVisible(true) callbackCapture.lastValue.onActivityStopped(activity) verify(activity).finish() } @Test fun activityLifecycle_statePreservedThroughInstanceSave() { val activity = mock<Activity>() val bundle = Bundle(1) run { val controller = EditWidgetsActivity.ActivityControllerImpl(activity) val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) controller.onWaitingForResult(true) callbackCapture.lastValue.onActivitySaveInstanceState(activity, bundle) } clearInvocations(activity) run { val controller = EditWidgetsActivity.ActivityControllerImpl(activity) val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) callbackCapture.lastValue.onActivityCreated(activity, bundle) callbackCapture.lastValue.onActivityStopped(activity) verify(activity, never()).finish() } } } packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +130 −0 Original line number Diff line number Diff line Loading @@ -16,7 +16,10 @@ package com.android.systemui.communal.widgets import android.app.Activity import android.app.Application.ActivityLifecycleCallbacks import android.content.Intent import android.content.IntentSender import android.os.Bundle import android.os.RemoteException import android.util.Log Loading @@ -34,6 +37,7 @@ import androidx.lifecycle.lifecycleScope import com.android.compose.theme.LocalAndroidColorScheme import com.android.compose.theme.PlatformTheme import com.android.internal.logging.UiEventLogger import com.android.systemui.Flags.communalEditWidgetsActivityFinishFix import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalTransitionKeys Loading Loading @@ -68,12 +72,106 @@ constructor( const val EXTRA_OPEN_WIDGET_PICKER_ON_START = "open_widget_picker_on_start" } /** * [ActivityController] handles closing the activity in the case it is backgrounded without * waiting for an activity result */ interface ActivityController { /** * Invoked when waiting for an activity result changes, either initiating such wait or * finishing due to the return of a result. */ fun onWaitingForResult(waitingForResult: Boolean) {} /** Set the visibility of the activity under control. */ fun setActivityFullyVisible(fullyVisible: Boolean) {} } /** * A nop ActivityController to be use when the communalEditWidgetsActivityFinishFix flag is * false. */ class NopActivityController : ActivityController /** * A functional ActivityController to be used when the communalEditWidgetsActivityFinishFix flag * is true. */ class ActivityControllerImpl(activity: Activity) : ActivityController { companion object { private const val STATE_EXTRA_IS_WAITING_FOR_RESULT = "extra_is_waiting_for_result" } private var waitingForResult = false private var activityFullyVisible = false init { activity.registerActivityLifecycleCallbacks( object : ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { waitingForResult = savedInstanceState?.getBoolean(STATE_EXTRA_IS_WAITING_FOR_RESULT) ?: false } override fun onActivityStarted(activity: Activity) { // Nothing to implement. } override fun onActivityResumed(activity: Activity) { // Nothing to implement. } override fun onActivityPaused(activity: Activity) { // Nothing to implement. } override fun onActivityStopped(activity: Activity) { // If we're not backgrounded due to waiting for a result (either widget // selection or configuration), and we are fully visible, then finish the // activity. if ( !waitingForResult && activityFullyVisible && !activity.isChangingConfigurations ) { activity.finish() } } override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { outState.putBoolean(STATE_EXTRA_IS_WAITING_FOR_RESULT, waitingForResult) } override fun onActivityDestroyed(activity: Activity) { // Nothing to implement. } } ) } override fun onWaitingForResult(waitingForResult: Boolean) { this.waitingForResult = waitingForResult } override fun setActivityFullyVisible(fullyVisible: Boolean) { activityFullyVisible = fullyVisible } } private val logger = Logger(logBuffer, "EditWidgetsActivity") private val widgetConfigurator by lazy { widgetConfiguratorFactory.create(this) } private var shouldOpenWidgetPickerOnStart = false private val activityController: ActivityController = if (communalEditWidgetsActivityFinishFix()) ActivityControllerImpl(this) else NopActivityController() private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(StartActivityForResult()) { result -> when (result.resultCode) { Loading Loading @@ -111,8 +209,10 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) listenForTransitionAndChangeScene() activityController.setActivityFullyVisible(false) communalViewModel.setEditModeOpen(true) val windowInsetsController = window.decorView.windowInsetsController Loading Loading @@ -159,6 +259,9 @@ constructor( communalViewModel.currentScene.first { it == CommunalScenes.Blank } communalViewModel.setEditModeState(EditModeState.SHOWING) // Inform the ActivityController that we are now fully visible. activityController.setActivityFullyVisible(true) // Show the widget picker, if necessary, after the edit activity has animated in. // Waiting until after the activity has appeared avoids transitions issues. if (shouldOpenWidgetPickerOnStart) { Loading Loading @@ -198,7 +301,34 @@ constructor( } } override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) { activityController.onWaitingForResult(true) super.startActivityForResult(intent, requestCode, options) } override fun startIntentSenderForResult( intent: IntentSender, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int, options: Bundle? ) { activityController.onWaitingForResult(true) super.startIntentSenderForResult( intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options ) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { activityController.onWaitingForResult(false) super.onActivityResult(requestCode, resultCode, data) if (requestCode == WidgetConfigurationController.REQUEST_CODE) { widgetConfigurator.setConfigurationResult(resultCode) Loading Loading
packages/SystemUI/aconfig/systemui.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -1031,6 +1031,16 @@ flag { } } flag { name: "communal_edit_widgets_activity_finish_fix" namespace: "systemui" description: "finish edit widgets activity when stopping" bug: "354725145" metadata { purpose: PURPOSE_BUGFIX } } flag { name: "app_clips_backlinks" namespace: "systemui" Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityControllerTest.kt 0 → 100644 +143 −0 Original line number Diff line number Diff line /* * 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.communal.widgets import android.app.Activity import android.app.Application.ActivityLifecycleCallbacks import android.os.Bundle import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @ExperimentalCoroutinesApi @SmallTest @RunWith(AndroidJUnit4::class) class EditWidgetsActivityControllerTest : SysuiTestCase() { @Test fun activityLifecycle_finishedWhenNotWaitingForResult() { val activity = mock<Activity>() val controller = EditWidgetsActivity.ActivityControllerImpl(activity) val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) controller.setActivityFullyVisible(true) callbackCapture.lastValue.onActivityStopped(activity) verify(activity).finish() } @Test fun activityLifecycle_notFinishedWhenOnStartCalledAfterOnStop() { val activity = mock<Activity>() val controller = EditWidgetsActivity.ActivityControllerImpl(activity) val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) controller.setActivityFullyVisible(false) callbackCapture.lastValue.onActivityStopped(activity) callbackCapture.lastValue.onActivityStarted(activity) verify(activity, never()).finish() } @Test fun activityLifecycle_notFinishedDuringConfigurationChange() { val activity = mock<Activity>() val controller = EditWidgetsActivity.ActivityControllerImpl(activity) val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) controller.setActivityFullyVisible(true) whenever(activity.isChangingConfigurations).thenReturn(true) callbackCapture.lastValue.onActivityStopped(activity) callbackCapture.lastValue.onActivityStarted(activity) verify(activity, never()).finish() } @Test fun activityLifecycle_notFinishedWhenWaitingForResult() { val activity = mock<Activity>() val controller = EditWidgetsActivity.ActivityControllerImpl(activity) val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) controller.onWaitingForResult(true) callbackCapture.lastValue.onActivityStopped(activity) verify(activity, never()).finish() } @Test fun activityLifecycle_finishedAfterResultReturned() { val activity = mock<Activity>() val controller = EditWidgetsActivity.ActivityControllerImpl(activity) val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) controller.onWaitingForResult(true) controller.onWaitingForResult(false) controller.setActivityFullyVisible(true) callbackCapture.lastValue.onActivityStopped(activity) verify(activity).finish() } @Test fun activityLifecycle_statePreservedThroughInstanceSave() { val activity = mock<Activity>() val bundle = Bundle(1) run { val controller = EditWidgetsActivity.ActivityControllerImpl(activity) val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) controller.onWaitingForResult(true) callbackCapture.lastValue.onActivitySaveInstanceState(activity, bundle) } clearInvocations(activity) run { val controller = EditWidgetsActivity.ActivityControllerImpl(activity) val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) callbackCapture.lastValue.onActivityCreated(activity, bundle) callbackCapture.lastValue.onActivityStopped(activity) verify(activity, never()).finish() } } }
packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +130 −0 Original line number Diff line number Diff line Loading @@ -16,7 +16,10 @@ package com.android.systemui.communal.widgets import android.app.Activity import android.app.Application.ActivityLifecycleCallbacks import android.content.Intent import android.content.IntentSender import android.os.Bundle import android.os.RemoteException import android.util.Log Loading @@ -34,6 +37,7 @@ import androidx.lifecycle.lifecycleScope import com.android.compose.theme.LocalAndroidColorScheme import com.android.compose.theme.PlatformTheme import com.android.internal.logging.UiEventLogger import com.android.systemui.Flags.communalEditWidgetsActivityFinishFix import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalTransitionKeys Loading Loading @@ -68,12 +72,106 @@ constructor( const val EXTRA_OPEN_WIDGET_PICKER_ON_START = "open_widget_picker_on_start" } /** * [ActivityController] handles closing the activity in the case it is backgrounded without * waiting for an activity result */ interface ActivityController { /** * Invoked when waiting for an activity result changes, either initiating such wait or * finishing due to the return of a result. */ fun onWaitingForResult(waitingForResult: Boolean) {} /** Set the visibility of the activity under control. */ fun setActivityFullyVisible(fullyVisible: Boolean) {} } /** * A nop ActivityController to be use when the communalEditWidgetsActivityFinishFix flag is * false. */ class NopActivityController : ActivityController /** * A functional ActivityController to be used when the communalEditWidgetsActivityFinishFix flag * is true. */ class ActivityControllerImpl(activity: Activity) : ActivityController { companion object { private const val STATE_EXTRA_IS_WAITING_FOR_RESULT = "extra_is_waiting_for_result" } private var waitingForResult = false private var activityFullyVisible = false init { activity.registerActivityLifecycleCallbacks( object : ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { waitingForResult = savedInstanceState?.getBoolean(STATE_EXTRA_IS_WAITING_FOR_RESULT) ?: false } override fun onActivityStarted(activity: Activity) { // Nothing to implement. } override fun onActivityResumed(activity: Activity) { // Nothing to implement. } override fun onActivityPaused(activity: Activity) { // Nothing to implement. } override fun onActivityStopped(activity: Activity) { // If we're not backgrounded due to waiting for a result (either widget // selection or configuration), and we are fully visible, then finish the // activity. if ( !waitingForResult && activityFullyVisible && !activity.isChangingConfigurations ) { activity.finish() } } override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { outState.putBoolean(STATE_EXTRA_IS_WAITING_FOR_RESULT, waitingForResult) } override fun onActivityDestroyed(activity: Activity) { // Nothing to implement. } } ) } override fun onWaitingForResult(waitingForResult: Boolean) { this.waitingForResult = waitingForResult } override fun setActivityFullyVisible(fullyVisible: Boolean) { activityFullyVisible = fullyVisible } } private val logger = Logger(logBuffer, "EditWidgetsActivity") private val widgetConfigurator by lazy { widgetConfiguratorFactory.create(this) } private var shouldOpenWidgetPickerOnStart = false private val activityController: ActivityController = if (communalEditWidgetsActivityFinishFix()) ActivityControllerImpl(this) else NopActivityController() private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(StartActivityForResult()) { result -> when (result.resultCode) { Loading Loading @@ -111,8 +209,10 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) listenForTransitionAndChangeScene() activityController.setActivityFullyVisible(false) communalViewModel.setEditModeOpen(true) val windowInsetsController = window.decorView.windowInsetsController Loading Loading @@ -159,6 +259,9 @@ constructor( communalViewModel.currentScene.first { it == CommunalScenes.Blank } communalViewModel.setEditModeState(EditModeState.SHOWING) // Inform the ActivityController that we are now fully visible. activityController.setActivityFullyVisible(true) // Show the widget picker, if necessary, after the edit activity has animated in. // Waiting until after the activity has appeared avoids transitions issues. if (shouldOpenWidgetPickerOnStart) { Loading Loading @@ -198,7 +301,34 @@ constructor( } } override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) { activityController.onWaitingForResult(true) super.startActivityForResult(intent, requestCode, options) } override fun startIntentSenderForResult( intent: IntentSender, requestCode: Int, fillInIntent: Intent?, flagsMask: Int, flagsValues: Int, extraFlags: Int, options: Bundle? ) { activityController.onWaitingForResult(true) super.startIntentSenderForResult( intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, options ) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { activityController.onWaitingForResult(false) super.onActivityResult(requestCode, resultCode, data) if (requestCode == WidgetConfigurationController.REQUEST_CODE) { widgetConfigurator.setConfigurationResult(resultCode) Loading