• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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