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.annotation.TargetApi; 19 import android.app.job.JobWorkItem; 20 import android.content.Context; 21 import android.net.Uri; 22 import android.text.TextUtils; 23 import com.android.dialer.common.concurrent.ThreadUtil; 24 import com.android.dialer.logging.DialerImpression; 25 import com.android.dialer.logging.Logger; 26 import com.android.voicemail.impl.VvmLog; 27 import com.android.voicemail.impl.transcribe.TranscriptionService.JobCallback; 28 import com.android.voicemail.impl.transcribe.grpc.TranscriptionClient; 29 import com.android.voicemail.impl.transcribe.grpc.TranscriptionClientFactory; 30 import com.google.internal.communications.voicemailtranscription.v1.AudioFormat; 31 import com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailRequest; 32 import com.google.protobuf.ByteString; 33 import io.grpc.Status; 34 import java.io.IOException; 35 import java.io.InputStream; 36 37 /** 38 * Background task to get a voicemail transcription and update the database. 39 * 40 * <pre> 41 * This task performs the following steps: 42 * 1. Update the transcription-state in the database to 'in-progress' 43 * 2. Create grpc client and transcription request 44 * 3. Make synchronous grpc transcription request to backend server 45 * 3a. On response 46 * Update the database with transcription (if successful) and new transcription-state 47 * 3b. On network error 48 * If retry-count < max then increment retry-count and retry the request 49 * Otherwise update the transcription-state in the database to 'transcription-failed' 50 * 4. Notify the callback that the work item is complete 51 * </pre> 52 */ 53 public class TranscriptionTask implements Runnable { 54 private static final String TAG = "TranscriptionTask"; 55 56 private final Context context; 57 private final JobCallback callback; 58 private final JobWorkItem workItem; 59 private final TranscriptionClientFactory clientFactory; 60 private final Uri voicemailUri; 61 private final TranscriptionDbHelper databaseHelper; 62 private ByteString audioData; 63 private AudioFormat encoding; 64 65 private static final int MAX_RETRIES = 2; 66 static final String AMR_PREFIX = "#!AMR\n"; 67 TranscriptionTask( Context context, JobCallback callback, JobWorkItem workItem, TranscriptionClientFactory clientFactory)68 public TranscriptionTask( 69 Context context, 70 JobCallback callback, 71 JobWorkItem workItem, 72 TranscriptionClientFactory clientFactory) { 73 this.context = context; 74 this.callback = callback; 75 this.workItem = workItem; 76 this.clientFactory = clientFactory; 77 this.voicemailUri = getVoicemailUri(workItem); 78 databaseHelper = new TranscriptionDbHelper(context, voicemailUri); 79 } 80 81 @Override run()82 public void run() { 83 VvmLog.i(TAG, "run"); 84 if (readAndValidateAudioFile()) { 85 updateTranscriptionState(VoicemailCompat.TRANSCRIPTION_IN_PROGRESS); 86 transcribeVoicemail(); 87 } else { 88 updateTranscriptionState(VoicemailCompat.TRANSCRIPTION_FAILED); 89 } 90 ThreadUtil.postOnUiThread( 91 () -> { 92 callback.onWorkCompleted(workItem); 93 }); 94 } 95 transcribeVoicemail()96 private void transcribeVoicemail() { 97 VvmLog.i(TAG, "transcribeVoicemail"); 98 TranscribeVoicemailRequest request = makeRequest(); 99 TranscriptionClient client = clientFactory.getClient(); 100 String transcript = null; 101 for (int i = 0; transcript == null && i < MAX_RETRIES; i++) { 102 VvmLog.i(TAG, "transcribeVoicemail, try: " + (i + 1)); 103 if (i == 0) { 104 Logger.get(context).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_REQUEST_SENT); 105 } else { 106 Logger.get(context).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_REQUEST_RETRY); 107 } 108 TranscriptionClient.TranscriptionResponseWrapper responseWrapper = 109 client.transcribeVoicemail(request); 110 if (responseWrapper.status != null) { 111 VvmLog.i(TAG, "transcribeVoicemail, status: " + responseWrapper.status.getCode()); 112 if (shouldRetryRequest(responseWrapper.status)) { 113 Logger.get(context) 114 .logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_RECOVERABLE_ERROR); 115 backoff(i); 116 } else { 117 Logger.get(context) 118 .logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_FATAL_ERROR); 119 break; 120 } 121 } else if (responseWrapper.response != null) { 122 if (!TextUtils.isEmpty(responseWrapper.response.getTranscript())) { 123 VvmLog.i(TAG, "transcribeVoicemail, got response"); 124 transcript = responseWrapper.response.getTranscript(); 125 Logger.get(context) 126 .logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_SUCCESS); 127 } else { 128 VvmLog.i(TAG, "transcribeVoicemail, empty transcription"); 129 Logger.get(context).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_EMPTY); 130 } 131 } else { 132 VvmLog.w(TAG, "transcribeVoicemail, no response"); 133 Logger.get(context).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_RESPONSE_INVALID); 134 } 135 } 136 137 int newState = 138 (transcript == null) 139 ? VoicemailCompat.TRANSCRIPTION_FAILED 140 : VoicemailCompat.TRANSCRIPTION_AVAILABLE; 141 updateTranscriptionAndState(transcript, newState); 142 } 143 shouldRetryRequest(Status status)144 private static boolean shouldRetryRequest(Status status) { 145 return status.getCode() == Status.Code.UNAVAILABLE; 146 } 147 backoff(int retryCount)148 private static void backoff(int retryCount) { 149 VvmLog.i(TAG, "backoff, count: " + retryCount); 150 try { 151 long millis = (1 << retryCount) * 1000; 152 Thread.sleep(millis); 153 } catch (InterruptedException e) { 154 VvmLog.w(TAG, "interrupted"); 155 Thread.currentThread().interrupt(); 156 } 157 } 158 updateTranscriptionAndState(String transcript, int newState)159 private void updateTranscriptionAndState(String transcript, int newState) { 160 databaseHelper.setTranscriptionAndState(transcript, newState); 161 } 162 updateTranscriptionState(int newState)163 private void updateTranscriptionState(int newState) { 164 databaseHelper.setTranscriptionState(newState); 165 } 166 makeRequest()167 private TranscribeVoicemailRequest makeRequest() { 168 return TranscribeVoicemailRequest.newBuilder() 169 .setVoicemailData(audioData) 170 .setAudioFormat(encoding) 171 .build(); 172 } 173 174 // Uses try-with-resource 175 @TargetApi(android.os.Build.VERSION_CODES.M) readAndValidateAudioFile()176 private boolean readAndValidateAudioFile() { 177 if (voicemailUri == null) { 178 VvmLog.i(TAG, "Transcriber.readAndValidateAudioFile, file not found."); 179 return false; 180 } else { 181 VvmLog.i(TAG, "Transcriber.readAndValidateAudioFile, reading: " + voicemailUri); 182 } 183 184 try (InputStream in = context.getContentResolver().openInputStream(voicemailUri)) { 185 audioData = ByteString.readFrom(in); 186 VvmLog.i(TAG, "Transcriber.readAndValidateAudioFile, read " + audioData.size() + " bytes"); 187 } catch (IOException e) { 188 VvmLog.e(TAG, "Transcriber.readAndValidateAudioFile", e); 189 return false; 190 } 191 192 if (audioData.startsWith(ByteString.copyFromUtf8(AMR_PREFIX))) { 193 encoding = AudioFormat.AMR_NB_8KHZ; 194 } else { 195 VvmLog.i(TAG, "Transcriber.readAndValidateAudioFile, unknown encoding"); 196 encoding = AudioFormat.AUDIO_FORMAT_UNSPECIFIED; 197 return false; 198 } 199 200 return true; 201 } 202 getVoicemailUri(JobWorkItem workItem)203 private static Uri getVoicemailUri(JobWorkItem workItem) { 204 return workItem.getIntent().getParcelableExtra(TranscriptionService.EXTRA_VOICEMAIL_URI); 205 } 206 } 207