Merge "Tweak change voicemail PIN UI" into nyc-mr1-dev
diff --git a/src/com/android/phone/vvm/omtp/ActivationTask.java b/src/com/android/phone/vvm/omtp/ActivationTask.java
index eb43073..e1fea4d 100644
--- a/src/com/android/phone/vvm/omtp/ActivationTask.java
+++ b/src/com/android/phone/vvm/omtp/ActivationTask.java
@@ -43,6 +43,7 @@
 import java.io.IOException;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
 
@@ -101,9 +102,7 @@
         // Check for signal before activating. The event often happen while boot and the
         // network is not connected yet. Launching activation will likely to cause the SMS
         // sending to fail and waste unnecessary time waiting for time out.
-        if (context.getSystemService(TelephonyManager.class)
-            .getServiceStateForSubscriber(subId).getState()
-            != ServiceState.STATE_IN_SERVICE) {
+        if (!hasSignal(context, subId)) {
             VvmLog.i(TAG, "Activation requested while not in service, rejecting");
         }
 
@@ -139,6 +138,12 @@
             return;
         }
 
+        if (!hasSignal(getContext(), subId)) {
+            VvmLog.i(TAG, "Service lost during activation, aborting");
+            // Don't retry, a new activation will be started after the signal returned.
+            return;
+        }
+
         OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(getContext(), subId);
         if (!helper.isValid()) {
             VvmLog.i(TAG, "VVM not supported on subId " + subId);
@@ -190,7 +195,7 @@
             data = mMessageData;
         } else {
             try (StatusSmsFetcher fetcher = new StatusSmsFetcher(getContext(), subId)) {
-                protocol.startActivation(helper);
+                protocol.startActivation(helper, fetcher.getSentIntent());
                 // Both the fetcher and OmtpMessageReceiver will be triggered, but
                 // OmtpMessageReceiver will just route the SMS back to ActivationTask, which will be
                 // rejected because the task is still running.
@@ -201,6 +206,10 @@
                 helper.handleEvent(status, OmtpEvents.CONFIG_STATUS_SMS_TIME_OUT);
                 fail();
                 return;
+            } catch (CancellationException e) {
+                VvmLog.e(TAG, "Unable to send status request SMS");
+                fail();
+                return;
             } catch (InterruptedException | ExecutionException | IOException e) {
                 VvmLog.e(TAG, "can't get future STATUS SMS", e);
                 fail();
@@ -257,6 +266,11 @@
         }
     }
 
+    private static boolean hasSignal(Context context, int subId) {
+        return context.getSystemService(TelephonyManager.class)
+                .getServiceStateForSubscriber(subId).getState() == ServiceState.STATE_IN_SERVICE;
+    }
+
     private static void queueActivationAfterProvisioned(Context context, int subId) {
         if (sDeviceProvisionedObserver == null) {
             sDeviceProvisionedObserver = new DeviceProvisionedObserver(context);
diff --git a/src/com/android/phone/vvm/omtp/OmtpVvmCarrierConfigHelper.java b/src/com/android/phone/vvm/omtp/OmtpVvmCarrierConfigHelper.java
index 147eaf1..0b321b5 100644
--- a/src/com/android/phone/vvm/omtp/OmtpVvmCarrierConfigHelper.java
+++ b/src/com/android/phone/vvm/omtp/OmtpVvmCarrierConfigHelper.java
@@ -16,6 +16,7 @@
 package com.android.phone.vvm.omtp;
 
 import android.annotation.Nullable;
+import android.app.PendingIntent;
 import android.content.Context;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.os.Bundle;
@@ -364,9 +365,9 @@
         }
     }
 
-    public void requestStatus() {
+    public void requestStatus(@Nullable PendingIntent sentIntent) {
         if (mProtocol != null) {
-            mProtocol.requestStatus(this);
+            mProtocol.requestStatus(this, sentIntent);
         }
     }
 
diff --git a/src/com/android/phone/vvm/omtp/protocol/VisualVoicemailProtocol.java b/src/com/android/phone/vvm/omtp/protocol/VisualVoicemailProtocol.java
index e0b6359..6e6d6ef 100644
--- a/src/com/android/phone/vvm/omtp/protocol/VisualVoicemailProtocol.java
+++ b/src/com/android/phone/vvm/omtp/protocol/VisualVoicemailProtocol.java
@@ -17,6 +17,7 @@
 package com.android.phone.vvm.omtp.protocol;
 
 import android.annotation.Nullable;
+import android.app.PendingIntent;
 import android.content.Context;
 import android.os.Bundle;
 import android.telecom.PhoneAccountHandle;
@@ -34,10 +35,10 @@
     /**
      * Activation should cause the carrier to respond with a STATUS SMS.
      */
-    public void startActivation(OmtpVvmCarrierConfigHelper config) {
+    public void startActivation(OmtpVvmCarrierConfigHelper config, PendingIntent sentIntent) {
         OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
         if (messageSender != null) {
-            messageSender.requestVvmActivation(null);
+            messageSender.requestVvmActivation(sentIntent);
         }
     }
 
@@ -58,10 +59,11 @@
         // Do nothing
     }
 
-    public void requestStatus(OmtpVvmCarrierConfigHelper config) {
+    public void requestStatus(OmtpVvmCarrierConfigHelper config,
+            @Nullable PendingIntent sentIntent) {
         OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
         if (messageSender != null) {
-            messageSender.requestVvmStatus(null);
+            messageSender.requestVvmStatus(sentIntent);
         }
     }
 
diff --git a/src/com/android/phone/vvm/omtp/protocol/Vvm3Protocol.java b/src/com/android/phone/vvm/omtp/protocol/Vvm3Protocol.java
index 39d2b34..72044e2 100644
--- a/src/com/android/phone/vvm/omtp/protocol/Vvm3Protocol.java
+++ b/src/com/android/phone/vvm/omtp/protocol/Vvm3Protocol.java
@@ -17,6 +17,7 @@
 package com.android.phone.vvm.omtp.protocol;
 
 import android.annotation.Nullable;
+import android.app.PendingIntent;
 import android.content.Context;
 import android.net.Network;
 import android.os.Bundle;
@@ -82,12 +83,13 @@
     private static final int DEFAULT_PIN_LENGTH = 6;
 
     @Override
-    public void startActivation(OmtpVvmCarrierConfigHelper config) {
+    public void startActivation(OmtpVvmCarrierConfigHelper config,
+            @Nullable PendingIntent sentIntent) {
         // VVM3 does not support activation SMS.
         // Send a status request which will start the provisioning process if the user is not
         // provisioned.
         VvmLog.i(TAG, "Activating");
-        config.requestStatus();
+        config.requestStatus(sentIntent);
     }
 
     @Override
@@ -215,7 +217,7 @@
                     helper.closeNewUserTutorial();
                     VvmLog.i(TAG, "new user: NUT closed");
 
-                    config.requestStatus();
+                    config.requestStatus(null);
                 }
             } catch (InitializingException | MessagingException | IOException e) {
                 config.handleEvent(status, OmtpEvents.VVM3_NEW_USER_SETUP_FAILED);
diff --git a/src/com/android/phone/vvm/omtp/scheduling/TaskSchedulerService.java b/src/com/android/phone/vvm/omtp/scheduling/TaskSchedulerService.java
index 1e099e8..3d6fcdb 100644
--- a/src/com/android/phone/vvm/omtp/scheduling/TaskSchedulerService.java
+++ b/src/com/android/phone/vvm/omtp/scheduling/TaskSchedulerService.java
@@ -19,6 +19,8 @@
 import android.annotation.MainThread;
 import android.annotation.Nullable;
 import android.annotation.WorkerThread;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
 import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
@@ -30,6 +32,7 @@
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.phone.Assert;
 import com.android.phone.NeededForTesting;
@@ -45,9 +48,25 @@
  */
 public class TaskSchedulerService extends Service {
 
-    private static final String TAG = "TaskSchedulerService";
+    private static final String TAG = "VvmTaskScheduler";
+
+    private static final String ACTION_WAKEUP = "action_wakeup";
 
     private static final int READY_TOLERANCE_MILLISECONDS = 100;
+
+    /**
+     * Threshold to determine whether to do a short or long sleep when a task is scheduled in the
+     * future.
+     *
+     * <p>A short sleep will continue to held the wake lock and use {@link
+     * Handler#postDelayed(Runnable, long)} to wait for the next task.
+     *
+     * <p>A long sleep will release the wake lock and set a {@link AlarmManager} alarm. The alarm is
+     * exact and will wake up the device. Note: as this service is run in the telephony process it
+     * does not seem to be restricted by doze or sleep, it will fire exactly at the moment. The
+     * unbundled version should take doze into account.
+     */
+    private static final int SHORT_SLEEP_THRESHOLD_MILLISECONDS = 60_000;
     /**
      * When there are no more tasks to be run the service should be stopped. But when all tasks has
      * finished there might still be more tasks in the message queue waiting to be processed,
@@ -141,7 +160,7 @@
         super.onCreate();
         mWakeLock = getSystemService(PowerManager.class)
                 .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
-        mWakeLock.acquire();
+        mWakeLock.setReferenceCounted(false);
         HandlerThread thread = new HandlerThread("VvmTaskSchedulerService");
         thread.start();
 
@@ -159,12 +178,20 @@
     @MainThread
     public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
         Assert.isMainThread();
-        Task task = createTask(intent, flags, startId);
-        if (task == null) {
-            VvmLog.e(TAG, "cannot create task form intent");
+        // maybeRunNextTask() will release the wakelock either by entering a long sleep or stopping
+        // the service.
+        mWakeLock.acquire();
+        if (ACTION_WAKEUP.equals(intent.getAction())) {
+            VvmLog.d(TAG, "woke up by AlarmManager");
         } else {
-            addTask(task);
+            Task task = createTask(intent, flags, startId);
+            if (task == null) {
+                VvmLog.e(TAG, "cannot create task form intent");
+            } else {
+                addTask(task);
+            }
         }
+        maybeRunNextTask();
         // STICKY means the service will be automatically restarted will the last intent if it is
         // killed.
         return START_NOT_STICKY;
@@ -187,7 +214,6 @@
         mMainThreadHandler.removeCallbacks(mStopServiceWithDelay);
         getTasks().add(task);
         maybeRunNextTask();
-
     }
 
     @MainThread
@@ -258,6 +284,8 @@
     @MainThread
     void runNextTask() {
         Assert.isMainThread();
+        // The current alarm is no longer valid, a new one will be set up if required.
+        getSystemService(AlarmManager.class).cancel(getWakeupIntent());
         if (getTasks().isEmpty()) {
             prepareStop();
             return;
@@ -280,15 +308,37 @@
         }
         VvmLog.d(TAG, "minimal wait time:" + minimalWaitTime);
         if (!mTaskAutoRunDisabledForTesting && minimalWaitTime != null) {
-            // No tests are currently ready. Sleep until the next one should be.
+            // No tasks are currently ready. Sleep until the next one should be.
             // If a new task is added during the sleep the service will wake immediately.
+            sleep(minimalWaitTime);
+        }
+    }
+
+    private void sleep(long timeMillis) {
+        if (timeMillis < SHORT_SLEEP_THRESHOLD_MILLISECONDS) {
             mMainThreadHandler.postDelayed(new Runnable() {
                 @Override
                 public void run() {
                     maybeRunNextTask();
                 }
-            }, minimalWaitTime);
+            }, timeMillis);
+            return;
         }
+
+        // Tasks does not have a strict timing requirement, use AlarmManager.set() so the OS could
+        // optimize the battery usage. As this service currently run in the telephony process the
+        // OS give it privileges to behave the same as setExact(), but set() is the targeted
+        // behavior once this is unbundled.
+        getSystemService(AlarmManager.class).set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                SystemClock.elapsedRealtime() + timeMillis,
+                getWakeupIntent());
+        mWakeLock.release();
+        VvmLog.d(TAG, "Long sleep for " + timeMillis + " millis");
+    }
+
+    private PendingIntent getWakeupIntent() {
+        Intent intent = new Intent(ACTION_WAKEUP, null, this, getClass());
+        return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
     }
 
     private void prepareStop() {
diff --git a/src/com/android/phone/vvm/omtp/sms/StatusSmsFetcher.java b/src/com/android/phone/vvm/omtp/sms/StatusSmsFetcher.java
index 2c37dd9..2f3cbfa 100644
--- a/src/com/android/phone/vvm/omtp/sms/StatusSmsFetcher.java
+++ b/src/com/android/phone/vvm/omtp/sms/StatusSmsFetcher.java
@@ -17,13 +17,17 @@
 package com.android.phone.vvm.omtp.sms;
 
 import android.annotation.MainThread;
+import android.annotation.Nullable;
 import android.annotation.WorkerThread;
+import android.app.Activity;
+import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.os.Bundle;
 import android.provider.VoicemailContract;
+import android.telephony.SmsManager;
 import com.android.phone.Assert;
 import com.android.phone.vvm.omtp.OmtpConstants;
 import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper;
@@ -31,6 +35,7 @@
 import com.android.phone.vvm.omtp.protocol.VisualVoicemailProtocol;
 import java.io.Closeable;
 import java.io.IOException;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
@@ -44,6 +49,8 @@
     private static final String TAG = "VvmStatusSmsFetcher";
 
     private static final long STATUS_SMS_TIMEOUT_MILLIS = 60_000;
+    private static final String ACTION_REQUEST_SENT_INTENT = "action_request_sent_intent";
+    private static final int ACTION_REQUEST_SENT_REQUEST_CODE = 0;
 
     private CompletableFuture<Bundle> mFuture = new CompletableFuture<>();
 
@@ -54,6 +61,7 @@
         mContext = context;
         mSubId = subId;
         IntentFilter filter = new IntentFilter(VoicemailContract.ACTION_VOICEMAIL_SMS_RECEIVED);
+        filter.addAction(ACTION_REQUEST_SENT_INTENT);
         context.registerReceiver(this, filter);
     }
 
@@ -63,16 +71,38 @@
     }
 
     @WorkerThread
-    public Bundle get()
-            throws InterruptedException, ExecutionException, TimeoutException {
+    @Nullable
+    public Bundle get() throws InterruptedException, ExecutionException, TimeoutException,
+            CancellationException {
         Assert.isNotMainThread();
         return mFuture.get(STATUS_SMS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
     }
 
+    public PendingIntent getSentIntent() {
+        Intent intent = new Intent(ACTION_REQUEST_SENT_INTENT);
+        // Because the receiver is registered dynamically, implicit intent must be used.
+        // There should only be a single status SMS request at a time.
+        return PendingIntent.getBroadcast(mContext, ACTION_REQUEST_SENT_REQUEST_CODE, intent,
+                PendingIntent.FLAG_CANCEL_CURRENT);
+    }
+
     @Override
     @MainThread
     public void onReceive(Context context, Intent intent) {
         Assert.isMainThread();
+        if (ACTION_REQUEST_SENT_INTENT.equals(intent.getAction())) {
+            int resultCode = getResultCode();
+
+            if (resultCode == Activity.RESULT_OK) {
+                VvmLog.d(TAG, "Request SMS successfully sent");
+                return;
+            }
+
+            VvmLog.e(TAG, "Request SMS send failed: " + sentSmsResultToString(resultCode));
+            mFuture.cancel(true);
+            return;
+        }
+
         int subId = intent.getExtras().getInt(VoicemailContract.EXTRA_VOICEMAIL_SMS_SUBID);
 
         if (mSubId != subId) {
@@ -105,4 +135,21 @@
             mFuture.complete(translatedBundle);
         }
     }
+
+    private static String sentSmsResultToString(int resultCode) {
+        switch (resultCode) {
+            case Activity.RESULT_OK:
+                return "OK";
+            case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
+                return "RESULT_ERROR_GENERIC_FAILURE";
+            case SmsManager.RESULT_ERROR_NO_SERVICE:
+                return "RESULT_ERROR_GENERIC_FAILURE";
+            case SmsManager.RESULT_ERROR_NULL_PDU:
+                return "RESULT_ERROR_GENERIC_FAILURE";
+            case SmsManager.RESULT_ERROR_RADIO_OFF:
+                return "RESULT_ERROR_GENERIC_FAILURE";
+            default:
+                return "UNKNOWN CODE: " + resultCode;
+        }
+    }
 }