diff options
| author | 2024-07-31 15:06:26 -0700 | |
|---|---|---|
| committer | 2024-08-22 16:00:11 -0700 | |
| commit | ef4e09d208df3618d2c39c91b1eb2c109ba02452 (patch) | |
| tree | cd2a617ffe93b0ff16e860b75084dac93bd91a53 | |
| parent | ec252cbd88fd6eceb410bc8845752ce9757b6852 (diff) | |
Finish EditWidgetsActivity when stopped.
This changelist finishes the EditWidgetActivity when stopped and not
waiting for result from another activity.
Test: atest EditWidgetsActivityControllerTest
Fixes: 354725145
Flag: com.android.systemui.communal_edit_widgets_activity_finish_fix
Change-Id: I47d39608e03afb1ad1227a39bd19cfd232527610
3 files changed, 283 insertions, 0 deletions
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 0b364ac4a176..631af745cdbe 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1032,6 +1032,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" description: "Enables Backlinks improvement feature in App Clips" diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityControllerTest.kt new file mode 100644 index 000000000000..3ba86254d2f4 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityControllerTest.kt @@ -0,0 +1,143 @@ +/* + * 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() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index b421e5932352..7b9868b82055 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -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 @@ -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 @@ -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) { @@ -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 @@ -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) { @@ -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) |