diff options
5 files changed, 315 insertions, 103 deletions
diff --git a/java/com/android/voicemail/impl/AndroidManifest.xml b/java/com/android/voicemail/impl/AndroidManifest.xml index 53636092a..e7ab5818e 100644 --- a/java/com/android/voicemail/impl/AndroidManifest.xml +++ b/java/com/android/voicemail/impl/AndroidManifest.xml @@ -138,5 +138,9 @@ android:name="com.android.internal.telephony.CARRIER_VVM_PACKAGE_INSTALLED" /> </intent-filter> </receiver> + + <receiver android:name="com.android.voicemail.impl.transcribe.GetTranscriptReceiver" + android:exported="false"> + </receiver> </application> </manifest> diff --git a/java/com/android/voicemail/impl/transcribe/GetTranscriptReceiver.java b/java/com/android/voicemail/impl/transcribe/GetTranscriptReceiver.java new file mode 100644 index 000000000..cc204ff53 --- /dev/null +++ b/java/com/android/voicemail/impl/transcribe/GetTranscriptReceiver.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.voicemail.impl.transcribe; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.SystemClock; +import android.support.annotation.Nullable; +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.logging.DialerImpression; +import com.android.dialer.logging.Logger; +import com.android.voicemail.impl.VvmLog; +import com.android.voicemail.impl.transcribe.grpc.GetTranscriptResponseAsync; +import com.android.voicemail.impl.transcribe.grpc.TranscriptionClient; +import com.android.voicemail.impl.transcribe.grpc.TranscriptionClientFactory; +import com.google.internal.communications.voicemailtranscription.v1.GetTranscriptRequest; +import com.google.internal.communications.voicemailtranscription.v1.TranscriptionStatus; + +/** + * This class uses the AlarmManager to poll for the result of a voicemail transcription request. + * Initially it waits for the estimated transcription time, and if the result is not available then + * it polls using an exponential backoff scheme. + */ +public class GetTranscriptReceiver extends BroadcastReceiver { + private static final String TAG = "GetTranscriptReceiver"; + static final String EXTRA_IS_INITIAL_ESTIMATED_WAIT = "extra_is_initial_estimated_wait"; + static final String EXTRA_VOICEMAIL_URI = "extra_voicemail_uri"; + static final String EXTRA_TRANSCRIPT_ID = "extra_transcript_id"; + 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"; + + // Schedule an initial alarm to begin checking for a voicemail transcription result. + static void beginPolling( + Context context, + Uri voicemailUri, + String transcriptId, + long estimatedTranscriptionTimeMillis, + TranscriptionConfigProvider configProvider) { + long initialDelayMillis = configProvider.getInitialGetTranscriptPollDelayMillis(); + long maxBackoffMillis = configProvider.getMaxGetTranscriptPollTimeMillis(); + int maxAttempts = configProvider.getMaxGetTranscriptPolls(); + double baseMultiplier = + ExponentialBaseCalculator.findBase(initialDelayMillis, maxBackoffMillis, maxAttempts); + Intent intent = + makeAlarmIntent( + context, voicemailUri, transcriptId, initialDelayMillis, baseMultiplier, maxAttempts); + // Add an extra to distinguish this initial estimated transcription wait from subsequent backoff + // waits + intent.putExtra(EXTRA_IS_INITIAL_ESTIMATED_WAIT, true); + VvmLog.i( + TAG, + String.format( + "beginPolling, check in %d millis, for: %s", + estimatedTranscriptionTimeMillis, transcriptId)); + scheduleAlarm(context, estimatedTranscriptionTimeMillis, intent); + } + + // Alarm fired, poll for transcription result on a background thread + @Override + public void onReceive(Context context, Intent intent) { + String transcriptId = intent.getStringExtra(EXTRA_TRANSCRIPT_ID); + VvmLog.i(TAG, "onReceive, for transcript id: " + transcriptId); + DialerExecutorComponent.get(context) + .dialerExecutorFactory() + .createNonUiTaskBuilder(new PollWorker(context)) + .onSuccess(this::onSuccess) + .onFailure(this::onFailure) + .build() + .executeParallel(intent); + } + + private void onSuccess(Void unused) { + VvmLog.i(TAG, "onSuccess"); + } + + private void onFailure(Throwable t) { + VvmLog.e(TAG, "onFailure", t); + } + + private static void scheduleAlarm(Context context, long delayMillis, Intent intent) { + PendingIntent alarmIntent = + PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + AlarmManager alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + alarmMgr.set( + AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + delayMillis, + alarmIntent); + } + + private static Intent makeAlarmIntent( + Context context, + Uri voicemailUri, + String transcriptId, + long delayMillis, + double baseMultiplier, + int remainingAttempts) { + Intent intent = new Intent(context, GetTranscriptReceiver.class); + 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); + return intent; + } + + private static class PollWorker implements Worker<Intent, Void> { + private final Context context; + + PollWorker(Context context) { + this.context = context; + } + + @Override + public Void doInBackground(Intent intent) { + String transcriptId = intent.getStringExtra(EXTRA_TRANSCRIPT_ID); + VvmLog.i(TAG, "doInBackground, for transcript id: " + transcriptId); + Pair<String, TranscriptionStatus> result = pollForTranscription(transcriptId); + if (result.first == null && result.second == null) { + // No result, try again if possible + Intent nextIntent = getNextAlarmIntent(intent); + if (nextIntent == null) { + VvmLog.i(TAG, "doInBackground, too many failures for: " + transcriptId); + result = new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY); + } else { + long nextDelayMillis = nextIntent.getLongExtra(EXTRA_DELAY_MILLIS, 0L); + VvmLog.i( + TAG, + String.format( + "doInBackground, check again in %d, for: %s", nextDelayMillis, transcriptId)); + scheduleAlarm(context, nextDelayMillis, nextIntent); + return null; + } + } + + // Got transcript or failed too many times + Uri voicemailUri = intent.getParcelableExtra(EXTRA_VOICEMAIL_URI); + TranscriptionDbHelper dbHelper = new TranscriptionDbHelper(context, voicemailUri); + TranscriptionTask.recordResult(context, result, dbHelper); + return null; + } + + private Pair<String, TranscriptionStatus> pollForTranscription(String transcriptId) { + VvmLog.i(TAG, "pollForTranscription, transcript id: " + transcriptId); + GetTranscriptRequest request = getGetTranscriptRequest(transcriptId); + TranscriptionClientFactory factory = null; + try { + factory = getTranscriptionClientFactory(context); + TranscriptionClient client = factory.getClient(); + Logger.get(context).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_POLL_REQUEST); + GetTranscriptResponseAsync response = client.sendGetTranscriptRequest(request); + if (response == null) { + VvmLog.i(TAG, "pollForTranscription, no transcription result."); + return new Pair<>(null, null); + } else if (response.isTranscribing()) { + VvmLog.i(TAG, "pollForTranscription, transcribing"); + return new Pair<>(null, null); + } else if (response.hasFatalError()) { + VvmLog.i(TAG, "pollForTranscription, fail. " + response.getErrorDescription()); + return new Pair<>(null, response.getTranscriptionStatus()); + } else { + VvmLog.i(TAG, "pollForTranscription, got transcription"); + return new Pair<>(response.getTranscript(), TranscriptionStatus.SUCCESS); + } + } finally { + if (factory != null) { + factory.shutdown(); + } + } + } + + private GetTranscriptRequest getGetTranscriptRequest(String transcriptionId) { + Assert.checkArgument(transcriptionId != null); + return GetTranscriptRequest.newBuilder().setTranscriptionId(transcriptionId).build(); + } + + private @Nullable Intent getNextAlarmIntent(Intent previous) { + int remainingAttempts = previous.getIntExtra(EXTRA_REMAINING_ATTEMPTS, 0); + double baseMultiplier = previous.getDoubleExtra(EXTRA_BASE_MULTIPLIER, 0); + long nextDelay = previous.getLongExtra(EXTRA_DELAY_MILLIS, 0); + if (!previous.getBooleanExtra(EXTRA_IS_INITIAL_ESTIMATED_WAIT, false)) { + // After waiting the estimated transcription time, start decrementing the remaining attempts + // and incrementing the backoff time delay + remainingAttempts--; + if (remainingAttempts <= 0) { + return null; + } + nextDelay = (long) (nextDelay * baseMultiplier); + } + return makeAlarmIntent( + context, + previous.getParcelableExtra(EXTRA_VOICEMAIL_URI), + previous.getStringExtra(EXTRA_TRANSCRIPT_ID), + nextDelay, + baseMultiplier, + remainingAttempts); + } + } + + private static TranscriptionClientFactory transcriptionClientFactoryForTesting; + + static void setTranscriptionClientFactoryForTesting(TranscriptionClientFactory factory) { + transcriptionClientFactoryForTesting = factory; + } + + private static TranscriptionClientFactory getTranscriptionClientFactory(Context context) { + if (transcriptionClientFactoryForTesting != null) { + return transcriptionClientFactoryForTesting; + } + return new TranscriptionClientFactory(context, new TranscriptionConfigProvider(context)); + } +} diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java b/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java index 98c8461f5..3d1755b64 100644 --- a/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java +++ b/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java @@ -18,6 +18,7 @@ package com.android.voicemail.impl.transcribe; import android.content.Context; import android.os.Build; import com.android.dialer.configprovider.ConfigProviderBindings; +import java.util.concurrent.TimeUnit; /** Provides configuration values needed to connect to the transcription server. */ public class TranscriptionConfigProvider { @@ -65,14 +66,24 @@ public class TranscriptionConfigProvider { .getLong("voicemail_transcription_max_transcription_retries", 2L); } - public long getMaxGetTranscriptPolls() { + public int getMaxGetTranscriptPolls() { + return (int) + ConfigProviderBindings.get(context) + .getLong("voicemail_transcription_max_get_transcript_polls", 20L); + } + + public long getInitialGetTranscriptPollDelayMillis() { return ConfigProviderBindings.get(context) - .getLong("voicemail_transcription_max_get_transcript_polls", 20L); + .getLong( + "voicemail_transcription_get_initial_transcript_poll_delay_millis", + TimeUnit.SECONDS.toMillis(1)); } - public long getGetTranscriptPollIntervalMillis() { + public long getMaxGetTranscriptPollTimeMillis() { return ConfigProviderBindings.get(context) - .getLong("voicemail_transcription_get_transcript_poll_interval_millis", 1000L); + .getLong( + "voicemail_transcription_get_max_transcript_poll_time_millis", + TimeUnit.MINUTES.toMillis(20)); } public boolean isVoicemailDonationAvailable() { @@ -97,6 +108,6 @@ public class TranscriptionConfigProvider { shouldUseSyncApi(), getMaxTranscriptionRetries(), getMaxGetTranscriptPolls(), - getGetTranscriptPollIntervalMillis()); + getMaxGetTranscriptPollTimeMillis()); } } diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java b/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java index 97cf89eef..d483f6fbd 100644 --- a/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java +++ b/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java @@ -21,7 +21,6 @@ import android.net.Uri; import android.support.annotation.MainThread; import android.support.annotation.VisibleForTesting; import android.telecom.PhoneAccountHandle; -import android.text.TextUtils; import android.util.Pair; import com.android.dialer.common.Assert; import com.android.dialer.common.concurrent.ThreadUtil; @@ -60,10 +59,10 @@ public abstract class TranscriptionTask implements Runnable { private final JobCallback callback; private final JobWorkItem workItem; private final TranscriptionClientFactory clientFactory; - private final Uri voicemailUri; + protected final Uri voicemailUri; protected final PhoneAccountHandle phoneAccountHandle; - private final TranscriptionDbHelper databaseHelper; protected final TranscriptionConfigProvider configProvider; + protected final TranscriptionDbHelper dbHelper; protected ByteString audioData; protected AudioFormat encoding; protected volatile boolean cancelled; @@ -86,7 +85,7 @@ public abstract class TranscriptionTask implements Runnable { this.voicemailUri = TranscriptionService.getVoicemailUri(workItem); this.phoneAccountHandle = TranscriptionService.getPhoneAccountHandle(workItem); this.configProvider = configProvider; - databaseHelper = new TranscriptionDbHelper(context, voicemailUri); + dbHelper = new TranscriptionDbHelper(context, voicemailUri); } @MainThread @@ -124,44 +123,7 @@ public abstract class TranscriptionTask implements Runnable { private void transcribeVoicemail() { VvmLog.i(TAG, "transcribeVoicemail"); - Pair<String, TranscriptionStatus> pair = getTranscription(); - String transcript = pair.first; - TranscriptionStatus status = pair.second; - if (!TextUtils.isEmpty(transcript)) { - updateTranscriptionAndState(transcript, VoicemailCompat.TRANSCRIPTION_AVAILABLE); - VvmLog.i(TAG, "transcribeVoicemail, got response"); - Logger.get(context).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_SUCCESS); - } else { - VvmLog.i(TAG, "transcribeVoicemail, transcription unsuccessful, " + status); - switch (status) { - case FAILED_NO_SPEECH_DETECTED: - updateTranscriptionAndState( - transcript, VoicemailCompat.TRANSCRIPTION_FAILED_NO_SPEECH_DETECTED); - Logger.get(context) - .logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_NO_SPEECH_DETECTED); - break; - case FAILED_LANGUAGE_NOT_SUPPORTED: - updateTranscriptionAndState( - transcript, VoicemailCompat.TRANSCRIPTION_FAILED_LANGUAGE_NOT_SUPPORTED); - Logger.get(context) - .logImpression( - DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_LANGUAGE_NOT_SUPPORTED); - break; - case EXPIRED: - updateTranscriptionAndState(transcript, VoicemailCompat.TRANSCRIPTION_FAILED); - Logger.get(context) - .logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_EXPIRED); - break; - default: - updateTranscriptionAndState( - transcript, - cancelled - ? VoicemailCompat.TRANSCRIPTION_NOT_STARTED - : VoicemailCompat.TRANSCRIPTION_FAILED); - Logger.get(context).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_EMPTY); - break; - } - } + recordResult(context, getTranscription(), dbHelper, cancelled); } protected TranscriptionResponse sendRequest(Request request) { @@ -213,12 +175,57 @@ public abstract class TranscriptionTask implements Runnable { } } - private void updateTranscriptionAndState(String transcript, int newState) { - databaseHelper.setTranscriptionAndState(transcript, newState); + protected void updateTranscriptionState(int newState) { + dbHelper.setTranscriptionState(newState); + } + + protected void updateTranscriptionAndState(String transcript, int newState) { + dbHelper.setTranscriptionAndState(transcript, newState); } - private void updateTranscriptionState(int newState) { - databaseHelper.setTranscriptionState(newState); + static void recordResult( + Context context, Pair<String, TranscriptionStatus> result, TranscriptionDbHelper dbHelper) { + recordResult(context, result, dbHelper, false); + } + + static void recordResult( + Context context, + Pair<String, TranscriptionStatus> result, + TranscriptionDbHelper dbHelper, + boolean cancelled) { + if (result.first != null) { + VvmLog.i(TAG, "recordResult, got transcription"); + dbHelper.setTranscriptionAndState(result.first, VoicemailCompat.TRANSCRIPTION_AVAILABLE); + Logger.get(context).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_SUCCESS); + } else if (result.second != null) { + VvmLog.i(TAG, "recordResult, failed to transcribe, reason: " + result.second); + switch (result.second) { + case FAILED_NO_SPEECH_DETECTED: + dbHelper.setTranscriptionState(VoicemailCompat.TRANSCRIPTION_FAILED_NO_SPEECH_DETECTED); + Logger.get(context) + .logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_NO_SPEECH_DETECTED); + break; + case FAILED_LANGUAGE_NOT_SUPPORTED: + dbHelper.setTranscriptionState( + VoicemailCompat.TRANSCRIPTION_FAILED_LANGUAGE_NOT_SUPPORTED); + Logger.get(context) + .logImpression( + DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_LANGUAGE_NOT_SUPPORTED); + break; + case EXPIRED: + dbHelper.setTranscriptionState(VoicemailCompat.TRANSCRIPTION_FAILED); + Logger.get(context) + .logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_EXPIRED); + break; + default: + dbHelper.setTranscriptionState( + cancelled + ? VoicemailCompat.TRANSCRIPTION_NOT_STARTED + : VoicemailCompat.TRANSCRIPTION_FAILED); + Logger.get(context).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_EMPTY); + break; + } + } } private boolean readAndValidateAudioFile() { diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java b/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java index 808bf0f87..7abf2484e 100644 --- a/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java +++ b/java/com/android/voicemail/impl/transcribe/TranscriptionTaskAsync.java @@ -19,17 +19,13 @@ import android.app.job.JobWorkItem; import android.content.Context; import android.support.annotation.VisibleForTesting; import android.util.Pair; -import com.android.dialer.common.Assert; import com.android.dialer.logging.DialerImpression; -import com.android.dialer.logging.Logger; import com.android.voicemail.VoicemailComponent; import com.android.voicemail.impl.VvmLog; import com.android.voicemail.impl.transcribe.TranscriptionService.JobCallback; -import com.android.voicemail.impl.transcribe.grpc.GetTranscriptResponseAsync; import com.android.voicemail.impl.transcribe.grpc.TranscriptionClientFactory; import com.android.voicemail.impl.transcribe.grpc.TranscriptionResponseAsync; import com.google.internal.communications.voicemailtranscription.v1.DonationPreference; -import com.google.internal.communications.voicemailtranscription.v1.GetTranscriptRequest; import com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailAsyncRequest; import com.google.internal.communications.voicemailtranscription.v1.TranscriptionStatus; @@ -73,8 +69,15 @@ public class TranscriptionTaskAsync extends TranscriptionTask { VvmLog.i(TAG, "getTranscription, failed to upload voicemail."); return new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY); } else { - waitForTranscription(uploadResponse); - return pollForTranscription(uploadResponse); + VvmLog.i(TAG, "getTranscription, begin polling for result."); + GetTranscriptReceiver.beginPolling( + context, + voicemailUri, + uploadResponse.getTranscriptionId(), + uploadResponse.getEstimatedWaitMillis(), + configProvider); + // This indicates that the result is not available yet + return new Pair<>(null, null); } } @@ -83,45 +86,6 @@ public class TranscriptionTaskAsync extends TranscriptionTask { return DialerImpression.Type.VVM_TRANSCRIPTION_REQUEST_SENT_ASYNC; } - private static void waitForTranscription(TranscriptionResponseAsync uploadResponse) { - long millis = uploadResponse.getEstimatedWaitMillis(); - VvmLog.i(TAG, "waitForTranscription, " + millis + " millis"); - sleep(millis); - } - - private Pair<String, TranscriptionStatus> pollForTranscription( - TranscriptionResponseAsync uploadResponse) { - VvmLog.i(TAG, "pollForTranscription"); - GetTranscriptRequest request = getGetTranscriptRequest(uploadResponse); - for (int i = 0; i < configProvider.getMaxGetTranscriptPolls(); i++) { - if (cancelled) { - VvmLog.i(TAG, "pollForTranscription, cancelled."); - return new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY); - } - Logger.get(context).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_POLL_REQUEST); - GetTranscriptResponseAsync response = - (GetTranscriptResponseAsync) - sendRequest((client) -> client.sendGetTranscriptRequest(request)); - if (cancelled) { - VvmLog.i(TAG, "pollForTranscription, cancelled."); - return new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY); - } else if (response == null) { - VvmLog.i(TAG, "pollForTranscription, no transcription result."); - } else if (response.isTranscribing()) { - VvmLog.i(TAG, "pollForTranscription, poll count: " + (i + 1)); - } else if (response.hasFatalError()) { - VvmLog.i(TAG, "pollForTranscription, fail. " + response.getErrorDescription()); - return new Pair<>(null, response.getTranscriptionStatus()); - } else { - VvmLog.i(TAG, "pollForTranscription, got transcription"); - return new Pair<>(response.getTranscript(), TranscriptionStatus.SUCCESS); - } - sleep(configProvider.getGetTranscriptPollIntervalMillis()); - } - VvmLog.i(TAG, "pollForTranscription, timed out."); - return new Pair<>(null, TranscriptionStatus.FAILED_NO_RETRY); - } - @VisibleForTesting TranscribeVoicemailAsyncRequest getUploadRequest() { TranscribeVoicemailAsyncRequest.Builder builder = @@ -145,11 +109,4 @@ public class TranscriptionTaskAsync extends TranscriptionTask { .getVoicemailClient() .isVoicemailDonationEnabled(context, phoneAccountHandle); } - - private GetTranscriptRequest getGetTranscriptRequest(TranscriptionResponseAsync uploadResponse) { - Assert.checkArgument(uploadResponse.getTranscriptionId() != null); - return GetTranscriptRequest.newBuilder() - .setTranscriptionId(uploadResponse.getTranscriptionId()) - .build(); - } } |