Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit df4241ca authored by Sunny Goyal's avatar Sunny Goyal
Browse files

Fixing Launcher preview leaking surface and memory

> Closing existing preview if a new request comes for same host token
> Closing in-memory icon db when closing preview
> Removing unnecessary wait blocks on UI thread and rendering
  view asynchronously
> Fixing preview loading failing on LauncherAppState access

Bug: 186712316
Bug: 187140897
Test: Manual
Change-Id: I045930b007e5dc015320224a197eee20a8354d17
parent fea72152
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -81,6 +81,8 @@ public class LauncherAppState {

    public LauncherAppState(Context context) {
        this(context, LauncherFiles.APP_ICONS_DB);
        Log.v(Launcher.TAG, "LauncherAppState initiated");
        Preconditions.assertUIThread();

        mInvariantDeviceProfile.addOnChangeListener(idp -> refreshAndReloadLauncher());

@@ -132,8 +134,6 @@ public class LauncherAppState {
    }

    public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
        Log.v(Launcher.TAG, "LauncherAppState initiated");
        Preconditions.assertUIThread();
        mContext = context;

        mInvariantDeviceProfile = InvariantDeviceProfile.INSTANCE.get(context);
@@ -142,6 +142,7 @@ public class LauncherAppState {
                iconCacheFileName, mIconProvider);
        mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache);
        mModel = new LauncherModel(context, this, mIconCache, new AppFilter(mContext));
        mOnTerminateCallback.add(mIconCache::close);
    }

    private void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
+82 −2
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ import static com.android.launcher3.Utilities.getPrefs;
import static com.android.launcher3.util.Themes.KEY_THEMED_ICONS;
import static com.android.launcher3.util.Themes.isThemedIconEnabled;

import android.annotation.TargetApi;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.pm.PackageManager;
@@ -12,14 +13,24 @@ import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Xml;

import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.InvariantDeviceProfile.GridOption;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.util.Executors;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -65,6 +76,11 @@ public class GridCustomizationsProvider extends ContentProvider {
    private static final String ICON_THEMED = "/icon_themed";
    private static final String BOOLEAN_VALUE = "boolean_value";

    private static final String KEY_SURFACE_PACKAGE = "surface_package";
    private static final String KEY_CALLBACK = "callback";

    private final ArrayMap<IBinder, PreviewLifecycleObserver> mActivePreviews = new ArrayMap<>();

    @Override
    public boolean onCreate() {
        return true;
@@ -177,10 +193,74 @@ public class GridCustomizationsProvider extends ContentProvider {
            return null;
        }

        if (!METHOD_GET_PREVIEW.equals(method)) {
        if (!Utilities.ATLEAST_R || !METHOD_GET_PREVIEW.equals(method)) {
            return null;
        }
        return getPreview(extras);
    }

    @TargetApi(Build.VERSION_CODES.R)
    private synchronized Bundle getPreview(Bundle request) {
        PreviewLifecycleObserver observer = null;
        try {
            PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer(getContext(), request);

            // Destroy previous
            destroyObserver(mActivePreviews.get(renderer.getHostToken()));

            observer = new PreviewLifecycleObserver(renderer);
            mActivePreviews.put(renderer.getHostToken(), observer);

            renderer.loadAsync();
            renderer.getHostToken().linkToDeath(observer, 0);

            Bundle result = new Bundle();
            result.putParcelable(KEY_SURFACE_PACKAGE, renderer.getSurfacePackage());

            Messenger messenger = new Messenger(new Handler(Looper.getMainLooper(), observer));
            Message msg = Message.obtain();
            msg.replyTo = messenger;
            result.putParcelable(KEY_CALLBACK, msg);
            return result;
        } catch (Exception e) {
            Log.e(TAG, "Unable to generate preview", e);
            if (observer != null) {
                destroyObserver(observer);
            }
            return null;
        }
    }

        return new PreviewSurfaceRenderer(getContext(), extras).render();
    private synchronized void destroyObserver(PreviewLifecycleObserver observer) {
        if (observer == null || observer.destroyed) {
            return;
        }
        observer.destroyed = true;
        Executors.MAIN_EXECUTOR.execute(observer.renderer::destroy);
        PreviewLifecycleObserver cached = mActivePreviews.get(observer.renderer.getHostToken());
        if (cached == observer) {
            mActivePreviews.remove(observer.renderer.getHostToken());
        }
    }

    private class PreviewLifecycleObserver implements Handler.Callback, DeathRecipient {

        public final PreviewSurfaceRenderer renderer;
        public boolean destroyed = false;

        PreviewLifecycleObserver(PreviewSurfaceRenderer renderer) {
            this.renderer = renderer;
        }

        @Override
        public boolean handleMessage(Message message) {
            destroyObserver(this);
            return true;
        }

        @Override
        public void binderDied() {
            destroyObserver(this);
        }
    }
}
+28 −172
Original line number Diff line number Diff line
@@ -23,7 +23,6 @@ import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
import static com.android.launcher3.model.ModelUtils.getMissingHotseatRanks;
import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;

import android.annotation.TargetApi;
import android.app.Fragment;
@@ -32,7 +31,6 @@ import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
@@ -43,7 +41,6 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
@@ -57,8 +54,6 @@ import com.android.launcher3.Hotseat;
import com.android.launcher3.InsettableFrameLayout;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.R;
import com.android.launcher3.WorkspaceLayoutManager;
@@ -67,13 +62,8 @@ import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.icons.BaseIconFactory;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.AllAppsList;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.LoaderResults;
import com.android.launcher3.model.LoaderTask;
import com.android.launcher3.model.ModelDelegate;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.WidgetsModel;
import com.android.launcher3.model.data.FolderInfo;
@@ -100,13 +90,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Utility class for generating the preview of Launcher for a given InvariantDeviceProfile.
@@ -120,8 +104,6 @@ import java.util.concurrent.TimeoutException;
public class LauncherPreviewRenderer extends ContextWrapper
        implements ActivityContext, WorkspaceLayoutManager, LayoutInflater.Factory2 {

    private static final String TAG = "LauncherPreviewRenderer";

    /**
     * Context used just for preview. It also provides a few objects (e.g. UserCache) just for
     * preview purposes.
@@ -138,9 +120,15 @@ public class LauncherPreviewRenderer extends ContextWrapper
        private final ConcurrentLinkedQueue<LauncherIconsForPreview> mIconPool =
                new ConcurrentLinkedQueue<>();

        private boolean mDestroyed = false;

        public PreviewContext(Context base, InvariantDeviceProfile idp) {
            super(base);
            mIdp = idp;
            mObjectMap.put(InvariantDeviceProfile.INSTANCE, idp);
            mObjectMap.put(LauncherAppState.INSTANCE,
                    new LauncherAppState(this, null /* iconCacheFileName */));

        }

        @Override
@@ -149,11 +137,9 @@ public class LauncherPreviewRenderer extends ContextWrapper
        }

        public void onDestroy() {
            CustomWidgetManager customWidgetManager = (CustomWidgetManager) mObjectMap.get(
                    CustomWidgetManager.INSTANCE);
            if (customWidgetManager != null) {
                customWidgetManager.onDestroy();
            }
            CustomWidgetManager.INSTANCE.get(this).onDestroy();
            LauncherAppState.INSTANCE.get(this).onTerminate();
            mDestroyed = true;
        }

        /**
@@ -162,17 +148,12 @@ public class LauncherPreviewRenderer extends ContextWrapper
         */
        public <T> T getObject(MainThreadInitializedObject<T> mainThreadInitializedObject,
                MainThreadInitializedObject.ObjectProvider<T> provider) {
            if (FeatureFlags.IS_STUDIO_BUILD && mDestroyed) {
                throw new RuntimeException("Context already destroyed");
            }
            if (!mAllowedObjects.contains(mainThreadInitializedObject)) {
                throw new IllegalStateException("Leaking unknown objects");
            }
            if (mainThreadInitializedObject == LauncherAppState.INSTANCE) {
                throw new IllegalStateException(
                        "Should not use MainThreadInitializedObject to initialize this with "
                                + "PreviewContext");
            }
            if (mainThreadInitializedObject == InvariantDeviceProfile.INSTANCE) {
                return (T) mIdp;
            }
            if (mObjectMap.containsKey(mainThreadInitializedObject)) {
                return (T) mObjectMap.get(mainThreadInitializedObject);
            }
@@ -210,7 +191,6 @@ public class LauncherPreviewRenderer extends ContextWrapper
    private final Context mContext;
    private final InvariantDeviceProfile mIdp;
    private final DeviceProfile mDp;
    private final boolean mMigrated;
    private final Rect mInsets;
    private final WorkspaceItemInfo mWorkspaceItemInfo;
    private final LayoutInflater mHomeElementInflater;
@@ -218,13 +198,12 @@ public class LauncherPreviewRenderer extends ContextWrapper
    private final Hotseat mHotseat;
    private final CellLayout mWorkspace;

    public LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp, boolean migrated) {
    public LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp) {
        super(context);
        mUiHandler = new Handler(Looper.getMainLooper());
        mContext = context;
        mIdp = idp;
        mDp = idp.getDeviceProfile(context).copy(context);
        mMigrated = migrated;

        // TODO: get correct insets once display cutout API is available.
        mInsets = new Rect();
@@ -265,8 +244,9 @@ public class LauncherPreviewRenderer extends ContextWrapper
    }

    /** Populate preview and render it. */
    public View getRenderedView() {
        populate();
    public View getRenderedView(BgDataModel dataModel,
            Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
        populate(dataModel, widgetProviderInfoMap);
        return mRootView;
    }

@@ -392,38 +372,17 @@ public class LauncherPreviewRenderer extends ContextWrapper
        }
    }

    private void populate() {
        WorkspaceFetcher fetcher;
        PreviewContext previewContext = null;
        if (mMigrated) {
            previewContext = new PreviewContext(mContext, mIdp);
            LauncherAppState appForPreview = new LauncherAppState(
                    previewContext, null /* iconCacheFileName */);
            fetcher = new WorkspaceItemsInfoFromPreviewFetcher(appForPreview);
            MODEL_EXECUTOR.execute(fetcher);
        } else {
            fetcher = new WorkspaceItemsInfoFetcher();
            LauncherAppState.getInstance(mContext).getModel().enqueueModelUpdateTask(
                    (LauncherModel.ModelUpdateTask) fetcher);
        }
        WorkspaceResult workspaceResult = fetcher.get();
        if (previewContext != null) {
            previewContext.onDestroy();
        }

        if (workspaceResult == null) {
            return;
        }

    private void populate(BgDataModel dataModel,
            Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
        // Separate the items that are on the current screen, and the other remaining items.
        ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
        ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
        ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
        ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
        filterCurrentWorkspaceItems(0 /* currentScreenId */,
                workspaceResult.mWorkspaceItems, currentWorkspaceItems,
                dataModel.workspaceItems, currentWorkspaceItems,
                otherWorkspaceItems);
        filterCurrentWorkspaceItems(0 /* currentScreenId */, workspaceResult.mAppWidgets,
        filterCurrentWorkspaceItems(0 /* currentScreenId */, dataModel.appWidgets,
                currentAppWidgets, otherAppWidgets);
        sortWorkspaceItemsSpatially(mIdp, currentWorkspaceItems);
        for (ItemInfo itemInfo : currentWorkspaceItems) {
@@ -444,12 +403,12 @@ public class LauncherPreviewRenderer extends ContextWrapper
            switch (itemInfo.itemType) {
                case Favorites.ITEM_TYPE_APPWIDGET:
                case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
                    if (mMigrated) {
                        inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo,
                                workspaceResult.mWidgetProvidersMap);
                    if (widgetProviderInfoMap != null) {
                        inflateAndAddWidgets(
                                (LauncherAppWidgetInfo) itemInfo, widgetProviderInfoMap);
                    } else {
                        inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo,
                                workspaceResult.mWidgetsModel);
                                dataModel.widgetsModel);
                    }
                    break;
                default:
@@ -458,8 +417,10 @@ public class LauncherPreviewRenderer extends ContextWrapper
        }
        IntArray ranks = getMissingHotseatRanks(currentWorkspaceItems,
                mDp.numShownHotseatIcons);
        List<ItemInfo> predictions = workspaceResult.mHotseatPredictions == null
                ? Collections.emptyList() : workspaceResult.mHotseatPredictions.items;
        FixedContainerItems hotseatpredictions =
                dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION);
        List<ItemInfo> predictions = hotseatpredictions == null
                ? Collections.emptyList() : hotseatpredictions.items;
        int count = Math.min(ranks.size(), predictions.size());
        for (int i = 0; i < count; i++) {
            int rank = ranks.get(i);
@@ -494,109 +455,4 @@ public class LauncherPreviewRenderer extends ContextWrapper
        view.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
        view.layout(0, 0, width, height);
    }

    private static class WorkspaceItemsInfoFetcher implements LauncherModel.ModelUpdateTask,
            WorkspaceFetcher {

        private final FutureTask<WorkspaceResult> mTask = new FutureTask<>(this);

        private LauncherAppState mApp;
        private LauncherModel mModel;
        private BgDataModel mBgDataModel;
        private AllAppsList mAllAppsList;

        @Override
        public void init(LauncherAppState app, LauncherModel model, BgDataModel dataModel,
                AllAppsList allAppsList, Executor uiExecutor) {
            mApp = app;
            mModel = model;
            mBgDataModel = dataModel;
            mAllAppsList = allAppsList;
        }

        @Override
        public FutureTask<WorkspaceResult> getTask() {
            return mTask;
        }

        @Override
        public void run() {
            mTask.run();
        }

        @Override
        public WorkspaceResult call() throws Exception {
            if (!mModel.isModelLoaded()) {
                Log.d(TAG, "Workspace not loaded, loading now");
                mModel.startLoaderForResults(
                        new LoaderResults(mApp, mBgDataModel, mAllAppsList, new Callbacks[0]));
                return null;
            }

            return new WorkspaceResult(mBgDataModel, mBgDataModel.widgetsModel, null);
        }
    }

    private static class WorkspaceItemsInfoFromPreviewFetcher extends LoaderTask implements
            WorkspaceFetcher {

        private final FutureTask<WorkspaceResult> mTask = new FutureTask<>(this);

        WorkspaceItemsInfoFromPreviewFetcher(LauncherAppState app) {
            super(app, null, new BgDataModel(), new ModelDelegate(), null);
        }

        @Override
        public FutureTask<WorkspaceResult> getTask() {
            return mTask;
        }

        @Override
        public void run() {
            mTask.run();
        }

        @Override
        public WorkspaceResult call() {
            List<ShortcutInfo> allShortcuts = new ArrayList<>();
            loadWorkspace(allShortcuts, LauncherSettings.Favorites.PREVIEW_CONTENT_URI,
                    LauncherSettings.Favorites.SCREEN + " = 0 or "
                            + LauncherSettings.Favorites.CONTAINER + " = "
                            + LauncherSettings.Favorites.CONTAINER_HOTSEAT);
            return new WorkspaceResult(mBgDataModel, null, mWidgetProvidersMap);
        }
    }

    private interface WorkspaceFetcher extends Runnable, Callable<WorkspaceResult> {
        FutureTask<WorkspaceResult> getTask();

        default WorkspaceResult get() {
            try {
                return getTask().get(5, TimeUnit.SECONDS);
            } catch (InterruptedException | ExecutionException | TimeoutException e) {
                Log.d(TAG, "Error fetching workspace items info", e);
                return null;
            }
        }
    }

    private static class WorkspaceResult {
        private final ArrayList<ItemInfo> mWorkspaceItems;
        private final ArrayList<LauncherAppWidgetInfo> mAppWidgets;
        private final FixedContainerItems mHotseatPredictions;
        private final WidgetsModel mWidgetsModel;
        private final Map<ComponentKey, AppWidgetProviderInfo> mWidgetProvidersMap;

        private WorkspaceResult(BgDataModel dataModel,
                WidgetsModel widgetsModel,
                Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
            synchronized (dataModel) {
                mWorkspaceItems = dataModel.workspaceItems;
                mAppWidgets = dataModel.appWidgets;
                mHotseatPredictions = dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION);
                mWidgetsModel = widgetsModel;
                mWidgetProvidersMap = widgetProviderInfoMap;
            }
        }
    }
}
+125 −93

File changed.

Preview size limit exceeded, changes collapsed.

+7 −0
Original line number Diff line number Diff line
@@ -130,6 +130,13 @@ public class IconCache extends BaseIconCache {
        }
    }

    /**
     * Closes the cache DB. This will clear any in-memory cache.
     */
    public void close() {
        mIconDb.close();
    }

    /**
     * Fetches high-res icon for the provided ItemInfo and updates the caller when done.
     *
Loading