Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e171a255 authored by William Leshner's avatar William Leshner Committed by Android (Google) Code Review
Browse files

Merge "Finish EditWidgetsActivity when stopped." into main

parents d53ce56d ef4e09d2
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -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"
+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()
        }
    }
}
+130 −0
Original line number Diff line number Diff line
@@ -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)