1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
|
/*
* 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.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.app.job.JobWorkItem;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
import android.support.v4.os.BuildCompat;
import android.text.TextUtils;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.constants.ScheduledJobIds;
import com.android.dialer.logging.DialerImpression;
import com.android.dialer.logging.Logger;
import com.android.dialer.strictmode.DialerStrictMode;
import com.android.voicemail.impl.transcribe.grpc.TranscriptionClientFactory;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Job scheduler callback for launching voicemail transcription tasks. The transcription tasks will
* run in the background and will typically last for approximately the length of the voicemail audio
* (since thats how long the backend transcription service takes to do the transcription).
*/
public class TranscriptionService extends JobService {
@VisibleForTesting static final String EXTRA_VOICEMAIL_URI = "extra_voicemail_uri";
private ExecutorService executorService;
private JobParameters jobParameters;
private TranscriptionClientFactory clientFactory;
private TranscriptionConfigProvider configProvider;
/** Callback used by a task to indicate it has finished processing its work item */
interface JobCallback {
void onWorkCompleted(JobWorkItem completedWorkItem);
}
// Schedule a task to transcribe the indicated voicemail, return true if transcription task was
// scheduled.
public static boolean scheduleNewVoicemailTranscriptionJob(
Context context, Uri voicemailUri, boolean highPriority) {
Assert.isMainThread();
if (BuildCompat.isAtLeastO()) {
LogUtil.i(
"TranscriptionService.scheduleNewVoicemailTranscriptionJob", "scheduling transcription");
Logger.get(context).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_VOICEMAIL_RECEIVED);
ComponentName componentName = new ComponentName(context, TranscriptionService.class);
JobInfo.Builder builder =
new JobInfo.Builder(ScheduledJobIds.VVM_TRANSCRIPTION_JOB, componentName);
if (highPriority) {
builder
.setMinimumLatency(0)
.setOverrideDeadline(0)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
} else {
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
}
JobScheduler scheduler = context.getSystemService(JobScheduler.class);
JobWorkItem workItem = makeWorkItem(voicemailUri);
return scheduler.enqueue(builder.build(), workItem) == JobScheduler.RESULT_SUCCESS;
} else {
LogUtil.i("TranscriptionService.scheduleNewVoicemailTranscriptionJob", "not supported");
return false;
}
}
// Cancel all transcription tasks
public static void cancelTranscriptions(Context context) {
Assert.isMainThread();
LogUtil.enterBlock("TranscriptionService.cancelTranscriptions");
JobScheduler scheduler = context.getSystemService(JobScheduler.class);
scheduler.cancel(ScheduledJobIds.VVM_TRANSCRIPTION_JOB);
}
public TranscriptionService() {
Assert.isMainThread();
}
@VisibleForTesting
TranscriptionService(
ExecutorService executorService,
TranscriptionClientFactory clientFactory,
TranscriptionConfigProvider configProvider) {
this.executorService = executorService;
this.clientFactory = clientFactory;
this.configProvider = configProvider;
}
@Override
public boolean onStartJob(JobParameters params) {
Assert.isMainThread();
LogUtil.enterBlock("TranscriptionService.onStartJob");
if (!getConfigProvider().isVoicemailTranscriptionEnabled()) {
LogUtil.i("TranscriptionService.onStartJob", "transcription not enabled, exiting.");
return false;
} else if (TextUtils.isEmpty(getConfigProvider().getServerAddress())) {
LogUtil.i("TranscriptionService.onStartJob", "transcription server not configured, exiting.");
return false;
} else {
LogUtil.i(
"TranscriptionService.onStartJob",
"transcription server address: " + configProvider.getServerAddress());
DialerStrictMode.disableDeathPenalty(); // Re-enabled in cleanup.
jobParameters = params;
return checkForWork();
}
}
@Override
public boolean onStopJob(JobParameters params) {
Assert.isMainThread();
LogUtil.enterBlock("TranscriptionService.onStopJob");
cleanup();
return true;
}
@Override
public void onDestroy() {
Assert.isMainThread();
LogUtil.enterBlock("TranscriptionService.onDestroy");
cleanup();
}
private void cleanup() {
if (clientFactory != null) {
clientFactory.shutdown();
clientFactory = null;
}
if (executorService != null) {
executorService.shutdownNow();
executorService = null;
}
DialerStrictMode.enableDeathPenalty();
}
@MainThread
private boolean checkForWork() {
Assert.isMainThread();
JobWorkItem workItem = jobParameters.dequeueWork();
if (workItem != null) {
TranscriptionTask task =
configProvider.shouldUseSyncApi()
? new TranscriptionTaskSync(
this, new Callback(), workItem, getClientFactory(), configProvider)
: new TranscriptionTaskAsync(
this, new Callback(), workItem, getClientFactory(), configProvider);
getExecutorService().execute(task);
return true;
} else {
return false;
}
}
static Uri getVoicemailUri(JobWorkItem workItem) {
return workItem.getIntent().getParcelableExtra(EXTRA_VOICEMAIL_URI);
}
private ExecutorService getExecutorService() {
if (executorService == null) {
// The common use case is transcribing a single voicemail so just use a single thread executor
// The reason we're not using DialerExecutor here is because the transcription task can be
// very long running (ie. multiple minutes).
executorService = Executors.newSingleThreadExecutor();
}
return executorService;
}
private class Callback implements JobCallback {
@Override
public void onWorkCompleted(JobWorkItem completedWorkItem) {
Assert.isMainThread();
LogUtil.i("TranscriptionService.Callback.onWorkCompleted", completedWorkItem.toString());
jobParameters.completeWork(completedWorkItem);
checkForWork();
}
}
private static JobWorkItem makeWorkItem(Uri voicemailUri) {
Intent intent = new Intent();
intent.putExtra(EXTRA_VOICEMAIL_URI, voicemailUri);
return new JobWorkItem(intent);
}
private TranscriptionConfigProvider getConfigProvider() {
if (configProvider == null) {
configProvider = new TranscriptionConfigProvider(this);
}
return configProvider;
}
private TranscriptionClientFactory getClientFactory() {
if (clientFactory == null) {
clientFactory = new TranscriptionClientFactory(this, getConfigProvider());
}
return clientFactory;
}
}
|