Wake up from the standby mode and display message on the keyguard screen

If the device is in the standby mode when receiving DISPLAY TEXT command
or GET INPUT/INKEY command from the SIM card, it must get out of the
standby mode, display a message on the keyguard screen. Once the device
is unlocked, it must display the screen or the dialog associated with
the command. Refer to the descriptions in GSMA PDATA.12.

Bug: 70004658
Test: Performed manual test cases for the affected scenarios.

Change-Id: Ieeafd446e89882b2af163677c67db5f734d1cd9d
diff --git a/src/com/android/stk/StkAppService.java b/src/com/android/stk/StkAppService.java
index dc551a6..a85402c 100644
--- a/src/com/android/stk/StkAppService.java
+++ b/src/com/android/stk/StkAppService.java
@@ -19,6 +19,7 @@
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.AlertDialog;
+import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
@@ -27,6 +28,7 @@
 import android.app.Activity;
 import android.app.ActivityManagerNative;
 import android.app.IProcessObserver;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
@@ -37,6 +39,7 @@
 import android.content.res.Resources.NotFoundException;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.media.RingtoneManager;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -64,6 +67,7 @@
 import android.content.IntentFilter;
 
 import com.android.internal.telephony.cat.AppInterface;
+import com.android.internal.telephony.cat.Input;
 import com.android.internal.telephony.cat.LaunchBrowserMode;
 import com.android.internal.telephony.cat.Menu;
 import com.android.internal.telephony.cat.Item;
@@ -108,7 +112,8 @@
         protected boolean mIsInputPending = false;
         protected boolean mIsMenuPending = false;
         protected boolean mIsDialogPending = false;
-        protected boolean responseNeeded = true;
+        protected boolean mNotificationOnKeyguard = false;
+        protected boolean mNoResponseFromUser = false;
         protected boolean launchBrowser = false;
         protected BrowserSettings mBrowserSettings = null;
         protected LinkedList<DelayedCmd> mCmdsQ = null;
@@ -264,6 +269,10 @@
     // system property to set the STK specific default url for launch browser proactive cmds
     private static final String STK_BROWSER_DEFAULT_URL_SYSPROP = "persist.radio.stk.default_url";
 
+    private static final int NOTIFICATION_ON_KEYGUARD = 1;
+    private static final long[] VIBRATION_PATTERN = new long[] { 0, 350, 250, 350 };
+    private BroadcastReceiver mUserPresentReceiver = null;
+
     @Override
     public void onCreate() {
         CatLog.d(LOG_TAG, "onCreate()+");
@@ -832,6 +841,7 @@
         mStkContext[slotId].mIsInputPending = false;
         mStkContext[slotId].mIsMenuPending = false;
         mStkContext[slotId].mIsDialogPending = false;
+        mStkContext[slotId].mNoResponseFromUser = false;
 
         if (mStkContext[slotId].mMainCmd == null) {
             CatLog.d(LOG_TAG, "[handleSessionEnd][mMainCmd is null!]");
@@ -1251,6 +1261,24 @@
             return;
         }
 
+        switch (args.getInt(RES_ID)) {
+            case RES_ID_MENU_SELECTION:
+            case RES_ID_INPUT:
+            case RES_ID_CONFIRM:
+            case RES_ID_CHOICE:
+            case RES_ID_BACKWARD:
+            case RES_ID_END_SESSION:
+                mStkContext[slotId].mNoResponseFromUser = false;
+                break;
+            case RES_ID_TIMEOUT:
+                cancelNotificationOnKeyguard(slotId);
+                mStkContext[slotId].mNoResponseFromUser = true;
+                break;
+            default:
+                // The other IDs cannot be used to judge if there is no response from user.
+                break;
+        }
+
         if (null != mStkContext[slotId].mCurrentCmd &&
                 null != mStkContext[slotId].mCurrentCmd.getCmdType()) {
             CatLog.d(LOG_TAG, "handleCmdResponse- cmdName[" +
@@ -1392,14 +1420,19 @@
         String uriString = STK_INPUT_URI + System.currentTimeMillis();
         //Set unique URI to create a new instance of activity for different slotId.
         Uri uriData = Uri.parse(uriString);
+        Input input = mStkContext[slotId].mCurrentCmd.geInput();
 
         CatLog.d(LOG_TAG, "launchInputActivity, slotId: " + slotId);
         newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                             | getFlagActivityNoUserAction(InitiatedByUserAction.unknown, slotId));
         newIntent.setClassName(PACKAGE_NAME, targetActivity);
-        newIntent.putExtra("INPUT", mStkContext[slotId].mCurrentCmd.geInput());
+        newIntent.putExtra("INPUT", input);
         newIntent.putExtra(SLOT_ID, slotId);
         newIntent.setData(uriData);
+
+        if (input != null) {
+            notifyUserIfNecessary(slotId, input.text);
+        }
         mContext.startActivity(newIntent);
     }
 
@@ -1411,22 +1444,133 @@
         String uriString = STK_DIALOG_URI + System.currentTimeMillis();
         //Set unique URI to create a new instance of activity for different slotId.
         Uri uriData = Uri.parse(uriString);
-        if (newIntent != null) {
-            newIntent.setClassName(PACKAGE_NAME, targetActivity);
-            newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
-                    | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
-                    | getFlagActivityNoUserAction(InitiatedByUserAction.unknown, slotId));
-            newIntent.setData(uriData);
-            newIntent.putExtra("TEXT", mStkContext[slotId].mCurrentCmd.geTextMessage());
-            newIntent.putExtra(SLOT_ID, slotId);
-            startActivity(newIntent);
-            // For display texts with immediate response, send the terminal response
-            // immediately. responseNeeded will be false, if display text command has
-            // the immediate response tlv.
-            if (!mStkContext[slotId].mCurrentCmd.geTextMessage().responseNeeded) {
-                sendResponse(RES_ID_CONFIRM, slotId, true);
+        TextMessage textMessage = mStkContext[slotId].mCurrentCmd.geTextMessage();
+
+        newIntent.setClassName(PACKAGE_NAME, targetActivity);
+        newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
+                | getFlagActivityNoUserAction(InitiatedByUserAction.unknown, slotId));
+        newIntent.setData(uriData);
+        newIntent.putExtra("TEXT", textMessage);
+        newIntent.putExtra(SLOT_ID, slotId);
+
+        if (textMessage != null) {
+            notifyUserIfNecessary(slotId, textMessage.text);
+        }
+        startActivity(newIntent);
+        // For display texts with immediate response, send the terminal response
+        // immediately. responseNeeded will be false, if display text command has
+        // the immediate response tlv.
+        if (!mStkContext[slotId].mCurrentCmd.geTextMessage().responseNeeded) {
+            sendResponse(RES_ID_CONFIRM, slotId, true);
+        }
+    }
+
+    private void notifyUserIfNecessary(int slotId, String message) {
+        createAllChannels();
+
+        if (mStkContext[slotId].mNoResponseFromUser) {
+            // No response from user was observed in the current session.
+            // Do nothing in that case in order to avoid turning on the screen again and again
+            // when the card repeatedly sends the same command in its retry procedure.
+            return;
+        }
+
+        PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
+
+        if (((KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE)).isKeyguardLocked()) {
+            // Display the notification on the keyguard screen
+            // if user cannot see the message from the card right now because of it.
+            // The notification can be dismissed if user removed the keyguard screen.
+            launchNotificationOnKeyguard(slotId, message);
+        } else if (!(pm.isInteractive() && isTopOfStack())) {
+            // User might be doing something but it is not related to the SIM Toolkit.
+            // Play the tone and do vibration in order to attract user's attention.
+            // User will see the input screen or the dialog soon in this case.
+            NotificationChannel channel = mNotificationManager
+                    .getNotificationChannel(STK_NOTIFICATION_CHANNEL_ID);
+            Uri uri = channel.getSound();
+            if (uri != null && !Uri.EMPTY.equals(uri)
+                    && (NotificationManager.IMPORTANCE_LOW) < channel.getImportance()) {
+                RingtoneManager.getRingtone(getApplicationContext(), uri).play();
+            }
+            long[] pattern = channel.getVibrationPattern();
+            if (pattern != null && channel.shouldVibrate()) {
+                ((Vibrator) this.getSystemService(Context.VIBRATOR_SERVICE))
+                        .vibrate(pattern, -1);
             }
         }
+
+        // Turn on the screen.
+        PowerManager.WakeLock wakelock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK
+                | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE, LOG_TAG);
+        wakelock.acquire();
+        wakelock.release();
+    }
+
+    private void launchNotificationOnKeyguard(int slotId, String message) {
+        Notification.Builder builder = new Notification.Builder(this, STK_NOTIFICATION_CHANNEL_ID);
+
+        builder.setStyle(new Notification.BigTextStyle(builder).bigText(message));
+        builder.setContentText(message);
+
+        Menu menu = getMainMenu(slotId);
+        if (menu == null || TextUtils.isEmpty(menu.title)) {
+            builder.setContentTitle(getResources().getString(R.string.app_name));
+        } else {
+            builder.setContentTitle(menu.title);
+        }
+
+        builder.setSmallIcon(com.android.internal.R.drawable.stat_notify_sim_toolkit);
+        builder.setOngoing(true);
+        builder.setOnlyAlertOnce(true);
+        builder.setColor(getResources().getColor(
+                com.android.internal.R.color.system_notification_accent_color));
+
+        registerUserPresentReceiver();
+        mNotificationManager.notify(getNotificationId(NOTIFICATION_ON_KEYGUARD, slotId),
+                builder.build());
+        mStkContext[slotId].mNotificationOnKeyguard = true;
+    }
+
+    private void cancelNotificationOnKeyguard(int slotId) {
+        mNotificationManager.cancel(getNotificationId(NOTIFICATION_ON_KEYGUARD, slotId));
+        mStkContext[slotId].mNotificationOnKeyguard = false;
+        unregisterUserPresentReceiver(slotId);
+    }
+
+    private synchronized void registerUserPresentReceiver() {
+        if (mUserPresentReceiver == null) {
+            mUserPresentReceiver = new BroadcastReceiver() {
+                @Override public void onReceive(Context context, Intent intent) {
+                    if (Intent.ACTION_USER_PRESENT.equals(intent.getAction())) {
+                        for (int slot = 0; slot < mSimCount; slot++) {
+                            cancelNotificationOnKeyguard(slot);
+                        }
+                    }
+                }
+            };
+            registerReceiver(mUserPresentReceiver, new IntentFilter(Intent.ACTION_USER_PRESENT));
+        }
+    }
+
+    private synchronized void unregisterUserPresentReceiver(int slotId) {
+        if (mUserPresentReceiver != null) {
+            for (int slot = PhoneConstants.SIM_ID_1; slot < mSimCount; slot++) {
+                if (slot != slotId) {
+                    if (mStkContext[slot].mNotificationOnKeyguard) {
+                        // The broadcast receiver is still necessary for other SIM card.
+                        return;
+                    }
+                }
+            }
+            unregisterReceiver(mUserPresentReceiver);
+            mUserPresentReceiver = null;
+        }
+    }
+
+    private int getNotificationId(int notificationType, int slotId) {
+        return getNotificationId(slotId) + (notificationType * mSimCount);
     }
 
     public boolean isStkDialogActivated(Context context) {
@@ -1696,17 +1840,15 @@
         //Set unique URI to create a new instance of activity for different slotId.
         Uri uriData = Uri.parse(uriString);
 
-        if (newIntent != null) {
-            newIntent.setClassName(this, targetActivity);
-            newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
-                    | Intent.FLAG_ACTIVITY_NO_HISTORY
-                    | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
-                    | getFlagActivityNoUserAction(InitiatedByUserAction.unknown, slotId));
-            newIntent.putExtra("TEXT", msg);
-            newIntent.putExtra(SLOT_ID, slotId);
-            newIntent.setData(uriData);
-            startActivity(newIntent);
-        }
+        newIntent.setClassName(this, targetActivity);
+        newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                | Intent.FLAG_ACTIVITY_NO_HISTORY
+                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
+                | getFlagActivityNoUserAction(InitiatedByUserAction.unknown, slotId));
+        newIntent.putExtra("TEXT", msg);
+        newIntent.putExtra(SLOT_ID, slotId);
+        newIntent.setData(uriData);
+        startActivity(newIntent);
     }
 
     private void launchBrowser(BrowserSettings settings) {
@@ -1791,6 +1933,7 @@
                     .setSmallIcon(com.android.internal.R.drawable.stat_notify_sim_toolkit);
             notificationBuilder.setContentIntent(pendingIntent);
             notificationBuilder.setOngoing(true);
+            notificationBuilder.setOnlyAlertOnce(true);
             // Set text and icon for the status bar and notification body.
             if (mStkContext[slotId].mIdleModeTextCmd.hasIconLoadFailed() ||
                     !msg.iconSelfExplanatory) {
@@ -1817,10 +1960,15 @@
      * ignore this call.
      */
     private void createAllChannels() {
-        mNotificationManager.createNotificationChannel(new NotificationChannel(
+        NotificationChannel notificationChannel = new NotificationChannel(
                 STK_NOTIFICATION_CHANNEL_ID,
                 getResources().getString(R.string.stk_channel_name),
-                NotificationManager.IMPORTANCE_MIN));
+                NotificationManager.IMPORTANCE_DEFAULT);
+
+        notificationChannel.enableVibration(true);
+        notificationChannel.setVibrationPattern(VIBRATION_PATTERN);
+
+        mNotificationManager.createNotificationChannel(notificationChannel);
     }
 
     private void launchToneDialog(int slotId) {