Moving slice loading on a background thread

> Also fixing slice icon loading, such that the request is cancelled
  if views are recycled quickly
> Loading widget label on dg thread and cancelling request if views
  are recycled quickly.

Bug: 179068415
Test: verified locally
Change-Id: Id5a524e2bf596862330a8170394aef9ffd708544
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 5007ffc..2367249 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -66,9 +66,9 @@
 import com.android.launcher3.graphics.PlaceHolderIconDrawable;
 import com.android.launcher3.graphics.PreloadIconDrawable;
 import com.android.launcher3.icons.DotRenderer;
-import com.android.launcher3.icons.IconCache.IconLoadRequest;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -171,7 +171,7 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mDisableRelayout = false;
 
-    private IconLoadRequest mIconLoadRequest;
+    private HandlerRunnable mIconLoadRequest;
 
     private boolean mEnableIconUpdateAnimation = false;
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 2334267..36963f1 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -98,13 +98,9 @@
 import android.widget.Toast;
 
 import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
 import androidx.annotation.VisibleForTesting;
-import androidx.lifecycle.Lifecycle;
-import androidx.lifecycle.LifecycleOwner;
-import androidx.lifecycle.LifecycleRegistry;
 
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
@@ -211,8 +207,7 @@
  * Default launcher application.
  */
 public class Launcher extends StatefulActivity<LauncherState> implements LauncherExterns,
-        Callbacks, InvariantDeviceProfile.OnIDPChangeListener, PluginListener<OverlayPlugin>,
-        LifecycleOwner {
+        Callbacks, InvariantDeviceProfile.OnIDPChangeListener, PluginListener<OverlayPlugin> {
     public static final String TAG = "Launcher";
 
     public static final ActivityTracker<Launcher> ACTIVITY_TRACKER = new ActivityTracker<>();
@@ -275,8 +270,6 @@
     private LauncherAppTransitionManager mAppTransitionManager;
     private Configuration mOldConfig;
 
-    private LifecycleRegistry mLifecycleRegistry;
-
     private LiveSearchManager mLiveSearchManager;
 
     @Thunk
@@ -392,12 +385,12 @@
         mIconCache = app.getIconCache();
         mAccessibilityDelegate = new LauncherAccessibilityDelegate(this);
 
-        mLiveSearchManager = new LiveSearchManager(this);
-
         mDragController = new DragController(this);
         mAllAppsController = new AllAppsTransitionController(this);
         mStateManager = new StateManager<>(this, NORMAL);
 
+        mLiveSearchManager = new LiveSearchManager(this);
+
         mOnboardingPrefs = createOnboardingPrefs(mSharedPrefs);
 
         mAppWidgetManager = new WidgetManagerHelper(this);
@@ -486,15 +479,6 @@
         if (Utilities.ATLEAST_R) {
             getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
         }
-
-        mLifecycleRegistry = new LifecycleRegistry(this);
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
-    }
-
-    @NonNull
-    @Override
-    public Lifecycle getLifecycle() {
-        return mLifecycleRegistry;
     }
 
     public LiveSearchManager getLiveSearchManager() {
@@ -913,7 +897,6 @@
 
     @Override
     protected void onStop() {
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
         super.onStop();
         if (mDeferOverlayCallbacks) {
             checkIfOverlayStillDeferred();
@@ -937,7 +920,6 @@
 
         mAppWidgetHost.setListenIfResumed(true);
         TraceHelper.INSTANCE.endSection(traceToken);
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
     }
 
     @Override
@@ -1091,7 +1073,6 @@
         }
 
         TraceHelper.INSTANCE.endSection(traceToken);
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED);
     }
 
     @Override
@@ -1099,7 +1080,6 @@
         // Ensure that items added to Launcher are queued until Launcher returns
         ItemInstallQueue.INSTANCE.get(this).pauseModelPush(FLAG_ACTIVITY_PAUSED);
 
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
         super.onPause();
         mDragController.cancelDrag();
         mLastTouchUpTime = -1;
@@ -1598,7 +1578,6 @@
         mAppTransitionManager.unregisterRemoteAnimations();
         mAppTransitionManager.unregisterRemoteTransitions();
         mUserChangedCallbackCloseable.close();
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
         mLiveSearchManager.stop();
     }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 2f805fd..a4e1f27 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -268,13 +268,10 @@
                 && !FeatureFlags.DISABLE_INITIAL_IME_IN_ALLAPPS.get() && BuildCompat.isAtLeastR()) {
             mInsetController.onAnimationEnd(mProgress);
             if (Float.compare(mProgress, 0f) == 0) {
-                mLauncher.getLiveSearchManager().start();
                 EditText editText = mAppsView.getSearchUiManager().getEditText();
                 if (editText != null && !mInsetController.showSearchEduIfNecessary()) {
                     editText.requestFocus();
                 }
-            } else {
-                mLauncher.getLiveSearchManager().stop();
             }
             // TODO: should make the controller hide synchronously
         }
diff --git a/src/com/android/launcher3/allapps/search/LiveSearchManager.java b/src/com/android/launcher3/allapps/search/LiveSearchManager.java
index c2f0b96..71aedb8 100644
--- a/src/com/android/launcher3/allapps/search/LiveSearchManager.java
+++ b/src/com/android/launcher3/allapps/search/LiveSearchManager.java
@@ -15,8 +15,14 @@
  */
 package com.android.launcher3.allapps.search;
 
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.widget.WidgetHostViewLoader.getDefaultOptionsForWidget;
 
+import android.app.Activity;
+import android.app.Application.ActivityLifecycleCallbacks;
 import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetManager;
@@ -26,39 +32,49 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.UserHandle;
+import android.util.Log;
 
-import androidx.lifecycle.LiveData;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+import androidx.lifecycle.Observer;
 import androidx.slice.Slice;
-import androidx.slice.widget.SliceLiveData;
+import androidx.slice.SliceViewManager;
+import androidx.slice.SliceViewManager.SliceCallback;
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.LauncherState;
 import com.android.launcher3.logging.InstanceId;
-import com.android.launcher3.logging.InstanceIdSequence;
+import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Optional;
 
 /**
  * Manages Lifecycle for Live search results
  */
-public class LiveSearchManager {
+public class LiveSearchManager implements StateListener<LauncherState> {
+
+    private static final String TAG = "LiveSearchManager";
 
     public static final int SEARCH_APPWIDGET_HOST_ID = 2048;
 
     private final Launcher mLauncher;
-    private final AppWidgetManager mAppWidgetManger;
+    private final HashMap<Uri, SliceLifeCycle> mUriSliceMap = new HashMap<>();
+
     private final HashMap<ComponentKey, SearchWidgetInfoContainer> mWidgetPlaceholders =
             new HashMap<>();
-    private final HashMap<Uri, LiveData<Slice>> mUriSliceMap = new HashMap<>();
     private SearchWidgetHost mSearchWidgetHost;
     private InstanceId mLogInstanceId;
 
     public LiveSearchManager(Launcher launcher) {
         mLauncher = launcher;
-        mAppWidgetManger = AppWidgetManager.getInstance(launcher);
+        mLauncher.getStateManager().addStateListener(this);
     }
 
     /**
@@ -67,78 +83,80 @@
      */
     public SearchWidgetInfoContainer getPlaceHolderWidget(AppWidgetProviderInfo providerInfo) {
         if (mSearchWidgetHost == null) {
-            throw new RuntimeException("AppWidgetHost has not been created yet");
+            mSearchWidgetHost = new SearchWidgetHost(mLauncher);
+            mSearchWidgetHost.startListening();
         }
 
         ComponentName provider = providerInfo.provider;
         UserHandle userHandle = providerInfo.getProfile();
 
         ComponentKey key = new ComponentKey(provider, userHandle);
-        SearchWidgetInfoContainer view = mWidgetPlaceholders.getOrDefault(key, null);
         if (mWidgetPlaceholders.containsKey(key)) {
             return mWidgetPlaceholders.get(key);
         }
+
         LauncherAppWidgetProviderInfo pinfo = LauncherAppWidgetProviderInfo.fromProviderInfo(
                 mLauncher, providerInfo);
         PendingAddWidgetInfo pendingAddWidgetInfo = new PendingAddWidgetInfo(pinfo);
 
         Bundle options = getDefaultOptionsForWidget(mLauncher, pendingAddWidgetInfo);
         int appWidgetId = mSearchWidgetHost.allocateAppWidgetId();
-        boolean success = mAppWidgetManger.bindAppWidgetIdIfAllowed(appWidgetId, userHandle,
-                provider, options);
+        boolean success = AppWidgetManager.getInstance(mLauncher)
+                .bindAppWidgetIdIfAllowed(appWidgetId, userHandle, provider, options);
         if (!success) {
+            mSearchWidgetHost.deleteAppWidgetId(appWidgetId);
             mWidgetPlaceholders.put(key, null);
             return null;
         }
 
-        view = (SearchWidgetInfoContainer) mSearchWidgetHost.createView(mLauncher, appWidgetId,
-                providerInfo);
+        SearchWidgetInfoContainer view = (SearchWidgetInfoContainer) mSearchWidgetHost.createView(
+                mLauncher, appWidgetId, providerInfo);
         view.setTag(pendingAddWidgetInfo);
         mWidgetPlaceholders.put(key, view);
         return view;
     }
 
     /**
-     * Creates {@link LiveData<Slice>} from Slice Uri. Caches created live data to be reused
-     * within the same search session. Removes previous observers when new SliceView request a
-     * live data for observation.
-     */
-    public LiveData<Slice> getSliceForUri(Uri sliceUri) {
-        LiveData<Slice> sliceLiveData = mUriSliceMap.getOrDefault(sliceUri, null);
-        if (sliceLiveData == null) {
-            sliceLiveData = SliceLiveData.fromUri(mLauncher, sliceUri);
-            mUriSliceMap.put(sliceUri, sliceLiveData);
-        }
-        return sliceLiveData;
-    }
-
-    /**
-     * Start search session
-     */
-    public void start() {
-        stop();
-        mLogInstanceId = new InstanceIdSequence().newInstanceId();
-        mSearchWidgetHost = new SearchWidgetHost(mLauncher);
-        mSearchWidgetHost.startListening();
-    }
-
-    /**
      * Stop search session
      */
     public void stop() {
+        clearWidgetHost();
+    }
+
+    private void clearWidgetHost() {
         if (mSearchWidgetHost != null) {
             mSearchWidgetHost.stopListening();
+            mSearchWidgetHost.clearViews();
             mSearchWidgetHost.deleteHost();
-            for (SearchWidgetInfoContainer placeholder : mWidgetPlaceholders.values()) {
-                placeholder.clearListeners();
-            }
             mWidgetPlaceholders.clear();
             mSearchWidgetHost = null;
         }
-        for (LiveData<Slice> liveData : mUriSliceMap.values()) {
-            liveData.removeObservers(mLauncher);
+    }
+
+    @Override
+    public void onStateTransitionComplete(LauncherState finalState) {
+        if (finalState != ALL_APPS) {
+            // Clear all search session related objects
+            mUriSliceMap.values().forEach(SliceLifeCycle::destroy);
+            mUriSliceMap.clear();
+
+            clearWidgetHost();
         }
-        mUriSliceMap.clear();
+    }
+
+    /**
+     * Adds a new observer for the provided uri and returns a callback to cancel this observer
+     */
+    public SafeCloseable addObserver(Uri uri, Observer<Slice> listener) {
+        SliceLifeCycle slc = mUriSliceMap.get(uri);
+        if (slc == null) {
+            slc = new SliceLifeCycle(uri, mLauncher);
+            mUriSliceMap.put(uri, slc);
+        }
+        slc.addListener(listener);
+
+        final SliceLifeCycle sliceLifeCycle = slc;
+        return () -> sliceLifeCycle.removeListener(listener);
     }
 
     /**
@@ -159,5 +177,121 @@
                 AppWidgetProviderInfo appWidget) {
             return new SearchWidgetInfoContainer(context);
         }
+
+        @Override
+        public void clearViews() {
+            super.clearViews();
+        }
+    }
+
+    private static class SliceLifeCycle
+            implements ActivityLifecycleCallbacks, SliceCallback {
+
+        private final Uri mUri;
+        private final Launcher mLauncher;
+        private final SliceViewManager mSliceViewManager;
+        private final ArrayList<Observer<Slice>> mListeners = new ArrayList<>();
+
+        private boolean mDestroyed = false;
+        private boolean mWasListening = false;
+
+        SliceLifeCycle(Uri uri, Launcher launcher) {
+            mUri = uri;
+            mLauncher = launcher;
+            mSliceViewManager = SliceViewManager.getInstance(launcher);
+            launcher.registerActivityLifecycleCallbacks(this);
+
+            if (launcher.isDestroyed()) {
+                onActivityDestroyed(launcher);
+            } else if (launcher.isStarted()) {
+                onActivityStarted(launcher);
+            }
+        }
+
+        @Override
+        public void onActivityDestroyed(Activity activity) {
+            destroy();
+        }
+
+        @Override
+        public void onActivityStarted(Activity activity) {
+            updateListening();
+        }
+
+        @Override
+        public void onActivityStopped(Activity activity) {
+            updateListening();
+        }
+
+        private void updateListening() {
+            boolean isListening = mDestroyed
+                    ? false
+                    : (mLauncher.isStarted() && !mListeners.isEmpty());
+            UI_HELPER_EXECUTOR.execute(() -> uploadListeningBg(isListening));
+        }
+
+        @WorkerThread
+        private void uploadListeningBg(boolean isListening) {
+            if (mWasListening != isListening) {
+                mWasListening = isListening;
+                if (isListening) {
+                    mSliceViewManager.registerSliceCallback(mUri, MAIN_EXECUTOR, this);
+                    // Update slice one-time on the different thread so that we can display
+                    // multiple slices in parallel
+                    THREAD_POOL_EXECUTOR.execute(this::updateSlice);
+                } else {
+                    mSliceViewManager.unregisterSliceCallback(mUri, this);
+                }
+            }
+        }
+
+        @UiThread
+        private void addListener(Observer<Slice> listener) {
+            mListeners.add(listener);
+            updateListening();
+        }
+
+        @UiThread
+        private void removeListener(Observer<Slice> listener) {
+            mListeners.remove(listener);
+            updateListening();
+        }
+
+        @WorkerThread
+        private void updateSlice() {
+            try {
+                Slice s = mSliceViewManager.bindSlice(mUri);
+                MAIN_EXECUTOR.execute(() -> onSliceUpdated(s));
+            } catch (Exception e) {
+                Log.d(TAG, "Error fetching slice", e);
+            }
+        }
+
+        @UiThread
+        @Override
+        public void onSliceUpdated(@Nullable Slice s) {
+            mListeners.forEach(l -> l.onChanged(s));
+        }
+
+        private void destroy() {
+            if (mDestroyed) {
+                return;
+            }
+            mDestroyed = true;
+            mLauncher.unregisterActivityLifecycleCallbacks(this);
+            mListeners.clear();
+        }
+
+        @Override
+        public void onActivityCreated(Activity activity, Bundle bundle) { }
+
+        @Override
+        public void onActivityPaused(Activity activity) { }
+
+        @Override
+        public void onActivityResumed(Activity activity) { }
+
+        @Override
+        public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { }
     }
 }
diff --git a/src/com/android/launcher3/allapps/search/SearchWidgetInfoContainer.java b/src/com/android/launcher3/allapps/search/SearchWidgetInfoContainer.java
index b5c2268..8e5f8cb 100644
--- a/src/com/android/launcher3/allapps/search/SearchWidgetInfoContainer.java
+++ b/src/com/android/launcher3/allapps/search/SearchWidgetInfoContainer.java
@@ -70,10 +70,4 @@
         mListeners.remove(hostView);
     }
 
-    /**
-     * Removes all AppWidgetHost update listeners
-     */
-    public void clearListeners() {
-        mListeners.clear();
-    }
 }
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index 8013557..61f2c2a 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -31,7 +31,6 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ShortcutInfo;
 import android.graphics.drawable.Drawable;
-import android.os.Handler;
 import android.os.Process;
 import android.os.UserHandle;
 import android.util.Log;
@@ -134,7 +133,7 @@
      * Fetches high-res icon for the provided ItemInfo and updates the caller when done.
      * @return a request ID that can be used to cancel the request.
      */
-    public IconLoadRequest updateIconInBackground(final ItemInfoUpdateReceiver caller,
+    public HandlerRunnable updateIconInBackground(final ItemInfoUpdateReceiver caller,
             final ItemInfoWithIcon info) {
         Preconditions.assertUIThread();
         if (mPendingIconRequestCount <= 0) {
@@ -142,20 +141,18 @@
         }
         mPendingIconRequestCount ++;
 
-        IconLoadRequest request = new IconLoadRequest(mWorkerHandler, this::onIconRequestEnd) {
-            @Override
-            public void run() {
-                if (info instanceof AppInfo || info instanceof WorkspaceItemInfo) {
-                    getTitleAndIcon(info, false);
-                } else if (info instanceof PackageItemInfo) {
-                    getTitleAndIconForApp((PackageItemInfo) info, false);
-                }
-                MAIN_EXECUTOR.execute(() -> {
-                    caller.reapplyItemInfo(info);
-                    onEnd();
-                });
-            }
-        };
+        HandlerRunnable<ItemInfoWithIcon> request = new HandlerRunnable<>(mWorkerHandler,
+                () -> {
+                    if (info instanceof AppInfo || info instanceof WorkspaceItemInfo) {
+                        getTitleAndIcon(info, false);
+                    } else if (info instanceof PackageItemInfo) {
+                        getTitleAndIconForApp((PackageItemInfo) info, false);
+                    }
+                    return info;
+                },
+                MAIN_EXECUTOR,
+                caller::reapplyItemInfo,
+                this::onIconRequestEnd);
         Utilities.postAsyncCallback(mWorkerHandler, request);
         return request;
     }
@@ -336,12 +333,6 @@
         return super.getEntryFromDB(cacheKey, entry, lowRes);
     }
 
-    public static abstract class IconLoadRequest extends HandlerRunnable {
-        IconLoadRequest(Handler handler, Runnable endRunnable) {
-            super(handler, endRunnable);
-        }
-    }
-
     /**
      * Interface for receiving itemInfo with high-res icon.
      */