diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java index 255ba1e054..7d0afe1aba 100644 --- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java @@ -90,12 +90,17 @@ public class LauncherTaskbarUIController extends TaskbarUIController { mHotseatController.init(); mLauncher.setTaskbarUIController(this); mKeyguardController = taskbarControllers.taskbarKeyguardController; + onLauncherResumedOrPaused(mLauncher.hasBeenResumed()); mIconAlignmentForResumedState.finishAnimation(); + onIconAlignmentRatioChanged(); } @Override protected void onDestroy() { + mIconAlignmentForResumedState.finishAnimation(); + mIconAlignmentForGestureState.finishAnimation(); + mHotseatController.cleanup(); setTaskbarViewVisible(true); mLauncher.getHotseat().setIconsAlpha(1f); diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java index d40242c42c..2facd4436c 100644 --- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java @@ -105,7 +105,7 @@ public class NavbarButtonsViewController { return true; }; - if (mContext.canShowNavButtons()) { + if (mContext.isThreeButtonNav()) { initButtons(mStartContainer, mEndContainer, mControllers.navButtonController); // Animate taskbar background when IME shows diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index 106ebe54dc..41426108e5 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -161,8 +161,8 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ mWindowManager.addView(mDragLayer, mWindowLayoutParams); } - public boolean canShowNavButtons() { - return ENABLE_THREE_BUTTON_TASKBAR && mNavMode == Mode.THREE_BUTTONS; + public boolean isThreeButtonNav() { + return mNavMode == Mode.THREE_BUTTONS; } @Override @@ -215,7 +215,7 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ } public void updateSysuiStateFlags(int systemUiStateFlags, boolean forceUpdate) { - if (!canShowNavButtons()) { + if (!isThreeButtonNav()) { return; } mControllers.navbarButtonsViewController.updateStateForSysuiFlags( diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java index 157053efdd..c48c28b9e5 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java @@ -61,7 +61,7 @@ public class TaskbarControllers { */ public void init() { navbarButtonsViewController.init(this); - if (taskbarActivityContext.canShowNavButtons()) { + if (taskbarActivityContext.isThreeButtonNav()) { rotationButtonController.init(); } taskbarDragLayerController.init(this); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java index 743619b6be..db5c387770 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java @@ -93,7 +93,7 @@ public class TaskbarDragLayerController { */ public void updateInsetsTouchability(InsetsInfo insetsInfo) { insetsInfo.touchableRegion.setEmpty(); - if (mActivity.canShowNavButtons()) { + if (mActivity.isThreeButtonNav()) { // Always have nav buttons be touchable mControllers.navbarButtonsViewController.addVisibleButtonsRegion( mTaskbarDragLayer, insetsInfo.touchableRegion); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarKeyguardController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarKeyguardController.java index b7799d8149..2936bd2146 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarKeyguardController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarKeyguardController.java @@ -57,7 +57,7 @@ public class TaskbarKeyguardController { mKeyguardSysuiFlags = interestingKeyguardFlags; mBouncerShowing = bouncerShowing; - if (!mContext.canShowNavButtons()) { + if (!mContext.isThreeButtonNav()) { // For gesture nav we don't need to deal with bouncer or showing taskbar when locked return; } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java index 8549ca2ac5..8b0f426699 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java @@ -94,10 +94,7 @@ public class TaskbarNavButtonController { } private void navigateToOverview() { - int commandType = mService.getOverviewCommandHelper().isOverviewVisible() ? - OverviewCommandHelper.TYPE_TOGGLE : - OverviewCommandHelper.TYPE_SHOW; - mService.getOverviewCommandHelper().addCommand(commandType); + mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_TOGGLE); } private void executeBack() { diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java index 728fec2732..2beef0a7ce 100644 --- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java +++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java @@ -136,11 +136,6 @@ public class OverviewCommandHelper { } } - public boolean isOverviewVisible() { - BaseActivityInterface activityInterface = - mOverviewComponentObserver.getActivityInterface(); - return activityInterface.getVisibleRecentsView() != null; - } /** * Executes the task and returns true if next task can be executed. If false, then the next * task is deferred until {@link #scheduleNextTask} is called diff --git a/res/values/config.xml b/res/values/config.xml index 299c80ef8b..04c359ede3 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -76,6 +76,9 @@ + + + diff --git a/robolectric_tests/src/com/android/launcher3/widget/CachingWidgetPreviewLoaderTest.java b/robolectric_tests/src/com/android/launcher3/widget/CachingWidgetPreviewLoaderTest.java new file mode 100644 index 0000000000..c18e26ce34 --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/widget/CachingWidgetPreviewLoaderTest.java @@ -0,0 +1,409 @@ +/* + * Copyright (C) 2021 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.widget; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.graphics.Bitmap; +import android.os.CancellationSignal; +import android.os.UserHandle; +import android.util.Size; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.icons.IconCache; +import com.android.launcher3.model.WidgetItem; +import com.android.launcher3.testing.TestActivity; +import com.android.launcher3.widget.WidgetPreviewLoader.WidgetPreviewLoadedCallback; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; + +import java.util.Arrays; +import java.util.Collections; + +@RunWith(RobolectricTestRunner.class) +public class CachingWidgetPreviewLoaderTest { + private static final Size SIZE_10_10 = new Size(10, 10); + private static final Size SIZE_20_20 = new Size(20, 20); + private static final String TEST_PACKAGE = "com.example.test"; + private static final ComponentName TEST_PROVIDER = + new ComponentName(TEST_PACKAGE, ".WidgetProvider"); + private static final ComponentName TEST_PROVIDER2 = + new ComponentName(TEST_PACKAGE, ".WidgetProvider2"); + private static final Bitmap BITMAP = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888); + private static final Bitmap BITMAP2 = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888); + + + @Mock private CancellationSignal mCancellationSignal; + @Mock private WidgetPreviewLoader mDelegate; + @Mock private IconCache mIconCache; + @Mock private DeviceProfile mDeviceProfile; + @Mock private LauncherAppWidgetProviderInfo mProviderInfo; + @Mock private LauncherAppWidgetProviderInfo mProviderInfo2; + @Mock private WidgetPreviewLoadedCallback mPreviewLoadedCallback; + @Mock private WidgetPreviewLoadedCallback mPreviewLoadedCallback2; + @Captor private ArgumentCaptor mCallbackCaptor; + + private TestActivity mTestActivity; + private CachingWidgetPreviewLoader mLoader; + private WidgetItem mWidgetItem; + private WidgetItem mWidgetItem2; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mLoader = new CachingWidgetPreviewLoader(mDelegate); + + mTestActivity = Robolectric.buildActivity(TestActivity.class).setup().get(); + mTestActivity.setDeviceProfile(mDeviceProfile); + + when(mDelegate.loadPreview(any(), any(), any(), any())).thenReturn(mCancellationSignal); + + mProviderInfo.provider = TEST_PROVIDER; + when(mProviderInfo.getProfile()).thenReturn(new UserHandle(0)); + + mProviderInfo2.provider = TEST_PROVIDER2; + when(mProviderInfo2.getProfile()).thenReturn(new UserHandle(0)); + + InvariantDeviceProfile testProfile = new InvariantDeviceProfile(); + testProfile.numRows = 5; + testProfile.numColumns = 5; + + mWidgetItem = new WidgetItem(mProviderInfo, testProfile, mIconCache); + mWidgetItem2 = new WidgetItem(mProviderInfo2, testProfile, mIconCache); + } + + @Test + public void getPreview_notInCache_shouldReturnNull() { + assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isNull(); + } + + @Test + public void getPreview_notInCache_shouldNotCallDelegate() { + mLoader.getPreview(mWidgetItem, SIZE_10_10); + + verifyZeroInteractions(mDelegate); + } + + @Test + public void getPreview_inCache_shouldReturnCachedBitmap() { + loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP); + + assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP); + } + + @Test + public void getPreview_otherSizeInCache_shouldReturnNull() { + loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP); + + assertThat(mLoader.getPreview(mWidgetItem, SIZE_20_20)).isNull(); + } + + @Test + public void getPreview_otherItemInCache_shouldReturnNull() { + loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP); + + assertThat(mLoader.getPreview(mWidgetItem2, SIZE_10_10)).isNull(); + } + + @Test + public void getPreview_shouldStoreMultipleSizesPerItem() { + loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP); + loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP2); + + assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP); + assertThat(mLoader.getPreview(mWidgetItem, SIZE_20_20)).isEqualTo(BITMAP2); + } + + @Test + public void loadPreview_notInCache_shouldStartLoading() { + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback); + + verify(mDelegate).loadPreview(eq(mTestActivity), eq(mWidgetItem), eq(SIZE_10_10), any()); + verifyZeroInteractions(mPreviewLoadedCallback); + } + + @Test + public void loadPreview_thenLoaded_shouldCallBack() { + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback); + verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture()); + WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue(); + + loaderCallback.onPreviewLoaded(BITMAP); + + verify(mPreviewLoadedCallback).onPreviewLoaded(BITMAP); + } + + @Test + public void loadPreview_thenCancelled_shouldCancelDelegateRequest() { + CancellationSignal cancellationSignal = + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback); + + cancellationSignal.cancel(); + + verify(mCancellationSignal).cancel(); + verifyZeroInteractions(mPreviewLoadedCallback); + assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isNull(); + } + + @Test + public void loadPreview_thenCancelled_otherCallListening_shouldNotCancelDelegateRequest() { + CancellationSignal cancellationSignal1 = + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback); + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2); + + cancellationSignal1.cancel(); + + verifyZeroInteractions(mCancellationSignal); + } + + @Test + public void loadPreview_thenCancelled_otherCallListening_loaded_shouldCallBackToNonCancelled() { + CancellationSignal cancellationSignal1 = + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback); + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2); + verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture()); + WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue(); + + cancellationSignal1.cancel(); + loaderCallback.onPreviewLoaded(BITMAP); + + verifyZeroInteractions(mPreviewLoadedCallback); + verify(mPreviewLoadedCallback2).onPreviewLoaded(BITMAP); + assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP); + } + + @Test + public void loadPreview_thenCancelled_bothCallsCancelled_shouldCancelDelegateRequest() { + CancellationSignal cancellationSignal1 = + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback); + CancellationSignal cancellationSignal2 = + mLoader.loadPreview( + mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2); + + cancellationSignal1.cancel(); + cancellationSignal2.cancel(); + + verify(mCancellationSignal).cancel(); + verifyZeroInteractions(mPreviewLoadedCallback); + verifyZeroInteractions(mPreviewLoadedCallback2); + assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isNull(); + } + + @Test + public void loadPreview_multipleCallbacks_shouldOnlyCallDelegateOnce() { + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback); + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2); + + verify(mDelegate).loadPreview(any(), any(), any(), any()); + } + + @Test + public void loadPreview_multipleCallbacks_shouldForwardResultToEachCallback() { + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback); + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2); + + verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture()); + WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue(); + + loaderCallback.onPreviewLoaded(BITMAP); + + verify(mPreviewLoadedCallback).onPreviewLoaded(BITMAP); + verify(mPreviewLoadedCallback2).onPreviewLoaded(BITMAP); + } + + @Test + public void loadPreview_inCache_shouldCallBackImmediately() { + loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP); + reset(mDelegate); + + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback); + + verify(mPreviewLoadedCallback).onPreviewLoaded(BITMAP); + verifyZeroInteractions(mDelegate); + } + + @Test + public void loadPreview_thenLoaded_thenCancelled_shouldNotRemovePreviewFromCache() { + CancellationSignal cancellationSignal = + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback); + verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture()); + WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue(); + loaderCallback.onPreviewLoaded(BITMAP); + + cancellationSignal.cancel(); + + assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP); + } + + @Test + public void isPreviewLoaded_notLoaded_shouldReturnFalse() { + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse(); + } + + @Test + public void isPreviewLoaded_otherSizeLoaded_shouldReturnFalse() { + loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP); + + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse(); + } + + @Test + public void isPreviewLoaded_otherItemLoaded_shouldReturnFalse() { + loadPreviewIntoCache(mWidgetItem2, SIZE_10_10, BITMAP); + + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse(); + } + + @Test + public void isPreviewLoaded_loaded_shouldReturnTrue() { + loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP); + + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isTrue(); + } + + @Test + public void clearPreviews_notInCache_shouldBeNoOp() { + mLoader.clearPreviews(Collections.singletonList(mWidgetItem)); + + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse(); + } + + @Test + public void clearPreviews_inCache_shouldRemovePreview() { + loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP); + + mLoader.clearPreviews(Collections.singletonList(mWidgetItem)); + + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse(); + } + + @Test + public void clearPreviews_inCache_multipleSizes_shouldRemoveAllSizes() { + loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP); + loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP); + + mLoader.clearPreviews(Collections.singletonList(mWidgetItem)); + + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse(); + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_20_20)).isFalse(); + } + + @Test + public void clearPreviews_inCache_otherItems_shouldOnlyRemoveSpecifiedItems() { + loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP); + loadPreviewIntoCache(mWidgetItem2, SIZE_10_10, BITMAP); + + mLoader.clearPreviews(Collections.singletonList(mWidgetItem)); + + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse(); + assertThat(mLoader.isPreviewLoaded(mWidgetItem2, SIZE_10_10)).isTrue(); + } + + @Test + public void clearPreviews_inCache_otherItems_shouldRemoveAllSpecifiedItems() { + loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP); + loadPreviewIntoCache(mWidgetItem2, SIZE_10_10, BITMAP); + + mLoader.clearPreviews(Arrays.asList(mWidgetItem, mWidgetItem2)); + + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse(); + assertThat(mLoader.isPreviewLoaded(mWidgetItem2, SIZE_10_10)).isFalse(); + } + + @Test + public void clearPreviews_loading_shouldCancelLoad() { + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback); + + mLoader.clearPreviews(Collections.singletonList(mWidgetItem)); + + verify(mCancellationSignal).cancel(); + } + + @Test + public void clearAll_cacheEmpty_shouldBeNoOp() { + mLoader.clearAll(); + + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse(); + } + + @Test + public void clearAll_inCache_shouldRemovePreview() { + loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP); + + mLoader.clearAll(); + + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse(); + } + + @Test + public void clearAll_inCache_multipleSizes_shouldRemoveAllSizes() { + loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP); + loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP); + + mLoader.clearAll(); + + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse(); + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_20_20)).isFalse(); + } + + @Test + public void clearAll_inCache_multipleItems_shouldRemoveAll() { + loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP); + loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP); + loadPreviewIntoCache(mWidgetItem2, SIZE_20_20, BITMAP); + + mLoader.clearAll(); + + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse(); + assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_20_20)).isFalse(); + assertThat(mLoader.isPreviewLoaded(mWidgetItem2, SIZE_20_20)).isFalse(); + } + + @Test + public void clearAll_loading_shouldCancelLoad() { + mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback); + + mLoader.clearAll(); + + verify(mCancellationSignal).cancel(); + } + + private void loadPreviewIntoCache(WidgetItem widgetItem, Size size, Bitmap bitmap) { + reset(mDelegate); + mLoader.loadPreview(mTestActivity, widgetItem, size, ignored -> {}); + verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture()); + WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue(); + + loaderCallback.onPreviewLoaded(bitmap); + } +} diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java index cc36f630c9..c946c7218f 100644 --- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java +++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java @@ -207,10 +207,9 @@ public final class WidgetsDiffReporterTest { // GIVEN the current list has app headers [A, B, E content]. ArrayList currentList = new ArrayList<>( List.of(mHeaderA, mHeaderB, mContentE)); - // GIVEN the new list has app headers [A, B, E content]. - List newList = List.of(mHeaderA, mHeaderB, mContentE); - // GIVEN the user has interacted with B. - mHeaderB.setIsWidgetListShown(true); + // GIVEN the new list has app headers [A, B, E content] and the user has interacted with B. + List newList = + List.of(mHeaderA, mHeaderB.withWidgetListShown(), mContentE); // WHEN computing the list difference. mWidgetsDiffReporter.process(currentList, newList, COMPARATOR); diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java index e1214ff39f..c730fc0d00 100644 --- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java +++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java @@ -33,13 +33,13 @@ import android.view.LayoutInflater; import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.InvariantDeviceProfile; -import com.android.launcher3.WidgetPreviewLoader; import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.ComponentWithLabel; import com.android.launcher3.icons.IconCache; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.util.PackageUserKey; +import com.android.launcher3.widget.DatabaseWidgetPreviewLoader; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.model.WidgetsListContentEntry; @@ -64,7 +64,7 @@ public final class WidgetsListAdapterTest { private static final String TEST_PACKAGE_PLACEHOLDER = "com.google.test"; @Mock private LayoutInflater mMockLayoutInflater; - @Mock private WidgetPreviewLoader mMockWidgetCache; + @Mock private DatabaseWidgetPreviewLoader mMockWidgetCache; @Mock private RecyclerView.AdapterDataObserver mListener; @Mock private IconCache mIconCache; diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java index 4e2a508394..81b0c5f89c 100644 --- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java +++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java @@ -34,7 +34,6 @@ import android.widget.TextView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.R; -import com.android.launcher3.WidgetPreviewLoader; import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.ComponentWithLabel; import com.android.launcher3.icons.IconCache; @@ -42,6 +41,7 @@ import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.testing.TestActivity; import com.android.launcher3.util.PackageUserKey; +import com.android.launcher3.widget.DatabaseWidgetPreviewLoader; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import com.android.launcher3.widget.model.WidgetsListHeaderEntry; @@ -79,7 +79,7 @@ public final class WidgetsListHeaderViewHolderBinderTest { @Mock private DeviceProfile mDeviceProfile; @Mock - private WidgetPreviewLoader mWidgetPreviewLoader; + private DatabaseWidgetPreviewLoader mWidgetPreviewLoader; @Mock private OnHeaderClickListener mOnHeaderClickListener; diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java index d6aea554f2..a0ba7c3307 100644 --- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java +++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java @@ -34,7 +34,6 @@ import android.widget.TextView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.R; -import com.android.launcher3.WidgetPreviewLoader; import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.ComponentWithLabel; import com.android.launcher3.icons.IconCache; @@ -42,6 +41,7 @@ import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.testing.TestActivity; import com.android.launcher3.util.PackageUserKey; +import com.android.launcher3.widget.DatabaseWidgetPreviewLoader; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry; @@ -79,7 +79,7 @@ public final class WidgetsListSearchHeaderViewHolderBinderTest { @Mock private DeviceProfile mDeviceProfile; @Mock - private WidgetPreviewLoader mWidgetPreviewLoader; + private DatabaseWidgetPreviewLoader mWidgetPreviewLoader; @Mock private OnHeaderClickListener mOnHeaderClickListener; diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java index 2f1326fdc0..8f9d132866 100644 --- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java +++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java @@ -38,13 +38,14 @@ import android.widget.TextView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.R; -import com.android.launcher3.WidgetPreviewLoader; import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.ComponentWithLabel; import com.android.launcher3.icons.IconCache; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.testing.TestActivity; +import com.android.launcher3.widget.CachingWidgetPreviewLoader; +import com.android.launcher3.widget.DatabaseWidgetPreviewLoader; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import com.android.launcher3.widget.WidgetCell; import com.android.launcher3.widget.model.WidgetsListContentEntry; @@ -85,7 +86,7 @@ public final class WidgetsListTableViewHolderBinderTest { @Mock private IconCache mIconCache; @Mock - private WidgetPreviewLoader mWidgetPreviewLoader; + private DatabaseWidgetPreviewLoader mWidgetPreviewLoader; @Mock private DeviceProfile mDeviceProfile; @@ -113,11 +114,10 @@ public final class WidgetsListTableViewHolderBinderTest { /* iconClickListener= */ view -> {}, /* iconLongClickListener= */ view -> false); mViewHolderBinder = new WidgetsListTableViewHolderBinder( - mContext, LayoutInflater.from(mTestActivity), mOnIconClickListener, mOnLongClickListener, - mWidgetPreviewLoader, + new CachingWidgetPreviewLoader(mWidgetPreviewLoader), new WidgetsListDrawableFactory(mTestActivity), widgetsListAdapter); } diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index b3d096c4b8..3d6be696bb 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -48,6 +48,7 @@ import com.android.launcher3.util.SafeCloseable; import com.android.launcher3.util.SettingsCache; import com.android.launcher3.util.SimpleBroadcastReceiver; import com.android.launcher3.util.Themes; +import com.android.launcher3.widget.DatabaseWidgetPreviewLoader; import com.android.launcher3.widget.custom.CustomWidgetManager; public class LauncherAppState { @@ -63,7 +64,7 @@ public class LauncherAppState { private final LauncherModel mModel; private final IconProvider mIconProvider; private final IconCache mIconCache; - private final WidgetPreviewLoader mWidgetCache; + private final DatabaseWidgetPreviewLoader mWidgetCache; private final InvariantDeviceProfile mInvariantDeviceProfile; private final RunnableList mOnTerminateCallback = new RunnableList(); @@ -138,7 +139,7 @@ public class LauncherAppState { mIconProvider = new IconProvider(context, Themes.isThemedIconEnabled(context)); mIconCache = new IconCache(mContext, mInvariantDeviceProfile, iconCacheFileName, mIconProvider); - mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache); + mWidgetCache = new DatabaseWidgetPreviewLoader(mContext, mIconCache); mModel = new LauncherModel(context, this, mIconCache, new AppFilter(mContext)); mOnTerminateCallback.add(mIconCache::close); } @@ -180,7 +181,7 @@ public class LauncherAppState { return mModel; } - public WidgetPreviewLoader getWidgetCache() { + public DatabaseWidgetPreviewLoader getWidgetCache() { return mWidgetCache; } diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java index 8076b27300..67f2a9eb32 100644 --- a/src/com/android/launcher3/allapps/AllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java @@ -31,6 +31,7 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; +import android.os.Bundle; import android.os.Parcelable; import android.os.Process; import android.text.Selection; @@ -82,6 +83,8 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo Insettable, OnDeviceProfileChangeListener, OnActivePageChangedListener, ScrimView.ScrimDrawingController { + private static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page"; + public static final float PULL_MULTIPLIER = .02f; public static final float FLING_VELOCITY_MULTIPLIER = 1200f; @@ -181,6 +184,23 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo } catch (Exception e) { Log.e("AllAppsContainerView", "restoreInstanceState viewId = 0", e); } + Bundle state = (Bundle) sparseArray.get(R.id.work_tab_state_id, null); + if (state != null) { + int currentPage = state.getInt(BUNDLE_KEY_CURRENT_PAGE, 0); + if (currentPage != 0) { + rebindAdapters(true); + mViewPager.setCurrentPage(currentPage); + } + } + } + + + @Override + protected void dispatchSaveInstanceState(SparseArray container) { + super.dispatchSaveInstanceState(container); + Bundle state = new Bundle(); + state.putInt(BUNDLE_KEY_CURRENT_PAGE, getCurrentPage()); + container.put(R.id.work_tab_state_id, state); } /** @@ -231,7 +251,9 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo private void resetWorkProfile() { boolean isEnabled = !mAllAppsStore.hasModelFlag(FLAG_QUIET_MODE_ENABLED); - mWorkModeSwitch.updateCurrentState(isEnabled); + if (mWorkModeSwitch != null) { + mWorkModeSwitch.updateCurrentState(isEnabled); + } mWorkAdapterProvider.updateCurrentState(isEnabled); mAH[AdapterHolder.WORK].applyPadding(); } diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java index 6d57aa61a9..a800d34758 100644 --- a/src/com/android/launcher3/allapps/WorkModeSwitch.java +++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java @@ -67,6 +67,7 @@ public class WorkModeSwitch extends Button implements Insettable, View.OnClickLi @Override protected void onFinishInflate() { super.onFinishInflate(); + setSelected(true); setOnClickListener(this); if (Utilities.ATLEAST_R) { mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this); diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java index c6add3190e..a27d5c8e06 100644 --- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java +++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java @@ -26,6 +26,7 @@ import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially import android.annotation.TargetApi; import android.app.Fragment; +import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; @@ -83,6 +84,8 @@ import com.android.launcher3.util.IntSet; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.BaseDragLayer; +import com.android.launcher3.widget.BaseLauncherAppWidgetHostView; +import com.android.launcher3.widget.LauncherAppWidgetHost; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import com.android.launcher3.widget.NavigableAppWidgetHostView; import com.android.launcher3.widget.custom.CustomWidgetManager; @@ -202,6 +205,7 @@ public class LauncherPreviewRenderer extends ContextWrapper private final InsettableFrameLayout mRootView; private final Hotseat mHotseat; private final CellLayout mWorkspace; + private final AppWidgetHost mAppWidgetHost; public LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp) { super(context); @@ -255,6 +259,10 @@ public class LauncherPreviewRenderer extends ContextWrapper mDp.workspacePadding.top, mDp.workspacePadding.right + mDp.cellLayoutPaddingLeftRightPx, mDp.workspacePadding.bottom); + + mAppWidgetHost = FeatureFlags.WIDGETS_IN_LAUNCHER_PREVIEW.get() + ? new LauncherPreviewAppWidgetHost(context) + : null; } /** Populate preview and render it. */ @@ -354,14 +362,20 @@ public class LauncherPreviewRenderer extends ContextWrapper private void inflateAndAddWidgets( LauncherAppWidgetInfo info, LauncherAppWidgetProviderInfo providerInfo) { - AppWidgetHostView view = new NavigableAppWidgetHostView(this) { - @Override - protected boolean shouldAllowDirectClick() { - return false; - } - }; - view.setAppWidget(-1, providerInfo); - view.updateAppWidget(null); + AppWidgetHostView view; + if (FeatureFlags.WIDGETS_IN_LAUNCHER_PREVIEW.get()) { + view = mAppWidgetHost.createView(mContext, info.appWidgetId, providerInfo); + } else { + view = new NavigableAppWidgetHostView(this) { + @Override + protected boolean shouldAllowDirectClick() { + return false; + } + }; + view.setAppWidget(-1, providerInfo); + view.updateAppWidget(null); + } + view.setTag(info); addInScreenFromBind(view, info); } @@ -477,4 +491,31 @@ public class LauncherPreviewRenderer extends ContextWrapper view.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY)); view.layout(0, 0, width, height); } + + private class LauncherPreviewAppWidgetHost extends AppWidgetHost { + + private LauncherPreviewAppWidgetHost(Context context) { + super(context, LauncherAppWidgetHost.APPWIDGET_HOST_ID); + } + + @Override + protected AppWidgetHostView onCreateView( + Context context, + int appWidgetId, + AppWidgetProviderInfo appWidget) { + return new LauncherPreviewAppWidgetHostView(LauncherPreviewRenderer.this); + } + } + + private static class LauncherPreviewAppWidgetHostView extends BaseLauncherAppWidgetHostView { + + private LauncherPreviewAppWidgetHostView(Context context) { + super(context); + } + + @Override + protected boolean shouldAllowDirectClick() { + return false; + } + } } diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index 34a21fe075..d373cf438e 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -28,6 +28,7 @@ import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission; import static com.android.launcher3.util.PackageManagerHelper.isSystemApp; +import android.annotation.SuppressLint; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.ContentResolver; @@ -40,6 +41,7 @@ import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; +import android.graphics.Point; import android.net.Uri; import android.os.Bundle; import android.os.UserHandle; @@ -50,6 +52,8 @@ import android.util.Log; import android.util.LongSparseArray; import android.util.TimingLogger; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; import com.android.launcher3.LauncherSettings; @@ -756,6 +760,9 @@ public class LoaderTask implements Runnable { if (widgetProviderInfo != null && (appWidgetInfo.spanX < widgetProviderInfo.minSpanX || appWidgetInfo.spanY < widgetProviderInfo.minSpanY)) { + logDeleteWidgetInfo(mApp.getInvariantDeviceProfile(), + widgetProviderInfo); + // This can happen when display size changes. c.markDeleted("Widget removed, min sizes not met: " + "span=" + appWidgetInfo.spanX + "x" @@ -982,6 +989,51 @@ public class LoaderTask implements Runnable { && (provider.provider.getPackageName() != null); } + @SuppressLint("NewApi") // Already added API check. + private static void logDeleteWidgetInfo(InvariantDeviceProfile idp, + LauncherAppWidgetProviderInfo widgetProviderInfo) { + FileLog.d(TAG, "Deleting " + widgetProviderInfo.getComponent() + + " due to min size constraint"); + Point cellSize = new Point(); + for (DeviceProfile deviceProfile : idp.supportedProfiles) { + deviceProfile.getCellSize(cellSize); + FileLog.d(TAG, "DeviceProfile available width: " + deviceProfile.availableWidthPx + + ", available height: " + deviceProfile.availableHeightPx + + ", cellLayoutBorderSpacingPx: " + deviceProfile.cellLayoutBorderSpacingPx + + ", cellSize: " + cellSize); + } + + StringBuilder widgetDimension = new StringBuilder(); + widgetDimension.append("Widget dimensions:\n") + .append("minResizeWidth: ") + .append(widgetProviderInfo.minResizeWidth) + .append("\n") + .append("minResizeHeight: ") + .append(widgetProviderInfo.minResizeHeight) + .append("\n") + .append("defaultWidth: ") + .append(widgetProviderInfo.minWidth) + .append("\n") + .append("defaultHeight: ") + .append(widgetProviderInfo.minHeight) + .append("\n"); + if (Utilities.ATLEAST_S) { + widgetDimension.append("targetCellWidth: ") + .append(widgetProviderInfo.targetCellWidth) + .append("\n") + .append("targetCellHeight: ") + .append(widgetProviderInfo.targetCellHeight) + .append("\n") + .append("maxResizeWidth: ") + .append(widgetProviderInfo.maxResizeWidth) + .append("\n") + .append("maxResizeHeight: ") + .append(widgetProviderInfo.maxResizeHeight) + .append("\n"); + } + FileLog.d(TAG, widgetDimension.toString()); + } + private static void logASplit(final TimingLogger logger, final String label) { logger.addSplit(label); if (DEBUG) { diff --git a/src/com/android/launcher3/model/WidgetItem.java b/src/com/android/launcher3/model/WidgetItem.java index 97071bbaad..7198d54611 100644 --- a/src/com/android/launcher3/model/WidgetItem.java +++ b/src/com/android/launcher3/model/WidgetItem.java @@ -1,7 +1,11 @@ package com.android.launcher3.model; +import static com.android.launcher3.Utilities.ATLEAST_S; + +import android.annotation.SuppressLint; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; +import android.content.res.Resources; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.Utilities; @@ -59,4 +63,15 @@ public class WidgetItem extends ComponentKey { } return false; } + + /** Returns whether this {@link WidgetItem} has a preview layout that can be used. */ + @SuppressLint("NewApi") // Already added API check. + public boolean hasPreviewLayout() { + return ATLEAST_S && widgetInfo != null && widgetInfo.previewLayout != Resources.ID_NULL; + } + + /** Returns whether this {@link WidgetItem} is for a shortcut rather than an app widget. */ + public boolean isShortcut() { + return activityInfo != null; + } } diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java index 2095a0de54..cb35f74135 100644 --- a/src/com/android/launcher3/popup/ArrowPopup.java +++ b/src/com/android/launcher3/popup/ArrowPopup.java @@ -252,9 +252,9 @@ public abstract class ArrowPopup> view.setBackgroundResource(R.drawable.single_item_primary); } else if (totalVisibleShortcuts > 1) { if (numVisibleShortcut == 0) { - view.setBackground(mRoundedTop); + view.setBackground(mRoundedTop.getConstantState().newDrawable()); } else if (numVisibleShortcut == (totalVisibleShortcuts - 1)) { - view.setBackground(mRoundedBottom); + view.setBackground(mRoundedBottom.getConstantState().newDrawable()); } else { view.setBackgroundResource(R.drawable.middle_item_primary); } diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutView.java b/src/com/android/launcher3/shortcuts/DeepShortcutView.java index cc658c9ba0..71d288c0ab 100644 --- a/src/com/android/launcher3/shortcuts/DeepShortcutView.java +++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java @@ -18,7 +18,13 @@ package com.android.launcher3.shortcuts; import android.content.Context; import android.content.pm.ShortcutInfo; +import android.content.res.ColorStateList; +import android.graphics.Color; import android.graphics.Point; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.RippleDrawable; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; @@ -30,16 +36,20 @@ import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.popup.PopupContainerWithArrow; +import com.android.launcher3.util.Themes; import com.android.launcher3.views.BubbleTextHolder; /** - * A {@link android.widget.FrameLayout} that contains a {@link DeepShortcutView}. - * This lets us animate the DeepShortcutView (icon and text) separately from the background. + * A {@link android.widget.FrameLayout} that contains an icon and a {@link BubbleTextView} for text. + * This lets us animate the child BubbleTextView's background (transparent ripple) separately from + * the {@link DeepShortcutView} background color. */ public class DeepShortcutView extends FrameLayout implements BubbleTextHolder { private static final Point sTempPoint = new Point(); + private final Drawable mTransparentDrawable = new ColorDrawable(Color.TRANSPARENT); + private BubbleTextView mBubbleText; private View mIconView; @@ -63,6 +73,43 @@ public class DeepShortcutView extends FrameLayout implements BubbleTextHolder { super.onFinishInflate(); mBubbleText = findViewById(R.id.bubble_text); mIconView = findViewById(R.id.icon); + tryUpdateTextBackground(); + } + + @Override + public void setBackground(Drawable background) { + super.setBackground(background); + tryUpdateTextBackground(); + } + + @Override + public void setBackgroundResource(int resid) { + super.setBackgroundResource(resid); + tryUpdateTextBackground(); + } + + /** + * Updates the text background to match the shape of this background (when applicable). + */ + private void tryUpdateTextBackground() { + if (!(getBackground() instanceof GradientDrawable) || mBubbleText == null) { + return; + } + GradientDrawable background = (GradientDrawable) getBackground(); + + int color = Themes.getAttrColor(getContext(), android.R.attr.colorControlHighlight); + GradientDrawable backgroundMask = new GradientDrawable(); + backgroundMask.setColor(color); + backgroundMask.setShape(GradientDrawable.RECTANGLE); + if (background.getCornerRadii() != null) { + backgroundMask.setCornerRadii(background.getCornerRadii()); + } else { + backgroundMask.setCornerRadius(background.getCornerRadius()); + } + + RippleDrawable drawable = new RippleDrawable(ColorStateList.valueOf(color), + mTransparentDrawable, backgroundMask); + mBubbleText.setBackground(drawable); } @Override diff --git a/src/com/android/launcher3/views/SpringRelativeLayout.java b/src/com/android/launcher3/views/SpringRelativeLayout.java index 8e3ac20574..8f814a1949 100644 --- a/src/com/android/launcher3/views/SpringRelativeLayout.java +++ b/src/com/android/launcher3/views/SpringRelativeLayout.java @@ -108,8 +108,6 @@ public class SpringRelativeLayout extends RelativeLayout { switch (direction) { case DIRECTION_TOP: return new EdgeEffectProxy(getContext(), mEdgeGlowTop); - case DIRECTION_BOTTOM: - return new EdgeEffectProxy(getContext(), mEdgeGlowBottom); } return super.createEdgeEffect(view, direction); } diff --git a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java new file mode 100644 index 0000000000..2742882b1f --- /dev/null +++ b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2021 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.widget; + +import android.content.Context; +import android.graphics.Outline; +import android.graphics.Rect; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.widget.RemoteViews; + +import androidx.annotation.UiThread; + +import com.android.launcher3.R; +import com.android.launcher3.util.Executors; + +/** + * Launcher AppWidgetHostView with support for rounded corners and a fallback View. + */ +public abstract class BaseLauncherAppWidgetHostView extends NavigableAppWidgetHostView { + + protected final LayoutInflater mInflater; + + private final Rect mEnforcedRectangle = new Rect(); + private final float mEnforcedCornerRadius; + private final ViewOutlineProvider mCornerRadiusEnforcementOutline = new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + if (mEnforcedRectangle.isEmpty() || mEnforcedCornerRadius <= 0) { + outline.setEmpty(); + } else { + outline.setRoundRect(mEnforcedRectangle, mEnforcedCornerRadius); + } + } + }; + + public BaseLauncherAppWidgetHostView(Context context) { + super(context); + + setExecutor(Executors.THREAD_POOL_EXECUTOR); + + mInflater = LayoutInflater.from(context); + mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(getContext()); + } + + @Override + protected View getErrorView() { + return mInflater.inflate(R.layout.appwidget_error, this, false); + } + + /** + * Fall back to error layout instead of showing widget. + */ + public void switchToErrorView() { + // Update the widget with 0 Layout id, to reset the view to error view. + updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + try { + super.onLayout(changed, left, top, right, bottom); + } catch (final RuntimeException e) { + post(this::switchToErrorView); + } + + enforceRoundedCorners(); + } + + @UiThread + private void resetRoundedCorners() { + setOutlineProvider(ViewOutlineProvider.BACKGROUND); + setClipToOutline(false); + } + + @UiThread + private void enforceRoundedCorners() { + if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled()) { + resetRoundedCorners(); + return; + } + View background = RoundedCornerEnforcement.findBackground(this); + if (background == null + || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) { + resetRoundedCorners(); + return; + } + RoundedCornerEnforcement.computeRoundedRectangle(this, + background, + mEnforcedRectangle); + setOutlineProvider(mCornerRadiusEnforcementOutline); + setClipToOutline(true); + } + + /** Returns the corner radius currently enforced, in pixels. */ + public float getEnforcedCornerRadius() { + return mEnforcedCornerRadius; + } + + /** Returns true if the corner radius are enforced for this App Widget. */ + public boolean hasEnforcedCornerRadius() { + return getClipToOutline(); + } +} diff --git a/src/com/android/launcher3/widget/CachingWidgetPreviewLoader.java b/src/com/android/launcher3/widget/CachingWidgetPreviewLoader.java new file mode 100644 index 0000000000..afceadd9a3 --- /dev/null +++ b/src/com/android/launcher3/widget/CachingWidgetPreviewLoader.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2021 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.widget; + +import android.graphics.Bitmap; +import android.os.CancellationSignal; +import android.util.Size; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.collection.ArrayMap; +import androidx.collection.ArraySet; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.model.WidgetItem; +import com.android.launcher3.util.ComponentKey; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** Wrapper around {@link DatabaseWidgetPreviewLoader} that contains caching logic. */ +public class CachingWidgetPreviewLoader implements WidgetPreviewLoader { + + @NonNull private final WidgetPreviewLoader mDelegate; + @NonNull private final Map> mCache = new ArrayMap<>(); + + public CachingWidgetPreviewLoader(@NonNull WidgetPreviewLoader delegate) { + mDelegate = delegate; + } + + /** Returns whether the preview is loaded for the item and size. */ + public boolean isPreviewLoaded(@NonNull WidgetItem item, @NonNull Size previewSize) { + return getPreview(item, previewSize) != null; + } + + /** Returns the cached preview for the item and size, or null if there is none. */ + @Nullable + public Bitmap getPreview(@NonNull WidgetItem item, @NonNull Size previewSize) { + CacheResult cacheResult = getCacheResult(item, previewSize); + if (cacheResult instanceof CacheResult.Loaded) { + return ((CacheResult.Loaded) cacheResult).mBitmap; + } else { + return null; + } + } + + @NonNull + private CacheResult getCacheResult(@NonNull WidgetItem item, @NonNull Size previewSize) { + synchronized (mCache) { + Map cacheResults = mCache.get(toComponentKey(item)); + if (cacheResults == null) { + return CacheResult.MISS; + } + + return cacheResults.getOrDefault(previewSize, CacheResult.MISS); + } + } + + /** + * Puts the result in the cache for the item and size. Returns the value previously in the + * cache, or null if there was none. + */ + @Nullable + private CacheResult putCacheResult( + @NonNull WidgetItem item, + @NonNull Size previewSize, + @Nullable CacheResult cacheResult) { + ComponentKey key = toComponentKey(item); + synchronized (mCache) { + Map cacheResults = mCache.getOrDefault(key, new ArrayMap<>()); + CacheResult previous; + if (cacheResult == null) { + previous = cacheResults.remove(previewSize); + if (cacheResults.isEmpty()) { + mCache.remove(key); + } else { + previous = cacheResults.put(previewSize, cacheResult); + mCache.put(key, cacheResults); + } + } else { + previous = cacheResults.put(previewSize, cacheResult); + mCache.put(key, cacheResults); + } + return previous; + } + } + + private void removeCacheResult(@NonNull WidgetItem item, @NonNull Size previewSize) { + ComponentKey key = toComponentKey(item); + synchronized (mCache) { + Map cacheResults = mCache.getOrDefault(key, new ArrayMap<>()); + cacheResults.remove(previewSize); + mCache.put(key, cacheResults); + } + } + + /** + * Gets the preview for the widget item and size, using the value in the cache if stored. + * + * @return a {@link CancellationSignal}, which can cancel the request before it loads + */ + @Override + @UiThread + @NonNull + public CancellationSignal loadPreview( + @NonNull BaseActivity activity, @NonNull WidgetItem item, @NonNull Size previewSize, + @NonNull WidgetPreviewLoadedCallback callback) { + CancellationSignal signal = new CancellationSignal(); + signal.setOnCancelListener(() -> { + synchronized (mCache) { + CacheResult cacheResult = getCacheResult(item, previewSize); + if (!(cacheResult instanceof CacheResult.Loading)) { + // If the key isn't actively loading, then this is a no-op. Cancelling loading + // shouldn't clear the cache if we've already loaded. + return; + } + + CacheResult.Loading prev = (CacheResult.Loading) cacheResult; + CacheResult.Loading updated = prev.withoutCallback(callback); + + if (updated.mCallbacks.isEmpty()) { + // If the last callback was removed, then cancel the underlying request in the + // delegate. + prev.mCancellationSignal.cancel(); + removeCacheResult(item, previewSize); + } else { + // If there are other callbacks still active, then don't cancel the delegate's + // request, just remove this callback from the set. + putCacheResult(item, previewSize, updated); + } + } + }); + + synchronized (mCache) { + CacheResult cacheResult = getCacheResult(item, previewSize); + if (cacheResult instanceof CacheResult.Loaded) { + // If the bitmap is already present in the cache, invoke the callback immediately. + callback.onPreviewLoaded(((CacheResult.Loaded) cacheResult).mBitmap); + return signal; + } + + if (cacheResult instanceof CacheResult.Loading) { + // If we're already loading the preview for this key, then just add the callback + // to the set we'll call after it loads. + CacheResult.Loading prev = (CacheResult.Loading) cacheResult; + putCacheResult(item, previewSize, prev.withCallback(callback)); + return signal; + } + + CancellationSignal delegateCancellationSignal = + mDelegate.loadPreview( + activity, + item, + previewSize, + preview -> { + CacheResult prev; + synchronized (mCache) { + prev = putCacheResult( + item, previewSize, new CacheResult.Loaded(preview)); + } + if (prev instanceof CacheResult.Loading) { + // Notify each stored callback that the preview has loaded. + ((CacheResult.Loading) prev).mCallbacks + .forEach(c -> c.onPreviewLoaded(preview)); + } else { + // If there isn't a loading object in the cache, then we were + // notified before adding this signal to the cache. Just + // call back to the provided callback, there can't be others. + callback.onPreviewLoaded(preview); + } + }); + ArraySet callbacks = new ArraySet<>(); + callbacks.add(callback); + putCacheResult( + item, + previewSize, + new CacheResult.Loading(delegateCancellationSignal, callbacks)); + } + + return signal; + } + + /** Clears all cached previews for {@code items}, cancelling any in-progress preview loading. */ + public void clearPreviews(Iterable items) { + List previousCacheResults = new ArrayList<>(); + synchronized (mCache) { + for (WidgetItem item : items) { + Map previousMap = mCache.remove(toComponentKey(item)); + if (previousMap != null) { + previousCacheResults.addAll(previousMap.values()); + } + } + } + + for (CacheResult previousCacheResult : previousCacheResults) { + if (previousCacheResult instanceof CacheResult.Loading) { + ((CacheResult.Loading) previousCacheResult).mCancellationSignal.cancel(); + } + } + } + + /** Clears all cached previews, cancelling any in-progress preview loading. */ + public void clearAll() { + List previousCacheResults; + synchronized (mCache) { + previousCacheResults = + mCache + .values() + .stream() + .flatMap(sizeToResult -> sizeToResult.values().stream()) + .collect(Collectors.toList()); + mCache.clear(); + } + + for (CacheResult previousCacheResult : previousCacheResults) { + if (previousCacheResult instanceof CacheResult.Loading) { + ((CacheResult.Loading) previousCacheResult).mCancellationSignal.cancel(); + } + } + } + + private abstract static class CacheResult { + static final CacheResult MISS = new CacheResult() {}; + + static final class Loading extends CacheResult { + @NonNull final CancellationSignal mCancellationSignal; + @NonNull final Set mCallbacks; + + Loading(@NonNull CancellationSignal cancellationSignal, + @NonNull Set callbacks) { + mCancellationSignal = cancellationSignal; + mCallbacks = callbacks; + } + + @NonNull + Loading withCallback(@NonNull WidgetPreviewLoadedCallback callback) { + if (mCallbacks.contains(callback)) return this; + Set newCallbacks = + new ArraySet<>(mCallbacks.size() + 1); + newCallbacks.addAll(mCallbacks); + newCallbacks.add(callback); + return new Loading(mCancellationSignal, newCallbacks); + } + + @NonNull + Loading withoutCallback(@NonNull WidgetPreviewLoadedCallback callback) { + if (!mCallbacks.contains(callback)) return this; + Set newCallbacks = + new ArraySet<>(mCallbacks.size() - 1); + for (WidgetPreviewLoadedCallback existingCallback : mCallbacks) { + if (!existingCallback.equals(callback)) { + newCallbacks.add(existingCallback); + } + } + return new Loading(mCancellationSignal, newCallbacks); + } + } + + static final class Loaded extends CacheResult { + @NonNull final Bitmap mBitmap; + + Loaded(@NonNull Bitmap bitmap) { + mBitmap = bitmap; + } + } + } + + @NonNull + private static ComponentKey toComponentKey(@NonNull WidgetItem item) { + return new ComponentKey(item.componentName, item.user); + } +} diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java similarity index 86% rename from src/com/android/launcher3/WidgetPreviewLoader.java rename to src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java index ff3584acd3..6de3e11ac8 100644 --- a/src/com/android/launcher3/WidgetPreviewLoader.java +++ b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java @@ -1,4 +1,19 @@ -package com.android.launcher3; +/* + * Copyright (C) 2021 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.widget; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; @@ -32,8 +47,14 @@ import android.util.LongSparseArray; import android.util.Pair; import android.util.Size; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.launcher3.BaseActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.LauncherFiles; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; import com.android.launcher3.icons.GraphicsUtils; import com.android.launcher3.icons.IconCache; import com.android.launcher3.icons.LauncherIcons; @@ -47,9 +68,6 @@ import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.SQLiteCacheHelper; import com.android.launcher3.util.Thunk; -import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; -import com.android.launcher3.widget.WidgetCell; -import com.android.launcher3.widget.WidgetManagerHelper; import com.android.launcher3.widget.util.WidgetSizes; import java.util.ArrayList; @@ -60,7 +78,8 @@ import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.ExecutionException; -public class WidgetPreviewLoader { +/** {@link WidgetPreviewLoader} that loads preview images from a {@link CacheDb}. */ +public class DatabaseWidgetPreviewLoader implements WidgetPreviewLoader { private static final String TAG = "WidgetPreviewLoader"; private static final boolean DEBUG = false; @@ -80,7 +99,7 @@ public class WidgetPreviewLoader { private final UserCache mUserCache; private final CacheDb mDb; - public WidgetPreviewLoader(Context context, IconCache iconCache) { + public DatabaseWidgetPreviewLoader(Context context, IconCache iconCache) { mContext = context; mIconCache = iconCache; mUserCache = UserCache.INSTANCE.get(context); @@ -89,16 +108,24 @@ public class WidgetPreviewLoader { /** * Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be - * called on UI thread + * called on UI thread. * * @return a request id which can be used to cancel the request. */ - public CancellationSignal getPreview(WidgetItem item, int previewWidth, - int previewHeight, WidgetCell caller) { + @Override + @NonNull + public CancellationSignal loadPreview( + @NonNull BaseActivity activity, + @NonNull WidgetItem item, + @NonNull Size previewSize, + @NonNull WidgetPreviewLoadedCallback callback) { + int previewWidth = previewSize.getWidth(); + int previewHeight = previewSize.getHeight(); String size = previewWidth + "x" + previewHeight; WidgetCacheKey key = new WidgetCacheKey(item.componentName, item.user, size); - PreviewLoadTask task = new PreviewLoadTask(key, item, previewWidth, previewHeight, caller); + PreviewLoadTask task = + new PreviewLoadTask(activity, key, item, previewWidth, previewHeight, callback); task.executeOnExecutor(Executors.THREAD_POOL_EXECUTOR); CancellationSignal signal = new CancellationSignal(); @@ -106,6 +133,7 @@ public class WidgetPreviewLoader { return signal; } + /** Clears the database storing previews. */ public void refresh() { mDb.clear(); } @@ -126,21 +154,37 @@ public class WidgetPreviewLoader { private static final String COLUMN_VERSION = "version"; private static final String COLUMN_PREVIEW_BITMAP = "preview_bitmap"; - public CacheDb(Context context) { + CacheDb(Context context) { super(context, LauncherFiles.WIDGET_PREVIEWS_DB, DB_VERSION, TABLE_NAME); } @Override public void onCreateTable(SQLiteDatabase database) { - database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + - COLUMN_COMPONENT + " TEXT NOT NULL, " + - COLUMN_USER + " INTEGER NOT NULL, " + - COLUMN_SIZE + " TEXT NOT NULL, " + - COLUMN_PACKAGE + " TEXT NOT NULL, " + - COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " + - COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " + - COLUMN_PREVIEW_BITMAP + " BLOB, " + - "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ", " + COLUMN_SIZE + ") " + + database.execSQL("CREATE TABLE IF NOT EXISTS " + + TABLE_NAME + + " (" + + COLUMN_COMPONENT + + " TEXT NOT NULL, " + + COLUMN_USER + + " INTEGER NOT NULL, " + + COLUMN_SIZE + + " TEXT NOT NULL, " + + COLUMN_PACKAGE + + " TEXT NOT NULL, " + + COLUMN_LAST_UPDATED + + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_VERSION + + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_PREVIEW_BITMAP + + " BLOB, " + + "PRIMARY KEY (" + + COLUMN_COMPONENT + + ", " + + COLUMN_USER + + ", " + + COLUMN_SIZE + + ") " + + ");"); } } @@ -149,7 +193,7 @@ public class WidgetPreviewLoader { ContentValues values = new ContentValues(); values.put(CacheDb.COLUMN_COMPONENT, key.componentName.flattenToShortString()); values.put(CacheDb.COLUMN_USER, mUserCache.getSerialNumberForUser(key.user)); - values.put(CacheDb.COLUMN_SIZE, key.size); + values.put(CacheDb.COLUMN_SIZE, key.mSize); values.put(CacheDb.COLUMN_PACKAGE, key.componentName.getPackageName()); values.put(CacheDb.COLUMN_VERSION, versions[0]); values.put(CacheDb.COLUMN_LAST_UPDATED, versions[1]); @@ -157,12 +201,14 @@ public class WidgetPreviewLoader { mDb.insertOrReplace(values); } + /** Removes the package from the preview database. */ public void removePackage(String packageName, UserHandle user) { removePackage(packageName, user, mUserCache.getSerialNumberForUser(user)); } - private void removePackage(String packageName, UserHandle user, long userSerial) { - synchronized(mPackageVersions) { + /** Removes the package from the preview database. */ + public void removePackage(String packageName, UserHandle user, long userSerial) { + synchronized (mPackageVersions) { mPackageVersions.remove(packageName); } @@ -264,7 +310,7 @@ public class WidgetPreviewLoader { new String[]{ key.componentName.flattenToShortString(), Long.toString(mUserCache.getSerialNumberForUser(key.user)), - key.size + key.mSize }); // If cancelled, skip getting the blob and decoding it into a bitmap if (loadTask.isCancelled()) { @@ -293,7 +339,7 @@ public class WidgetPreviewLoader { } /** - * Returns generatedPreview for a widget and if the preview should be saved in persistent + * Returns a generated preview for a widget and if the preview should be saved in persistent * storage. * @param launcher * @param item @@ -344,8 +390,10 @@ public class WidgetPreviewLoader { if (drawable != null) { drawable = mutateOnMainThread(drawable); } else { - Log.w(TAG, "Can't load widget preview drawable 0x" + - Integer.toHexString(info.previewImage) + " for provider: " + info.provider); + Log.w(TAG, "Can't load widget preview drawable 0x" + + Integer.toHexString(info.previewImage) + + " for provider: " + + info.provider); } } @@ -379,8 +427,8 @@ public class WidgetPreviewLoader { scale = maxPreviewWidth / (float) (previewWidth); } if (scale != 1f) { - previewWidth = Math.max((int)(scale * previewWidth), 1); - previewHeight = Math.max((int)(scale * previewHeight), 1); + previewWidth = Math.max((int) (scale * previewWidth), 1); + previewHeight = Math.max((int) (scale * previewHeight), 1); } final Canvas c = new Canvas(); @@ -554,13 +602,13 @@ public class WidgetPreviewLoader { } } - public class PreviewLoadTask extends AsyncTask + private class PreviewLoadTask extends AsyncTask implements CancellationSignal.OnCancelListener { @Thunk final WidgetCacheKey mKey; private final WidgetItem mInfo; private final int mPreviewHeight; private final int mPreviewWidth; - private final WidgetCell mCaller; + private final WidgetPreviewLoadedCallback mCallback; private final BaseActivity mActivity; @Thunk long[] mVersions; @Thunk Bitmap mBitmapToRecycle; @@ -568,14 +616,14 @@ public class WidgetPreviewLoader { @Nullable private Bitmap mUnusedPreviewBitmap; private boolean mSaveToDB = false; - PreviewLoadTask(WidgetCacheKey key, WidgetItem info, int previewWidth, - int previewHeight, WidgetCell caller) { + PreviewLoadTask(BaseActivity activity, WidgetCacheKey key, WidgetItem info, + int previewWidth, int previewHeight, WidgetPreviewLoadedCallback callback) { + mActivity = activity; mKey = key; mInfo = info; mPreviewHeight = previewHeight; mPreviewWidth = previewWidth; - mCaller = caller; - mActivity = BaseActivity.fromContext(mCaller.getContext()); + mCallback = callback; if (DEBUG) { Log.d(TAG, String.format("%s, %s, %d, %d", mKey, mInfo, mPreviewHeight, mPreviewWidth)); @@ -593,9 +641,9 @@ public class WidgetPreviewLoader { synchronized (mUnusedBitmaps) { // Check if we can re-use a bitmap for (Bitmap candidate : mUnusedBitmaps) { - if (candidate != null && candidate.isMutable() && - candidate.getWidth() == mPreviewWidth && - candidate.getHeight() == mPreviewHeight) { + if (candidate != null && candidate.isMutable() + && candidate.getWidth() == mPreviewWidth + && candidate.getHeight() == mPreviewHeight) { unusedBitmap = candidate; mUnusedBitmaps.remove(unusedBitmap); break; @@ -638,7 +686,7 @@ public class WidgetPreviewLoader { @Override protected void onPostExecute(final Bitmap preview) { - mCaller.applyPreview(preview); + mCallback.onPreviewLoaded(preview); // Write the generated preview to the DB in the worker thread if (mVersions != null) { @@ -716,21 +764,21 @@ public class WidgetPreviewLoader { private static final class WidgetCacheKey extends ComponentKey { - @Thunk final String size; + @Thunk final String mSize; - public WidgetCacheKey(ComponentName componentName, UserHandle user, String size) { + WidgetCacheKey(ComponentName componentName, UserHandle user, String size) { super(componentName, user); - this.size = size; + this.mSize = size; } @Override public int hashCode() { - return super.hashCode() ^ size.hashCode(); + return super.hashCode() ^ mSize.hashCode(); } @Override public boolean equals(Object o) { - return super.equals(o) && ((WidgetCacheKey) o).size.equals(size); + return super.equals(o) && ((WidgetCacheKey) o).mSize.equals(mSize); } } } diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java index 50ab422114..fa50dfb667 100644 --- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java +++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java @@ -16,26 +16,20 @@ package com.android.launcher3.widget; -import static com.android.launcher3.Utilities.getBoundsForViewInDragLayer; -import static com.android.launcher3.Utilities.setRect; - import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.content.res.Configuration; import android.graphics.Canvas; -import android.graphics.Outline; import android.graphics.Rect; import android.graphics.RectF; import android.os.Handler; import android.os.SystemClock; import android.util.SparseBooleanArray; import android.util.SparseIntArray; -import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; -import android.view.ViewOutlineProvider; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.AdapterView; import android.widget.Advanceable; @@ -43,7 +37,6 @@ import android.widget.RemoteViews; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.UiThread; import com.android.launcher3.CheckLongPressHelper; import com.android.launcher3.Launcher; @@ -51,9 +44,9 @@ import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.keyboard.ViewGroupFocusHelper; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; -import com.android.launcher3.util.Executors; import com.android.launcher3.util.Themes; import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener; import com.android.launcher3.widget.dragndrop.AppWidgetHostViewDragListener; @@ -63,7 +56,7 @@ import java.util.List; /** * {@inheritDoc} */ -public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView +public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView implements TouchCompleteListener, View.OnLongClickListener, LocalColorExtractor.Listener { @@ -78,8 +71,6 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView // Maximum duration for which updates can be deferred. private static final long UPDATE_LOCK_TIMEOUT_MILLIS = 1000; - protected final LayoutInflater mInflater; - private final CheckLongPressHelper mLongPressHelper; protected final Launcher mLauncher; private final Workspace mWorkspace; @@ -102,21 +93,9 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView private final Rect mCurrentWidgetSize = new Rect(); private final Rect mWidgetSizeAtDrag = new Rect(); - private final float[] mTmpFloatArray = new float[4]; private final RectF mTempRectF = new RectF(); - private final Rect mEnforcedRectangle = new Rect(); - private final float mEnforcedCornerRadius; - private final ViewOutlineProvider mCornerRadiusEnforcementOutline = new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - if (mEnforcedRectangle.isEmpty() || mEnforcedCornerRadius <= 0) { - outline.setEmpty(); - } else { - outline.setRoundRect(mEnforcedRectangle, mEnforcedCornerRadius); - } - } - }; private final Object mUpdateLock = new Object(); + private final ViewGroupFocusHelper mDragLayerRelativeCoordinateHelper; private long mDeferUpdatesUntilMillis = 0; private RemoteViews mMostRecentRemoteViews; @@ -125,18 +104,16 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView mLauncher = Launcher.getLauncher(context); mWorkspace = mLauncher.getWorkspace(); mLongPressHelper = new CheckLongPressHelper(this, this); - mInflater = LayoutInflater.from(context); setAccessibilityDelegate(mLauncher.getAccessibilityDelegate()); setBackgroundResource(R.drawable.widget_internal_focus_bg); - setExecutor(Executors.THREAD_POOL_EXECUTOR); if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) { setOnLightBackground(true); } mColorExtractor = LocalColorExtractor.newInstance(getContext()); mColorExtractor.setListener(this); - mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(getContext()); + mDragLayerRelativeCoordinateHelper = new ViewGroupFocusHelper(mLauncher.getDragLayer()); } @Override @@ -166,11 +143,6 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView return true; } - @Override - protected View getErrorView() { - return mInflater.inflate(R.layout.appwidget_error, this, false); - } - @Override public void updateAppWidget(RemoteViews remoteViews) { synchronized (mUpdateLock) { @@ -305,40 +277,17 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView } } - public void switchToErrorView() { - // Update the widget with 0 Layout id, to reset the view to error view. - updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0)); - } - @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - try { - super.onLayout(changed, left, top, right, bottom); - } catch (final RuntimeException e) { - post(new Runnable() { - @Override - public void run() { - switchToErrorView(); - } - }); - } + super.onLayout(changed, left, top, right, bottom); mIsScrollable = checkScrollableRecursively(this); if (!mIsInDragMode && getTag() instanceof LauncherAppWidgetInfo) { - mCurrentWidgetSize.left = left; - mCurrentWidgetSize.right = right; - mCurrentWidgetSize.top = top; - mCurrentWidgetSize.bottom = bottom; - LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag(); - getBoundsForViewInDragLayer(mLauncher.getDragLayer(), (View) getParent(), - mCurrentWidgetSize, true, mTmpFloatArray, mTempRectF); - setRect(mTempRectF, mCurrentWidgetSize); + mDragLayerRelativeCoordinateHelper.viewToRect(this, mCurrentWidgetSize); updateColorExtraction(mCurrentWidgetSize, mWorkspace.getPageIndexForScreenId(info.screenId)); } - - enforceRoundedCorners(); } /** Starts the drag mode. */ @@ -358,7 +307,6 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView mIsInDragMode = false; mDragListener = null; mWidgetSizeAtDrag.setEmpty(); - requestLayout(); } /** @@ -510,40 +458,4 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView } return false; } - - @UiThread - private void resetRoundedCorners() { - setOutlineProvider(ViewOutlineProvider.BACKGROUND); - setClipToOutline(false); - } - - @UiThread - private void enforceRoundedCorners() { - if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled()) { - resetRoundedCorners(); - return; - } - View background = RoundedCornerEnforcement.findBackground(this); - if (background == null - || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) { - resetRoundedCorners(); - return; - } - RoundedCornerEnforcement.computeRoundedRectangle(this, - background, - mEnforcedRectangle); - setOutlineProvider(mCornerRadiusEnforcementOutline); - setClipToOutline(true); - } - - /** Returns the corner radius currently enforced, in pixels. */ - public float getEnforcedCornerRadius() { - return mEnforcedCornerRadius; - } - - /** Returns true if the corner radius are enforced for this App Widget. */ - public boolean hasEnforcedCornerRadius() { - return getClipToOutline(); - } - } diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java index b1ccfd9591..91529be30b 100644 --- a/src/com/android/launcher3/widget/WidgetCell.java +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -19,7 +19,6 @@ package com.android.launcher3.widget; import static com.android.launcher3.Utilities.ATLEAST_S; import android.content.Context; -import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -44,7 +43,6 @@ import com.android.launcher3.BaseActivity; import com.android.launcher3.CheckLongPressHelper; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; -import com.android.launcher3.WidgetPreviewLoader; import com.android.launcher3.icons.FastBitmapDrawable; import com.android.launcher3.icons.RoundDrawableWrapper; import com.android.launcher3.model.WidgetItem; @@ -222,21 +220,18 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { return; } - if (ATLEAST_S - && mRemoteViewsPreview == null - && item.widgetInfo != null - && item.widgetInfo.previewLayout != Resources.ID_NULL) { - mAppWidgetHostViewPreview = new LauncherAppWidgetHostView(getContext()); - LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo = - LauncherAppWidgetProviderInfo.fromProviderInfo(getContext(), - item.widgetInfo.clone()); - // A hack to force the initial layout to be the preview layout since there is no API for - // rendering a preview layout for work profile apps yet. For non-work profile layout, a - // proper solution is to use RemoteViews(PackageName, LayoutId). - launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout; - setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, - launcherAppWidgetProviderInfo, /* remoteViews= */ null); - } + if (!item.hasPreviewLayout()) return; + + mAppWidgetHostViewPreview = new LauncherAppWidgetHostView(getContext()); + LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo = + LauncherAppWidgetProviderInfo.fromProviderInfo(getContext(), + item.widgetInfo.clone()); + // A hack to force the initial layout to be the preview layout since there is no API for + // rendering a preview layout for work profile apps yet. For non-work profile layout, a + // proper solution is to use RemoteViews(PackageName, LayoutId). + launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout; + setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, + launcherAppWidgetProviderInfo, /* remoteViews= */ null); } private void setAppWidgetHostViewPreview( @@ -344,22 +339,25 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { if (mActiveRequest != null) { return; } - mActiveRequest = mWidgetPreviewLoader.getPreview(mItem, mPreviewWidth, mPreviewHeight, - this); + mActiveRequest = mWidgetPreviewLoader.loadPreview( + BaseActivity.fromContext(getContext()), mItem, + new Size(mPreviewWidth, mPreviewHeight), + this::applyPreview); } /** Sets the widget preview image size in number of cells. */ - public void setPreviewSize(int spanX, int spanY) { - setPreviewSize(spanX, spanY, 1f); + public Size setPreviewSize(int spanX, int spanY) { + return setPreviewSize(spanX, spanY, 1f); } /** Sets the widget preview image size, in number of cells, and preview scale. */ - public void setPreviewSize(int spanX, int spanY, float previewScale) { + public Size setPreviewSize(int spanX, int spanY, float previewScale) { DeviceProfile deviceProfile = mActivity.getDeviceProfile(); Size widgetSize = WidgetSizes.getWidgetSizePx(deviceProfile, spanX, spanY); mPreviewWidth = widgetSize.getWidth(); mPreviewHeight = widgetSize.getHeight(); mPreviewScale = previewScale; + return widgetSize; } @Override diff --git a/src/com/android/launcher3/widget/WidgetPreviewLoader.java b/src/com/android/launcher3/widget/WidgetPreviewLoader.java new file mode 100644 index 0000000000..ff5c82f872 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetPreviewLoader.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 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.widget; + +import android.graphics.Bitmap; +import android.os.CancellationSignal; +import android.util.Size; + +import androidx.annotation.NonNull; +import androidx.annotation.UiThread; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.model.WidgetItem; + +/** Asynchronous loader of preview bitmaps for {@link WidgetItem}s. */ +public interface WidgetPreviewLoader { + /** + * Loads a widget preview and calls back to {@code callback} when complete. + * + * @return a {@link CancellationSignal} which can be used to cancel the request. + */ + @NonNull + @UiThread + CancellationSignal loadPreview( + @NonNull BaseActivity activity, + @NonNull WidgetItem item, + @NonNull Size previewSize, + @NonNull WidgetPreviewLoadedCallback callback); + + /** Callback class for requests to {@link WidgetPreviewLoader}. */ + interface WidgetPreviewLoadedCallback { + void onPreviewLoaded(@NonNull Bitmap preview); + } +} diff --git a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java index 73bae6f484..abc79ff966 100644 --- a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java +++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java @@ -59,6 +59,19 @@ public abstract class WidgetsListBaseEntry { @Rank public abstract int getRank(); + /** + * Marker interface for subclasses that are headers for widget list items. + * + * @param The type of this class. + */ + public interface Header> { + /** Returns whether the widget list is currently expanded. */ + boolean isWidgetListShown(); + + /** Returns a copy of the item with the widget list shown. */ + T withWidgetListShown(); + } + @Retention(SOURCE) @IntDef({RANK_WIDGETS_LIST_HEADER, RANK_WIDGETS_LIST_SEARCH_HEADER, RANK_WIDGETS_LIST_CONTENT}) public @interface Rank { diff --git a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java index 1fdc39959a..5b3ea94a2d 100644 --- a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java +++ b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java @@ -21,41 +21,33 @@ import com.android.launcher3.model.data.PackageItemInfo; import java.util.List; /** An information holder for an app which has widgets or/and shortcuts. */ -public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry { +public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry + implements WidgetsListBaseEntry.Header { public final int widgetsCount; public final int shortcutsCount; - private boolean mIsWidgetListShown = false; - private boolean mHasEntryUpdated = false; + private final boolean mIsWidgetListShown; public WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName, List items) { + this(pkgItem, titleSectionName, items, /* isWidgetListShown= */ false); + } + + private WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName, + List items, boolean isWidgetListShown) { super(pkgItem, titleSectionName, items); widgetsCount = (int) items.stream().filter(item -> item.widgetInfo != null).count(); shortcutsCount = Math.max(0, items.size() - widgetsCount); - } - - /** Sets if the widgets list associated with this header is shown. */ - public void setIsWidgetListShown(boolean isWidgetListShown) { - if (mIsWidgetListShown != isWidgetListShown) { - this.mIsWidgetListShown = isWidgetListShown; - mHasEntryUpdated = true; - } else { - mHasEntryUpdated = false; - } + mIsWidgetListShown = isWidgetListShown; } /** Returns {@code true} if the widgets list associated with this header is shown. */ + @Override public boolean isWidgetListShown() { return mIsWidgetListShown; } - /** Returns {@code true} if this entry has been updated due to user interactions. */ - public boolean hasEntryUpdated() { - return mHasEntryUpdated; - } - @Override public String toString() { return "Header:" + mPkgItem.packageName + ":" + mWidgets.size(); @@ -72,6 +64,18 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry { if (!(obj instanceof WidgetsListHeaderEntry)) return false; WidgetsListHeaderEntry otherEntry = (WidgetsListHeaderEntry) obj; return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem) - && mTitleSectionName.equals(otherEntry.mTitleSectionName); + && mTitleSectionName.equals(otherEntry.mTitleSectionName) + && mIsWidgetListShown == otherEntry.mIsWidgetListShown; + } + + /** Returns a copy of this {@link WidgetsListHeaderEntry} with the widget list shown. */ + @Override + public WidgetsListHeaderEntry withWidgetListShown() { + if (mIsWidgetListShown) return this; + return new WidgetsListHeaderEntry( + mPkgItem, + mTitleSectionName, + mWidgets, + /* isWidgetListShown= */ true); } } diff --git a/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java index 2aec3f8fd9..c0f89bcbcc 100644 --- a/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java +++ b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java @@ -21,36 +21,28 @@ import com.android.launcher3.model.data.PackageItemInfo; import java.util.List; /** An information holder for an app which has widgets or/and shortcuts, to be shown in search. */ -public final class WidgetsListSearchHeaderEntry extends WidgetsListBaseEntry { +public final class WidgetsListSearchHeaderEntry extends WidgetsListBaseEntry + implements WidgetsListBaseEntry.Header { - private boolean mIsWidgetListShown = false; - private boolean mHasEntryUpdated = false; + private final boolean mIsWidgetListShown; public WidgetsListSearchHeaderEntry(PackageItemInfo pkgItem, String titleSectionName, List items) { - super(pkgItem, titleSectionName, items); + this(pkgItem, titleSectionName, items, /* isWidgetListShown= */ false); } - /** Sets if the widgets list associated with this header is shown. */ - public void setIsWidgetListShown(boolean isWidgetListShown) { - if (mIsWidgetListShown != isWidgetListShown) { - this.mIsWidgetListShown = isWidgetListShown; - mHasEntryUpdated = true; - } else { - mHasEntryUpdated = false; - } + private WidgetsListSearchHeaderEntry(PackageItemInfo pkgItem, String titleSectionName, + List items, boolean isWidgetListShown) { + super(pkgItem, titleSectionName, items); + mIsWidgetListShown = isWidgetListShown; } /** Returns {@code true} if the widgets list associated with this header is shown. */ + @Override public boolean isWidgetListShown() { return mIsWidgetListShown; } - /** Returns {@code true} if this entry has been updated due to user interactions. */ - public boolean hasEntryUpdated() { - return mHasEntryUpdated; - } - @Override public String toString() { return "SearchHeader:" + mPkgItem.packageName + ":" + mWidgets.size(); @@ -67,6 +59,18 @@ public final class WidgetsListSearchHeaderEntry extends WidgetsListBaseEntry { if (!(obj instanceof WidgetsListSearchHeaderEntry)) return false; WidgetsListSearchHeaderEntry otherEntry = (WidgetsListSearchHeaderEntry) obj; return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem) - && mTitleSectionName.equals(otherEntry.mTitleSectionName); + && mTitleSectionName.equals(otherEntry.mTitleSectionName) + && mIsWidgetListShown; + } + + /** Returns a copy of this {@link WidgetsListSearchHeaderEntry} with the widget list shown. */ + @Override + public WidgetsListSearchHeaderEntry withWidgetListShown() { + if (mIsWidgetListShown) return this; + return new WidgetsListSearchHeaderEntry( + mPkgItem, + mTitleSectionName, + mWidgets, + /* isWidgetListShown= */ true); } } diff --git a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java index 42896ba350..dfe447a38e 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java +++ b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java @@ -177,7 +177,7 @@ public class WidgetsDiffReporter { */ private boolean hasHeaderUpdated(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow) { if (newRow instanceof WidgetsListHeaderEntry && curRow instanceof WidgetsListHeaderEntry) { - return ((WidgetsListHeaderEntry) newRow).hasEntryUpdated() || !curRow.equals(newRow); + return !curRow.equals(newRow); } if (newRow instanceof WidgetsListSearchHeaderEntry && curRow instanceof WidgetsListSearchHeaderEntry) { diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java index 6863c60a37..5d9adf0e71 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java @@ -20,6 +20,7 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import android.content.Context; import android.os.Process; import android.util.Log; +import android.util.Size; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; @@ -35,19 +36,25 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.Adapter; import androidx.recyclerview.widget.RecyclerView.ViewHolder; +import com.android.launcher3.BaseActivity; +import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.R; -import com.android.launcher3.WidgetPreviewLoader; import com.android.launcher3.icons.IconCache; +import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.recyclerview.ViewHolderBinder; import com.android.launcher3.util.LabelComparator; import com.android.launcher3.util.PackageUserKey; +import com.android.launcher3.widget.CachingWidgetPreviewLoader; +import com.android.launcher3.widget.DatabaseWidgetPreviewLoader; import com.android.launcher3.widget.WidgetCell; +import com.android.launcher3.widget.WidgetPreviewLoader.WidgetPreviewLoadedCallback; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.model.WidgetsListContentEntry; import com.android.launcher3.widget.model.WidgetsListHeaderEntry; import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry; +import com.android.launcher3.widget.util.WidgetSizes; import java.util.ArrayList; import java.util.Arrays; @@ -79,7 +86,9 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC private static final int VIEW_TYPE_WIDGETS_HEADER = R.id.view_type_widgets_header; private static final int VIEW_TYPE_WIDGETS_SEARCH_HEADER = R.id.view_type_widgets_search_header; + private final Context mContext; private final Launcher mLauncher; + private final CachingWidgetPreviewLoader mCachingPreviewLoader; private final WidgetsDiffReporter mDiffReporter; private final SparseArray mViewHolderBinders = new SparseArray<>(); private final WidgetsListTableViewHolderBinder mWidgetsListTableViewHolderBinder; @@ -97,16 +106,23 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC .equals(mWidgetsContentVisiblePackageUserKey); @Nullable private Predicate mFilter = null; @Nullable private RecyclerView mRecyclerView; + @Nullable private PackageUserKey mPendingClickHeader; + private int mShortcutPreviewPadding; + + private final WidgetPreviewLoadedCallback mPreviewLoadedCallback = + ignored -> updateVisibleEntries(); public WidgetsListAdapter(Context context, LayoutInflater layoutInflater, - WidgetPreviewLoader widgetPreviewLoader, IconCache iconCache, + DatabaseWidgetPreviewLoader widgetPreviewLoader, IconCache iconCache, OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) { + mContext = context; mLauncher = Launcher.getLauncher(context); + mCachingPreviewLoader = new CachingWidgetPreviewLoader(widgetPreviewLoader); mDiffReporter = new WidgetsDiffReporter(iconCache, this); WidgetsListDrawableFactory listDrawableFactory = new WidgetsListDrawableFactory(context); - mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(context, + mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder( layoutInflater, iconClickListener, iconLongClickListener, - widgetPreviewLoader, listDrawableFactory, /* listAdapter= */ this); + mCachingPreviewLoader, listDrawableFactory, /* listAdapter= */ this); mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListTableViewHolderBinder); mViewHolderBinders.put( VIEW_TYPE_WIDGETS_HEADER, @@ -122,6 +138,9 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC /* onHeaderClickListener= */ this, listDrawableFactory, /* listAdapter= */ this)); + mShortcutPreviewPadding = + 2 * context.getResources() + .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding); } @Override @@ -177,6 +196,7 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC /** Updates the widget list based on {@code tempEntries}. */ public void setWidgets(List tempEntries) { + mCachingPreviewLoader.clearAll(); mAllEntries = tempEntries.stream().sorted(mRowComparator) .collect(Collectors.toList()); if (shouldClearVisibleEntries()) { @@ -189,36 +209,110 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC public void setWidgetsOnSearch(List searchResults) { // Forget the expanded package every time widget list is refreshed in search mode. mWidgetsContentVisiblePackageUserKey = null; + cancelLoadingPreviews(); setWidgets(searchResults); } private void updateVisibleEntries() { - mAllEntries.forEach(entry -> { - if (entry instanceof WidgetsListHeaderEntry) { - ((WidgetsListHeaderEntry) entry).setIsWidgetListShown( - isHeaderForVisibleContent(entry)); - } else if (entry instanceof WidgetsListSearchHeaderEntry) { - ((WidgetsListSearchHeaderEntry) entry).setIsWidgetListShown( - isHeaderForVisibleContent(entry)); - } - }); + // If not all previews are ready, then defer this update and try again after the preview + // loads. + if (!ensureAllPreviewsReady()) return; + + // Get the current top of the header with the matching key before adjusting the visible + // entries. + OptionalInt previousPositionForPackageUserKey = + getPositionForPackageUserKey(mPendingClickHeader); + OptionalInt topForPackageUserKey = + getOffsetForPosition(previousPositionForPackageUserKey); + List newVisibleEntries = mAllEntries.stream() .filter(entry -> (mFilter == null || mFilter.test(entry)) && mHeaderAndSelectedContentFilter.test(entry)) + .map(entry -> { + // Adjust the original entries to expand headers for the selected content. + if (entry instanceof WidgetsListBaseEntry.Header + && matchesKey(entry, mWidgetsContentVisiblePackageUserKey)) { + return ((WidgetsListBaseEntry.Header) entry).withWidgetListShown(); + } + return entry; + }) .collect(Collectors.toList()); + mDiffReporter.process(mVisibleEntries, newVisibleEntries, mRowComparator); + + if (mPendingClickHeader != null) { + // Get the position for the clicked header after adjusting the visible entries. The + // position may have changed if another header had previously been expanded. + OptionalInt positionForPackageUserKey = + getPositionForPackageUserKey(mPendingClickHeader); + scrollToPositionAndMaintainOffset(positionForPackageUserKey, topForPackageUserKey); + mPendingClickHeader = null; + } } - /** Returns whether {@code entry} matches {@link #mWidgetsContentVisiblePackageUserKey}. */ - private boolean isHeaderForVisibleContent(WidgetsListBaseEntry entry) { - return isHeaderForPackageUserKey(entry, mWidgetsContentVisiblePackageUserKey); + /** + * Checks that all preview images are loaded and starts loading for those that aren't ready. + * + * @return true if all previews are ready and the data can be updated, false otherwise. + */ + private boolean ensureAllPreviewsReady() { + boolean allReady = true; + BaseActivity activity = BaseActivity.fromContext(mContext); + for (WidgetsListBaseEntry entry : mAllEntries) { + if (!(entry instanceof WidgetsListContentEntry)) continue; + + WidgetsListContentEntry contentEntry = (WidgetsListContentEntry) entry; + if (!matchesKey(entry, mWidgetsContentVisiblePackageUserKey)) { + // If the entry isn't visible, clear any loaded previews. + mCachingPreviewLoader.clearPreviews(contentEntry.mWidgets); + continue; + } + + for (int i = 0; i < entry.mWidgets.size(); i++) { + WidgetItem widgetItem = entry.mWidgets.get(i); + DeviceProfile deviceProfile = activity.getDeviceProfile(); + Size widgetSize = + WidgetSizes.getWidgetSizePx( + deviceProfile, + widgetItem.spanX, + widgetItem.spanY); + if (widgetItem.isShortcut()) { + widgetSize = + new Size( + widgetSize.getWidth() + mShortcutPreviewPadding, + widgetSize.getHeight() + mShortcutPreviewPadding); + } + + if (widgetItem.hasPreviewLayout() + || mCachingPreviewLoader.isPreviewLoaded(widgetItem, widgetSize)) { + // The widget is ready if it can be rendered with a preview layout or if its + // preview bitmap is in the cache. + continue; + } + + // If we've reached this point, we should load the preview for the widget. + allReady = false; + mCachingPreviewLoader.loadPreview( + activity, + widgetItem, + widgetSize, + mPreviewLoadedCallback); + } + } + return allReady; } /** Returns whether {@code entry} matches {@code key}. */ - private boolean isHeaderForPackageUserKey(WidgetsListBaseEntry entry, PackageUserKey key) { - return (entry instanceof WidgetsListHeaderEntry - || entry instanceof WidgetsListSearchHeaderEntry) - && new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user).equals(key); + private static boolean isHeaderForPackageUserKey( + @NonNull WidgetsListBaseEntry entry, @Nullable PackageUserKey key) { + return entry instanceof WidgetsListBaseEntry.Header && matchesKey(entry, key); + } + + private static boolean matchesKey( + @NonNull WidgetsListBaseEntry entry, @Nullable PackageUserKey key) { + if (key == null) return false; + return entry.mPkgItem.packageName.equals(key.mPackageName) + && entry.mPkgItem.user.equals(key.mUser); } /** @@ -227,6 +321,7 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC public void resetExpandedHeader() { if (mWidgetsContentVisiblePackageUserKey != null) { mWidgetsContentVisiblePackageUserKey = null; + cancelLoadingPreviews(); updateVisibleEntries(); } } @@ -285,6 +380,8 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC // Ignore invalid clicks, such as collapsing a package that isn't currently expanded. if (!showWidgets && !packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) return; + cancelLoadingPreviews(); + if (showWidgets) { mWidgetsContentVisiblePackageUserKey = packageUserKey; mLauncher.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_APP_EXPANDED); @@ -292,17 +389,15 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC mWidgetsContentVisiblePackageUserKey = null; } - // Get the current top of the header with the matching key before adjusting the visible - // entries. - OptionalInt topForPackageUserKey = - getOffsetForPosition(getPositionForPackageUserKey(packageUserKey)); + // Store the header that was clicked so that its position will be maintained the next time + // we update the entries. + mPendingClickHeader = packageUserKey; updateVisibleEntries(); + } - // Get the position for the clicked header after adjusting the visible entries. The - // position may have changed if another header had previously been expanded. - OptionalInt positionForPackageUserKey = getPositionForPackageUserKey(packageUserKey); - scrollToPositionAndMaintainOffset(positionForPackageUserKey, topForPackageUserKey); + private void cancelLoadingPreviews() { + mCachingPreviewLoader.clearAll(); } /** Returns the position of the currently expanded header, or empty if it's not present. */ @@ -315,7 +410,8 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC * Returns the position of {@code key} in {@link #mVisibleEntries}, or empty if it's not * present. */ - private OptionalInt getPositionForPackageUserKey(PackageUserKey key) { + @NonNull + private OptionalInt getPositionForPackageUserKey(@Nullable PackageUserKey key) { return IntStream.range(0, mVisibleEntries.size()) .filter(index -> isHeaderForPackageUserKey(mVisibleEntries.get(index), key)) .findFirst(); diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java index 7e8c55bfe6..7b526639f5 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java @@ -18,8 +18,9 @@ package com.android.launcher3.widget.picker; import static com.android.launcher3.widget.picker.WidgetsListDrawableState.LAST; import static com.android.launcher3.widget.picker.WidgetsListDrawableState.MIDDLE; -import android.content.Context; +import android.graphics.Bitmap; import android.util.Log; +import android.util.Size; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; @@ -30,9 +31,9 @@ import android.widget.TableLayout; import android.widget.TableRow; import com.android.launcher3.R; -import com.android.launcher3.WidgetPreviewLoader; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.recyclerview.ViewHolderBinder; +import com.android.launcher3.widget.CachingWidgetPreviewLoader; import com.android.launcher3.widget.WidgetCell; import com.android.launcher3.widget.model.WidgetsListContentEntry; import com.android.launcher3.widget.util.WidgetsTableUtils; @@ -52,17 +53,16 @@ public final class WidgetsListTableViewHolderBinder private final LayoutInflater mLayoutInflater; private final OnClickListener mIconClickListener; private final OnLongClickListener mIconLongClickListener; - private final WidgetPreviewLoader mWidgetPreviewLoader; private final WidgetsListDrawableFactory mListDrawableFactory; + private final CachingWidgetPreviewLoader mWidgetPreviewLoader; private final WidgetsListAdapter mWidgetsListAdapter; private boolean mApplyBitmapDeferred = false; public WidgetsListTableViewHolderBinder( - Context context, LayoutInflater layoutInflater, OnClickListener iconClickListener, OnLongClickListener iconLongClickListener, - WidgetPreviewLoader widgetPreviewLoader, + CachingWidgetPreviewLoader widgetPreviewLoader, WidgetsListDrawableFactory listDrawableFactory, WidgetsListAdapter listAdapter) { mLayoutInflater = layoutInflater; @@ -75,7 +75,7 @@ public final class WidgetsListTableViewHolderBinder /** * Defers applying bitmap on all the {@link WidgetCell} at - * {@link #bindViewHolder(WidgetsRowViewHolder, WidgetsListContentEntry)} if + * {@link #bindViewHolder(WidgetsRowViewHolder, WidgetsListContentEntry, int)} if * {@code applyBitmapDeferred} is {@code true}. */ public void setApplyBitmapDeferred(boolean applyBitmapDeferred) { @@ -124,10 +124,15 @@ public final class WidgetsListTableViewHolderBinder WidgetCell widget = (WidgetCell) row.getChildAt(j); widget.clear(); WidgetItem widgetItem = widgetItemsPerRow.get(j); - widget.setPreviewSize(widgetItem.spanX, widgetItem.spanY); + Size previewSize = widget.setPreviewSize(widgetItem.spanX, widgetItem.spanY); widget.applyFromCellItem(widgetItem, mWidgetPreviewLoader); widget.setApplyBitmapDeferred(mApplyBitmapDeferred); - widget.ensurePreview(); + Bitmap preview = mWidgetPreviewLoader.getPreview(widgetItem, previewSize); + if (preview == null) { + widget.ensurePreview(); + } else { + widget.applyPreview(preview); + } widget.setVisibility(View.VISIBLE); } } diff --git a/src/com/android/launcher3/widget/util/WidgetSizes.java b/src/com/android/launcher3/widget/util/WidgetSizes.java index fcc0dde889..5c8ea7299b 100644 --- a/src/com/android/launcher3/widget/util/WidgetSizes.java +++ b/src/com/android/launcher3/widget/util/WidgetSizes.java @@ -99,13 +99,11 @@ public final class WidgetSizes { */ public static Bundle getWidgetSizeOptions(Context context, ComponentName provider, int spanX, int spanY) { - boolean shouldInsetWidgets = - LauncherAppState.getIDP(context).getDeviceProfile(context).shouldInsetWidgets(); ArrayList sizes = getWidgetSizes(context, spanX, spanY); Rect padding = getDefaultPaddingForWidget(context, provider, null); float density = context.getResources().getDisplayMetrics().density; - float xPaddingDips = shouldInsetWidgets ? (padding.left + padding.right) / density : 0; - float yPaddingDips = shouldInsetWidgets ? (padding.top + padding.bottom) / density : 0; + float xPaddingDips = (padding.left + padding.right) / density; + float yPaddingDips = (padding.top + padding.bottom) / density; ArrayList paddedSizes = sizes.stream() .map(size -> new SizeF( diff --git a/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java index b35c75b429..df5d465d96 100644 --- a/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java +++ b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java @@ -127,7 +127,7 @@ public class PersonalWorkSlidingTabStrip extends LinearLayout implements PageInd @Override public void setActiveMarker(int activePage) { updateTabTextColor(activePage); - updateIndicatorPosition(activePage); + updateIndicatorPosition(mIsRtl ? 1 - activePage : activePage); if (mOnActivePageChangedListener != null && mLastActivePage != activePage) { mOnActivePageChangedListener.onActivePageChanged(activePage); } diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java index f5c1efa07e..96e82224b1 100644 --- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java +++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java @@ -39,6 +39,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.net.Uri; import android.os.Bundle; +import android.os.DeadObjectException; import android.os.Parcelable; import android.os.RemoteException; import android.os.SystemClock; @@ -269,6 +270,9 @@ public final class LauncherInstrumentation { try (ContentProviderClient client = getContext().getContentResolver() .acquireContentProviderClient(mTestProviderUri)) { return client.call(request, null, null); + } catch (DeadObjectException e) { + fail("Launcher crashed"); + return null; } catch (RemoteException e) { throw new RuntimeException(e); } diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java index 51e331de42..99d988926a 100644 --- a/tests/tapl/com/android/launcher3/tapl/Widgets.java +++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java @@ -185,8 +185,12 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer { targetAppSelector); if (headerTitle != null) { // If we find the header and it has not been expanded, let's click it to see the - // widgets list. - if (!hasHeaderExpanded) { + // widgets list. Note that we wait until the header is out of the gesture region at + // the bottom of the screen, because tapping there in Launcher3 causes NexusLauncher + // to briefly appear to handle the gesture, which can break our test. + boolean isHeaderOutOfGestureRegion = headerTitle.getVisibleCenter().y + < mLauncher.getBottomGestureStartOnScreen(); + if (!hasHeaderExpanded && isHeaderOutOfGestureRegion) { log("Header has not been expanded. Click to expand."); hasHeaderExpanded = true; mLauncher.clickLauncherObject(headerTitle);