Merge "Make sure that voicemail transcriptions are processed serially"
diff --git a/java/com/android/voicemail/impl/AndroidManifest.xml b/java/com/android/voicemail/impl/AndroidManifest.xml
index e7ab581..1998d35 100644
--- a/java/com/android/voicemail/impl/AndroidManifest.xml
+++ b/java/com/android/voicemail/impl/AndroidManifest.xml
@@ -141,6 +141,10 @@
 
     <receiver android:name="com.android.voicemail.impl.transcribe.GetTranscriptReceiver"
         android:exported="false">
+      <intent-filter>
+        <action
+            android:name="com.android.voicemail.impl.transcribe.GetTranscriptReceiver.POLL_ALARM" />
+      </intent-filter>
     </receiver>
   </application>
 </manifest>
diff --git a/java/com/android/voicemail/impl/transcribe/GetTranscriptReceiver.java b/java/com/android/voicemail/impl/transcribe/GetTranscriptReceiver.java
index cc204ff..cbf1657 100644
--- a/java/com/android/voicemail/impl/transcribe/GetTranscriptReceiver.java
+++ b/java/com/android/voicemail/impl/transcribe/GetTranscriptReceiver.java
@@ -23,11 +23,13 @@
 import android.net.Uri;
 import android.os.SystemClock;
 import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
 import android.util.Pair;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.backoff.ExponentialBaseCalculator;
 import com.android.dialer.common.concurrent.DialerExecutor.Worker;
 import com.android.dialer.common.concurrent.DialerExecutorComponent;
+import com.android.dialer.common.concurrent.ThreadUtil;
 import com.android.dialer.logging.DialerImpression;
 import com.android.dialer.logging.Logger;
 import com.android.voicemail.impl.VvmLog;
@@ -36,6 +38,7 @@
 import com.android.voicemail.impl.transcribe.grpc.TranscriptionClientFactory;
 import com.google.internal.communications.voicemailtranscription.v1.GetTranscriptRequest;
 import com.google.internal.communications.voicemailtranscription.v1.TranscriptionStatus;
+import java.util.List;
 
 /**
  * This class uses the AlarmManager to poll for the result of a voicemail transcription request.
@@ -50,6 +53,9 @@
   static final String EXTRA_DELAY_MILLIS = "extra_delay_millis";
   static final String EXTRA_BASE_MULTIPLIER = "extra_base_multiplier";
   static final String EXTRA_REMAINING_ATTEMPTS = "extra_remaining_attempts";
+  static final String EXTRA_PHONE_ACCOUNT = "extra_phone_account";
+  static final String POLL_ALARM_ACTION =
+      "com.android.voicemail.impl.transcribe.GetTranscriptReceiver.POLL_ALARM";
 
   // Schedule an initial alarm to begin checking for a voicemail transcription result.
   static void beginPolling(
@@ -57,7 +63,9 @@
       Uri voicemailUri,
       String transcriptId,
       long estimatedTranscriptionTimeMillis,
-      TranscriptionConfigProvider configProvider) {
+      TranscriptionConfigProvider configProvider,
+      PhoneAccountHandle account) {
+    Assert.checkState(!hasPendingAlarm(context));
     long initialDelayMillis = configProvider.getInitialGetTranscriptPollDelayMillis();
     long maxBackoffMillis = configProvider.getMaxGetTranscriptPollTimeMillis();
     int maxAttempts = configProvider.getMaxGetTranscriptPolls();
@@ -65,7 +73,13 @@
         ExponentialBaseCalculator.findBase(initialDelayMillis, maxBackoffMillis, maxAttempts);
     Intent intent =
         makeAlarmIntent(
-            context, voicemailUri, transcriptId, initialDelayMillis, baseMultiplier, maxAttempts);
+            context,
+            voicemailUri,
+            transcriptId,
+            initialDelayMillis,
+            baseMultiplier,
+            maxAttempts,
+            account);
     // Add an extra to distinguish this initial estimated transcription wait from subsequent backoff
     // waits
     intent.putExtra(EXTRA_IS_INITIAL_ESTIMATED_WAIT, true);
@@ -77,9 +91,17 @@
     scheduleAlarm(context, estimatedTranscriptionTimeMillis, intent);
   }
 
+  static boolean hasPendingAlarm(Context context) {
+    Intent intent = makeBaseAlarmIntent(context);
+    return getPendingIntent(context, intent, PendingIntent.FLAG_NO_CREATE) != null;
+  }
+
   // Alarm fired, poll for transcription result on a background thread
   @Override
   public void onReceive(Context context, Intent intent) {
+    if (intent == null || !POLL_ALARM_ACTION.equals(intent.getAction())) {
+      return;
+    }
     String transcriptId = intent.getStringExtra(EXTRA_TRANSCRIPT_ID);
     VvmLog.i(TAG, "onReceive, for transcript id: " + transcriptId);
     DialerExecutorComponent.get(context)
@@ -101,7 +123,7 @@
 
   private static void scheduleAlarm(Context context, long delayMillis, Intent intent) {
     PendingIntent alarmIntent =
-        PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+        getPendingIntent(context, intent, PendingIntent.FLAG_UPDATE_CURRENT);
     AlarmManager alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
     alarmMgr.set(
         AlarmManager.ELAPSED_REALTIME_WAKEUP,
@@ -109,22 +131,46 @@
         alarmIntent);
   }
 
+  private static boolean cancelAlarm(Context context, Intent intent) {
+    PendingIntent alarmIntent = getPendingIntent(context, intent, PendingIntent.FLAG_NO_CREATE);
+    if (alarmIntent != null) {
+      AlarmManager alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+      alarmMgr.cancel(alarmIntent);
+      alarmIntent.cancel();
+      return true;
+    } else {
+      return false;
+    }
+  }
+
   private static Intent makeAlarmIntent(
       Context context,
       Uri voicemailUri,
       String transcriptId,
       long delayMillis,
       double baseMultiplier,
-      int remainingAttempts) {
-    Intent intent = new Intent(context, GetTranscriptReceiver.class);
+      int remainingAttempts,
+      PhoneAccountHandle account) {
+    Intent intent = makeBaseAlarmIntent(context);
     intent.putExtra(EXTRA_VOICEMAIL_URI, voicemailUri);
     intent.putExtra(EXTRA_TRANSCRIPT_ID, transcriptId);
     intent.putExtra(EXTRA_DELAY_MILLIS, delayMillis);
     intent.putExtra(EXTRA_BASE_MULTIPLIER, baseMultiplier);
     intent.putExtra(EXTRA_REMAINING_ATTEMPTS, remainingAttempts);
+    intent.putExtra(EXTRA_PHONE_ACCOUNT, account);
     return intent;
   }
 
+  private static Intent makeBaseAlarmIntent(Context context) {
+    Intent intent = new Intent(context.getApplicationContext(), GetTranscriptReceiver.class);
+    intent.setAction(POLL_ALARM_ACTION);
+    return intent;
+  }
+
+  private static PendingIntent getPendingIntent(Context context, Intent intent, int flags) {
+    return PendingIntent.getBroadcast(context.getApplicationContext(), 0, intent, flags);
+  }
+
   private static class PollWorker implements Worker<Intent, Void> {
     private final Context context;
 
@@ -158,9 +204,33 @@
       Uri voicemailUri = intent.getParcelableExtra(EXTRA_VOICEMAIL_URI);
       TranscriptionDbHelper dbHelper = new TranscriptionDbHelper(context, voicemailUri);
       TranscriptionTask.recordResult(context, result, dbHelper);
+
+      // Check if there are other pending transcriptions
+      PhoneAccountHandle account = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT);
+      processPendingTranscriptions(account);
       return null;
     }
 
+    private void processPendingTranscriptions(PhoneAccountHandle account) {
+      TranscriptionDbHelper dbHelper = new TranscriptionDbHelper(context);
+      List<Uri> inProgress = dbHelper.getTranscribingVoicemails();
+      if (!inProgress.isEmpty()) {
+        Uri uri = inProgress.get(0);
+        VvmLog.i(TAG, "getPendingTranscription, found pending transcription " + uri);
+        if (hasPendingAlarm(context)) {
+          // Cancel the current alarm so that the next transcription task won't be postponed
+          cancelAlarm(context, makeBaseAlarmIntent(context));
+        }
+        ThreadUtil.postOnUiThread(
+            () -> {
+              TranscriptionService.scheduleNewVoicemailTranscriptionJob(
+                  context, uri, account, true);
+            });
+      } else {
+        VvmLog.i(TAG, "getPendingTranscription, no more pending transcriptions");
+      }
+    }
+
     private Pair<String, TranscriptionStatus> pollForTranscription(String transcriptId) {
       VvmLog.i(TAG, "pollForTranscription, transcript id: " + transcriptId);
       GetTranscriptRequest request = getGetTranscriptRequest(transcriptId);
@@ -214,7 +284,8 @@
           previous.getStringExtra(EXTRA_TRANSCRIPT_ID),
           nextDelay,
           baseMultiplier,
-          remainingAttempts);
+          remainingAttempts,
+          previous.getParcelableExtra(EXTRA_PHONE_ACCOUNT));
     }
   }
 
diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionDbHelper.java b/java/com/android/voicemail/impl/transcribe/TranscriptionDbHelper.java
index a9a3722..24c07cc 100644
--- a/java/com/android/voicemail/impl/transcribe/TranscriptionDbHelper.java
+++ b/java/com/android/voicemail/impl/transcribe/TranscriptionDbHelper.java
@@ -22,11 +22,10 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.Build.VERSION_CODES;
+import android.os.Build;
 import android.provider.VoicemailContract.Voicemails;
 import android.support.annotation.VisibleForTesting;
 import android.support.annotation.WorkerThread;
-import android.support.v4.os.BuildCompat;
 import android.util.Pair;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
@@ -35,7 +34,7 @@
 import java.util.List;
 
 /** Helper class for reading and writing transcription data in the database */
-@TargetApi(VERSION_CODES.O)
+@TargetApi(Build.VERSION_CODES.O)
 public class TranscriptionDbHelper {
   @VisibleForTesting
   static final String[] PROJECTION =
@@ -63,9 +62,8 @@
   }
 
   @WorkerThread
-  @TargetApi(VERSION_CODES.M) // used for try with resources
   Pair<String, Integer> getTranscriptionAndState() {
-    Assert.checkState(BuildCompat.isAtLeastO());
+    Assert.checkState(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O);
     Assert.isWorkerThread();
     try (Cursor cursor = contentResolver.query(uri, PROJECTION, null, null, null)) {
       if (cursor == null) {
@@ -84,9 +82,8 @@
   }
 
   @WorkerThread
-  @TargetApi(VERSION_CODES.M) // used for try with resources
   List<Uri> getUntranscribedVoicemails() {
-    Assert.checkArgument(BuildCompat.isAtLeastO());
+    Assert.checkState(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O);
     Assert.isWorkerThread();
     List<Uri> untranscribed = new ArrayList<>();
     String whereClause =
@@ -105,6 +102,25 @@
   }
 
   @WorkerThread
+  List<Uri> getTranscribingVoicemails() {
+    Assert.checkState(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O);
+    Assert.isWorkerThread();
+    List<Uri> inProgress = new ArrayList<>();
+    String whereClause = VoicemailCompat.TRANSCRIPTION_STATE + "=?";
+    String[] whereArgs = {String.valueOf(VoicemailCompat.TRANSCRIPTION_IN_PROGRESS)};
+    try (Cursor cursor = contentResolver.query(uri, PROJECTION, whereClause, whereArgs, null)) {
+      if (cursor == null) {
+        LogUtil.e("TranscriptionDbHelper.getTranscribingVoicemails", "query failed.");
+      } else {
+        while (cursor.moveToNext()) {
+          inProgress.add(ContentUris.withAppendedId(uri, cursor.getLong(ID)));
+        }
+      }
+    }
+    return inProgress;
+  }
+
+  @WorkerThread
   void setTranscriptionState(int transcriptionState) {
     Assert.isWorkerThread();
     LogUtil.i(
diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionService.java b/java/com/android/voicemail/impl/transcribe/TranscriptionService.java
index 0f53003..c206c08 100644
--- a/java/com/android/voicemail/impl/transcribe/TranscriptionService.java
+++ b/java/com/android/voicemail/impl/transcribe/TranscriptionService.java
@@ -63,8 +63,7 @@
   }
 
   // Schedule a task to transcribe the indicated voicemail, return true if transcription task was
-  // scheduled. If the PhoneAccountHandle is null then the voicemail will not be considered for
-  // donation.
+  // scheduled.
   @MainThread
   public static boolean scheduleNewVoicemailTranscriptionJob(
       Context context, Uri voicemailUri, PhoneAccountHandle account, boolean highPriority) {
diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java b/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java
index bb7aa5f..f6035fd 100644
--- a/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java
+++ b/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java
@@ -58,9 +58,24 @@
   protected Pair<String, TranscriptionStatus> getTranscription() {
     VvmLog.i(TAG, "getTranscription");
 
+    if (GetTranscriptReceiver.hasPendingAlarm(context)) {
+      // Don't start a transcription while another is still active
+      VvmLog.i(
+          TAG,
+          "getTranscription, pending transcription, postponing transcription of: " + voicemailUri);
+      return new Pair<>(null, null);
+    }
+
+    TranscribeVoicemailAsyncRequest uploadRequest = getUploadRequest();
+    VvmLog.i(
+        TAG,
+        "getTranscription, uploading voicemail: "
+            + voicemailUri
+            + ", id: "
+            + uploadRequest.getTranscriptionId());
     TranscriptionResponseAsync uploadResponse =
         (TranscriptionResponseAsync)
-            sendRequest((client) -> client.sendUploadRequest(getUploadRequest()));
+            sendRequest((client) -> client.sendUploadRequest(uploadRequest));
 
     if (cancelled) {
       VvmLog.i(TAG, "getTranscription, cancelled.");
@@ -72,13 +87,14 @@
       VvmLog.i(TAG, "getTranscription, upload error: " + uploadResponse.status);
       return new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY);
     } else {
-      VvmLog.i(TAG, "getTranscription, begin polling for result.");
+      VvmLog.i(TAG, "getTranscription, begin polling for: " + uploadResponse.getTranscriptionId());
       GetTranscriptReceiver.beginPolling(
           context,
           voicemailUri,
           uploadResponse.getTranscriptionId(),
           uploadResponse.getEstimatedWaitMillis(),
-          configProvider);
+          configProvider,
+          phoneAccountHandle);
       // This indicates that the result is not available yet
       return new Pair<>(null, null);
     }