diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java index 01d5ff0e15..56fc4d1c0b 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java @@ -17,7 +17,6 @@ package com.android.launcher3.uioverrides; import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; -import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetHostView; @@ -100,7 +99,7 @@ public final class QuickstepWidgetHolder extends LauncherWidgetHolder { // for concurrent modification. new ArrayList<>(h.mProviderChangedListeners).forEach( ProviderChangedListener::notifyWidgetProvidersChanged))), - UI_HELPER_EXECUTOR.getLooper()); + getWidgetHolderExecutor().getLooper()); if (WIDGETS_ENABLED) { sWidgetHost.startListening(); } @@ -199,8 +198,10 @@ public final class QuickstepWidgetHolder extends LauncherWidgetHolder { return; } - sWidgetHost.setAppWidgetHidden(); - setListeningFlag(false); + getWidgetHolderExecutor().execute(() -> { + sWidgetHost.setAppWidgetHidden(); + setListeningFlag(false); + }); } @Override diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 4e566abddc..d90580158b 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -2797,9 +2797,11 @@ public class Launcher extends StatefulActivity } private void updateDisallowBack() { - if (BuildCompat.isAtLeastV() && Flags.enableDesktopWindowingMode() - && mDeviceProfile.isTablet) { - // TODO(b/330183377) disable back in launcher when when we productionize + if (BuildCompat.isAtLeastV() + && Flags.enableDesktopWindowingMode() + && !Flags.enableDesktopWindowingWallpaperActivity() + && mDeviceProfile.isTablet) { + // TODO(b/333533253): Clean up after desktop wallpaper activity flag is rolled out return; } LauncherRootView rv = getRootView(); diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index dc6968c7d9..312c6f412a 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -209,7 +209,7 @@ public class LoaderTask implements Runnable { mApp.getContext().getContentResolver(), "launcher_broadcast_installed_apps", /* def= */ 0); - if (launcherBroadcastInstalledApps == 1) { + if (launcherBroadcastInstalledApps == 1 && mIsRestoreFromBackup) { List broadcastModels = FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast( mPmHelper, diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java index a5e22c5899..1fb8c8306f 100644 --- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java +++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java @@ -20,6 +20,7 @@ import static android.app.Activity.RESULT_CANCELED; import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED; import static com.android.launcher3.Flags.enableWorkspaceInflation; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo; import android.appwidget.AppWidgetHost; @@ -36,6 +37,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import com.android.launcher3.BaseActivity; import com.android.launcher3.BaseDraggingActivity; @@ -44,6 +46,7 @@ import com.android.launcher3.Utilities; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; +import com.android.launcher3.util.LooperExecutor; import com.android.launcher3.util.ResourceBasedOverride; import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.widget.LauncherAppWidgetHost.ListenableHostView; @@ -51,6 +54,7 @@ import com.android.launcher3.widget.custom.CustomWidgetManager; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.IntConsumer; /** @@ -77,7 +81,7 @@ public class LauncherWidgetHolder { protected final SparseArray mViews = new SparseArray<>(); protected final List mProviderChangedListeners = new ArrayList<>(); - protected int mFlags = FLAG_STATE_IS_NORMAL; + protected AtomicInteger mFlags = new AtomicInteger(FLAG_STATE_IS_NORMAL); // TODO(b/191735836): Replace with ActivityOptions.KEY_SPLASH_SCREEN_STYLE when un-hidden private static final String KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle"; @@ -96,6 +100,10 @@ public class LauncherWidgetHolder { context, appWidgetRemovedCallback, mProviderChangedListeners); } + protected LooperExecutor getWidgetHolderExecutor() { + return UI_HELPER_EXECUTOR; + } + /** * Starts listening to the widget updates from the server side */ @@ -104,21 +112,23 @@ public class LauncherWidgetHolder { return; } - try { - mWidgetHost.startListening(); - } catch (Exception e) { - if (!Utilities.isBinderSizeError(e)) { - throw new RuntimeException(e); + getWidgetHolderExecutor().execute(() -> { + try { + mWidgetHost.startListening(); + } catch (Exception e) { + if (!Utilities.isBinderSizeError(e)) { + throw new RuntimeException(e); + } + // We're willing to let this slide. The exception is being caused by the list of + // RemoteViews which is being passed back. The startListening relationship will + // have been established by this point, and we will end up populating the + // widgets upon bind anyway. See issue 14255011 for more context. } - // We're willing to let this slide. The exception is being caused by the list of - // RemoteViews which is being passed back. The startListening relationship will - // have been established by this point, and we will end up populating the - // widgets upon bind anyway. See issue 14255011 for more context. - } - // TODO: Investigate why widgetHost.startListening() always return non-empty updates - setListeningFlag(true); + // TODO: Investigate why widgetHost.startListening() always return non-empty updates + setListeningFlag(true); - updateDeferredView(); + MAIN_EXECUTOR.execute(() -> updateDeferredView()); + }); } /** @@ -282,16 +292,23 @@ public class LauncherWidgetHolder { if (!WIDGETS_ENABLED) { return; } - mWidgetHost.stopListening(); - setListeningFlag(false); + getWidgetHolderExecutor().execute(() -> { + mWidgetHost.stopListening(); + setListeningFlag(false); + }); } + /** + * Update {@link FLAG_LISTENING} on {@link mFlags} after making binder calls from + * {@link sWidgetHost}. + */ + @WorkerThread protected void setListeningFlag(final boolean isListening) { if (isListening) { - mFlags |= FLAG_LISTENING; + mFlags.updateAndGet(old -> old | FLAG_LISTENING); return; } - mFlags &= ~FLAG_LISTENING; + mFlags.updateAndGet(old -> old & ~FLAG_LISTENING); } /** @@ -373,7 +390,7 @@ public class LauncherWidgetHolder { * as a result of using the same flow. */ protected LauncherAppWidgetHostView recycleExistingView(LauncherAppWidgetHostView view) { - if ((mFlags & FLAG_LISTENING) == 0) { + if ((mFlags.get() & FLAG_LISTENING) == 0) { if (view instanceof PendingAppWidgetHostView pv && pv.isDeferredWidget()) { return view; } else { @@ -395,7 +412,7 @@ public class LauncherWidgetHolder { @NonNull protected LauncherAppWidgetHostView createViewInternal( int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) { - if ((mFlags & FLAG_LISTENING) == 0) { + if ((mFlags.get() & FLAG_LISTENING) == 0) { // Since the launcher hasn't started listening to widget updates, we can't simply call // host.createView here because the later will make a binder call to retrieve // RemoteViews from system process. @@ -460,7 +477,7 @@ public class LauncherWidgetHolder { * @return True if the host is listening to the updates, false otherwise */ public boolean isListening() { - return (mFlags & FLAG_LISTENING) != 0; + return (mFlags.get() & FLAG_LISTENING) != 0; } /** @@ -469,16 +486,17 @@ public class LauncherWidgetHolder { */ private void setShouldListenFlag(int flag, boolean on) { if (on) { - mFlags |= flag; + mFlags.updateAndGet(old -> old | flag); } else { - mFlags &= ~flag; + mFlags.updateAndGet(old -> old & ~flag); } final boolean listening = isListening(); - if (!listening && shouldListen(mFlags)) { + int currentFlag = mFlags.get(); + if (!listening && shouldListen(currentFlag)) { // Postpone starting listening until all flags are on. startListening(); - } else if (listening && (mFlags & FLAG_ACTIVITY_STARTED) == 0) { + } else if (listening && (currentFlag & FLAG_ACTIVITY_STARTED) == 0) { // Postpone stopping listening until the activity is stopped. stopListening(); } diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt index 28a001f990..d16674c6c2 100644 --- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt +++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt @@ -1,10 +1,13 @@ package com.android.launcher3.model import android.appwidget.AppWidgetManager +import android.content.Intent import android.os.UserHandle import android.platform.test.flag.junit.SetFlagsRule +import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.launcher3.Flags import com.android.launcher3.InvariantDeviceProfile import com.android.launcher3.LauncherAppState @@ -14,6 +17,7 @@ import com.android.launcher3.icons.IconCache import com.android.launcher3.icons.cache.CachingLogic import com.android.launcher3.icons.cache.IconCacheUpdateHandler import com.android.launcher3.pm.UserCache +import com.android.launcher3.provider.RestoreDbTask import com.android.launcher3.ui.TestViewHelpers import com.android.launcher3.util.Executors.MODEL_EXECUTOR import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext @@ -21,21 +25,30 @@ import com.android.launcher3.util.LooperIdleLock import com.android.launcher3.util.UserIconInfo import com.google.common.truth.Truth import java.util.concurrent.CountDownLatch +import junit.framework.Assert.assertEquals import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyList +import org.mockito.ArgumentMatchers.anyMap import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.times -import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations +import org.mockito.MockitoSession import org.mockito.Spy +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doReturn +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness private const val INSERTION_STATEMENT_FILE = "databases/workspace_items.sql" @@ -43,6 +56,20 @@ private const val INSERTION_STATEMENT_FILE = "databases/workspace_items.sql" @RunWith(AndroidJUnit4::class) class LoaderTaskTest { private var context = SandboxModelContext() + private val expectedBroadcastModel = + FirstScreenBroadcastModel( + installerPackage = "installerPackage", + pendingCollectionItems = mutableSetOf("pendingCollectionItem"), + pendingWidgetItems = mutableSetOf("pendingWidgetItem"), + pendingHotseatItems = mutableSetOf("pendingHotseatItem"), + pendingWorkspaceItems = mutableSetOf("pendingWorkspaceItem"), + installedHotseatItems = mutableSetOf("installedHotseatItem"), + installedWorkspaceItems = mutableSetOf("installedWorkspaceItem"), + firstScreenInstalledWidgets = mutableSetOf("installedFirstScreenWidget"), + secondaryScreenInstalledWidgets = mutableSetOf("installedSecondaryScreenWidget") + ) + private lateinit var mockitoSession: MockitoSession + @Mock private lateinit var app: LauncherAppState @Mock private lateinit var bgAllAppsList: AllAppsList @Mock private lateinit var modelDelegate: ModelDelegate @@ -61,7 +88,11 @@ class LoaderTaskTest { @Before fun setup() { MockitoAnnotations.initMocks(this) - + mockitoSession = + ExtendedMockito.mockitoSession() + .strictness(Strictness.LENIENT) + .mockStatic(FirstScreenBroadcastHelper::class.java) + .startMocking() val idp = InvariantDeviceProfile().apply { numRows = 5 @@ -90,6 +121,7 @@ class LoaderTaskTest { @After fun tearDown() { context.onDestroy() + mockitoSession.finishMocking() } @Test @@ -166,6 +198,141 @@ class LoaderTaskTest { verify(bgAllAppsList, Mockito.never()) .setFlags(BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED, true) } + + @Test + fun `When launcher_broadcast_installed_apps and is restore then send installed item broadcast`() { + // Given + val spyContext = spy(context) + `when`(app.context).thenReturn(spyContext) + whenever( + FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast( + anyOrNull(), + anyList(), + anyMap(), + anyList() + ) + ) + .thenReturn(listOf(expectedBroadcastModel)) + + whenever( + FirstScreenBroadcastHelper.sendBroadcastsForModels( + spyContext, + listOf(expectedBroadcastModel) + ) + ) + .thenCallRealMethod() + + Settings.Secure.putInt(spyContext.contentResolver, "launcher_broadcast_installed_apps", 1) + RestoreDbTask.setPending(spyContext) + + // When + LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder) + .runSyncOnBackgroundThread() + + // Then + val argumentCaptor = ArgumentCaptor.forClass(Intent::class.java) + verify(spyContext).sendBroadcast(argumentCaptor.capture()) + val actualBroadcastIntent = argumentCaptor.value + assertEquals(expectedBroadcastModel.installerPackage, actualBroadcastIntent.`package`) + assertEquals( + ArrayList(expectedBroadcastModel.installedWorkspaceItems), + actualBroadcastIntent.getStringArrayListExtra("workspaceInstalledItems") + ) + assertEquals( + ArrayList(expectedBroadcastModel.installedHotseatItems), + actualBroadcastIntent.getStringArrayListExtra("hotseatInstalledItems") + ) + assertEquals( + ArrayList( + expectedBroadcastModel.firstScreenInstalledWidgets + + expectedBroadcastModel.secondaryScreenInstalledWidgets + ), + actualBroadcastIntent.getStringArrayListExtra("widgetInstalledItems") + ) + assertEquals( + ArrayList(expectedBroadcastModel.pendingCollectionItems), + actualBroadcastIntent.getStringArrayListExtra("folderItem") + ) + assertEquals( + ArrayList(expectedBroadcastModel.pendingWorkspaceItems), + actualBroadcastIntent.getStringArrayListExtra("workspaceItem") + ) + assertEquals( + ArrayList(expectedBroadcastModel.pendingHotseatItems), + actualBroadcastIntent.getStringArrayListExtra("hotseatItem") + ) + assertEquals( + ArrayList(expectedBroadcastModel.pendingWidgetItems), + actualBroadcastIntent.getStringArrayListExtra("widgetItem") + ) + } + + @Test + fun `When not a restore then installed item broadcast not sent`() { + // Given + val spyContext = spy(context) + `when`(app.context).thenReturn(spyContext) + whenever( + FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast( + anyOrNull(), + anyList(), + anyMap(), + anyList() + ) + ) + .thenReturn(listOf(expectedBroadcastModel)) + + whenever( + FirstScreenBroadcastHelper.sendBroadcastsForModels( + spyContext, + listOf(expectedBroadcastModel) + ) + ) + .thenCallRealMethod() + + Settings.Secure.putInt(spyContext.contentResolver, "launcher_broadcast_installed_apps", 1) + + // When + LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder) + .runSyncOnBackgroundThread() + + // Then + verify(spyContext, times(0)).sendBroadcast(any(Intent::class.java)) + } + + @Test + fun `When launcher_broadcast_installed_apps false then installed item broadcast not sent`() { + // Given + val spyContext = spy(context) + `when`(app.context).thenReturn(spyContext) + whenever( + FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast( + anyOrNull(), + anyList(), + anyMap(), + anyList() + ) + ) + .thenReturn(listOf(expectedBroadcastModel)) + + whenever( + FirstScreenBroadcastHelper.sendBroadcastsForModels( + spyContext, + listOf(expectedBroadcastModel) + ) + ) + .thenCallRealMethod() + + Settings.Secure.putInt(spyContext.contentResolver, "launcher_broadcast_installed_apps", 0) + RestoreDbTask.setPending(spyContext) + + // When + LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder) + .runSyncOnBackgroundThread() + + // Then + verify(spyContext, times(0)).sendBroadcast(any(Intent::class.java)) + } } private fun LoaderTask.runSyncOnBackgroundThread() {