diff --git a/go/quickstep/src/com/android/launcher3/util/MainThreadInitializedObject.java b/go/quickstep/src/com/android/launcher3/util/MainThreadInitializedObject.java new file mode 100644 index 0000000000..e1f3508018 --- /dev/null +++ b/go/quickstep/src/com/android/launcher3/util/MainThreadInitializedObject.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 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.launcher3.util; + +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; + +import android.content.Context; +import android.os.Looper; + +import java.util.concurrent.ExecutionException; + +/** + * Utility class for defining singletons which are initiated on main thread. + */ +public class MainThreadInitializedObject { + + private final ObjectProvider mProvider; + private T mValue; + + public MainThreadInitializedObject(ObjectProvider provider) { + mProvider = provider; + } + + public T get(Context context) { + Context app = context.getApplicationContext(); + if (app instanceof ObjectSandbox sc) { + return sc.getObject(this); + } + + if (mValue == null) { + if (Looper.myLooper() == Looper.getMainLooper()) { + mValue = TraceHelper.allowIpcs("main.thread.object", () -> mProvider.get(app)); + } else { + try { + return MAIN_EXECUTOR.submit(() -> get(context)).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + } + return mValue; + } + + public interface ObjectProvider { + + T get(Context context); + } + + /** Sandbox for isolating {@link MainThreadInitializedObject} instances from Launcher. */ + public interface ObjectSandbox { + + /** + * Find a cached object from mObjectMap if we have already created one. If not, generate + * an object using the provider. + */ + T getObject(MainThreadInitializedObject object); + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java index 384468c9fc..8cb6cd30b6 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -86,11 +86,11 @@ import com.android.launcher3.taskbar.bubbles.BubbleControllers; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.LauncherBindableItemsContainer; -import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.launcher3.util.MultiTranslateDelegate; import com.android.launcher3.util.MultiValueAlpha; +import com.android.launcher3.util.SandboxContext; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.SingleTask; import com.android.systemui.shared.recents.model.Task; diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java index 06a939a06d..91f9e53eaa 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java @@ -41,7 +41,7 @@ import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.pm.UserCache; import com.android.launcher3.util.AllModulesForTest; -import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; +import com.android.launcher3.util.SandboxContext; import com.android.launcher3.util.UserIconInfo; import com.android.systemui.shared.system.SysUiStatsLog; diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt index adfbca567d..8d20ba8e7f 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt @@ -28,7 +28,7 @@ import com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE import com.android.launcher3.model.data.TaskViewItemInfo.Companion.createTaskViewAtom import com.android.launcher3.pm.UserCache import com.android.launcher3.util.AllModulesForTest -import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext +import com.android.launcher3.util.SandboxContext import com.android.launcher3.util.SplitConfigurationOptions import com.android.launcher3.util.TransformingTouchDelegate import com.android.launcher3.util.UserIconInfo diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt index 3cf912c8e3..f22580719f 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt @@ -20,7 +20,6 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode import com.android.launcher3.util.DisplayController -import com.android.launcher3.util.MainThreadInitializedObject import com.android.launcher3.util.NavigationMode import org.junit.rules.TestRule import org.junit.runner.Description @@ -31,8 +30,8 @@ import org.mockito.kotlin.spy /** * Allows tests to specify which Taskbar [Mode] to run under. * - * [context] should match the test's target context, so that [MainThreadInitializedObject] instances - * are properly sandboxed. + * [context] should match the test's target context, so that Dagger singleton instances are properly + * sandboxed. * * Annotate tests with [TaskbarMode] to set a mode. If the annotation is omitted for any tests, this * rule is a no-op. diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt index e6806b70bf..6d53e8e312 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt @@ -30,7 +30,6 @@ import com.android.launcher3.util.AllModulesMinusWMProxy import com.android.launcher3.util.DaggerSingletonTracker import com.android.launcher3.util.DisplayController import com.android.launcher3.util.FakePrefsModule -import com.android.launcher3.util.MainThreadInitializedObject.ObjectSandbox import com.android.launcher3.util.SandboxApplication import com.android.launcher3.util.SettingsCache import com.android.launcher3.util.SettingsCacheSandbox @@ -58,7 +57,7 @@ private constructor( private val base: SandboxApplication, val virtualDisplay: VirtualDisplay, private val params: SandboxParams, -) : ContextWrapper(base), ObjectSandbox by base, TestRule { +) : ContextWrapper(base), TestRule { val settingsCacheSandbox = SettingsCacheSandbox() diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java index 99a34ea69c..b3056f5c46 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java @@ -55,7 +55,7 @@ import com.android.launcher3.dagger.LauncherAppSingleton; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.util.AllModulesForTest; import com.android.launcher3.util.DisplayController; -import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; +import com.android.launcher3.util.SandboxContext; import com.android.quickstep.DeviceConfigWrapper; import com.android.quickstep.GestureState; import com.android.quickstep.InputConsumer; diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java index 740b87b02e..3836f7d3ac 100644 --- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java +++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java @@ -94,7 +94,7 @@ import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; -import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; +import com.android.launcher3.util.SandboxContext; import com.android.launcher3.util.WindowBounds; import com.android.launcher3.util.window.WindowManagerProxy; import com.android.launcher3.views.ActivityContext; diff --git a/src/com/android/launcher3/util/DaggerSingletonObject.java b/src/com/android/launcher3/util/DaggerSingletonObject.java index a245761fb7..a4bd30a193 100644 --- a/src/com/android/launcher3/util/DaggerSingletonObject.java +++ b/src/com/android/launcher3/util/DaggerSingletonObject.java @@ -24,8 +24,7 @@ import com.android.launcher3.dagger.LauncherComponentProvider; import java.util.function.Function; /** - * A class to provide DaggerSingleton objects in a traditional way for - * {@link MainThreadInitializedObject}. + * A class to provide DaggerSingleton objects in a traditional way. * We should delete this class at the end and use @Inject to get dagger provided singletons. */ diff --git a/src/com/android/launcher3/util/DaggerSingletonTracker.java b/src/com/android/launcher3/util/DaggerSingletonTracker.java index b7a88dbf44..34b376091e 100644 --- a/src/com/android/launcher3/util/DaggerSingletonTracker.java +++ b/src/com/android/launcher3/util/DaggerSingletonTracker.java @@ -45,7 +45,7 @@ public class DaggerSingletonTracker implements SafeCloseable { * Adds the SafeCloseable Singletons to the mLauncherAppSingletons list. * This helps to track the singletons and close them appropriately. * See {@link DaggerSingletonTracker#close()} and - * {@link MainThreadInitializedObject.SandboxContext#onDestroy()} + * {@link SandboxContext#onDestroy()} */ public void addCloseable(SafeCloseable closeable) { MAIN_EXECUTOR.execute(() -> { diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java deleted file mode 100644 index 356a5517e2..0000000000 --- a/src/com/android/launcher3/util/MainThreadInitializedObject.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright (C) 2018 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.launcher3.util; - -import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; - -import android.content.Context; -import android.os.Looper; -import android.util.Log; - -import androidx.annotation.UiThread; -import androidx.annotation.VisibleForTesting; - -import com.android.launcher3.LauncherApplication; -import com.android.launcher3.util.ResourceBasedOverride.Overrides; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; - -/** - * Utility class for defining singletons which are initiated on main thread. - * - * TODO(b/361850561): Do not delete MainThreadInitializedObject until we find a way to - * unregister and understand how singleton objects are destroyed in dagger graph. - */ -public class MainThreadInitializedObject { - - private final ObjectProvider mProvider; - private T mValue; - - public MainThreadInitializedObject(ObjectProvider provider) { - mProvider = provider; - } - - public T get(Context context) { - Context app = context.getApplicationContext(); - if (app instanceof ObjectSandbox sc) { - return sc.getObject(this); - } - - if (mValue == null) { - if (Looper.myLooper() == Looper.getMainLooper()) { - mValue = TraceHelper.allowIpcs("main.thread.object", () -> mProvider.get(app)); - } else { - try { - return MAIN_EXECUTOR.submit(() -> get(context)).get(); - } catch (InterruptedException|ExecutionException e) { - throw new RuntimeException(e); - } - } - } - return mValue; - } - - /** - * Executes the callback is the value is already created - * @return true if the callback was executed, false otherwise - */ - public boolean executeIfCreated(Consumer callback) { - T v = mValue; - if (v != null) { - callback.accept(v); - return true; - } else { - return false; - } - } - - @VisibleForTesting - public void initializeForTesting(T value) { - mValue = value; - } - - /** - * Initializes a provider based on resource overrides - */ - public static MainThreadInitializedObject - forOverride(Class clazz, int resourceId) { - return new MainThreadInitializedObject<>(c -> Overrides.getObject(clazz, c, resourceId)); - } - - public interface ObjectProvider { - - T get(Context context); - } - - /** Sandbox for isolating {@link MainThreadInitializedObject} instances from Launcher. */ - public interface ObjectSandbox { - - /** - * Find a cached object from mObjectMap if we have already created one. If not, generate - * an object using the provider. - */ - T getObject(MainThreadInitializedObject object); - - - /** - * Put a value into cache, can be used to put mocked MainThreadInitializedObject - * instances. - */ - void putObject(MainThreadInitializedObject object, T value); - - /** - * Returns whether this sandbox should cleanup all objects when its destroyed or leave it - * to the GC. - * These objects can have listeners attached to the system server and mey not be able to get - * GCed themselves when running on a device. - * Some environments like Robolectric tear down the whole system at the end of the test, - * so manual cleanup may not be required. - */ - default boolean shouldCleanUpOnDestroy() { - return true; - } - - @UiThread - default T createObject(MainThreadInitializedObject object) { - return object.mProvider.get((Context) this); - } - } - - /** - * Abstract Context which allows custom implementations for - * {@link MainThreadInitializedObject} providers - */ - public static class SandboxContext extends LauncherApplication implements ObjectSandbox { - - private static final String TAG = "SandboxContext"; - - private final Map mObjectMap = new HashMap<>(); - private final ArrayList mOrderedObjects = new ArrayList<>(); - - private final Object mDestroyLock = new Object(); - private boolean mDestroyed = false; - - public SandboxContext(Context base) { - attachBaseContext(base); - } - - @Override - public Context getApplicationContext() { - return this; - } - - @Override - public boolean shouldCleanUpOnDestroy() { - return (getBaseContext().getApplicationContext() instanceof ObjectSandbox os) - ? os.shouldCleanUpOnDestroy() : true; - } - - public void onDestroy() { - if (shouldCleanUpOnDestroy()) { - cleanUpObjects(); - } - } - - protected void cleanUpObjects() { - getAppComponent().getDaggerSingletonTracker().close(); - synchronized (mDestroyLock) { - // Destroy in reverse order - for (int i = mOrderedObjects.size() - 1; i >= 0; i--) { - mOrderedObjects.get(i).close(); - } - mDestroyed = true; - } - } - - @Override - public T getObject(MainThreadInitializedObject object) { - synchronized (mDestroyLock) { - if (mDestroyed) { - Log.e(TAG, "Static object access with a destroyed context"); - } - T t = (T) mObjectMap.get(object); - if (t != null) { - return t; - } - if (Looper.myLooper() == Looper.getMainLooper()) { - t = createObject(object); - mObjectMap.put(object, t); - mOrderedObjects.add(t); - return t; - } - } - - try { - return MAIN_EXECUTOR.submit(() -> getObject(object)).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } - } - - @Override - public void putObject( - MainThreadInitializedObject object, T value) { - mObjectMap.put(object, value); - } - } -} diff --git a/src/com/android/launcher3/util/SandboxContext.kt b/src/com/android/launcher3/util/SandboxContext.kt new file mode 100644 index 0000000000..c6224e2327 --- /dev/null +++ b/src/com/android/launcher3/util/SandboxContext.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2025 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.launcher3.util + +import android.content.Context +import com.android.launcher3.LauncherApplication + +/** Abstract Context which allows custom implementations for dagger components. */ +open class SandboxContext(base: Context?) : LauncherApplication() { + init { + base?.let { attachBaseContext(it) } + } + + override fun getApplicationContext(): Context { + return this + } + + /** + * Returns whether this sandbox should cleanup all objects when its destroyed or leave it to the + * GC. These objects can have listeners attached to the system server and mey not be able to get + * GCed themselves when running on a device. Some environments like Robolectric tear down the + * whole system at the end of the test, so manual cleanup may not be required. + */ + open fun shouldCleanUpOnDestroy(): Boolean { + return (getBaseContext().getApplicationContext() as? SandboxContext) + ?.shouldCleanUpOnDestroy() ?: true + } + + fun onDestroy() { + if (shouldCleanUpOnDestroy()) { + cleanUpObjects() + } + } + + open protected fun cleanUpObjects() { + appComponent.daggerSingletonTracker.close() + } + + companion object { + private const val TAG = "SandboxContext" + } +} diff --git a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt index 3658989384..ad6afcf1ac 100644 --- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt @@ -35,8 +35,8 @@ import com.android.launcher3.testing.shared.ResourceUtils import com.android.launcher3.util.AllModulesMinusWMProxy import com.android.launcher3.util.DisplayController import com.android.launcher3.util.FakePrefsModule -import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext import com.android.launcher3.util.NavigationMode +import com.android.launcher3.util.SandboxContext import com.android.launcher3.util.WindowBounds import com.android.launcher3.util.rule.TestStabilityRule import com.android.launcher3.util.rule.setFlags diff --git a/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java index cee5559c39..4458e8fb01 100644 --- a/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java +++ b/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java @@ -56,7 +56,6 @@ import com.android.launcher3.model.BgDataModel; import com.android.launcher3.model.BgDataModel.Callbacks; import com.android.launcher3.model.ModelDbController; import com.android.launcher3.testing.TestInformationProvider; -import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt index 0da8891e47..2fa4cad61d 100644 --- a/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt +++ b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt @@ -27,7 +27,6 @@ import android.os.UserHandle import android.view.Display import androidx.test.core.app.ApplicationProvider import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext -import com.android.launcher3.util.MainThreadInitializedObject.ObjectSandbox import org.junit.Rule import org.junit.rules.ExternalResource import org.junit.rules.TestRule @@ -69,7 +68,7 @@ class SandboxApplication private constructor(private val base: SandboxApplicatio // Defer to the true application to decide whether to clean up. For instance, we do not want // to cleanup under Robolectric. val app = ApplicationProvider.getApplicationContext() - return if (app is ObjectSandbox) app.shouldCleanUpOnDestroy() else true + return (app as? SandboxContext)?.shouldCleanUpOnDestroy() ?: true } override fun apply(statement: Statement, description: Description): Statement { diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplicationTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplicationTest.kt index d87a4063ec..8a21cff6a7 100644 --- a/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplicationTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplicationTest.kt @@ -16,7 +16,6 @@ package com.android.launcher3.util -import android.content.Context import android.hardware.display.DisplayManager import android.view.Display import android.view.Display.DEFAULT_DISPLAY @@ -63,16 +62,4 @@ class SandboxApplicationTest { onDestroy() } } - - @Test - fun testGetObject_objectCreatesDisplayContext_isSandboxed() { - class TestSingleton(context: Context) : SafeCloseable { - override fun close() = Unit - - val displayContext = context.createDisplayContext(display) - } - - val displayContext = MainThreadInitializedObject { TestSingleton(it) }[app].displayContext - assertThat(displayContext.applicationContext).isEqualTo(app) - } } diff --git a/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java b/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java index 393282f442..98d067c86a 100644 --- a/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java +++ b/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java @@ -18,8 +18,6 @@ package com.android.launcher3.util; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; -import static com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; - import android.content.ContextWrapper; import androidx.annotation.Nullable; @@ -44,7 +42,7 @@ import java.util.concurrent.CountDownLatch; * There are 2 constructors in this class. The base context can be {@link SandboxContext} or * Instrumentation target context. * Using {@link SandboxContext} as base context allows custom implementations for - * MainThreadInitializedObject providers. + * providing objects in Dagger components. */ public class TestSandboxModelContextWrapper extends ActivityContextWrapper implements