Removing widget preview caching

> All previews are generated on demand when the corresponding
  header expands
> Using ItemAnimator to animate layout changes when preview loads

Bug: 196238313
Test: Manual
Change-Id: I0cb859c8443c2c536399e4063f58baecfc7416ad
This commit is contained in:
Sunny Goyal 2021-08-11 15:13:17 -07:00
parent 48b012b148
commit ed2a55f413
24 changed files with 241 additions and 1633 deletions

View File

@ -1,409 +0,0 @@
/*
* 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 final Size SIZE_10_10 = new Size(10, 10);
private final Size SIZE_20_20 = new Size(20, 20);
private static final String TEST_PACKAGE = "com.example.test";
private final ComponentName TEST_PROVIDER =
new ComponentName(TEST_PACKAGE, ".WidgetProvider");
private final ComponentName TEST_PROVIDER2 =
new ComponentName(TEST_PACKAGE, ".WidgetProvider2");
private final Bitmap BITMAP = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
private 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);
}
}

View File

@ -39,7 +39,6 @@ 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 +63,6 @@ public final class WidgetsListAdapterTest {
private static final String TEST_PACKAGE_PLACEHOLDER = "com.google.test";
@Mock private LayoutInflater mMockLayoutInflater;
@Mock private DatabaseWidgetPreviewLoader mMockWidgetCache;
@Mock private RecyclerView.AdapterDataObserver mListener;
@Mock private IconCache mIconCache;
@ -81,7 +79,7 @@ public final class WidgetsListAdapterTest {
mTestProfile.numRows = 5;
mTestProfile.numColumns = 5;
mUserHandle = Process.myUserHandle();
mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, mMockWidgetCache,
mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater,
mIconCache, () -> 0, null, null);
mAdapter.registerAdapterDataObserver(mListener);

View File

@ -23,6 +23,8 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
import static java.util.Collections.EMPTY_LIST;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
@ -116,7 +118,7 @@ public final class WidgetsListHeaderViewHolderBinderTest {
APP_NAME,
TEST_PACKAGE,
/* numOfWidgets= */ 3);
mViewHolderBinder.bindViewHolder(viewHolder, entry, /* position= */ 0);
mViewHolderBinder.bindViewHolder(viewHolder, entry, /* position= */ 0, EMPTY_LIST);
TextView appTitle = widgetsListHeader.findViewById(R.id.app_title);
TextView appSubtitle = widgetsListHeader.findViewById(R.id.app_subtitle);
@ -134,7 +136,7 @@ public final class WidgetsListHeaderViewHolderBinderTest {
TEST_PACKAGE,
/* numOfWidgets= */ 3);
mViewHolderBinder.bindViewHolder(viewHolder, entry, /* position= */ 0);
mViewHolderBinder.bindViewHolder(viewHolder, entry, /* position= */ 0, EMPTY_LIST);
widgetsListHeader.callOnClick();
verify(mOnHeaderClickListener).onHeaderClicked(eq(true),

View File

@ -23,6 +23,8 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
import static java.util.Collections.EMPTY_LIST;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
@ -116,7 +118,7 @@ public final class WidgetsListSearchHeaderViewHolderBinderTest {
APP_NAME,
TEST_PACKAGE,
/* numOfWidgets= */ 3);
mViewHolderBinder.bindViewHolder(viewHolder, entry, /* position= */ 0);
mViewHolderBinder.bindViewHolder(viewHolder, entry, /* position= */ 0, EMPTY_LIST);
TextView appTitle = widgetsListHeader.findViewById(R.id.app_title);
TextView appSubtitle = widgetsListHeader.findViewById(R.id.app_subtitle);
@ -135,7 +137,7 @@ public final class WidgetsListSearchHeaderViewHolderBinderTest {
TEST_PACKAGE,
/* numOfWidgets= */ 3);
mViewHolderBinder.bindViewHolder(viewHolder, entry, /* position= */ 0);
mViewHolderBinder.bindViewHolder(viewHolder, entry, /* position= */ 0, EMPTY_LIST);
widgetsListHeader.callOnClick();
verify(mOnHeaderClickListener).onHeaderClicked(eq(true),

View File

@ -23,6 +23,8 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.robolectric.Shadows.shadowOf;
import static java.util.Collections.EMPTY_LIST;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
@ -44,7 +46,6 @@ 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;
@ -111,7 +112,6 @@ public final class WidgetsListTableViewHolderBinderTest {
LayoutInflater.from(mTestActivity),
mOnIconClickListener,
mOnLongClickListener,
new CachingWidgetPreviewLoader(mWidgetPreviewLoader),
new WidgetsListDrawableFactory(mTestActivity));
}
@ -128,13 +128,13 @@ public final class WidgetsListTableViewHolderBinderTest {
APP_NAME,
TEST_PACKAGE,
/* numOfWidgets= */ 3);
mViewHolderBinder.bindViewHolder(viewHolder, entry, /* position= */ 0);
mViewHolderBinder.bindViewHolder(viewHolder, entry, /* position= */ 0, EMPTY_LIST);
shadowOf(getMainLooper()).idle();
// THEN the table container has one row, which contains 3 widgets.
// View: .SampleWidget0 | .SampleWidget1 | .SampleWidget2
assertThat(viewHolder.mTableContainer.getChildCount()).isEqualTo(1);
TableRow row = (TableRow) viewHolder.mTableContainer.getChildAt(0);
assertThat(viewHolder.tableContainer.getChildCount()).isEqualTo(1);
TableRow row = (TableRow) viewHolder.tableContainer.getChildAt(0);
assertThat(row.getChildCount()).isEqualTo(3);
// Widget 0 label is .SampleWidget0.
assertWidgetCellWithLabel(row.getChildAt(0), ".SampleWidget0");

View File

@ -48,7 +48,6 @@ 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 {
@ -64,7 +63,6 @@ public class LauncherAppState {
private final LauncherModel mModel;
private final IconProvider mIconProvider;
private final IconCache mIconCache;
private final DatabaseWidgetPreviewLoader mWidgetCache;
private final InvariantDeviceProfile mInvariantDeviceProfile;
private final RunnableList mOnTerminateCallback = new RunnableList();
@ -139,7 +137,6 @@ public class LauncherAppState {
mIconProvider = new IconProvider(context, Themes.isThemedIconEnabled(context));
mIconCache = new IconCache(mContext, mInvariantDeviceProfile,
iconCacheFileName, mIconProvider);
mWidgetCache = new DatabaseWidgetPreviewLoader(mContext, mIconCache);
mModel = new LauncherModel(context, this, mIconCache, new AppFilter(mContext));
mOnTerminateCallback.add(mIconCache::close);
}
@ -155,7 +152,6 @@ public class LauncherAppState {
LauncherIcons.clearPool();
mIconCache.updateIconParams(
mInvariantDeviceProfile.fillResIconDpi, mInvariantDeviceProfile.iconBitmapSize);
mWidgetCache.refresh();
mModel.forceReload();
}
@ -181,10 +177,6 @@ public class LauncherAppState {
return mModel;
}
public DatabaseWidgetPreviewLoader getWidgetCache() {
return mWidgetCache;
}
public InvariantDeviceProfile getInvariantDeviceProfile() {
return mInvariantDeviceProfile;
}

View File

@ -286,9 +286,7 @@ public class AddItemActivity extends BaseActivity
@Override
protected void onPostExecute(WidgetItem item) {
mWidgetCell.setPreviewSize(item);
mWidgetCell.applyFromCellItem(item, mApp.getWidgetCache());
mWidgetCell.ensurePreview();
mWidgetCell.applyFromCellItem(item);
}
}.executeOnExecutor(MODEL_EXECUTOR);
// TODO: Create a worker looper executor and reuse that everywhere.

View File

@ -123,7 +123,6 @@ public class PackageUpdatedTask extends BaseModelUpdateTask {
iconCache.updateIconsForPkg(packages[i], mUser);
activitiesLists.put(
packages[i], appsList.updatePackage(context, packages[i], mUser));
app.getWidgetCache().removePackage(packages[i], mUser);
// The update may have changed which shortcuts/widgets are available.
// Refresh the widgets for the package if we have an activity running.
@ -148,7 +147,6 @@ public class PackageUpdatedTask extends BaseModelUpdateTask {
for (int i = 0; i < N; i++) {
if (DEBUG) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]);
appsList.removePackage(packages[i], mUser);
app.getWidgetCache().removePackage(packages[i], mUser);
}
flagOp = FlagOp.addFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE);
break;

View File

@ -22,6 +22,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
* Creates and populates views with data
@ -46,7 +47,7 @@ public interface ViewHolderBinder<T, V extends ViewHolder> {
V newViewHolder(ViewGroup parent);
/** Populate UI references in {@link ViewHolder} with data. */
void bindViewHolder(V viewHolder, T data, @ListPosition int position);
void bindViewHolder(V viewHolder, T data, @ListPosition int position, List<Object> payloads);
/**
* Called when the view is recycled. Views are recycled in batches once they are sufficiently

View File

@ -1,289 +0,0 @@
/*
* 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);
}
}

View File

@ -16,21 +16,9 @@
package com.android.launcher3.widget;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
@ -39,72 +27,40 @@ import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.Process;
import android.os.UserHandle;
import android.util.Log;
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.LauncherAppState;
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.BitmapRenderer;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.icons.ShadowGenerator;
import com.android.launcher3.icons.cache.HandlerRunnable;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.pm.ShortcutConfigActivityInfo;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.Executors;
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.util.WidgetSizes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
/** {@link WidgetPreviewLoader} that loads preview images from a {@link CacheDb}. */
public class DatabaseWidgetPreviewLoader implements WidgetPreviewLoader {
/** Utility class to load widget previews */
public class DatabaseWidgetPreviewLoader {
private static final String TAG = "WidgetPreviewLoader";
private static final boolean DEBUG = false;
private final HashMap<String, long[]> mPackageVersions = new HashMap<>();
/**
* Weak reference objects, do not prevent their referents from being made finalizable,
* finalized, and then reclaimed.
* Note: synchronized block used for this variable is expensive and the block should always
* be posted to a background thread.
*/
@Thunk final Set<Bitmap> mUnusedBitmaps = Collections.newSetFromMap(new WeakHashMap<>());
private final Context mContext;
private final IconCache mIconCache;
private final UserCache mUserCache;
private final CacheDb mDb;
private final BaseActivity mContext;
private final float mPreviewBoxCornerRadius;
public DatabaseWidgetPreviewLoader(Context context, IconCache iconCache) {
public DatabaseWidgetPreviewLoader(BaseActivity context) {
mContext = context;
mIconCache = iconCache;
mUserCache = UserCache.INSTANCE.get(context);
mDb = new CacheDb(context);
float previewCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context);
mPreviewBoxCornerRadius = previewCornerRadius > 0
? previewCornerRadius
@ -117,251 +73,29 @@ public class DatabaseWidgetPreviewLoader implements WidgetPreviewLoader {
*
* @return a request id which can be used to cancel the request.
*/
@Override
@NonNull
public CancellationSignal loadPreview(
@NonNull BaseActivity activity,
public HandlerRunnable loadPreview(
@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(activity, key, item, previewWidth, previewHeight, callback);
task.executeOnExecutor(Executors.THREAD_POOL_EXECUTOR);
CancellationSignal signal = new CancellationSignal();
signal.setOnCancelListener(task);
return signal;
}
/** Clears the database storing previews. */
public void refresh() {
mDb.clear();
}
/**
* The DB holds the generated previews for various components. Previews can also have different
* sizes (landscape vs portrait).
*/
private static class CacheDb extends SQLiteCacheHelper {
private static final int DB_VERSION = 9;
private static final String TABLE_NAME = "shortcut_and_widget_previews";
private static final String COLUMN_COMPONENT = "componentName";
private static final String COLUMN_USER = "profileId";
private static final String COLUMN_SIZE = "size";
private static final String COLUMN_PACKAGE = "packageName";
private static final String COLUMN_LAST_UPDATED = "lastUpdated";
private static final String COLUMN_VERSION = "version";
private static final String COLUMN_PREVIEW_BITMAP = "preview_bitmap";
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
+ ") "
+
");");
}
}
@Thunk void writeToDb(WidgetCacheKey key, long[] versions, Bitmap preview) {
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.mSize);
values.put(CacheDb.COLUMN_PACKAGE, key.componentName.getPackageName());
values.put(CacheDb.COLUMN_VERSION, versions[0]);
values.put(CacheDb.COLUMN_LAST_UPDATED, versions[1]);
values.put(CacheDb.COLUMN_PREVIEW_BITMAP, GraphicsUtils.flattenBitmap(preview));
mDb.insertOrReplace(values);
}
/** Removes the package from the preview database. */
public void removePackage(String packageName, UserHandle user) {
removePackage(packageName, user, mUserCache.getSerialNumberForUser(user));
}
/** Removes the package from the preview database. */
public void removePackage(String packageName, UserHandle user, long userSerial) {
synchronized (mPackageVersions) {
mPackageVersions.remove(packageName);
}
mDb.delete(
CacheDb.COLUMN_PACKAGE + " = ? AND " + CacheDb.COLUMN_USER + " = ?",
new String[]{packageName, Long.toString(userSerial)});
}
/**
* Updates the persistent DB:
* 1. Any preview generated for an old package version is removed
* 2. Any preview for an absent package is removed
* This ensures that we remove entries for packages which changed while the launcher was dead.
*
* @param packageUser if provided, specifies that list only contains previews for the
* given package/user, otherwise the list contains all previews
*/
public void removeObsoletePreviews(ArrayList<? extends ComponentKey> list,
@Nullable PackageUserKey packageUser) {
Preconditions.assertWorkerThread();
LongSparseArray<HashSet<String>> validPackages = new LongSparseArray<>();
for (ComponentKey key : list) {
final long userId = mUserCache.getSerialNumberForUser(key.user);
HashSet<String> packages = validPackages.get(userId);
if (packages == null) {
packages = new HashSet<>();
validPackages.put(userId, packages);
}
packages.add(key.componentName.getPackageName());
}
LongSparseArray<HashSet<String>> packagesToDelete = new LongSparseArray<>();
long passedUserId = packageUser == null ? 0
: mUserCache.getSerialNumberForUser(packageUser.mUser);
Cursor c = null;
try {
c = mDb.query(
new String[]{CacheDb.COLUMN_USER, CacheDb.COLUMN_PACKAGE,
CacheDb.COLUMN_LAST_UPDATED, CacheDb.COLUMN_VERSION},
null, null);
while (c.moveToNext()) {
long userId = c.getLong(0);
String pkg = c.getString(1);
long lastUpdated = c.getLong(2);
long version = c.getLong(3);
if (packageUser != null && (!pkg.equals(packageUser.mPackageName)
|| userId != passedUserId)) {
// This preview is associated with a different package/user, no need to remove.
continue;
}
HashSet<String> packages = validPackages.get(userId);
if (packages != null && packages.contains(pkg)) {
long[] versions = getPackageVersion(pkg);
if (versions[0] == version && versions[1] == lastUpdated) {
// Every thing checks out
continue;
}
}
// We need to delete this package.
packages = packagesToDelete.get(userId);
if (packages == null) {
packages = new HashSet<>();
packagesToDelete.put(userId, packages);
}
packages.add(pkg);
}
for (int i = 0; i < packagesToDelete.size(); i++) {
long userId = packagesToDelete.keyAt(i);
UserHandle user = mUserCache.getUserForSerialNumber(userId);
for (String pkg : packagesToDelete.valueAt(i)) {
removePackage(pkg, user, userId);
}
}
} catch (SQLException e) {
Log.e(TAG, "Error updating widget previews", e);
} finally {
if (c != null) {
c.close();
}
}
}
/**
* Reads the preview bitmap from the DB or null if the preview is not in the DB.
*/
@Thunk Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask) {
Cursor cursor = null;
try {
cursor = mDb.query(
new String[]{CacheDb.COLUMN_PREVIEW_BITMAP},
CacheDb.COLUMN_COMPONENT + " = ? AND " + CacheDb.COLUMN_USER + " = ? AND "
+ CacheDb.COLUMN_SIZE + " = ?",
new String[]{
key.componentName.flattenToShortString(),
Long.toString(mUserCache.getSerialNumberForUser(key.user)),
key.mSize
});
// If cancelled, skip getting the blob and decoding it into a bitmap
if (loadTask.isCancelled()) {
return null;
}
if (cursor.moveToNext()) {
byte[] blob = cursor.getBlob(0);
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inBitmap = recycle;
try {
if (!loadTask.isCancelled()) {
return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts);
}
} catch (Exception e) {
return null;
}
}
} catch (SQLException e) {
Log.w(TAG, "Error loading preview from DB", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return null;
@NonNull Consumer<Bitmap> callback) {
Handler handler = Executors.UI_HELPER_EXECUTOR.getHandler();
HandlerRunnable<Bitmap> request = new HandlerRunnable<>(handler,
() -> generatePreview(item, previewSize.getWidth(), previewSize.getHeight()),
MAIN_EXECUTOR,
callback);
Utilities.postAsyncCallback(handler, request);
return request;
}
/**
* Returns a generated preview for a widget and if the preview should be saved in persistent
* storage.
* @param launcher
* @param item
* @param recycle
* @param previewWidth
* @param previewHeight
* @return Pair<Bitmap, Boolean>
*/
private Pair<Bitmap, Boolean> generatePreview(BaseActivity launcher, WidgetItem item,
Bitmap recycle,
int previewWidth, int previewHeight) {
private Bitmap generatePreview(WidgetItem item, int previewWidth, int previewHeight) {
if (item.widgetInfo != null) {
return generateWidgetPreview(launcher, item.widgetInfo,
previewWidth, recycle, null);
return generateWidgetPreview(item.widgetInfo, previewWidth, null);
} else {
return new Pair<>(generateShortcutPreview(launcher, item.activityInfo,
previewWidth, previewHeight, recycle), false);
return generateShortcutPreview(item.activityInfo, previewWidth, previewHeight);
}
}
@ -369,16 +103,12 @@ public class DatabaseWidgetPreviewLoader implements WidgetPreviewLoader {
* Generates the widget preview from either the {@link WidgetManagerHelper} or cache
* and add badge at the bottom right corner.
*
* @param launcher
* @param info information about the widget
* @param maxPreviewWidth width of the preview on either workspace or tray
* @param preview bitmap that can be recycled
* @param preScaledWidthOut return the width of the returned bitmap
* @return Pair<Bitmap (the preview) , Boolean (should be stored in db)>
*/
public Pair<Bitmap, Boolean> generateWidgetPreview(BaseActivity launcher,
LauncherAppWidgetProviderInfo info,
int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) {
public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info,
int maxPreviewWidth, int[] preScaledWidthOut) {
// Load the preview image if possible
if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE;
@ -409,117 +139,96 @@ public class DatabaseWidgetPreviewLoader implements WidgetPreviewLoader {
int previewWidth;
int previewHeight;
boolean savePreviewImage = widgetPreviewExists || info.previewImage == 0;
if (widgetPreviewExists && drawable.getIntrinsicWidth() > 0
&& drawable.getIntrinsicHeight() > 0) {
previewWidth = drawable.getIntrinsicWidth();
previewHeight = drawable.getIntrinsicHeight();
} else {
DeviceProfile dp = launcher.getDeviceProfile();
DeviceProfile dp = mContext.getDeviceProfile();
Size widgetSize = WidgetSizes.getWidgetPaddedSizePx(mContext, info.provider, dp, spanX,
spanY);
previewWidth = widgetSize.getWidth();
previewHeight = widgetSize.getHeight();
}
// Scale to fit width only - let the widget preview be clipped in the
// vertical dimension
float scale = 1f;
if (preScaledWidthOut != null) {
preScaledWidthOut[0] = previewWidth;
}
if (previewWidth > maxPreviewWidth) {
scale = maxPreviewWidth / (float) (previewWidth);
}
// Scale to fit width only - let the widget preview be clipped in the
// vertical dimension
final float scale = previewWidth > maxPreviewWidth
? (maxPreviewWidth / (float) (previewWidth)) : 1f;
if (scale != 1f) {
previewWidth = Math.max((int) (scale * previewWidth), 1);
previewHeight = Math.max((int) (scale * previewHeight), 1);
}
final Canvas c = new Canvas();
if (preview == null) {
// If no bitmap was provided, then allocate a new one with the right size.
preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);
c.setBitmap(preview);
} else {
// If a bitmap was passed in, attempt to reconfigure the bitmap to the same dimensions
// as the preview.
try {
preview.reconfigure(previewWidth, previewHeight, preview.getConfig());
} catch (IllegalArgumentException e) {
// This occurs if the preview can't be reconfigured for any reason. In this case,
// allocate a new bitmap with the right size.
preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);
}
final int previewWidthF = previewWidth;
final int previewHeightF = previewHeight;
final Drawable drawableF = drawable;
c.setBitmap(preview);
c.drawColor(0, PorterDuff.Mode.CLEAR);
}
// Draw the scaled preview into the final bitmap
if (widgetPreviewExists) {
drawable.setBounds(0, 0, previewWidth, previewHeight);
drawable.draw(c);
} else {
RectF boxRect;
// Draw horizontal and vertical lines to represent individual columns.
final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
if (Utilities.ATLEAST_S) {
boxRect = new RectF(/* left= */ 0, /* top= */ 0, /* right= */
previewWidth, /* bottom= */ previewHeight);
p.setStyle(Paint.Style.FILL);
p.setColor(Color.WHITE);
float roundedCorner = mContext.getResources().getDimension(
android.R.dimen.system_app_widget_background_radius);
c.drawRoundRect(boxRect, roundedCorner, roundedCorner, p);
return BitmapRenderer.createHardwareBitmap(previewWidth, previewHeight, c -> {
// Draw the scaled preview into the final bitmap
if (widgetPreviewExists) {
drawableF.setBounds(0, 0, previewWidthF, previewHeightF);
drawableF.draw(c);
} else {
boxRect = drawBoxWithShadow(c, previewWidth, previewHeight);
}
RectF boxRect;
p.setStyle(Paint.Style.STROKE);
p.setStrokeWidth(mContext.getResources()
.getDimension(R.dimen.widget_preview_cell_divider_width));
p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
// Draw horizontal and vertical lines to represent individual columns.
final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
float t = boxRect.left;
float tileSize = boxRect.width() / spanX;
for (int i = 1; i < spanX; i++) {
t += tileSize;
c.drawLine(t, 0, t, previewHeight, p);
}
if (Utilities.ATLEAST_S) {
boxRect = new RectF(/* left= */ 0, /* top= */ 0, /* right= */
previewWidthF, /* bottom= */ previewHeightF);
t = boxRect.top;
tileSize = boxRect.height() / spanY;
for (int i = 1; i < spanY; i++) {
t += tileSize;
c.drawLine(0, t, previewWidth, t, p);
}
// Draw icon in the center.
try {
Drawable icon =
mIconCache.getFullResIcon(info.provider.getPackageName(), info.icon);
if (icon != null) {
int appIconSize = launcher.getDeviceProfile().iconSizePx;
int iconSize = (int) Math.min(appIconSize * scale,
Math.min(boxRect.width(), boxRect.height()));
icon = mutateOnMainThread(icon);
int hoffset = (previewWidth - iconSize) / 2;
int yoffset = (previewHeight - iconSize) / 2;
icon.setBounds(hoffset, yoffset, hoffset + iconSize, yoffset + iconSize);
icon.draw(c);
p.setStyle(Paint.Style.FILL);
p.setColor(Color.WHITE);
float roundedCorner = mContext.getResources().getDimension(
android.R.dimen.system_app_widget_background_radius);
c.drawRoundRect(boxRect, roundedCorner, roundedCorner, p);
} else {
boxRect = drawBoxWithShadow(c, previewWidthF, previewHeightF);
}
p.setStyle(Paint.Style.STROKE);
p.setStrokeWidth(mContext.getResources()
.getDimension(R.dimen.widget_preview_cell_divider_width));
p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
float t = boxRect.left;
float tileSize = boxRect.width() / spanX;
for (int i = 1; i < spanX; i++) {
t += tileSize;
c.drawLine(t, 0, t, previewHeightF, p);
}
t = boxRect.top;
tileSize = boxRect.height() / spanY;
for (int i = 1; i < spanY; i++) {
t += tileSize;
c.drawLine(0, t, previewWidthF, t, p);
}
// Draw icon in the center.
try {
Drawable icon = LauncherAppState.getInstance(mContext).getIconCache()
.getFullResIcon(info.provider.getPackageName(), info.icon);
if (icon != null) {
int appIconSize = mContext.getDeviceProfile().iconSizePx;
int iconSize = (int) Math.min(appIconSize * scale,
Math.min(boxRect.width(), boxRect.height()));
icon = mutateOnMainThread(icon);
int hoffset = (previewWidthF - iconSize) / 2;
int yoffset = (previewHeightF - iconSize) / 2;
icon.setBounds(hoffset, yoffset, hoffset + iconSize, yoffset + iconSize);
icon.draw(c);
}
} catch (Resources.NotFoundException e) {
}
} catch (Resources.NotFoundException e) {
savePreviewImage = false;
}
c.setBitmap(null);
}
return new Pair<>(preview, savePreviewImage);
});
}
private RectF drawBoxWithShadow(Canvas c, int width, int height) {
@ -537,42 +246,29 @@ public class DatabaseWidgetPreviewLoader implements WidgetPreviewLoader {
return builder.bounds;
}
private Bitmap generateShortcutPreview(BaseActivity launcher, ShortcutConfigActivityInfo info,
int maxWidth, int maxHeight, Bitmap preview) {
int iconSize = launcher.getDeviceProfile().allAppsIconSizePx;
int padding = launcher.getResources()
private Bitmap generateShortcutPreview(
ShortcutConfigActivityInfo info, int maxWidth, int maxHeight) {
int iconSize = mContext.getDeviceProfile().allAppsIconSizePx;
int padding = mContext.getResources()
.getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
int size = iconSize + 2 * padding;
if (maxHeight < size || maxWidth < size) {
throw new RuntimeException("Max size is too small for preview");
}
final Canvas c = new Canvas();
if (preview == null || preview.getWidth() < size || preview.getHeight() < size) {
preview = Bitmap.createBitmap(size, size, Config.ARGB_8888);
c.setBitmap(preview);
} else {
if (preview.getWidth() > size || preview.getHeight() > size) {
preview.reconfigure(size, size, preview.getConfig());
}
return BitmapRenderer.createHardwareBitmap(size, size, c -> {
drawBoxWithShadow(c, size, size);
// Reusing bitmap. Clear it.
c.setBitmap(preview);
c.drawColor(0, PorterDuff.Mode.CLEAR);
}
LauncherIcons li = LauncherIcons.obtain(mContext);
Drawable icon = li.createBadgedIconBitmap(
mutateOnMainThread(info.getFullResIcon(
LauncherAppState.getInstance(mContext).getIconCache())),
Process.myUserHandle(), 0).newIcon(mContext);
li.recycle();
drawBoxWithShadow(c, size, size);
LauncherIcons li = LauncherIcons.obtain(mContext);
Drawable icon = li.createBadgedIconBitmap(
mutateOnMainThread(info.getFullResIcon(mIconCache)),
Process.myUserHandle(), 0).newIcon(launcher);
li.recycle();
icon.setBounds(padding, padding, padding + iconSize, padding + iconSize);
icon.draw(c);
c.setBitmap(null);
return preview;
icon.setBounds(padding, padding, padding + iconSize, padding + iconSize);
icon.draw(c);
});
}
private Drawable mutateOnMainThread(final Drawable drawable) {
@ -585,206 +281,4 @@ public class DatabaseWidgetPreviewLoader implements WidgetPreviewLoader {
throw new RuntimeException(e);
}
}
/**
* @return an array of containing versionCode and lastUpdatedTime for the package.
*/
@Thunk long[] getPackageVersion(String packageName) {
synchronized (mPackageVersions) {
long[] versions = mPackageVersions.get(packageName);
if (versions == null) {
versions = new long[2];
try {
PackageInfo info = mContext.getPackageManager().getPackageInfo(packageName,
PackageManager.GET_UNINSTALLED_PACKAGES);
versions[0] = info.versionCode;
versions[1] = info.lastUpdateTime;
} catch (NameNotFoundException e) {
Log.e(TAG, "PackageInfo not found", e);
}
mPackageVersions.put(packageName, versions);
}
return versions;
}
}
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 WidgetPreviewLoadedCallback mCallback;
private final BaseActivity mActivity;
@Thunk long[] mVersions;
@Thunk Bitmap mBitmapToRecycle;
@Nullable private Bitmap mUnusedPreviewBitmap;
private boolean mSaveToDB = false;
PreviewLoadTask(BaseActivity activity, WidgetCacheKey key, WidgetItem info,
int previewWidth, int previewHeight, WidgetPreviewLoadedCallback callback) {
mActivity = activity;
mKey = key;
mInfo = info;
mPreviewHeight = previewHeight;
mPreviewWidth = previewWidth;
mCallback = callback;
if (DEBUG) {
Log.d(TAG, String.format("%s, %s, %d, %d",
mKey, mInfo, mPreviewHeight, mPreviewWidth));
}
}
@Override
protected Bitmap doInBackground(Void... params) {
Bitmap unusedBitmap = null;
// If already cancelled before this gets to run in the background, then return early
if (isCancelled()) {
return null;
}
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) {
unusedBitmap = candidate;
mUnusedBitmaps.remove(unusedBitmap);
break;
}
}
}
// creating a bitmap is expensive. Do not do this inside synchronized block.
if (unusedBitmap == null) {
unusedBitmap = Bitmap.createBitmap(mPreviewWidth, mPreviewHeight, Config.ARGB_8888);
}
// If cancelled now, don't bother reading the preview from the DB
if (isCancelled()) {
return unusedBitmap;
}
Bitmap preview = readFromDb(mKey, unusedBitmap, this);
// Only consider generating the preview if we have not cancelled the task already
if (!isCancelled() && preview == null) {
// Fetch the version info before we generate the preview, so that, in-case the
// app was updated while we are generating the preview, we use the old version info,
// which would gets re-written next time.
boolean persistable = mInfo.activityInfo == null
|| mInfo.activityInfo.isPersistable();
mVersions = persistable ? getPackageVersion(mKey.componentName.getPackageName())
: null;
// it's not in the db... we need to generate it
Pair<Bitmap, Boolean> pair = generatePreview(mActivity, mInfo, unusedBitmap,
mPreviewWidth, mPreviewHeight);
preview = pair.first;
if (preview != unusedBitmap) {
mUnusedPreviewBitmap = unusedBitmap;
}
this.mSaveToDB = pair.second;
}
return preview;
}
@Override
protected void onPostExecute(final Bitmap preview) {
mCallback.onPreviewLoaded(preview);
// Write the generated preview to the DB in the worker thread
if (mVersions != null) {
MODEL_EXECUTOR.post(new Runnable() {
@Override
public void run() {
if (mUnusedPreviewBitmap != null) {
// If we didn't end up using the bitmap, it can be added back into the
// recycled set.
synchronized (mUnusedBitmaps) {
mUnusedBitmaps.add(mUnusedPreviewBitmap);
}
}
if (!isCancelled() && mSaveToDB) {
// If we are still using this preview, then write it to the DB and then
// let the normal clear mechanism recycle the bitmap
writeToDb(mKey, mVersions, preview);
mBitmapToRecycle = preview;
} else {
// If we've already cancelled, then skip writing the bitmap to the DB
// and manually add the bitmap back to the recycled set
synchronized (mUnusedBitmaps) {
mUnusedBitmaps.add(preview);
}
}
}
});
} else {
// If we don't need to write to disk, then ensure the preview gets recycled by
// the normal clear mechanism
mBitmapToRecycle = preview;
}
}
@Override
protected void onCancelled(final Bitmap preview) {
// If we've cancelled while the task is running, then can return the bitmap to the
// recycled set immediately. Otherwise, it will be recycled after the preview is written
// to disk.
if (preview != null) {
MODEL_EXECUTOR.post(new Runnable() {
@Override
public void run() {
synchronized (mUnusedBitmaps) {
mUnusedBitmaps.add(preview);
}
}
});
}
}
@Override
public void onCancel() {
cancel(true);
// This only handles the case where the PreviewLoadTask is cancelled after the task has
// successfully completed (including having written to disk when necessary). In the
// other cases where it is cancelled while the task is running, it will be cleaned up
// in the tasks's onCancelled() call, and if cancelled while the task is writing to
// disk, it will be cancelled in the task's onPostExecute() call.
if (mBitmapToRecycle != null) {
MODEL_EXECUTOR.post(new Runnable() {
@Override
public void run() {
synchronized (mUnusedBitmaps) {
mUnusedBitmaps.add(mBitmapToRecycle);
}
mBitmapToRecycle = null;
}
});
}
}
}
private static final class WidgetCacheKey extends ComponentKey {
@Thunk final String mSize;
WidgetCacheKey(ComponentName componentName, UserHandle user, String size) {
super(componentName, user);
this.mSize = size;
}
@Override
public int hashCode() {
return super.hashCode() ^ mSize.hashCode();
}
@Override
public boolean equals(Object o) {
return super.equals(o) && ((WidgetCacheKey) o).mSize.equals(mSize);
}
}
}

View File

@ -140,10 +140,9 @@ public class PendingItemDragHelper extends DragPreviewProvider {
.addDragListener(new AppWidgetHostViewDragListener(launcher));
}
if (preview == null && mAppWidgetHostViewPreview == null) {
Drawable p = new FastBitmapDrawable(
app.getWidgetCache().generateWidgetPreview(launcher,
createWidgetInfo.info, maxWidth, null,
previewSizeBeforeScale).first);
Drawable p = new FastBitmapDrawable(new DatabaseWidgetPreviewLoader(launcher)
.generateWidgetPreview(
createWidgetInfo.info, maxWidth, previewSizeBeforeScale));
if (RoundedCornerEnforcement.isRoundedCornerEnabled()) {
p = new RoundDrawableWrapper(p, mEnforcedRoundedCornersForWidget);
}

View File

@ -26,14 +26,12 @@ import static com.android.launcher3.Utilities.ATLEAST_S;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.CancellationSignal;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Size;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.accessibility.AccessibilityNodeInfo;
@ -42,6 +40,7 @@ import android.widget.LinearLayout;
import android.widget.RemoteViews;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.BaseActivity;
@ -51,9 +50,13 @@ import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.RoundDrawableWrapper;
import com.android.launcher3.icons.cache.HandlerRunnable;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.widget.util.WidgetSizes;
import java.util.function.Consumer;
/**
* Represents the individual cell of the widget inside the widget tray. The preview is drawn
* horizontally centered, and scaled down if needed.
@ -63,7 +66,7 @@ import com.android.launcher3.widget.util.WidgetSizes;
* transition from the view to drag view, so when adding padding support, DnD would need to
* consider the appropriate scaling factor.
*/
public class WidgetCell extends LinearLayout implements OnLayoutChangeListener {
public class WidgetCell extends LinearLayout {
private static final String TAG = "WidgetCell";
private static final boolean DEBUG = false;
@ -115,14 +118,11 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener {
protected WidgetItem mItem;
private WidgetPreviewLoader mWidgetPreviewLoader;
private final DatabaseWidgetPreviewLoader mWidgetPreviewLoader;
protected CancellationSignal mActiveRequest;
protected HandlerRunnable mActiveRequest;
private boolean mAnimatePreview = true;
private boolean mApplyBitmapDeferred = false;
private Drawable mDeferredDrawable;
protected final BaseActivity mActivity;
private final CheckLongPressHelper mLongPressHelper;
private final float mEnforcedCornerRadius;
@ -144,6 +144,7 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener {
super(context, attrs, defStyle);
mActivity = BaseActivity.fromContext(context);
mWidgetPreviewLoader = new DatabaseWidgetPreviewLoader(mActivity);
mLongPressHelper = new CheckLongPressHelper(this);
mLongPressHelper.setLongPressTimeoutFactor(1);
@ -218,7 +219,36 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener {
this.mSourceContainer = sourceContainer;
}
public void applyFromCellItem(WidgetItem item, WidgetPreviewLoader loader) {
/**
* Applies the item to this view
*/
public void applyFromCellItem(WidgetItem item) {
applyFromCellItem(item, 1f);
}
/**
* Applies the item to this view
*/
public void applyFromCellItem(WidgetItem item, float previewScale) {
applyFromCellItem(item, previewScale, this::applyPreview, null);
}
/**
* Applies the item to this view
* @param item item to apply
* @param previewScale factor to scale the preview
* @param callback callback when preview is loaded in case the preview is being loaded or cached
* @param cachedPreview previously cached preview bitmap is present
*/
public void applyFromCellItem(WidgetItem item, float previewScale,
@NonNull Consumer<Bitmap> callback, @Nullable Bitmap cachedPreview) {
// setPreviewSize
DeviceProfile deviceProfile = mActivity.getDeviceProfile();
Size widgetSize = WidgetSizes.getWidgetItemSizePx(getContext(), deviceProfile, item);
mTargetPreviewWidth = widgetSize.getWidth();
mTargetPreviewHeight = widgetSize.getHeight();
mPreviewContainerScale = previewScale;
applyPreviewOnAppWidgetHostView(item);
Context context = getContext();
@ -240,14 +270,14 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener {
}
}
mWidgetPreviewLoader = loader;
if (item.activityInfo != null) {
setTag(new PendingAddShortcutInfo(item.activityInfo));
} else {
setTag(new PendingAddWidgetInfo(item.widgetInfo, mSourceContainer));
}
}
ensurePreviewWithCallback(callback, cachedPreview);
}
private void applyPreviewOnAppWidgetHostView(WidgetItem item) {
if (mRemoteViewsPreview != null) {
@ -294,37 +324,15 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener {
return mAppWidgetHostViewPreview;
}
/**
* Sets if applying bitmap preview should be deferred. The UI will still load the bitmap, but
* will not cause invalidate, so that when deferring is disabled later, all the bitmaps are
* ready.
* This prevents invalidates while the animation is running.
*/
public void setApplyBitmapDeferred(boolean isDeferred) {
if (mApplyBitmapDeferred != isDeferred) {
mApplyBitmapDeferred = isDeferred;
if (!mApplyBitmapDeferred && mDeferredDrawable != null) {
applyPreview(mDeferredDrawable);
mDeferredDrawable = null;
}
}
}
public void setAnimatePreview(boolean shouldAnimate) {
mAnimatePreview = shouldAnimate;
}
public void applyPreview(Bitmap bitmap) {
FastBitmapDrawable drawable = new FastBitmapDrawable(bitmap);
applyPreview(new RoundDrawableWrapper(drawable, mEnforcedCornerRadius));
}
private void applyPreview(Bitmap bitmap) {
if (bitmap != null) {
Drawable drawable = new RoundDrawableWrapper(
new FastBitmapDrawable(bitmap), mEnforcedCornerRadius);
private void applyPreview(Drawable drawable) {
if (mApplyBitmapDeferred) {
mDeferredDrawable = drawable;
return;
}
if (drawable != null) {
// Scale down the preview size if it's wider than the cell.
float scale = 1f;
if (mTargetPreviewWidth > 0) {
@ -349,6 +357,10 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener {
} else {
mWidgetImageContainer.setAlpha(1f);
}
if (mActiveRequest != null) {
mActiveRequest.cancel();
mActiveRequest = null;
}
}
private void setContainerSize(int width, int height) {
@ -358,7 +370,13 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener {
mWidgetImageContainer.setLayoutParams(layoutParams);
}
public void ensurePreview() {
/**
* Ensures that the preview is already loaded or being loaded. If the preview is not loaded,
* it applies the provided cachedPreview. If that is null, it starts a loader and notifies the
* callback on successful load.
*/
private void ensurePreviewWithCallback(Consumer<Bitmap> callback,
@Nullable Bitmap cachedPreview) {
if (mAppWidgetHostViewPreview != null) {
int containerWidth = (int) (mTargetPreviewWidth * mPreviewContainerScale);
int containerHeight = (int) (mTargetPreviewHeight * mPreviewContainerScale);
@ -382,38 +400,18 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener {
mAppWidgetHostViewPreview.setLayoutParams(params);
mWidgetImageContainer.addView(mAppWidgetHostViewPreview, /* index= */ 0);
mWidgetImage.setVisibility(View.GONE);
applyPreview((Drawable) null);
applyPreview(null);
return;
}
if (cachedPreview != null) {
applyPreview(cachedPreview);
return;
}
if (mActiveRequest != null) {
return;
}
mActiveRequest = mWidgetPreviewLoader.loadPreview(
BaseActivity.fromContext(getContext()), mItem,
new Size(mTargetPreviewWidth, mTargetPreviewHeight),
this::applyPreview);
}
/** Sets the widget preview image size in number of cells. */
public Size setPreviewSize(WidgetItem widgetItem) {
return setPreviewSize(widgetItem, 1f);
}
/** Sets the widget preview image size, in number of cells, and preview scale. */
public Size setPreviewSize(WidgetItem widgetItem, float previewScale) {
DeviceProfile deviceProfile = mActivity.getDeviceProfile();
Size widgetSize = WidgetSizes.getWidgetItemSizePx(getContext(), deviceProfile, widgetItem);
mTargetPreviewWidth = widgetSize.getWidth();
mTargetPreviewHeight = widgetSize.getHeight();
mPreviewContainerScale = previewScale;
return widgetSize;
}
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
int oldTop, int oldRight, int oldBottom) {
removeOnLayoutChangeListener(this);
ensurePreview();
mItem, new Size(mTargetPreviewWidth, mTargetPreviewHeight), callback);
}
@Override
@ -429,17 +427,6 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener {
mLongPressHelper.cancelLongPress();
}
/**
* Helper method to get the string info of the tag.
*/
private String getTagToString() {
if (getTag() instanceof PendingAddWidgetInfo ||
getTag() instanceof PendingAddShortcutInfo) {
return getTag().toString();
}
return "";
}
private static NavigableAppWidgetHostView createAppWidgetHostView(Context context) {
return new NavigableAppWidgetHostView(context) {
@Override
@ -450,12 +437,7 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener {
}
private static boolean isLauncherContext(Context context) {
try {
Launcher.getLauncher(context);
return true;
} catch (Exception e) {
return false;
}
return ActivityContext.lookupContext(context) instanceof Launcher;
}
@Override

View File

@ -1,47 +0,0 @@
/*
* 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);
}
}

View File

@ -37,7 +37,6 @@ import android.widget.TableRow;
import android.widget.TextView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.model.WidgetItem;
@ -199,11 +198,7 @@ public class WidgetsBottomSheet extends BaseWidgetSheet {
tableRow.setGravity(Gravity.TOP);
row.forEach(widgetItem -> {
WidgetCell widget = addItemCell(tableRow);
widget.setPreviewSize(widgetItem);
widget.applyFromCellItem(widgetItem, LauncherAppState.getInstance(mActivityContext)
.getWidgetCache());
widget.ensurePreview();
widget.setVisibility(View.VISIBLE);
widget.applyFromCellItem(widgetItem);
});
widgetsTable.addView(tableRow);
});

View File

@ -687,7 +687,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
.findFirst()
.orElse(null);
if (viewHolderForTip != null) {
return ((ViewGroup) viewHolderForTip.mTableContainer.getChildAt(0)).getChildAt(0);
return ((ViewGroup) viewHolderForTip.tableContainer.getChildAt(0)).getChildAt(0);
}
return null;
@ -745,7 +745,6 @@ public class WidgetsFullSheet extends BaseWidgetSheet
mWidgetsListAdapter = new WidgetsListAdapter(
context,
LayoutInflater.from(context),
apps.getWidgetCache(),
apps.getIconCache(),
this::getEmptySpaceHeight,
/* iconClickListener= */ WidgetsFullSheet.this,
@ -784,7 +783,6 @@ public class WidgetsFullSheet extends BaseWidgetSheet
if (mAdapterType == PRIMARY || mAdapterType == WORK) {
mWidgetsRecyclerView.addOnAttachStateChangeListener(mBindScrollbarInSearchMode);
}
mWidgetsListAdapter.setApplyBitmapDeferred(false, mWidgetsRecyclerView);
mWidgetsListAdapter.setMaxHorizontalSpansPerRow(mMaxSpansPerRow);
}
}

View File

@ -24,14 +24,12 @@ import android.content.Context;
import android.graphics.Rect;
import android.os.Process;
import android.util.Log;
import android.util.Size;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.widget.TableRow;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -41,29 +39,22 @@ import androidx.recyclerview.widget.RecyclerView.Adapter;
import androidx.recyclerview.widget.RecyclerView.LayoutParams;
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.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.WidgetListSpaceEntry;
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;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@ -94,12 +85,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;
private final WidgetListBaseRowEntryComparator mRowComparator =
new WidgetListBaseRowEntryComparator();
@ -115,26 +103,21 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
@Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
@Nullable private RecyclerView mRecyclerView;
@Nullable private PackageUserKey mPendingClickHeader;
private final int mShortcutPreviewPadding;
private final int mSpacingBetweenEntries;
private int mMaxSpanSize = 4;
private final WidgetPreviewLoadedCallback mPreviewLoadedCallback =
ignored -> updateVisibleEntries();
public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
DatabaseWidgetPreviewLoader widgetPreviewLoader, IconCache iconCache,
IntSupplier emptySpaceHeightProvider,
IconCache iconCache, IntSupplier emptySpaceHeightProvider,
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(
layoutInflater, iconClickListener, iconLongClickListener,
mCachingPreviewLoader, listDrawableFactory);
mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListTableViewHolderBinder);
mViewHolderBinders.put(
VIEW_TYPE_WIDGETS_LIST,
new WidgetsListTableViewHolderBinder(
layoutInflater, iconClickListener, iconLongClickListener,
listDrawableFactory));
mViewHolderBinders.put(
VIEW_TYPE_WIDGETS_HEADER,
new WidgetsListHeaderViewHolderBinder(
@ -150,9 +133,6 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
mViewHolderBinders.put(
VIEW_TYPE_WIDGETS_SPACE,
new WidgetsSpaceViewHolderBinder(emptySpaceHeightProvider));
mShortcutPreviewPadding =
2 * context.getResources()
.getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
mSpacingBetweenEntries =
context.getResources().getDimensionPixelSize(R.dimen.widget_list_entry_spacing);
}
@ -186,28 +166,6 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
mFilter = filter;
}
/**
* Defers applying bitmap on all the {@link WidgetCell} in the {@param rv}.
*
* @see WidgetCell#setApplyBitmapDeferred(boolean)
*/
public void setApplyBitmapDeferred(boolean isDeferred, RecyclerView rv) {
mWidgetsListTableViewHolderBinder.setApplyBitmapDeferred(isDeferred);
for (int i = rv.getChildCount() - 1; i >= 0; i--) {
ViewHolder viewHolder = rv.getChildViewHolder(rv.getChildAt(i));
if (viewHolder.getItemViewType() == VIEW_TYPE_WIDGETS_LIST) {
WidgetsRowViewHolder holder = (WidgetsRowViewHolder) viewHolder;
for (int j = holder.mTableContainer.getChildCount() - 1; j >= 0; j--) {
TableRow row = (TableRow) holder.mTableContainer.getChildAt(j);
for (int k = row.getChildCount() - 1; k >= 0; k--) {
((WidgetCell) row.getChildAt(k)).setApplyBitmapDeferred(isDeferred);
}
}
}
}
}
@Override
public int getItemCount() {
return mVisibleEntries.size();
@ -233,7 +191,6 @@ 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.clear();
mAllEntries.add(new WidgetListSpaceEntry());
tempEntries.stream().sorted(mRowComparator).forEach(mAllEntries::add);
@ -247,15 +204,10 @@ 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() {
// 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 =
@ -293,54 +245,6 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
}
}
/**
* 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.getWidgetItemSizePx(mContext, deviceProfile,
widgetItem);
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 static boolean isHeaderForPackageUserKey(
@ -361,13 +265,17 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
public void resetExpandedHeader() {
if (mWidgetsContentVisiblePackageUserKey != null) {
mWidgetsContentVisiblePackageUserKey = null;
cancelLoadingPreviews();
updateVisibleEntries();
}
}
@Override
public void onBindViewHolder(ViewHolder holder, int pos) {
public void onBindViewHolder(ViewHolder holder, int position) {
onBindViewHolder(holder, position, Collections.EMPTY_LIST);
}
@Override
public void onBindViewHolder(ViewHolder holder, int pos, List<Object> payloads) {
ViewHolderBinder viewHolderBinder = mViewHolderBinders.get(getItemViewType(pos));
WidgetsListBaseEntry entry = mVisibleEntries.get(pos);
@ -376,7 +284,7 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
if (pos == (getItemCount() - 1)) {
listPos |= POSITION_LAST;
}
viewHolderBinder.bindViewHolder(holder, mVisibleEntries.get(pos), listPos);
viewHolderBinder.bindViewHolder(holder, mVisibleEntries.get(pos), listPos, payloads);
holder.itemView.setTag(R.id.tag_widget_entry, entry);
}
@ -430,8 +338,6 @@ 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);
@ -446,16 +352,6 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
updateVisibleEntries();
}
private void cancelLoadingPreviews() {
mCachingPreviewLoader.clearAll();
}
/** Returns the position of the currently expanded header, or empty if it's not present. */
public OptionalInt getSelectedHeaderPosition() {
if (mWidgetsContentVisiblePackageUserKey == null) return OptionalInt.empty();
return getPositionForPackageUserKey(mWidgetsContentVisiblePackageUserKey);
}
/**
* Returns the position of {@code key} in {@link #mVisibleEntries}, or empty if it's not
* present.

View File

@ -23,6 +23,8 @@ import com.android.launcher3.recyclerview.ViewHolderBinder;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import java.util.List;
/**
* Binds data from {@link WidgetsListHeaderEntry} to UI elements in {@link WidgetsListHeaderHolder}.
*/
@ -50,7 +52,7 @@ public final class WidgetsListHeaderViewHolderBinder implements
@Override
public void bindViewHolder(WidgetsListHeaderHolder viewHolder, WidgetsListHeaderEntry data,
@ListPosition int position) {
@ListPosition int position, List<Object> payloads) {
WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
widgetsListHeader.applyFromItemInfoWithIcon(data);
widgetsListHeader.setExpanded(data.isWidgetListShown());

View File

@ -24,6 +24,8 @@ import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
import java.util.List;
/**
* Binds data from {@link WidgetsListHeaderEntry} to UI elements in {@link WidgetsListHeaderHolder}.
*/
@ -51,7 +53,7 @@ public final class WidgetsListSearchHeaderViewHolderBinder implements
@Override
public void bindViewHolder(WidgetsListSearchHeaderHolder viewHolder,
WidgetsListSearchHeaderEntry data, @ListPosition int position) {
WidgetsListSearchHeaderEntry data, @ListPosition int position, List<Object> payloads) {
WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
widgetsListHeader.applyFromItemInfoWithIcon(data);
widgetsListHeader.setExpanded(data.isWidgetListShown());

View File

@ -20,7 +20,7 @@ import static com.android.launcher3.widget.picker.WidgetsListDrawableState.MIDDL
import android.graphics.Bitmap;
import android.util.Log;
import android.util.Size;
import android.util.Pair;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@ -33,7 +33,6 @@ import android.widget.TableRow;
import com.android.launcher3.R;
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;
@ -53,31 +52,18 @@ public final class WidgetsListTableViewHolderBinder
private final OnClickListener mIconClickListener;
private final OnLongClickListener mIconLongClickListener;
private final WidgetsListDrawableFactory mListDrawableFactory;
private final CachingWidgetPreviewLoader mWidgetPreviewLoader;
private boolean mApplyBitmapDeferred = false;
public WidgetsListTableViewHolderBinder(
LayoutInflater layoutInflater,
OnClickListener iconClickListener,
OnLongClickListener iconLongClickListener,
CachingWidgetPreviewLoader widgetPreviewLoader,
WidgetsListDrawableFactory listDrawableFactory) {
mLayoutInflater = layoutInflater;
mIconClickListener = iconClickListener;
mIconLongClickListener = iconLongClickListener;
mWidgetPreviewLoader = widgetPreviewLoader;
mListDrawableFactory = listDrawableFactory;
}
/**
* Defers applying bitmap on all the {@link WidgetCell} at
* {@link #bindViewHolder(WidgetsRowViewHolder, WidgetsListContentEntry, int)} if
* {@code applyBitmapDeferred} is {@code true}.
*/
public void setApplyBitmapDeferred(boolean applyBitmapDeferred) {
mApplyBitmapDeferred = applyBitmapDeferred;
}
@Override
public WidgetsRowViewHolder newViewHolder(ViewGroup parent) {
if (DEBUG) {
@ -87,25 +73,30 @@ public final class WidgetsListTableViewHolderBinder
WidgetsRowViewHolder viewHolder =
new WidgetsRowViewHolder(mLayoutInflater.inflate(
R.layout.widgets_table_container, parent, false));
viewHolder.mTableContainer.setBackgroundDrawable(
viewHolder.tableContainer.setBackgroundDrawable(
mListDrawableFactory.createContentBackgroundDrawable());
return viewHolder;
}
@Override
public void bindViewHolder(WidgetsRowViewHolder holder, WidgetsListContentEntry entry,
@ListPosition int position) {
WidgetsListTableView table = holder.mTableContainer;
@ListPosition int position, List<Object> payloads) {
for (Object payload : payloads) {
Pair<WidgetItem, Bitmap> pair = (Pair) payload;
holder.previewCache.put(pair.first, pair.second);
}
WidgetsListTableView table = holder.tableContainer;
if (DEBUG) {
Log.d(TAG, String.format("onBindViewHolder [widget#=%d, table.getChildCount=%d]",
entry.mWidgets.size(), table.getChildCount()));
}
table.setListDrawableState(((position & POSITION_LAST) != 0) ? LAST : MIDDLE);
List<ArrayList<WidgetItem>> widgetItemsTable =
WidgetsTableUtils.groupWidgetItemsIntoTable(
entry.mWidgets, entry.getMaxSpanSizeInCells());
recycleTableBeforeBinding(table, widgetItemsTable);
// Bind the widget items.
for (int i = 0; i < widgetItemsTable.size(); i++) {
List<WidgetItem> widgetItemsPerRow = widgetItemsTable.get(i);
@ -115,16 +106,14 @@ public final class WidgetsListTableViewHolderBinder
WidgetCell widget = (WidgetCell) row.getChildAt(j);
widget.clear();
WidgetItem widgetItem = widgetItemsPerRow.get(j);
Size previewSize = widget.setPreviewSize(widgetItem);
widget.applyFromCellItem(widgetItem, mWidgetPreviewLoader);
widget.setApplyBitmapDeferred(mApplyBitmapDeferred);
Bitmap preview = mWidgetPreviewLoader.getPreview(widgetItem, previewSize);
if (preview == null) {
widget.ensurePreview();
} else {
widget.applyPreview(preview);
}
widget.setVisibility(View.VISIBLE);
// When preview loads, notify adapter to rebind the item and possibly animate
widget.applyFromCellItem(widgetItem, 1f,
bitmap -> holder.getBindingAdapter().notifyItemChanged(
holder.getBindingAdapterPosition(),
Pair.create(widgetItem, bitmap)),
holder.previewCache.get(widgetItem));
}
}
}
@ -165,6 +154,7 @@ public final class WidgetsListTableViewHolderBinder
View preview = widget.findViewById(R.id.widget_preview_container);
preview.setOnClickListener(mIconClickListener);
preview.setOnLongClickListener(mIconLongClickListener);
widget.setAnimatePreview(false);
tableRow.addView(widget);
}
}
@ -173,9 +163,10 @@ public final class WidgetsListTableViewHolderBinder
@Override
public void unbindViewHolder(WidgetsRowViewHolder holder) {
int numOfRows = holder.mTableContainer.getChildCount();
int numOfRows = holder.tableContainer.getChildCount();
holder.previewCache.clear();
for (int i = 0; i < numOfRows; i++) {
TableRow tableRow = (TableRow) holder.mTableContainer.getChildAt(i);
TableRow tableRow = (TableRow) holder.tableContainer.getChildAt(i);
int numOfCols = tableRow.getChildCount();
for (int j = 0; j < numOfCols; j++) {
WidgetCell widget = (WidgetCell) tableRow.getChildAt(j);

View File

@ -32,7 +32,6 @@ import androidx.annotation.Nullable;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.widget.WidgetCell;
@ -109,10 +108,7 @@ public final class WidgetsRecommendationTableLayout extends TableLayout {
for (WidgetItem widgetItem : widgetItems) {
WidgetCell widgetCell = addItemCell(tableRow);
widgetCell.setPreviewSize(widgetItem, data.mPreviewScale);
widgetCell.applyFromCellItem(widgetItem,
LauncherAppState.getInstance(getContext()).getWidgetCache());
widgetCell.ensurePreview();
widgetCell.applyFromCellItem(widgetItem, data.mPreviewScale);
}
addView(tableRow);
}

View File

@ -15,20 +15,26 @@
*/
package com.android.launcher3.widget.picker;
import android.graphics.Bitmap;
import android.view.View;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.android.launcher3.R;
import com.android.launcher3.model.WidgetItem;
import java.util.HashMap;
import java.util.Map;
/** A {@link ViewHolder} for showing widgets of an app in the full widget picker. */
public final class WidgetsRowViewHolder extends ViewHolder {
public final WidgetsListTableView mTableContainer;
public final WidgetsListTableView tableContainer;
public final Map<WidgetItem, Bitmap> previewCache = new HashMap<>();
public WidgetsRowViewHolder(View v) {
super(v);
mTableContainer = v.findViewById(R.id.widgets_table);
tableContainer = v.findViewById(R.id.widgets_table);
}
}

View File

@ -27,6 +27,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.android.launcher3.recyclerview.ViewHolderBinder;
import com.android.launcher3.widget.model.WidgetListSpaceEntry;
import java.util.List;
import java.util.function.IntSupplier;
/**
@ -47,7 +48,8 @@ public class WidgetsSpaceViewHolderBinder
}
@Override
public void bindViewHolder(ViewHolder holder, WidgetListSpaceEntry data, int position) {
public void bindViewHolder(ViewHolder holder, WidgetListSpaceEntry data,
@ListPosition int position, List<Object> payloads) {
((EmptySpaceView) holder.itemView).setFixedHeight(mEmptySpaceHeightProvider.getAsInt());
}

View File

@ -150,7 +150,6 @@ public class WidgetsModel {
}
}
app.getWidgetCache().removeObsoletePreviews(widgetsAndShortcuts, packageUser);
return updatedItems;
}