From a0935232992a333847f845324327a27748115f64 Mon Sep 17 00:00:00 2001 From: Stevie Kideckel Date: Mon, 14 Jun 2021 14:55:07 +0000 Subject: [PATCH] Load widget preview images before adding the rows to the adapter This allows for smooth animations as the items will have their full height resolved Bug: 190892517 Test: verified locally Test: CachingWidgetPreviewLoaderTest Change-Id: I86afc6951a4cc82ae45e63795791d259a3bb3cda --- .../CachingWidgetPreviewLoaderTest.java | 409 ++++++++++++++++++ .../picker/WidgetsDiffReporterTest.java | 7 +- .../widget/picker/WidgetsListAdapterTest.java | 4 +- ...WidgetsListHeaderViewHolderBinderTest.java | 4 +- ...sListSearchHeaderViewHolderBinderTest.java | 4 +- .../WidgetsListTableViewHolderBinderTest.java | 8 +- .../android/launcher3/LauncherAppState.java | 7 +- .../android/launcher3/model/WidgetItem.java | 15 + .../widget/CachingWidgetPreviewLoader.java | 289 +++++++++++++ .../DatabaseWidgetPreviewLoader.java} | 136 ++++-- .../android/launcher3/widget/WidgetCell.java | 42 +- .../launcher3/widget/WidgetPreviewLoader.java | 47 ++ .../widget/model/WidgetsListBaseEntry.java | 13 + .../widget/model/WidgetsListHeaderEntry.java | 42 +- .../model/WidgetsListSearchHeaderEntry.java | 40 +- .../widget/picker/WidgetsDiffReporter.java | 2 +- .../widget/picker/WidgetsListAdapter.java | 154 +++++-- .../WidgetsListTableViewHolderBinder.java | 21 +- 18 files changed, 1086 insertions(+), 158 deletions(-) create mode 100644 robolectric_tests/src/com/android/launcher3/widget/CachingWidgetPreviewLoaderTest.java create mode 100644 src/com/android/launcher3/widget/CachingWidgetPreviewLoader.java rename src/com/android/launcher3/{WidgetPreviewLoader.java => widget/DatabaseWidgetPreviewLoader.java} (86%) create mode 100644 src/com/android/launcher3/widget/WidgetPreviewLoader.java 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/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/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/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); } }