Merge "Load widget preview images before adding the rows to the adapter" into sc-dev

This commit is contained in:
Stevie Kideckel
2021-06-18 22:37:32 +00:00
committed by Android (Google) Code Review
18 changed files with 1086 additions and 158 deletions
@@ -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<WidgetPreviewLoadedCallback> 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);
}
}
@@ -207,10 +207,9 @@ public final class WidgetsDiffReporterTest {
// GIVEN the current list has app headers [A, B, E content].
ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
List.of(mHeaderA, mHeaderB, mContentE));
// GIVEN the new list has app headers [A, B, E content].
List<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> newList =
List.of(mHeaderA, mHeaderB.withWidgetListShown(), mContentE);
// WHEN computing the list difference.
mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
@@ -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;
@@ -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;
@@ -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;
@@ -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);
}
@@ -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;
}
@@ -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;
}
}
@@ -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<ComponentKey, Map<Size, CacheResult>> 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<Size, CacheResult> 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<Size, CacheResult> 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<Size, CacheResult> 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<WidgetPreviewLoadedCallback> 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<WidgetItem> items) {
List<CacheResult> previousCacheResults = new ArrayList<>();
synchronized (mCache) {
for (WidgetItem item : items) {
Map<Size, CacheResult> 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<CacheResult> 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<WidgetPreviewLoadedCallback> mCallbacks;
Loading(@NonNull CancellationSignal cancellationSignal,
@NonNull Set<WidgetPreviewLoadedCallback> callbacks) {
mCancellationSignal = cancellationSignal;
mCallbacks = callbacks;
}
@NonNull
Loading withCallback(@NonNull WidgetPreviewLoadedCallback callback) {
if (mCallbacks.contains(callback)) return this;
Set<WidgetPreviewLoadedCallback> 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<WidgetPreviewLoadedCallback> 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);
}
}
@@ -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<Void, Void, Bitmap>
private class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap>
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);
}
}
}
@@ -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
@@ -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);
}
}
@@ -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 <T> The type of this class.
*/
public interface Header<T extends WidgetsListBaseEntry & Header<T>> {
/** 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 {
@@ -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<WidgetsListHeaderEntry> {
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<WidgetItem> items) {
this(pkgItem, titleSectionName, items, /* isWidgetListShown= */ false);
}
private WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
List<WidgetItem> 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);
}
}
@@ -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<WidgetsListSearchHeaderEntry> {
private boolean mIsWidgetListShown = false;
private boolean mHasEntryUpdated = false;
private final boolean mIsWidgetListShown;
public WidgetsListSearchHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
List<WidgetItem> 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<WidgetItem> 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);
}
}
@@ -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) {
@@ -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<ViewHolder> 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<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
private final WidgetsListTableViewHolderBinder mWidgetsListTableViewHolderBinder;
@@ -97,16 +106,23 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
.equals(mWidgetsContentVisiblePackageUserKey);
@Nullable private Predicate<WidgetsListBaseEntry> 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<ViewHolder> 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<ViewHolder> implements OnHeaderC
/** Updates the widget list based on {@code tempEntries}. */
public void setWidgets(List<WidgetsListBaseEntry> tempEntries) {
mCachingPreviewLoader.clearAll();
mAllEntries = tempEntries.stream().sorted(mRowComparator)
.collect(Collectors.toList());
if (shouldClearVisibleEntries()) {
@@ -189,36 +209,110 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
public void setWidgetsOnSearch(List<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> 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<ViewHolder> implements OnHeaderC
public void resetExpandedHeader() {
if (mWidgetsContentVisiblePackageUserKey != null) {
mWidgetsContentVisiblePackageUserKey = null;
cancelLoadingPreviews();
updateVisibleEntries();
}
}
@@ -285,6 +380,8 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> 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<ViewHolder> 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<ViewHolder> 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();
@@ -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);
}
}