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