Show promise app icon in All Apps while installation process.

This CL only modifies the model and is behind a feature flag
which per default is set to false.
The app icon will appear as a promise icon, it reacts on icon
or label changes and the icon will be remove on finishing the
installation process. With this CL the progress of the installation
process is not visible.

Bug: 23952570

Change-Id: I510825d0b0b1b01eb14f7e50f0a2358b0d8b99b5
diff --git a/src/com/android/launcher3/AllAppsList.java b/src/com/android/launcher3/AllAppsList.java
index 5b42cad..d7f0180 100644
--- a/src/com/android/launcher3/AllAppsList.java
+++ b/src/com/android/launcher3/AllAppsList.java
@@ -18,10 +18,15 @@
 
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
+import android.os.Process;
 import android.os.UserHandle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 
 import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.ItemInfoMatcher;
 
@@ -69,7 +74,7 @@
         if (!mAppFilter.shouldShowApp(info.componentName)) {
             return;
         }
-        if (findActivity(data, info.componentName, info.user)) {
+        if (findAppInfo(info.componentName, info.user) != null) {
             return;
         }
         mIconCache.getTitleAndIcon(info, activityInfo, true /* useLowResIcon */);
@@ -78,6 +83,25 @@
         added.add(info);
     }
 
+    public void addPromiseApp(Context context,
+                              PackageInstallerCompat.PackageInstallInfo installInfo) {
+        ApplicationInfo applicationInfo = LauncherAppsCompat.getInstance(context)
+                .getApplicationInfo(installInfo.packageName, 0, Process.myUserHandle());
+        // only if not yet installed
+        if (applicationInfo == null) {
+            PromiseAppInfo info = new PromiseAppInfo(installInfo);
+            mIconCache.getTitleAndIcon(info, info.usingLowResIcon);
+            data.add(info);
+            added.add(info);
+        }
+    }
+
+    public void removePromiseApp(AppInfo appInfo) {
+        // the <em>removed</em> list is handled by the caller
+        // so not adding it here
+        data.remove(appInfo);
+    }
+
     public void clear() {
         data.clear();
         // TODO: do we clear these too?
@@ -169,9 +193,7 @@
             // Find enabled activities and add them to the adapter
             // Also updates existing activities with new labels/icons
             for (final LauncherActivityInfo info : matches) {
-                AppInfo applicationInfo = findApplicationInfoLocked(
-                        info.getComponentName().getPackageName(), user,
-                        info.getComponentName().getClassName());
+                AppInfo applicationInfo = findAppInfo(info.getComponentName(), user);
                 if (applicationInfo == null) {
                     add(new AppInfo(context, info, user), info);
                 } else {
@@ -208,28 +230,14 @@
     }
 
     /**
-     * Returns whether <em>apps</em> contains <em>component</em>.
+     * Find an AppInfo object for the given componentName
+     *
+     * @return the corresponding AppInfo or null
      */
-    private static boolean findActivity(ArrayList<AppInfo> apps, ComponentName component,
-            UserHandle user) {
-        final int N = apps.size();
-        for (int i = 0; i < N; i++) {
-            final AppInfo info = apps.get(i);
-            if (info.user.equals(user) && info.componentName.equals(component)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Find an ApplicationInfo object for the given packageName and className.
-     */
-    private AppInfo findApplicationInfoLocked(String packageName, UserHandle user,
-            String className) {
+    private @Nullable AppInfo findAppInfo(@NonNull ComponentName componentName,
+                                          @NonNull UserHandle user) {
         for (AppInfo info: data) {
-            if (user.equals(info.user) && packageName.equals(info.componentName.getPackageName())
-                    && className.equals(info.componentName.getClassName())) {
+            if (componentName.equals(info.componentName) && user.equals(info.user)) {
                 return info;
             }
         }
diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java
index 1f473a2..a5552aa 100644
--- a/src/com/android/launcher3/IconCache.java
+++ b/src/com/android/launcher3/IconCache.java
@@ -437,9 +437,10 @@
      * Updates {@param application} only if a valid entry is found.
      */
     public synchronized void updateTitleAndIcon(AppInfo application) {
+        boolean usePackageIcon = application instanceof PromiseAppInfo;
         CacheEntry entry = cacheLocked(application.componentName,
                 Provider.<LauncherActivityInfo>of(null),
-                application.user, false, application.usingLowResIcon);
+                application.user, usePackageIcon, application.usingLowResIcon);
         if (entry.icon != null && !isDefaultIcon(entry.icon, application.user)) {
             applyCacheEntry(entry, application);
         }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 35811d3..707ec86 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -25,7 +25,9 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
+import android.content.pm.PackageInstaller;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -573,6 +575,25 @@
                 screensUri, null, null, null, LauncherSettings.WorkspaceScreens.SCREEN_RANK));
     }
 
+    public void onInstallSessionCreated(final PackageInstallInfo sessionInfo) {
+        enqueueModelUpdateTask(new ExtendedModelTask() {
+            @Override
+            public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+                apps.addPromiseApp(app.getContext(), sessionInfo);
+                if (!apps.added.isEmpty()) {
+                    final ArrayList<AppInfo> arrayList = new ArrayList<>(apps.added);
+                    apps.added.clear();
+                    scheduleCallbackTask(new CallbackTask() {
+                        @Override
+                        public void execute(Callbacks callbacks) {
+                            callbacks.bindAppsAdded(null, null, null, arrayList);
+                        }
+                    });
+                }
+            }
+        });
+    }
+
     /**
      * Runnable for the thread that loads the contents of the launcher:
      *   - workspace icons
@@ -1691,10 +1712,21 @@
                                 });
                 }
             }
+
+            if (FeatureFlags.LAUNCHER3_PROMISE_APPS_IN_ALL_APPS) {
+                // get all active sessions and add them to the all apps list
+                PackageInstallerCompat installer = PackageInstallerCompat.getInstance(mContext);
+                for (PackageInstaller.SessionInfo info : installer.getAllVerifiedSessions()) {
+                    mBgAllAppsList.addPromiseApp(mContext,
+                            PackageInstallInfo.fromInstallingState(info));
+                }
+            }
+
             // Huh? Shouldn't this be inside the Runnable below?
             final ArrayList<AppInfo> added = mBgAllAppsList.added;
             mBgAllAppsList.added = new ArrayList<AppInfo>();
 
+
             // Post callback on main thread
             mUiExecutor.execute(new Runnable() {
                 public void run() {
diff --git a/src/com/android/launcher3/PromiseAppInfo.java b/src/com/android/launcher3/PromiseAppInfo.java
new file mode 100644
index 0000000..04ba1d3
--- /dev/null
+++ b/src/com/android/launcher3/PromiseAppInfo.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017 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;
+
+import android.content.Intent;
+import android.support.annotation.NonNull;
+
+import com.android.launcher3.compat.PackageInstallerCompat;
+
+public class PromiseAppInfo extends AppInfo {
+
+    public int level = 0;
+
+    public PromiseAppInfo(@NonNull PackageInstallerCompat.PackageInstallInfo installInfo) {
+        componentName = installInfo.componentName;
+        intent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_LAUNCHER)
+                .setComponent(componentName)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                        | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+    }
+
+    @Override
+    public ShortcutInfo makeShortcut() {
+        ShortcutInfo shortcut = new ShortcutInfo(this);
+        shortcut.setInstallProgress(level);
+        // We need to update the component name when the apk is installed
+        shortcut.status |= ShortcutInfo.FLAG_AUTOINTALL_ICON;
+        // Since the user is manually placing it on homescreen, it should not be auto-removed later
+        shortcut.status |= ShortcutInfo.FLAG_RESTORE_STARTED;
+        return shortcut;
+    }
+}
diff --git a/src/com/android/launcher3/compat/PackageInstallerCompat.java b/src/com/android/launcher3/compat/PackageInstallerCompat.java
index c7fe0ce..112cca5 100644
--- a/src/com/android/launcher3/compat/PackageInstallerCompat.java
+++ b/src/com/android/launcher3/compat/PackageInstallerCompat.java
@@ -16,9 +16,13 @@
 
 package com.android.launcher3.compat;
 
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.PackageInstaller;
+import android.support.annotation.NonNull;
 
 import java.util.HashMap;
+import java.util.List;
 
 public abstract class PackageInstallerCompat {
 
@@ -46,19 +50,34 @@
     public abstract void onStop();
 
     public static final class PackageInstallInfo {
+        public final ComponentName componentName;
         public final String packageName;
+        public final int state;
+        public final int progress;
 
-        public int state;
-        public int progress;
-
-        public PackageInstallInfo(String packageName) {
-            this.packageName = packageName;
+        private PackageInstallInfo(@NonNull PackageInstaller.SessionInfo info) {
+            this.state = STATUS_INSTALLING;
+            this.packageName = info.getAppPackageName();
+            this.componentName = new ComponentName(packageName, "");
+            this.progress = (int) (info.getProgress() * 100f);
         }
 
         public PackageInstallInfo(String packageName, int state, int progress) {
-            this.packageName = packageName;
             this.state = state;
+            this.packageName = packageName;
+            this.componentName = new ComponentName(packageName, "");
             this.progress = progress;
         }
+
+        public static PackageInstallInfo fromInstallingState(PackageInstaller.SessionInfo info) {
+            return new PackageInstallInfo(info);
+        }
+
+        public static PackageInstallInfo fromState(int state, String packageName) {
+            return new PackageInstallInfo(packageName, state, 0 /* progress */);
+        }
+
     }
+
+    public abstract List<PackageInstaller.SessionInfo> getAllVerifiedSessions();
 }
diff --git a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
index b87582f..d7cd032 100644
--- a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
+++ b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.compat;
 
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionCallback;
 import android.content.pm.PackageInstaller.SessionInfo;
@@ -28,23 +29,31 @@
 import com.android.launcher3.IconCache;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.Thunk;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
 
 public class PackageInstallerCompatVL extends PackageInstallerCompat {
 
+    private static final boolean DEBUG = false;
+
     @Thunk final SparseArray<String> mActiveSessions = new SparseArray<>();
 
     @Thunk final PackageInstaller mInstaller;
     private final IconCache mCache;
     private final Handler mWorker;
+    private final Context mAppContext;
+    private final HashMap<String,Boolean> mSessionVerifiedMap = new HashMap<>();
 
     PackageInstallerCompatVL(Context context) {
+        mAppContext = context.getApplicationContext();
         mInstaller = context.getPackageManager().getPackageInstaller();
         mCache = LauncherAppState.getInstance(context).getIconCache();
         mWorker = new Handler(LauncherModel.getWorkerLooper());
-
         mInstaller.registerSessionCallback(mCallback, mWorker);
     }
 
@@ -52,7 +61,7 @@
     public HashMap<String, Integer> updateAndGetActiveSessionCache() {
         HashMap<String, Integer> activePackages = new HashMap<>();
         UserHandle user = Process.myUserHandle();
-        for (SessionInfo info : mInstaller.getAllSessions()) {
+        for (SessionInfo info : getAllVerifiedSessions()) {
             addSessionInfoToCache(info, user);
             if (info.getAppPackageName() != null) {
                 activePackages.put(info.getAppPackageName(), (int) (info.getProgress() * 100));
@@ -86,7 +95,14 @@
 
         @Override
         public void onCreated(int sessionId) {
-            pushSessionDisplayToLauncher(sessionId);
+            SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId);
+            if (FeatureFlags.LAUNCHER3_PROMISE_APPS_IN_ALL_APPS && sessionInfo != null) {
+                LauncherAppState app = LauncherAppState.getInstanceNoCreate();
+                if (app != null) {
+                    app.getModel().onInstallSessionCreated(
+                            PackageInstallInfo.fromInstallingState(sessionInfo));
+                }
+            }
         }
 
         @Override
@@ -97,18 +113,17 @@
             mActiveSessions.remove(sessionId);
 
             if (packageName != null) {
-                sendUpdate(new PackageInstallInfo(packageName,
-                        success ? STATUS_INSTALLED : STATUS_FAILED, 0));
+                sendUpdate(PackageInstallInfo.fromState(
+                        success ? STATUS_INSTALLED : STATUS_FAILED,
+                        packageName));
             }
         }
 
         @Override
         public void onProgressChanged(int sessionId, float progress) {
-            SessionInfo session = mInstaller.getSessionInfo(sessionId);
+            SessionInfo session = verify(mInstaller.getSessionInfo(sessionId));
             if (session != null && session.getAppPackageName() != null) {
-                sendUpdate(new PackageInstallInfo(session.getAppPackageName(),
-                        STATUS_INSTALLING,
-                        (int) (session.getProgress() * 100)));
+                sendUpdate(PackageInstallInfo.fromInstallingState(session));
             }
         }
 
@@ -120,16 +135,46 @@
             pushSessionDisplayToLauncher(sessionId);
         }
 
-        private void pushSessionDisplayToLauncher(int sessionId) {
-            SessionInfo session = mInstaller.getSessionInfo(sessionId);
+        private SessionInfo pushSessionDisplayToLauncher(int sessionId) {
+            SessionInfo session = verify(mInstaller.getSessionInfo(sessionId));
             if (session != null && session.getAppPackageName() != null) {
+                mActiveSessions.put(sessionId, session.getAppPackageName());
                 addSessionInfoToCache(session, Process.myUserHandle());
                 LauncherAppState app = LauncherAppState.getInstanceNoCreate();
-
                 if (app != null) {
                     app.getModel().updateSessionDisplayInfo(session.getAppPackageName());
                 }
+                return session;
             }
+            return null;
         }
     };
+
+    private PackageInstaller.SessionInfo verify(PackageInstaller.SessionInfo sessionInfo) {
+        if (sessionInfo == null || sessionInfo.getInstallerPackageName() == null) {
+            return null;
+        }
+        String pkg = sessionInfo.getAppPackageName();
+        synchronized (mSessionVerifiedMap) {
+            if (!mSessionVerifiedMap.containsKey(pkg)) {
+                LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(mAppContext);
+                boolean hasSystemFlag = launcherApps.getApplicationInfo(pkg,
+                        ApplicationInfo.FLAG_SYSTEM, Process.myUserHandle()) != null;
+                mSessionVerifiedMap.put(pkg, DEBUG || hasSystemFlag);
+            }
+        }
+        return mSessionVerifiedMap.get(pkg) ? sessionInfo : null;
+    }
+
+    @Override
+    public List<SessionInfo> getAllVerifiedSessions() {
+        List<SessionInfo> list = new ArrayList<>(mInstaller.getAllSessions());
+        Iterator<SessionInfo> it = list.iterator();
+        while (it.hasNext()) {
+            if (verify(it.next()) == null) {
+                it.remove();
+            }
+        }
+        return list;
+    }
 }
diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
index 5d04325..9c5f189 100644
--- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
+++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
@@ -18,15 +18,18 @@
 import android.content.ComponentName;
 
 import com.android.launcher3.AllAppsList;
+import com.android.launcher3.AppInfo;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherModel.CallbackTask;
 import com.android.launcher3.LauncherModel.Callbacks;
+import com.android.launcher3.PromiseAppInfo;
 import com.android.launcher3.ShortcutInfo;
 import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
 
+import java.util.ArrayList;
 import java.util.HashSet;
 
 /**
@@ -47,6 +50,46 @@
             return;
         }
 
+        synchronized (apps) {
+            final ArrayList<AppInfo> updated = new ArrayList<>();
+            final ArrayList<AppInfo> removed = new ArrayList<>();
+            for (int i=0; i < apps.size(); i++) {
+                final AppInfo appInfo = apps.get(i);
+                final ComponentName tgtComp = appInfo.getTargetComponent();
+                if (tgtComp != null && tgtComp.getPackageName().equals(mInstallInfo.packageName)) {
+                    if (appInfo instanceof PromiseAppInfo) {
+                        final PromiseAppInfo promiseAppInfo = (PromiseAppInfo) appInfo;
+                        if (mInstallInfo.state == PackageInstallerCompat.STATUS_INSTALLING) {
+                            promiseAppInfo.level = mInstallInfo.progress;
+                            updated.add(appInfo);
+                        } else if (mInstallInfo.state == PackageInstallerCompat.STATUS_FAILED
+                                || mInstallInfo.state == PackageInstallerCompat.STATUS_INSTALLED) {
+                            apps.removePromiseApp(appInfo);
+                            removed.add(appInfo);
+                        }
+                    }
+                }
+            }
+            if (!updated.isEmpty()) {
+                scheduleCallbackTask(new CallbackTask() {
+                    @Override
+                    public void execute(Callbacks callbacks) {
+                        // TODO: this currently causes unnecessary relayouts
+                        // we need to introduce a new bindPromiseAppsChanged
+                        callbacks.bindAppsUpdated(updated);
+                    }
+                });
+            }
+            if (!removed.isEmpty()) {
+                scheduleCallbackTask(new CallbackTask() {
+                    @Override
+                    public void execute(Callbacks callbacks) {
+                        callbacks.bindAppInfosRemoved(removed);
+                    }
+                });
+            }
+        }
+
         synchronized (dataModel) {
             final HashSet<ItemInfo> updates = new HashSet<>();
             for (ItemInfo info : dataModel.itemsIdMap) {
@@ -56,7 +99,6 @@
                     if (si.isPromise() && (cn != null)
                             && mInstallInfo.packageName.equals(cn.getPackageName())) {
                         si.setInstallProgress(mInstallInfo.progress);
-
                         if (mInstallInfo.state == PackageInstallerCompat.STATUS_FAILED) {
                             // Mark this info as broken.
                             si.status &= ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE;
diff --git a/src_flags/com/android/launcher3/config/FeatureFlags.java b/src_flags/com/android/launcher3/config/FeatureFlags.java
index 87e9871..9e20748 100644
--- a/src_flags/com/android/launcher3/config/FeatureFlags.java
+++ b/src_flags/com/android/launcher3/config/FeatureFlags.java
@@ -36,7 +36,8 @@
     public static boolean LAUNCHER3_DIRECT_SCROLL = true;
     // When enabled while all-apps open, the soft input will be set to adjust resize .
     public static boolean LAUNCHER3_UPDATE_SOFT_INPUT_MODE = false;
-
+    // When enabled the promise icon is visible in all apps while installation an app.
+    public static boolean LAUNCHER3_PROMISE_APPS_IN_ALL_APPS = false;
 
     // Feature flag to enable moving the QSB on the 0th screen of the workspace.
     public static final boolean QSB_ON_FIRST_SCREEN = true;
diff --git a/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java b/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
index d655562..ed893c4 100644
--- a/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
+++ b/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
@@ -21,9 +21,8 @@
     }
 
     private PackageInstallStateChangedTask newTask(String pkg, int progress) {
-        PackageInstallInfo installInfo = new PackageInstallInfo(pkg);
-        installInfo.progress = progress;
-        installInfo.state = PackageInstallerCompat.STATUS_INSTALLING;
+        int state = PackageInstallerCompat.STATUS_INSTALLING;
+        PackageInstallInfo installInfo = new PackageInstallInfo(pkg, state, progress);
         return new PackageInstallStateChangedTask(installInfo);
     }