Add ongoing notification for instant apps

Show a BTW notification when instant app is top of either of the
stacks.

Bug: 35152138
Test: Open ephemeral app, look at notifications.
Change-Id: Id992dd893a6632e9b95948a9e11167762f42bc49
diff --git a/packages/SystemUI/res/drawable/instant_icon.xml b/packages/SystemUI/res/drawable/instant_icon.xml
new file mode 100644
index 0000000..0039c81
--- /dev/null
+++ b/packages/SystemUI/res/drawable/instant_icon.xml
@@ -0,0 +1,30 @@
+<!--
+    Copyright (C) 2015 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="40dp"
+        android:height="40dp"
+        android:viewportWidth="2.2"
+        android:viewportHeight="2.2">
+
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M.1,1.1
+        c0,.55  .45,1   1,1
+        c.55,0  1,-.45  1,-1
+        c0,-.55 -.45,-1 -1,-1
+        c-.55,0 -1,.45  -1,1z
+        M1.15,.95 l.5,0 l-.7,1 l0.1,-.7 l-.5,0 l.7,-1 z"/>
+</vector>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 7f30c83..407cddf 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -147,4 +147,6 @@
     <color name="ksh_key_item_color">@color/material_grey_600</color>
     <color name="ksh_key_item_background">@color/material_grey_100</color>
 
+    <color name="instant_apps_color">#ff4d5a64</color>
+
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index b825cfb..815c41f 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1810,4 +1810,13 @@
     <!-- Title for the notification channel for problems with storage (i.e. low disk). [CHAR LIMIT=NONE] -->
     <string name="notification_channel_storage">Storage</string>
 
+    <!-- App label of the instant apps notification [CHAR LIMIT=60] -->
+    <string name="instant_apps">Instant Apps</string>
+
+    <!-- Message of the instant apps notification indicating they don't need install [CHAR LIMIT=NONE] -->
+    <string name="instant_apps_message">Instant apps don\'t require installation.</string>
+
+    <!-- Action label for launching app info on the specified app [CHAR LIMIT=20] -->
+    <string name="app_info">App info</string>
+
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index 93f874d..aebc2c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -17,30 +17,57 @@
 package com.android.systemui.statusbar.phone;
 
 import android.app.ActivityManager;
+import android.app.ActivityManager.RunningAppProcessInfo;
+import android.app.ActivityManager.StackId;
+import android.app.ActivityManager.StackInfo;
 import android.app.AlarmManager;
 import android.app.AlarmManager.AlarmClockInfo;
+import android.app.AppGlobals;
+import android.app.Notification;
+import android.app.Notification.Action;
+import android.app.Notification.BigTextStyle;
+import android.app.Notification.Style;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.app.SynchronousUserSwitchObserver;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.UserInfo;
+import android.graphics.drawable.Icon;
 import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.provider.Settings.Global;
+import android.service.notification.StatusBarNotification;
 import android.telecom.TelecomManager;
+import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Log;
+import android.util.Pair;
 
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.telephony.IccCardConstants;
 import com.android.internal.telephony.TelephonyIntents;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.R.string;
 import com.android.systemui.SysUiServiceProvider;
 import com.android.systemui.qs.tiles.DndTile;
 import com.android.systemui.qs.tiles.RotationLockTile;
+import com.android.systemui.recents.Recents;
+import com.android.systemui.recents.misc.SystemServicesProxy;
+import com.android.systemui.recents.misc.SystemServicesProxy.TaskStackListener;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.CommandQueue.Callbacks;
 import com.android.systemui.statusbar.policy.BluetoothController;
@@ -58,6 +85,10 @@
 import com.android.systemui.statusbar.policy.RotationLockController.RotationLockControllerCallback;
 import com.android.systemui.statusbar.policy.UserInfoController;
 import com.android.systemui.statusbar.policy.ZenModeController;
+import com.android.systemui.util.NotificationChannels;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * This class contains all of the policy about which icons are installed in the status
@@ -96,6 +127,7 @@
     private final ZenModeController mZenController;
     private final DeviceProvisionedController mProvisionedController;
     private final KeyguardMonitor mKeyguardMonitor;
+    private final ArraySet<Pair<String, Integer>> mCurrentNotifs = new ArraySet<>();
 
     // Assume it's all good unless we hear otherwise.  We don't always seem
     // to get broadcasts that it *is* there.
@@ -163,7 +195,7 @@
         }
 
         // TTY status
-        mIconController.setIcon(mSlotTty,  R.drawable.stat_sys_tty_mode, null);
+        mIconController.setIcon(mSlotTty, R.drawable.stat_sys_tty_mode, null);
         mIconController.setIconVisibility(mSlotTty, false);
 
         // bluetooth status
@@ -212,6 +244,15 @@
         mKeyguardMonitor.addCallback(this);
 
         SysUiServiceProvider.getComponent(mContext, CommandQueue.class).addCallbacks(this);
+        SystemServicesProxy.getInstance(mContext).registerTaskStackListener(mTaskListener);
+
+        // Clear out all old notifications on startup (only present in the case where sysui dies)
+        NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
+        for (StatusBarNotification notification : noMan.getActiveNotifications()) {
+            if (notification.getId() == SystemMessage.NOTE_INSTANT_APPS) {
+                noMan.cancel(notification.getTag(), notification.getId());
+            }
+        }
     }
 
     public void destroy() {
@@ -226,6 +267,10 @@
         mKeyguardMonitor.removeCallback(this);
         SysUiServiceProvider.getComponent(mContext, CommandQueue.class).removeCallbacks(this);
         mContext.unregisterReceiver(mIntentReceiver);
+
+        NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
+        mCurrentNotifs.forEach(v -> noMan.cancelAsUser(v.first, SystemMessage.NOTE_INSTANT_APPS,
+                new UserHandle(v.second)));
     }
 
     @Override
@@ -423,8 +468,10 @@
     }
 
     private void updateManagedProfile() {
-        if (DEBUG) Log.v(TAG, "updateManagedProfile: mManagedProfileFocused: "
-                + mManagedProfileFocused);
+        if (DEBUG) {
+            Log.v(TAG, "updateManagedProfile: mManagedProfileFocused: "
+                    + mManagedProfileFocused);
+        }
         final boolean showIcon;
         if (mManagedProfileFocused && !mKeyguardMonitor.isShowing()) {
             showIcon = true;
@@ -445,6 +492,76 @@
         }
     }
 
+    private void updateForegroundInstantApps() {
+        NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
+        ArraySet<Pair<String, Integer>> notifs = new ArraySet<>(mCurrentNotifs);
+        IPackageManager pm = AppGlobals.getPackageManager();
+        mCurrentNotifs.clear();
+        try {
+            int[] STACKS_TO_CHECK = new int[]{
+                    StackId.FULLSCREEN_WORKSPACE_STACK_ID,
+                    StackId.DOCKED_STACK_ID,
+            };
+            for (int i = 0; i < STACKS_TO_CHECK.length; i++) {
+                StackInfo info = ActivityManager.getService().getStackInfo(STACKS_TO_CHECK[i]);
+                if (info == null || info.topActivity == null) continue;
+                String pkg = info.topActivity.getPackageName();
+                if (!hasNotif(notifs, pkg, info.userId)) {
+                    // TODO: Optimize by not always needing to get application info.
+                    // Maybe cache non-ephemeral packages?
+                    ApplicationInfo appInfo = pm.getApplicationInfo(pkg, 0, info.userId);
+                    if (appInfo.isInstantApp()) {
+                        postEphemeralNotif(pkg, info.userId, appInfo, noMan);
+                    }
+                }
+            }
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+        // Cancel all the leftover notifications that don't have a foreground process anymore.
+        notifs.forEach(v -> noMan.cancelAsUser(v.first, SystemMessage.NOTE_INSTANT_APPS,
+                new UserHandle(v.second)));
+    }
+
+    private void postEphemeralNotif(String pkg, int userId, ApplicationInfo appInfo,
+            NotificationManager noMan) {
+        final Bundle extras = new Bundle();
+        extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
+                mContext.getString(R.string.instant_apps));
+        mCurrentNotifs.add(new Pair<>(pkg, userId));
+        String message = mContext.getString(R.string.instant_apps_message);
+        PendingIntent appInfoAction = PendingIntent.getActivity(mContext, 0,
+                new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+                        .setData(Uri.fromParts("package", pkg, null)), 0);
+        // TODO: Add action for go to web as well.
+        Action action = new Notification.Action.Builder(null, mContext.getString(R.string.app_info),
+                appInfoAction).build();
+
+        noMan.notifyAsUser(pkg, SystemMessage.NOTE_INSTANT_APPS,
+                new Notification.Builder(mContext, NotificationChannels.GENERAL)
+                        .addExtras(extras)
+                        .addAction(action)
+                        .setContentIntent(appInfoAction)
+                        .setColor(mContext.getColor(R.color.instant_apps_color))
+                        .setContentTitle(appInfo.loadLabel(mContext.getPackageManager()))
+                        .setLargeIcon(Icon.createWithResource(pkg, appInfo.icon))
+                        .setSmallIcon(Icon.createWithResource(mContext.getPackageName(),
+                                R.drawable.instant_icon))
+                        .setContentText(message)
+                        .setOngoing(true)
+                        .build(),
+                new UserHandle(userId));
+    }
+
+    private boolean hasNotif(ArraySet<Pair<String, Integer>> notifs, String pkg, int userId) {
+        Pair<String, Integer> key = new Pair<>(pkg, userId);
+        if (notifs.remove(key)) {
+            mCurrentNotifs.add(key);
+            return true;
+        }
+        return false;
+    }
+
     private final SynchronousUserSwitchObserver mUserSwitchListener =
             new SynchronousUserSwitchObserver() {
                 @Override
@@ -466,6 +583,7 @@
                             profileChanged(newUserId);
                             updateQuietState();
                             updateManagedProfile();
+                            updateForegroundInstantApps();
                         }
                     });
                 }
@@ -497,20 +615,22 @@
 
     private final NextAlarmController.NextAlarmChangeCallback mNextAlarmCallback =
             new NextAlarmController.NextAlarmChangeCallback() {
-        @Override
-        public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) {
-            updateAlarm();
-        }
-    };
+                @Override
+                public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) {
+                    updateAlarm();
+                }
+            };
 
     @Override
     public void appTransitionStarting(long startTime, long duration, boolean forced) {
         updateManagedProfile();
+        updateForegroundInstantApps();
     }
 
     @Override
     public void onKeyguardShowingChanged() {
         updateManagedProfile();
+        updateForegroundInstantApps();
     }
 
     @Override
@@ -524,6 +644,11 @@
     }
 
     @Override
+    public void preloadRecentApps() {
+        updateForegroundInstantApps();
+    }
+
+    @Override
     public void onRotationLockStateChanged(boolean rotationLocked, boolean affordanceVisible) {
         boolean portrait = RotationLockTile.isCurrentOrientationLockPortrait(
                 mRotationLockController, mContext);
@@ -561,6 +686,14 @@
         mIconController.setIconVisibility(mSlotDataSaver, isDataSaving);
     }
 
+    private final TaskStackListener mTaskListener = new TaskStackListener() {
+        @Override
+        public void onTaskStackChanged() {
+            // Listen for changes to stacks and then check which instant apps are foreground.
+            updateForegroundInstantApps();
+        }
+    };
+
     private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index 5b91776..74f5cf5 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -52,6 +52,10 @@
     // Package: com.android.systemui
     NOTE_PLUGIN = 6;
 
+    // Notify the user that instant app is running.
+    // Package: com.android.systemui
+    NOTE_INSTANT_APPS = 7;
+
     // Confirm that the user wants to remove the guest account.
     // Package: com.android.systemui
     NOTE_REMOVE_GUEST = 1010;