e0420ab2bc
This could happen when the QuickstepWidgetHolder is not initialized on the main thread, or destroy() is not called on the main thread, while a random widget is removed at the same time which iterates the widget holder list and calls the callbacks. Also fixing an issue with memory leak that is caused by the strong reference in WeakHashMap Test: Presubmit Fix: 271362384 Change-Id: I621ffb29c167cef9842758c5fdefd6bb66dd487e
364 lines
13 KiB
Java
364 lines
13 KiB
Java
/**
|
|
* Copyright (C) 2022 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.uioverrides;
|
|
|
|
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
|
|
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
|
|
|
|
import android.appwidget.AppWidgetHost;
|
|
import android.appwidget.AppWidgetHostView;
|
|
import android.appwidget.AppWidgetProviderInfo;
|
|
import android.content.Context;
|
|
import android.util.Log;
|
|
import android.util.SparseArray;
|
|
import android.widget.RemoteViews;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.UiThread;
|
|
import androidx.annotation.WorkerThread;
|
|
|
|
import com.android.launcher3.config.FeatureFlags;
|
|
import com.android.launcher3.model.WidgetsModel;
|
|
import com.android.launcher3.util.IntSet;
|
|
import com.android.launcher3.widget.LauncherAppWidgetHostView;
|
|
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
|
|
import com.android.launcher3.widget.LauncherWidgetHolder;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.WeakHashMap;
|
|
import java.util.function.BiConsumer;
|
|
import java.util.function.IntConsumer;
|
|
|
|
/**
|
|
* {@link LauncherWidgetHolder} that puts the app widget host in the background
|
|
*/
|
|
public final class QuickstepWidgetHolder extends LauncherWidgetHolder {
|
|
|
|
private static final String TAG = "QuickstepWidgetHolder";
|
|
|
|
private static final UpdateKey<AppWidgetProviderInfo> KEY_PROVIDER_UPDATE =
|
|
AppWidgetHostView::onUpdateProviderInfo;
|
|
private static final UpdateKey<RemoteViews> KEY_VIEWS_UPDATE =
|
|
AppWidgetHostView::updateAppWidget;
|
|
private static final UpdateKey<Integer> KEY_VIEW_DATA_CHANGED =
|
|
AppWidgetHostView::onViewDataChanged;
|
|
|
|
private static final List<QuickstepWidgetHolder> sHolders = new ArrayList<>();
|
|
private static final SparseArray<QuickstepWidgetHolderListener> sListeners =
|
|
new SparseArray<>();
|
|
|
|
private static AppWidgetHost sWidgetHost = null;
|
|
|
|
private final SparseArray<AppWidgetHostView> mViews = new SparseArray<>();
|
|
|
|
private final @Nullable RemoteViews.InteractionHandler mInteractionHandler;
|
|
|
|
private final @NonNull IntConsumer mAppWidgetRemovedCallback;
|
|
|
|
private final ArrayList<ProviderChangedListener> mProviderChangedListeners = new ArrayList<>();
|
|
// Map to all pending updated keyed with appWidgetId;
|
|
private final SparseArray<PendingUpdate> mPendingUpdateMap = new SparseArray<>();
|
|
|
|
private QuickstepWidgetHolder(@NonNull Context context,
|
|
@Nullable IntConsumer appWidgetRemovedCallback,
|
|
@Nullable RemoteViews.InteractionHandler interactionHandler) {
|
|
super(context, appWidgetRemovedCallback);
|
|
mAppWidgetRemovedCallback = appWidgetRemovedCallback != null ? appWidgetRemovedCallback
|
|
: i -> {};
|
|
mInteractionHandler = interactionHandler;
|
|
MAIN_EXECUTOR.execute(() -> sHolders.add(this));
|
|
}
|
|
|
|
@Override
|
|
@NonNull
|
|
protected AppWidgetHost createHost(@NonNull Context context,
|
|
@Nullable IntConsumer appWidgetRemovedCallback) {
|
|
if (sWidgetHost == null) {
|
|
sWidgetHost = new QuickstepAppWidgetHost(context.getApplicationContext(),
|
|
i -> MAIN_EXECUTOR.execute(() ->
|
|
sHolders.forEach(h -> h.mAppWidgetRemovedCallback.accept(i))),
|
|
() -> MAIN_EXECUTOR.execute(() ->
|
|
sHolders.forEach(h -> h.mProviderChangedListeners.forEach(
|
|
ProviderChangedListener::notifyWidgetProvidersChanged))),
|
|
UI_HELPER_EXECUTOR.getLooper());
|
|
if (!WidgetsModel.GO_DISABLE_WIDGETS) {
|
|
sWidgetHost.startListening();
|
|
}
|
|
}
|
|
return sWidgetHost;
|
|
}
|
|
|
|
@Override
|
|
protected void updateDeferredView() {
|
|
super.updateDeferredView();
|
|
int count = mPendingUpdateMap.size();
|
|
for (int i = 0; i < count; i++) {
|
|
int widgetId = mPendingUpdateMap.keyAt(i);
|
|
AppWidgetHostView view = mViews.get(widgetId);
|
|
if (view == null) {
|
|
continue;
|
|
}
|
|
PendingUpdate pendingUpdate = mPendingUpdateMap.valueAt(i);
|
|
if (pendingUpdate == null) {
|
|
continue;
|
|
}
|
|
if (pendingUpdate.providerInfo != null) {
|
|
KEY_PROVIDER_UPDATE.accept(view, pendingUpdate.providerInfo);
|
|
}
|
|
if (pendingUpdate.remoteViews != null) {
|
|
KEY_VIEWS_UPDATE.accept(view, pendingUpdate.remoteViews);
|
|
}
|
|
pendingUpdate.changedViews.forEach(
|
|
viewId -> KEY_VIEW_DATA_CHANGED.accept(view, viewId));
|
|
}
|
|
mPendingUpdateMap.clear();
|
|
}
|
|
|
|
private <T> void onWidgetUpdate(int widgetId, UpdateKey<T> key, T data) {
|
|
if (isListening()) {
|
|
AppWidgetHostView view = mViews.get(widgetId);
|
|
if (view == null) {
|
|
return;
|
|
}
|
|
key.accept(view, data);
|
|
return;
|
|
}
|
|
|
|
PendingUpdate pendingUpdate = mPendingUpdateMap.get(widgetId);
|
|
if (pendingUpdate == null) {
|
|
pendingUpdate = new PendingUpdate();
|
|
mPendingUpdateMap.put(widgetId, pendingUpdate);
|
|
}
|
|
|
|
if (KEY_PROVIDER_UPDATE.equals(key)) {
|
|
// For provider change, remove all updates
|
|
pendingUpdate.providerInfo = (AppWidgetProviderInfo) data;
|
|
pendingUpdate.remoteViews = null;
|
|
pendingUpdate.changedViews.clear();
|
|
} else if (KEY_VIEWS_UPDATE.equals(key)) {
|
|
// For views update, remove all previous updates, except the provider
|
|
pendingUpdate.remoteViews = (RemoteViews) data;
|
|
pendingUpdate.changedViews.clear();
|
|
} else if (KEY_VIEW_DATA_CHANGED.equals(key)) {
|
|
pendingUpdate.changedViews.add((Integer) data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete the specified app widget from the host
|
|
* @param appWidgetId The ID of the app widget to be deleted
|
|
*/
|
|
@Override
|
|
public void deleteAppWidgetId(int appWidgetId) {
|
|
super.deleteAppWidgetId(appWidgetId);
|
|
sListeners.remove(appWidgetId);
|
|
}
|
|
|
|
/**
|
|
* Called when the launcher is destroyed
|
|
*/
|
|
@Override
|
|
public void destroy() {
|
|
try {
|
|
MAIN_EXECUTOR.submit(() -> sHolders.remove(this)).get();
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Failed to remove self from holder list", e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected boolean shouldListen(int flags) {
|
|
return (flags & (FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED))
|
|
== (FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED);
|
|
}
|
|
|
|
/**
|
|
* Add a listener that is triggered when the providers of the widgets are changed
|
|
* @param listener The listener that notifies when the providers changed
|
|
*/
|
|
@Override
|
|
public void addProviderChangeListener(
|
|
@NonNull LauncherWidgetHolder.ProviderChangedListener listener) {
|
|
mProviderChangedListeners.add(listener);
|
|
}
|
|
|
|
/**
|
|
* Remove the specified listener from the host
|
|
* @param listener The listener that is to be removed from the host
|
|
*/
|
|
@Override
|
|
public void removeProviderChangeListener(
|
|
LauncherWidgetHolder.ProviderChangedListener listener) {
|
|
mProviderChangedListeners.remove(listener);
|
|
}
|
|
|
|
/**
|
|
* Stop the host from updating the widget views
|
|
*/
|
|
@Override
|
|
public void stopListening() {
|
|
if (WidgetsModel.GO_DISABLE_WIDGETS) {
|
|
return;
|
|
}
|
|
|
|
sWidgetHost.setAppWidgetHidden();
|
|
setListeningFlag(false);
|
|
}
|
|
|
|
/**
|
|
* Create a view for the specified app widget
|
|
* @param context The activity context for which the view is created
|
|
* @param appWidgetId The ID of the widget
|
|
* @param appWidget The {@link LauncherAppWidgetProviderInfo} of the widget
|
|
* @return A view for the widget
|
|
*/
|
|
@NonNull
|
|
@Override
|
|
public LauncherAppWidgetHostView createView(@NonNull Context context, int appWidgetId,
|
|
@NonNull LauncherAppWidgetProviderInfo appWidget) {
|
|
LauncherAppWidgetHostView widgetView = getPendingView(appWidgetId);
|
|
if (widgetView != null) {
|
|
removePendingView(appWidgetId);
|
|
} else {
|
|
widgetView = new LauncherAppWidgetHostView(context);
|
|
}
|
|
widgetView.setInteractionHandler(mInteractionHandler);
|
|
widgetView.setAppWidget(appWidgetId, appWidget);
|
|
mViews.put(appWidgetId, widgetView);
|
|
|
|
QuickstepWidgetHolderListener listener = sListeners.get(appWidgetId);
|
|
if (listener == null) {
|
|
listener = new QuickstepWidgetHolderListener(appWidgetId);
|
|
sWidgetHost.setListener(appWidgetId, listener);
|
|
sListeners.put(appWidgetId, listener);
|
|
}
|
|
RemoteViews remoteViews = listener.addHolder(this);
|
|
widgetView.updateAppWidget(remoteViews);
|
|
|
|
return widgetView;
|
|
}
|
|
|
|
/**
|
|
* Clears all the views from the host
|
|
*/
|
|
@Override
|
|
public void clearViews() {
|
|
for (int i = sListeners.size() - 1; i >= 0; i--) {
|
|
sListeners.valueAt(i).mListeningHolders.remove(this);
|
|
}
|
|
}
|
|
|
|
private static class QuickstepWidgetHolderListener
|
|
implements AppWidgetHost.AppWidgetHostListener {
|
|
|
|
// Static listeners should use a set that is backed by WeakHashMap to avoid memory leak
|
|
private final Set<QuickstepWidgetHolder> mListeningHolders = Collections.newSetFromMap(
|
|
new WeakHashMap<>());
|
|
|
|
private final int mWidgetId;
|
|
|
|
private @Nullable RemoteViews mRemoteViews;
|
|
|
|
QuickstepWidgetHolderListener(int widgetId) {
|
|
mWidgetId = widgetId;
|
|
}
|
|
|
|
@UiThread
|
|
@Nullable
|
|
public RemoteViews addHolder(@NonNull QuickstepWidgetHolder holder) {
|
|
mListeningHolders.add(holder);
|
|
return mRemoteViews;
|
|
}
|
|
|
|
@Override
|
|
@WorkerThread
|
|
public void onUpdateProviderInfo(@Nullable AppWidgetProviderInfo info) {
|
|
mRemoteViews = null;
|
|
executeOnMainExecutor(KEY_PROVIDER_UPDATE, info);
|
|
}
|
|
|
|
@Override
|
|
@WorkerThread
|
|
public void updateAppWidget(@Nullable RemoteViews views) {
|
|
mRemoteViews = views;
|
|
executeOnMainExecutor(KEY_VIEWS_UPDATE, mRemoteViews);
|
|
}
|
|
|
|
@Override
|
|
@WorkerThread
|
|
public void onViewDataChanged(int viewId) {
|
|
executeOnMainExecutor(KEY_VIEW_DATA_CHANGED, viewId);
|
|
}
|
|
|
|
private <T> void executeOnMainExecutor(UpdateKey<T> key, T data) {
|
|
MAIN_EXECUTOR.execute(() -> mListeningHolders.forEach(holder ->
|
|
holder.onWidgetUpdate(mWidgetId, key, data)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@code HolderFactory} subclass that takes an interaction handler as one of the parameters
|
|
* when creating a new instance.
|
|
*/
|
|
public static class QuickstepHolderFactory extends HolderFactory {
|
|
|
|
@SuppressWarnings("unused")
|
|
public QuickstepHolderFactory(Context context) { }
|
|
|
|
@Override
|
|
public LauncherWidgetHolder newInstance(@NonNull Context context,
|
|
@Nullable IntConsumer appWidgetRemovedCallback) {
|
|
return newInstance(context, appWidgetRemovedCallback, null);
|
|
}
|
|
|
|
/**
|
|
* @param context The context of the caller
|
|
* @param appWidgetRemovedCallback The callback that is called when widgets are removed
|
|
* @param interactionHandler The interaction handler when the widgets are clicked
|
|
* @return A new {@link LauncherWidgetHolder} instance
|
|
*/
|
|
public LauncherWidgetHolder newInstance(@NonNull Context context,
|
|
@Nullable IntConsumer appWidgetRemovedCallback,
|
|
@Nullable RemoteViews.InteractionHandler interactionHandler) {
|
|
|
|
if (!FeatureFlags.ENABLE_WIDGET_HOST_IN_BACKGROUND.get()) {
|
|
return new LauncherWidgetHolder(context, appWidgetRemovedCallback) {
|
|
@Override
|
|
protected AppWidgetHost createHost(Context context,
|
|
@Nullable IntConsumer appWidgetRemovedCallback) {
|
|
AppWidgetHost host = super.createHost(context, appWidgetRemovedCallback);
|
|
host.setInteractionHandler(interactionHandler);
|
|
return host;
|
|
}
|
|
};
|
|
}
|
|
return new QuickstepWidgetHolder(context, appWidgetRemovedCallback, interactionHandler);
|
|
}
|
|
}
|
|
|
|
private static class PendingUpdate {
|
|
public final IntSet changedViews = new IntSet();
|
|
public AppWidgetProviderInfo providerInfo;
|
|
public RemoteViews remoteViews;
|
|
}
|
|
|
|
private interface UpdateKey<T> extends BiConsumer<AppWidgetHostView, T> { }
|
|
}
|