Implementing a package install progress listener for L

issue: 15835307

Change-Id: I71aaea087963f2e0e1206447190cbe23c174057d
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index ffa7ec3..ab94814 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -434,65 +434,48 @@
     }
 
     public void applyState() {
-        final int progressLevel;
-        final int state = getState();
-        if (DEBUG) Log.d(TAG, "applying icon state: " + state);
-
-        switch(state) {
-            case ShortcutInfo.PACKAGE_STATE_DEFAULT:
-                super.setText(mDefaultText);
-                progressLevel = 100;
-                break;
-
-            case ShortcutInfo.PACKAGE_STATE_ENQUEUED:
-                setText(R.string.package_state_enqueued);
-                progressLevel = 0;
-                break;
-
-            case ShortcutInfo.PACKAGE_STATE_DOWNLOADING:
-                setText(R.string.package_state_downloading);
-                // TODO(sunnygoyal): fix progress
-                progressLevel = 30;
-                break;
-
-            case ShortcutInfo.PACKAGE_STATE_INSTALLING:
-                setText(R.string.package_state_installing);
-                progressLevel = 100;
-                break;
-
-            case ShortcutInfo.PACKAGE_STATE_ERROR:
-                setText(R.string.package_state_error);
-                progressLevel = 0;
-                break;
-
-            case ShortcutInfo.PACKAGE_STATE_UNKNOWN:
-            default:
-                progressLevel = 0;
-                setText(R.string.package_state_unknown);
-                break;
-        }
-
-        Drawable[] drawables = getCompoundDrawables();
-        Drawable top = drawables[1];
-        if ((top != null) && !(top instanceof PreloadIconDrawable)) {
-            top = new PreloadIconDrawable(top, getResources());
-            setCompoundDrawables(drawables[0], top, drawables[2], drawables[3]);
-        }
-        if (top != null) {
-            top.setLevel(progressLevel);
-            if ((top instanceof PreloadIconDrawable)
-                    && (state == ShortcutInfo.PACKAGE_STATE_DEFAULT)) {
-                ((PreloadIconDrawable) top).maybePerformFinishedAnimation();
-            }
-        }
-    }
-
-    private int getState() {
-        if (! (getTag() instanceof ShortcutInfo)) {
-            return ShortcutInfo.PACKAGE_STATE_DEFAULT;
-        } else {
+        if (getTag() instanceof ShortcutInfo) {
             ShortcutInfo info = (ShortcutInfo) getTag();
-            return info.getState();
+            final int state = info.getState();
+
+            final int progressLevel;
+            if (DEBUG) Log.d(TAG, "applying icon state: " + state);
+
+            switch(state) {
+                case ShortcutInfo.PACKAGE_STATE_DEFAULT:
+                    progressLevel = 100;
+                    break;
+
+                case ShortcutInfo.PACKAGE_STATE_INSTALLING:
+                    setText(R.string.package_state_installing);
+                    progressLevel = info.getProgress();
+                    break;
+
+                case ShortcutInfo.PACKAGE_STATE_ERROR:
+                case ShortcutInfo.PACKAGE_STATE_UNKNOWN:
+                default:
+                    progressLevel = 0;
+                    setText(R.string.package_state_unknown);
+                    break;
+            }
+
+            Drawable[] drawables = getCompoundDrawables();
+            Drawable top = drawables[1];
+            if (top != null) {
+                final PreloadIconDrawable preloadDrawable;
+                if (top instanceof PreloadIconDrawable) {
+                    preloadDrawable = (PreloadIconDrawable) top;
+                } else {
+                    preloadDrawable = new PreloadIconDrawable(top, getResources());
+                    setCompoundDrawables(drawables[0], preloadDrawable, drawables[2], drawables[3]);
+                }
+
+                preloadDrawable.setLevel(progressLevel);
+                if (state == ShortcutInfo.PACKAGE_STATE_DEFAULT) {
+                    preloadDrawable.maybePerformFinishedAnimation();
+                }
+
+            }
         }
     }
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index a5bf690..b5bed4c 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -82,10 +82,9 @@
 import android.view.MotionEvent;
 import android.view.Surface;
 import android.view.View;
-import android.view.ViewAnimationUtils;
-import android.view.Window;
 import android.view.View.OnClickListener;
 import android.view.View.OnLongClickListener;
+import android.view.ViewAnimationUtils;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
@@ -93,9 +92,7 @@
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.AccelerateDecelerateInterpolator;
-import android.view.animation.AccelerateInterpolator;
 import android.view.animation.DecelerateInterpolator;
-import android.view.animation.LinearInterpolator;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.Advanceable;
 import android.widget.FrameLayout;
@@ -107,10 +104,10 @@
 import com.android.launcher3.PagedView.PageSwitchListener;
 import com.android.launcher3.compat.LauncherActivityInfoCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.compat.PackageInstallerCompat;
+import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.PagedView.PageSwitchListener;
 
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
@@ -1055,12 +1052,15 @@
         }
         mWorkspace.updateInteractionForState();
         mWorkspace.onResume();
+
+        PackageInstallerCompat.getInstance(this).onResume();
     }
 
     @Override
     protected void onPause() {
         // Ensure that items added to Launcher are queued until Launcher returns
         InstallShortcutReceiver.enableInstallQueue();
+        PackageInstallerCompat.getInstance(this).onPause();
 
         super.onPause();
         mPaused = true;
@@ -2028,6 +2028,7 @@
         mWorkspace = null;
         mDragController = null;
 
+        PackageInstallerCompat.getInstance(this).onStop();
         LauncherAnimUtils.onDestroyActivity();
     }
 
@@ -4478,6 +4479,7 @@
             mWorkspace.getUniqueComponents(true, null);
             mIntentsOnWorkspaceFromUpgradePath = mWorkspace.getUniqueComponents(true, null);
         }
+        PackageInstallerCompat.getInstance(this).onFinishBind();
     }
 
     private void sendLoadingCompleteBroadcastIfNecessary() {
@@ -4591,9 +4593,10 @@
      *
      * Implementation of the method from LauncherModel.Callbacks.
      */
-    public void updatePackageState(String pkgName, int state) {
+    @Override
+    public void updatePackageState(ArrayList<PackageInstallInfo> installInfo) {
         if (mWorkspace != null) {
-            mWorkspace.updatePackageState(pkgName, state);
+            mWorkspace.updatePackageState(installInfo);
         }
     }
 
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 27bcd81..4ab4e4b 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -17,7 +17,11 @@
 package com.android.launcher3;
 
 import android.app.SearchManager;
-import android.content.*;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.ContentObserver;
@@ -25,8 +29,10 @@
 import android.util.Log;
 
 import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
 
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
 
 public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks {
     private static final String TAG = "LauncherAppState";
@@ -251,8 +257,7 @@
         return getInstance().mBuildInfo.isDogfoodBuild();
     }
 
-    public void setPackageState(String pkgName, int state) {
-        if (DEBUG) Log.d(TAG, "setPackageState(" + pkgName + ", " +  state  + ")");
-        mModel.setPackageState(pkgName, state);
+    public void setPackageState(ArrayList<PackageInstallInfo> installInfo) {
+        mModel.setPackageState(installInfo);
     }
 }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index e115bf1..86edaef 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -55,6 +55,7 @@
 import com.android.launcher3.InstallWidgetReceiver.WidgetMimeTypeHandlerData;
 import com.android.launcher3.compat.LauncherActivityInfoCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
 
@@ -195,7 +196,7 @@
                                   ArrayList<ItemInfo> addAnimated,
                                   ArrayList<AppInfo> addedApps);
         public void bindAppsUpdated(ArrayList<AppInfo> apps);
-        public void updatePackageState(String pkgName, int state);
+        public void updatePackageState(ArrayList<PackageInstallInfo> installInfo);
         public void bindComponentsRemoved(ArrayList<String> packageNames,
                         ArrayList<AppInfo> appInfos, UserHandleCompat user);
         public void bindPackagesUpdated(ArrayList<Object> widgetsAndShortcuts);
@@ -332,13 +333,13 @@
         return null;
     }
 
-    public void setPackageState(final String pkgName, final int state) {
+    public void setPackageState(final ArrayList<PackageInstallInfo> installInfo) {
         // Process the updated package state
         Runnable r = new Runnable() {
             public void run() {
                 Callbacks callbacks = mCallbacks != null ? mCallbacks.get() : null;
                 if (callbacks != null) {
-                    callbacks.updatePackageState(pkgName, state);
+                    callbacks.updatePackageState(installInfo);
                 }
             }
         };
diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java
index dc019b2..612b0a5 100644
--- a/src/com/android/launcher3/ShortcutInfo.java
+++ b/src/com/android/launcher3/ShortcutInfo.java
@@ -42,13 +42,7 @@
     public static final int PACKAGE_STATE_DEFAULT = 0;
 
     /** {@link #mState} meaning some external entity has promised to install this package. */
-    public static final int PACKAGE_STATE_ENQUEUED = 1;
-
-    /** {@link #mState} meaning but some external entity is downloading this package. */
-    public static final int PACKAGE_STATE_DOWNLOADING = 2;
-
-    /** {@link #mState} meaning some external entity is installing this package. */
-    public static final int PACKAGE_STATE_INSTALLING = 3;
+    public static final int PACKAGE_STATE_INSTALLING = 1;
 
     /**
      * The intent used to start the application.
@@ -89,6 +83,11 @@
      */
     protected int mState;
 
+    /**
+     * The installation progress [0-100] of the package that this shortcut represents.
+     */
+    protected int mProgress;
+
     long firstInstallTime;
     int flags = 0;
 
@@ -237,16 +236,24 @@
 
     public boolean isAbandoned() {
         return isPromise()
-                && (mState == ShortcutInfo.PACKAGE_STATE_ERROR
-                        || mState == ShortcutInfo.PACKAGE_STATE_UNKNOWN);
+                && (mState == PACKAGE_STATE_ERROR
+                        || mState == PACKAGE_STATE_UNKNOWN);
     }
 
-    public int getState() {
-        return mState;
+    public int getProgress() {
+        return mProgress;
+    }
+
+    public void setProgress(int progress) {
+        mProgress = progress;
     }
 
     public void setState(int state) {
         mState = state;
     }
+
+    public int getState() {
+        return mState;
+    }
 }
 
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 6d9b34e..7c8708b 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -67,6 +67,7 @@
 import com.android.launcher3.FolderIcon.FolderRingAnimator;
 import com.android.launcher3.Launcher.CustomContentCallbacks;
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
 import com.android.launcher3.compat.UserHandleCompat;
 
 import java.util.ArrayList;
@@ -4864,26 +4865,32 @@
         }
     }
 
-    public void updatePackageState(final String pkgName, final int state) {
-        mapOverItems(MAP_RECURSE, new ItemOperator() {
-            @Override
-            public boolean evaluate(ItemInfo info, View v, View parent) {
-                if (info instanceof ShortcutInfo
-                        && ((ShortcutInfo) info).isPromiseFor(pkgName)
-                        && v instanceof BubbleTextView) {
-                    ((ShortcutInfo) info).setState(state);
-                    ((BubbleTextView)v).applyState();
-                }
-                // process all the shortcuts
-                return false;
-            }
-        });
+    public void updatePackageState(ArrayList<PackageInstallInfo> installInfos) {
+        HashSet<String> completedPackages = new HashSet<>();
 
-        if (state == ShortcutInfo.PACKAGE_STATE_DEFAULT) {
-            // Update any pending widget
-            HashSet<String> packages = new HashSet<String>();
-            packages.add(pkgName);
-            restorePendingWidgets(packages);
+        for (final PackageInstallInfo installInfo : installInfos) {
+            mapOverItems(MAP_RECURSE, new ItemOperator() {
+                @Override
+                public boolean evaluate(ItemInfo info, View v, View parent) {
+                    if (info instanceof ShortcutInfo
+                            && ((ShortcutInfo) info).isPromiseFor(installInfo.packageName)
+                            && v instanceof BubbleTextView) {
+                        ((ShortcutInfo) info).setProgress(installInfo.progress);
+                        ((ShortcutInfo) info).setState(installInfo.state);
+                        ((BubbleTextView)v).applyState();
+                    }
+                    // process all the shortcuts
+                    return false;
+                }
+            });
+
+            if (installInfo.state == ShortcutInfo.PACKAGE_STATE_DEFAULT) {
+                completedPackages.add(installInfo.packageName);
+            }
+        }
+
+        if (!completedPackages.isEmpty()) {
+            restorePendingWidgets(completedPackages);
         }
     }
 
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompat.java b/src/com/android/launcher3/compat/LauncherAppsCompat.java
index 8d978d4..cc0203d 100644
--- a/src/com/android/launcher3/compat/LauncherAppsCompat.java
+++ b/src/com/android/launcher3/compat/LauncherAppsCompat.java
@@ -20,17 +20,10 @@
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.UserHandle;
-import android.os.UserManager;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 public abstract class LauncherAppsCompat {
 
diff --git a/src/com/android/launcher3/compat/PackageInstallerCompat.java b/src/com/android/launcher3/compat/PackageInstallerCompat.java
new file mode 100644
index 0000000..89a2157
--- /dev/null
+++ b/src/com/android/launcher3/compat/PackageInstallerCompat.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2014 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.compat;
+
+import android.content.Context;
+
+import com.android.launcher3.Utilities;
+
+public abstract class PackageInstallerCompat {
+
+    private static final Object sInstanceLock = new Object();
+    private static PackageInstallerCompat sInstance;
+
+    public static PackageInstallerCompat getInstance(Context context) {
+        synchronized (sInstanceLock) {
+            if (sInstance == null) {
+                if (Utilities.isLmp()) {
+                    sInstance = new PackageInstallerCompatVL(context);
+                } else {
+                    sInstance = new PackageInstallerCompatV16(context) { };
+                }
+            }
+            return sInstance;
+        }
+    }
+
+    public abstract void onPause();
+
+    public abstract void onResume();
+
+    public abstract void onFinishBind();
+
+    public abstract void onStop();
+
+    public abstract void recordPackageUpdate(String packageName, int state, int progress);
+
+    public static final class PackageInstallInfo {
+        public final String packageName;
+
+        public int state;
+        public int progress;
+
+        public PackageInstallInfo(String packageName) {
+            this.packageName = packageName;
+        }
+
+        public PackageInstallInfo(String packageName, int state, int progress) {
+            this.packageName = packageName;
+            this.state = state;
+            this.progress = progress;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/compat/PackageInstallerCompatV16.java b/src/com/android/launcher3/compat/PackageInstallerCompatV16.java
new file mode 100644
index 0000000..653a88c
--- /dev/null
+++ b/src/com/android/launcher3/compat/PackageInstallerCompatV16.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2014 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.compat;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.ShortcutInfo;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.JSONStringer;
+import org.json.JSONTokener;
+
+import java.util.ArrayList;
+
+public class PackageInstallerCompatV16 extends PackageInstallerCompat {
+
+    private static final String TAG = "PackageInstallerCompatV16";
+    private static final boolean DEBUG = false;
+
+    private static final String KEY_PROGRESS = "progress";
+    private static final String KEY_STATE = "state";
+
+    private static final String PREFS =
+            "com.android.launcher3.compat.PackageInstallerCompatV16.queue";
+
+    protected final SharedPreferences mPrefs;
+
+    boolean mUseQueue;
+    boolean mFinishedBind;
+    boolean mReplayPending;
+
+    PackageInstallerCompatV16(Context context) {
+        mPrefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
+    }
+
+    @Override
+    public void onPause() {
+        mUseQueue = true;
+        if (DEBUG) Log.d(TAG, "updates paused");
+    }
+
+    @Override
+    public void onResume() {
+        mUseQueue = false;
+        if (mFinishedBind) {
+            replayUpdates();
+        }
+    }
+
+    @Override
+    public void onFinishBind() {
+        mFinishedBind = true;
+        if (!mUseQueue) {
+            replayUpdates();
+        }
+    }
+
+    @Override
+    public void onStop() { }
+
+    private void replayUpdates() {
+        if (DEBUG) Log.d(TAG, "updates resumed");
+        LauncherAppState app = LauncherAppState.getInstanceNoCreate();
+        if (app == null) {
+            mReplayPending = true; // try again later
+            if (DEBUG) Log.d(TAG, "app is null, delaying send");
+            return;
+        }
+        mReplayPending = false;
+        ArrayList<PackageInstallInfo> updates = new ArrayList<>();
+        for (String packageName: mPrefs.getAll().keySet()) {
+            final String json = mPrefs.getString(packageName, null);
+            if (!TextUtils.isEmpty(json)) {
+                updates.add(infoFromJson(packageName, json));
+            }
+        }
+        if (!updates.isEmpty()) {
+            sendUpdate(app, updates);
+        }
+    }
+
+    /**
+     * This should be called by the implementations to register a package update.
+     */
+    @Override
+    public synchronized void recordPackageUpdate(String packageName, int state, int progress) {
+        SharedPreferences.Editor editor = mPrefs.edit();
+        PackageInstallInfo installInfo = new PackageInstallInfo(packageName);
+        installInfo.progress = progress;
+        installInfo.state = state;
+        if (state == ShortcutInfo.PACKAGE_STATE_DEFAULT) {
+            // no longer necessary to track this package
+            editor.remove(packageName);
+            if (DEBUG) Log.d(TAG, "no longer tracking " + packageName);
+        } else {
+            editor.putString(packageName, infoToJson(installInfo));
+            if (DEBUG)
+                Log.d(TAG, "saved state: " + infoToJson(installInfo)
+                        + " for package: " + packageName);
+
+        }
+        editor.commit();
+
+        if (!mUseQueue) {
+            if (mReplayPending) {
+                replayUpdates();
+            } else {
+                LauncherAppState app = LauncherAppState.getInstanceNoCreate();
+                ArrayList<PackageInstallInfo> update = new ArrayList<>();
+                update.add(installInfo);
+                sendUpdate(app, update);
+            }
+        }
+    }
+
+    private void sendUpdate(LauncherAppState app, ArrayList<PackageInstallInfo> updates) {
+        if (app == null) {
+            mReplayPending = true; // try again later
+            if (DEBUG) Log.d(TAG, "app is null, delaying send");
+        } else {
+            app.setPackageState(updates);
+        }
+    }
+
+    private static PackageInstallInfo infoFromJson(String packageName, String json) {
+        PackageInstallInfo info = new PackageInstallInfo(packageName);
+        try {
+            JSONObject object = (JSONObject) new JSONTokener(json).nextValue();
+            info.state = object.getInt(KEY_STATE);
+            info.progress = object.getInt(KEY_PROGRESS);
+        } catch (JSONException e) {
+            Log.e(TAG, "failed to deserialize app state update", e);
+        }
+        return info;
+    }
+
+    private static String infoToJson(PackageInstallInfo info) {
+        String value = null;
+        try {
+            JSONStringer json = new JSONStringer()
+                    .object()
+                    .key(KEY_STATE).value(info.state)
+                    .key(KEY_PROGRESS).value(info.progress)
+                    .endObject();
+            value = json.toString();
+        } catch (JSONException e) {
+            Log.e(TAG, "failed to serialize app state update", e);
+        }
+        return value;
+    }
+}
diff --git a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
new file mode 100644
index 0000000..7f6302f
--- /dev/null
+++ b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2014 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.compat;
+
+import android.content.Context;
+import android.content.pm.InstallSessionInfo;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionCallback;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.ShortcutInfo;
+
+import java.util.ArrayList;
+
+public class PackageInstallerCompatVL extends PackageInstallerCompat {
+
+    private static final String TAG = "PackageInstallerCompatVL";
+    private static final boolean DEBUG = false;
+
+    private final SparseArray<InstallSessionInfo> mPendingReplays = new SparseArray<>();
+    private final PackageInstaller mInstaller;
+
+    private boolean mResumed;
+    private boolean mBound;
+
+    PackageInstallerCompatVL(Context context) {
+        mInstaller = context.getPackageManager().getPackageInstaller();
+
+        mResumed = false;
+        mBound = false;
+
+        mInstaller.addSessionCallback(mCallback);
+        // On start, send updates for all active sessions
+        for (InstallSessionInfo info : mInstaller.getAllSessions()) {
+            mPendingReplays.append(info.getSessionId(), info);
+        }
+    }
+
+    @Override
+    public void onStop() {
+        mInstaller.removeSessionCallback(mCallback);
+    }
+
+    @Override
+    public void onFinishBind() {
+        mBound = true;
+        replayUpdates(null);
+    }
+
+    @Override
+    public void onPause() {
+        mResumed = false;
+    }
+
+    @Override
+    public void onResume() {
+        mResumed = true;
+        replayUpdates(null);
+    }
+
+    @Override
+    public void recordPackageUpdate(String packageName, int state, int progress) {
+        // No op
+    }
+
+    private void replayUpdates(PackageInstallInfo newInfo) {
+        if (DEBUG) Log.d(TAG, "updates resumed");
+        if (!mResumed || !mBound) {
+            // Not yet ready
+            return;
+        }
+        if ((mPendingReplays.size() == 0) && (newInfo == null)) {
+            // Nothing to update
+            return;
+        }
+
+        LauncherAppState app = LauncherAppState.getInstanceNoCreate();
+        if (app == null) {
+            // Try again later
+            if (DEBUG) Log.d(TAG, "app is null, delaying send");
+            return;
+        }
+
+        ArrayList<PackageInstallInfo> updates = new ArrayList<>();
+        if (newInfo != null) {
+            updates.add(newInfo);
+        }
+        for (int i = mPendingReplays.size() - 1; i > 0; i--) {
+            InstallSessionInfo session = mPendingReplays.valueAt(i);
+            if (session.getAppPackageName() != null) {
+                updates.add(new PackageInstallInfo(session.getAppPackageName(),
+                        ShortcutInfo.PACKAGE_STATE_INSTALLING,
+                        (int) (session.getProgress() * 100)));
+            }
+        }
+        mPendingReplays.clear();
+        if (!updates.isEmpty()) {
+            app.setPackageState(updates);
+        }
+    }
+
+    private final SessionCallback mCallback = new SessionCallback() {
+
+        @Override
+        public void onCreated(int sessionId) {
+            InstallSessionInfo session = mInstaller.getSessionInfo(sessionId);
+            if (session != null) {
+                mPendingReplays.put(sessionId, session);
+                replayUpdates(null);
+            }
+        }
+
+        @Override
+        public void onFinished(int sessionId, boolean success) {
+            mPendingReplays.remove(sessionId);
+            InstallSessionInfo session = mInstaller.getSessionInfo(sessionId);
+            if ((session != null) && (session.getAppPackageName() != null)) {
+                // Replay all updates with a one time update for this installed package. No
+                // need to store this record for future updates, as the app list will get
+                // refreshed on resume.
+                replayUpdates(new PackageInstallInfo(session.getAppPackageName(),
+                        success ? ShortcutInfo.PACKAGE_STATE_DEFAULT
+                                : ShortcutInfo.PACKAGE_STATE_ERROR, 0));
+            }
+        }
+
+        @Override
+        public void onProgressChanged(int sessionId, float progress) {
+            InstallSessionInfo session = mInstaller.getSessionInfo(sessionId);
+            if (session != null) {
+                mPendingReplays.put(sessionId, session);
+                replayUpdates(null);
+            }
+        }
+
+        @Override
+        public void onOpened(int sessionId) { }
+
+        @Override
+        public void onClosed(int sessionId) { }
+
+    };
+}