1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 package com.android.voicemail.impl.transcribe;
17 
18 import android.app.AlarmManager;
19 import android.app.PendingIntent;
20 import android.content.BroadcastReceiver;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.net.Uri;
24 import android.os.SystemClock;
25 import android.support.annotation.Nullable;
26 import android.telecom.PhoneAccountHandle;
27 import android.util.Pair;
28 import com.android.dialer.common.Assert;
29 import com.android.dialer.common.backoff.ExponentialBaseCalculator;
30 import com.android.dialer.common.concurrent.DialerExecutor.Worker;
31 import com.android.dialer.common.concurrent.DialerExecutorComponent;
32 import com.android.dialer.common.concurrent.ThreadUtil;
33 import com.android.dialer.logging.DialerImpression;
34 import com.android.dialer.logging.Logger;
35 import com.android.voicemail.impl.VvmLog;
36 import com.android.voicemail.impl.transcribe.grpc.GetTranscriptResponseAsync;
37 import com.android.voicemail.impl.transcribe.grpc.TranscriptionClient;
38 import com.android.voicemail.impl.transcribe.grpc.TranscriptionClientFactory;
39 import com.google.internal.communications.voicemailtranscription.v1.GetTranscriptRequest;
40 import com.google.internal.communications.voicemailtranscription.v1.TranscriptionStatus;
41 import java.util.List;
42 
43 /**
44  * This class uses the AlarmManager to poll for the result of a voicemail transcription request.
45  * Initially it waits for the estimated transcription time, and if the result is not available then
46  * it polls using an exponential backoff scheme.
47  */
48 public class GetTranscriptReceiver extends BroadcastReceiver {
49   private static final String TAG = "GetTranscriptReceiver";
50   static final String EXTRA_IS_INITIAL_ESTIMATED_WAIT = "extra_is_initial_estimated_wait";
51   static final String EXTRA_VOICEMAIL_URI = "extra_voicemail_uri";
52   static final String EXTRA_TRANSCRIPT_ID = "extra_transcript_id";
53   static final String EXTRA_DELAY_MILLIS = "extra_delay_millis";
54   static final String EXTRA_BASE_MULTIPLIER = "extra_base_multiplier";
55   static final String EXTRA_REMAINING_ATTEMPTS = "extra_remaining_attempts";
56   static final String EXTRA_PHONE_ACCOUNT = "extra_phone_account";
57   static final String POLL_ALARM_ACTION =
58       "com.android.voicemail.impl.transcribe.GetTranscriptReceiver.POLL_ALARM";
59 
60   // Schedule an initial alarm to begin checking for a voicemail transcription result.
beginPolling( Context context, Uri voicemailUri, String transcriptId, long estimatedTranscriptionTimeMillis, TranscriptionConfigProvider configProvider, PhoneAccountHandle account)61   static void beginPolling(
62       Context context,
63       Uri voicemailUri,
64       String transcriptId,
65       long estimatedTranscriptionTimeMillis,
66       TranscriptionConfigProvider configProvider,
67       PhoneAccountHandle account) {
68     Assert.checkState(!hasPendingAlarm(context));
69     long initialDelayMillis = configProvider.getInitialGetTranscriptPollDelayMillis();
70     long maxBackoffMillis = configProvider.getMaxGetTranscriptPollTimeMillis();
71     int maxAttempts = configProvider.getMaxGetTranscriptPolls();
72     double baseMultiplier =
73         ExponentialBaseCalculator.findBase(initialDelayMillis, maxBackoffMillis, maxAttempts);
74     Intent intent =
75         makeAlarmIntent(
76             context,
77             voicemailUri,
78             transcriptId,
79             initialDelayMillis,
80             baseMultiplier,
81             maxAttempts,
82             account);
83     // Add an extra to distinguish this initial estimated transcription wait from subsequent backoff
84     // waits
85     intent.putExtra(EXTRA_IS_INITIAL_ESTIMATED_WAIT, true);
86     VvmLog.i(
87         TAG,
88         String.format(
89             "beginPolling, check in %d millis, for: %s",
90             estimatedTranscriptionTimeMillis, transcriptId));
91     scheduleAlarm(context, estimatedTranscriptionTimeMillis, intent);
92   }
93 
hasPendingAlarm(Context context)94   static boolean hasPendingAlarm(Context context) {
95     Intent intent = makeBaseAlarmIntent(context);
96     return getPendingIntent(context, intent, PendingIntent.FLAG_NO_CREATE) != null;
97   }
98 
99   // Alarm fired, poll for transcription result on a background thread
100   @Override
onReceive(Context context, Intent intent)101   public void onReceive(Context context, Intent intent) {
102     if (intent == null || !POLL_ALARM_ACTION.equals(intent.getAction())) {
103       return;
104     }
105     String transcriptId = intent.getStringExtra(EXTRA_TRANSCRIPT_ID);
106     VvmLog.i(TAG, "onReceive, for transcript id: " + transcriptId);
107     DialerExecutorComponent.get(context)
108         .dialerExecutorFactory()
109         .createNonUiTaskBuilder(new PollWorker(context))
110         .onSuccess(this::onSuccess)
111         .onFailure(this::onFailure)
112         .build()
113         .executeParallel(intent);
114   }
115 
onSuccess(Void unused)116   private void onSuccess(Void unused) {
117     VvmLog.i(TAG, "onSuccess");
118   }
119 
onFailure(Throwable t)120   private void onFailure(Throwable t) {
121     VvmLog.e(TAG, "onFailure", t);
122   }
123 
scheduleAlarm(Context context, long delayMillis, Intent intent)124   private static void scheduleAlarm(Context context, long delayMillis, Intent intent) {
125     PendingIntent alarmIntent =
126         getPendingIntent(context, intent, PendingIntent.FLAG_UPDATE_CURRENT);
127     AlarmManager alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
128     alarmMgr.set(
129         AlarmManager.ELAPSED_REALTIME_WAKEUP,
130         SystemClock.elapsedRealtime() + delayMillis,
131         alarmIntent);
132   }
133 
cancelAlarm(Context context, Intent intent)134   private static boolean cancelAlarm(Context context, Intent intent) {
135     PendingIntent alarmIntent = getPendingIntent(context, intent, PendingIntent.FLAG_NO_CREATE);
136     if (alarmIntent != null) {
137       AlarmManager alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
138       alarmMgr.cancel(alarmIntent);
139       alarmIntent.cancel();
140       return true;
141     } else {
142       return false;
143     }
144   }
145 
makeAlarmIntent( Context context, Uri voicemailUri, String transcriptId, long delayMillis, double baseMultiplier, int remainingAttempts, PhoneAccountHandle account)146   private static Intent makeAlarmIntent(
147       Context context,
148       Uri voicemailUri,
149       String transcriptId,
150       long delayMillis,
151       double baseMultiplier,
152       int remainingAttempts,
153       PhoneAccountHandle account) {
154     Intent intent = makeBaseAlarmIntent(context);
155     intent.putExtra(EXTRA_VOICEMAIL_URI, voicemailUri);
156     intent.putExtra(EXTRA_TRANSCRIPT_ID, transcriptId);
157     intent.putExtra(EXTRA_DELAY_MILLIS, delayMillis);
158     intent.putExtra(EXTRA_BASE_MULTIPLIER, baseMultiplier);
159     intent.putExtra(EXTRA_REMAINING_ATTEMPTS, remainingAttempts);
160     intent.putExtra(EXTRA_PHONE_ACCOUNT, account);
161     return intent;
162   }
163 
makeBaseAlarmIntent(Context context)164   private static Intent makeBaseAlarmIntent(Context context) {
165     Intent intent = new Intent(context.getApplicationContext(), GetTranscriptReceiver.class);
166     intent.setAction(POLL_ALARM_ACTION);
167     return intent;
168   }
169 
getPendingIntent(Context context, Intent intent, int flags)170   private static PendingIntent getPendingIntent(Context context, Intent intent, int flags) {
171     return PendingIntent.getBroadcast(context.getApplicationContext(), 0, intent, flags);
172   }
173 
174   private static class PollWorker implements Worker<Intent, Void> {
175     private final Context context;
176 
PollWorker(Context context)177     PollWorker(Context context) {
178       this.context = context;
179     }
180 
181     @Override
doInBackground(Intent intent)182     public Void doInBackground(Intent intent) {
183       String transcriptId = intent.getStringExtra(EXTRA_TRANSCRIPT_ID);
184       VvmLog.i(TAG, "doInBackground, for transcript id: " + transcriptId);
185       Pair<String, TranscriptionStatus> result = pollForTranscription(transcriptId);
186       if (result.first == null && result.second == null) {
187         // No result, try again if possible
188         Intent nextIntent = getNextAlarmIntent(intent);
189         if (nextIntent == null) {
190           VvmLog.i(TAG, "doInBackground, too many failures for: " + transcriptId);
191           result = new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY);
192         } else {
193           long nextDelayMillis = nextIntent.getLongExtra(EXTRA_DELAY_MILLIS, 0L);
194           VvmLog.i(
195               TAG,
196               String.format(
197                   "doInBackground, check again in %d, for: %s", nextDelayMillis, transcriptId));
198           scheduleAlarm(context, nextDelayMillis, nextIntent);
199           return null;
200         }
201       }
202 
203       // Got transcript or failed too many times
204       Uri voicemailUri = intent.getParcelableExtra(EXTRA_VOICEMAIL_URI);
205       TranscriptionDbHelper dbHelper = new TranscriptionDbHelper(context, voicemailUri);
206       TranscriptionTask.recordResult(context, result, dbHelper);
207 
208       // Check if there are other pending transcriptions
209       PhoneAccountHandle account = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT);
210       processPendingTranscriptions(account);
211       return null;
212     }
213 
processPendingTranscriptions(PhoneAccountHandle account)214     private void processPendingTranscriptions(PhoneAccountHandle account) {
215       TranscriptionDbHelper dbHelper = new TranscriptionDbHelper(context);
216       List<Uri> inProgress = dbHelper.getTranscribingVoicemails();
217       if (!inProgress.isEmpty()) {
218         Uri uri = inProgress.get(0);
219         VvmLog.i(TAG, "getPendingTranscription, found pending transcription " + uri);
220         if (hasPendingAlarm(context)) {
221           // Cancel the current alarm so that the next transcription task won't be postponed
222           cancelAlarm(context, makeBaseAlarmIntent(context));
223         }
224         ThreadUtil.postOnUiThread(
225             () -> {
226               TranscriptionService.scheduleNewVoicemailTranscriptionJob(
227                   context, uri, account, true);
228             });
229       } else {
230         VvmLog.i(TAG, "getPendingTranscription, no more pending transcriptions");
231       }
232     }
233 
pollForTranscription(String transcriptId)234     private Pair<String, TranscriptionStatus> pollForTranscription(String transcriptId) {
235       VvmLog.i(TAG, "pollForTranscription, transcript id: " + transcriptId);
236       GetTranscriptRequest request = getGetTranscriptRequest(transcriptId);
237       TranscriptionClientFactory factory = null;
238       try {
239         factory = getTranscriptionClientFactory(context);
240         TranscriptionClient client = factory.getClient();
241         Logger.get(context).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_POLL_REQUEST);
242         GetTranscriptResponseAsync response = client.sendGetTranscriptRequest(request);
243         if (response == null) {
244           VvmLog.i(TAG, "pollForTranscription, no transcription result.");
245           return new Pair<>(null, null);
246         } else if (response.isTranscribing()) {
247           VvmLog.i(TAG, "pollForTranscription, transcribing");
248           return new Pair<>(null, null);
249         } else if (response.hasFatalError()) {
250           VvmLog.i(TAG, "pollForTranscription, fail. " + response.getErrorDescription());
251           return new Pair<>(null, response.getTranscriptionStatus());
252         } else {
253           VvmLog.i(TAG, "pollForTranscription, got transcription");
254           return new Pair<>(response.getTranscript(), TranscriptionStatus.SUCCESS);
255         }
256       } finally {
257         if (factory != null) {
258           factory.shutdown();
259         }
260       }
261     }
262 
getGetTranscriptRequest(String transcriptionId)263     private GetTranscriptRequest getGetTranscriptRequest(String transcriptionId) {
264       Assert.checkArgument(transcriptionId != null);
265       return GetTranscriptRequest.newBuilder().setTranscriptionId(transcriptionId).build();
266     }
267 
getNextAlarmIntent(Intent previous)268     private @Nullable Intent getNextAlarmIntent(Intent previous) {
269       int remainingAttempts = previous.getIntExtra(EXTRA_REMAINING_ATTEMPTS, 0);
270       double baseMultiplier = previous.getDoubleExtra(EXTRA_BASE_MULTIPLIER, 0);
271       long nextDelay = previous.getLongExtra(EXTRA_DELAY_MILLIS, 0);
272       if (!previous.getBooleanExtra(EXTRA_IS_INITIAL_ESTIMATED_WAIT, false)) {
273         // After waiting the estimated transcription time, start decrementing the remaining attempts
274         // and incrementing the backoff time delay
275         remainingAttempts--;
276         if (remainingAttempts <= 0) {
277           return null;
278         }
279         nextDelay = (long) (nextDelay * baseMultiplier);
280       }
281       return makeAlarmIntent(
282           context,
283           previous.getParcelableExtra(EXTRA_VOICEMAIL_URI),
284           previous.getStringExtra(EXTRA_TRANSCRIPT_ID),
285           nextDelay,
286           baseMultiplier,
287           remainingAttempts,
288           previous.getParcelableExtra(EXTRA_PHONE_ACCOUNT));
289     }
290   }
291 
292   private static TranscriptionClientFactory transcriptionClientFactoryForTesting;
293 
setTranscriptionClientFactoryForTesting(TranscriptionClientFactory factory)294   static void setTranscriptionClientFactoryForTesting(TranscriptionClientFactory factory) {
295     transcriptionClientFactoryForTesting = factory;
296   }
297 
getTranscriptionClientFactory(Context context)298   static TranscriptionClientFactory getTranscriptionClientFactory(Context context) {
299     if (transcriptionClientFactoryForTesting != null) {
300       return transcriptionClientFactoryForTesting;
301     }
302     return new TranscriptionClientFactory(context, new TranscriptionConfigProvider(context));
303   }
304 }
305