Files
Lawnchair/src/com/android/launcher3/widget/CachingWidgetPreviewLoader.java
T
Stevie Kideckel a093523299 Load widget preview images before adding the rows to the adapter
This allows for smooth animations as the items will have their full
height resolved

Bug: 190892517
Test: verified locally
Test: CachingWidgetPreviewLoaderTest
Change-Id: I86afc6951a4cc82ae45e63795791d259a3bb3cda
2021-06-18 19:18:29 +00:00

290 lines
12 KiB
Java

/*
* 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);
}
}