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.job.JobInfo; 19 import android.app.job.JobParameters; 20 import android.app.job.JobScheduler; 21 import android.app.job.JobService; 22 import android.app.job.JobWorkItem; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.net.Uri; 27 import android.os.Build; 28 import android.support.annotation.MainThread; 29 import android.support.annotation.VisibleForTesting; 30 import android.telecom.PhoneAccountHandle; 31 import android.text.TextUtils; 32 import com.android.dialer.common.Assert; 33 import com.android.dialer.common.LogUtil; 34 import com.android.dialer.constants.ScheduledJobIds; 35 import com.android.dialer.logging.DialerImpression; 36 import com.android.dialer.logging.Logger; 37 import com.android.voicemail.CarrierConfigKeys; 38 import com.android.voicemail.VoicemailClient; 39 import com.android.voicemail.VoicemailComponent; 40 import com.android.voicemail.impl.transcribe.grpc.TranscriptionClientFactory; 41 import java.util.concurrent.ExecutorService; 42 import java.util.concurrent.Executors; 43 44 /** 45 * Job scheduler callback for launching voicemail transcription tasks. The transcription tasks will 46 * run in the background and will typically last for approximately the length of the voicemail audio 47 * (since thats how long the backend transcription service takes to do the transcription). 48 */ 49 public class TranscriptionService extends JobService { 50 @VisibleForTesting static final String EXTRA_VOICEMAIL_URI = "extra_voicemail_uri"; 51 @VisibleForTesting static final String EXTRA_ACCOUNT_HANDLE = "extra_account_handle"; 52 53 private ExecutorService executorService; 54 private JobParameters jobParameters; 55 private TranscriptionClientFactory clientFactory; 56 private TranscriptionConfigProvider configProvider; 57 private TranscriptionTask activeTask; 58 private boolean stopped; 59 60 /** Callback used by a task to indicate it has finished processing its work item */ 61 interface JobCallback { onWorkCompleted(JobWorkItem completedWorkItem)62 void onWorkCompleted(JobWorkItem completedWorkItem); 63 } 64 65 // Schedule a task to transcribe the indicated voicemail, return true if transcription task was 66 // scheduled. 67 @MainThread scheduleNewVoicemailTranscriptionJob( Context context, Uri voicemailUri, PhoneAccountHandle account, boolean highPriority)68 public static boolean scheduleNewVoicemailTranscriptionJob( 69 Context context, Uri voicemailUri, PhoneAccountHandle account, boolean highPriority) { 70 Assert.isMainThread(); 71 if (!canTranscribeVoicemail(context, account)) { 72 return false; 73 } 74 75 LogUtil.i( 76 "TranscriptionService.scheduleNewVoicemailTranscriptionJob", "scheduling transcription"); 77 Logger.get(context).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_VOICEMAIL_RECEIVED); 78 79 ComponentName componentName = new ComponentName(context, TranscriptionService.class); 80 JobInfo.Builder builder = 81 new JobInfo.Builder(ScheduledJobIds.VVM_TRANSCRIPTION_JOB, componentName); 82 if (highPriority) { 83 builder 84 .setMinimumLatency(0) 85 .setOverrideDeadline(0) 86 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); 87 } else { 88 builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); 89 } 90 JobScheduler scheduler = context.getSystemService(JobScheduler.class); 91 JobWorkItem workItem = makeWorkItem(voicemailUri, account); 92 return scheduler.enqueue(builder.build(), workItem) == JobScheduler.RESULT_SUCCESS; 93 } 94 canTranscribeVoicemail(Context context, PhoneAccountHandle account)95 private static boolean canTranscribeVoicemail(Context context, PhoneAccountHandle account) { 96 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 97 LogUtil.i("TranscriptionService.canTranscribeVoicemail", "not supported by sdk"); 98 return false; 99 } 100 VoicemailClient client = VoicemailComponent.get(context).getVoicemailClient(); 101 if (!client.hasAcceptedTos(context, account)) { 102 LogUtil.i("TranscriptionService.canTranscribeVoicemail", "hasn't accepted TOS"); 103 return false; 104 } 105 if (!Boolean.parseBoolean( 106 client.getCarrierConfigString( 107 context, account, CarrierConfigKeys.VVM_CARRIER_ALLOWS_OTT_TRANSCRIPTION_STRING))) { 108 LogUtil.i( 109 "TranscriptionService.canTranscribeVoicemail", "carrier doesn't allow transcription"); 110 return false; 111 } 112 return true; 113 } 114 115 // Cancel all transcription tasks 116 @MainThread cancelTranscriptions(Context context)117 public static void cancelTranscriptions(Context context) { 118 Assert.isMainThread(); 119 LogUtil.enterBlock("TranscriptionService.cancelTranscriptions"); 120 JobScheduler scheduler = context.getSystemService(JobScheduler.class); 121 scheduler.cancel(ScheduledJobIds.VVM_TRANSCRIPTION_JOB); 122 } 123 124 @MainThread TranscriptionService()125 public TranscriptionService() { 126 Assert.isMainThread(); 127 } 128 129 @VisibleForTesting TranscriptionService( ExecutorService executorService, TranscriptionClientFactory clientFactory, TranscriptionConfigProvider configProvider)130 TranscriptionService( 131 ExecutorService executorService, 132 TranscriptionClientFactory clientFactory, 133 TranscriptionConfigProvider configProvider) { 134 this.executorService = executorService; 135 this.clientFactory = clientFactory; 136 this.configProvider = configProvider; 137 } 138 139 @Override 140 @MainThread onStartJob(JobParameters params)141 public boolean onStartJob(JobParameters params) { 142 Assert.isMainThread(); 143 LogUtil.enterBlock("TranscriptionService.onStartJob"); 144 if (!getConfigProvider().isVoicemailTranscriptionAvailable()) { 145 LogUtil.i("TranscriptionService.onStartJob", "transcription not available, exiting."); 146 return false; 147 } else if (TextUtils.isEmpty(getConfigProvider().getServerAddress())) { 148 LogUtil.i("TranscriptionService.onStartJob", "transcription server not configured, exiting."); 149 return false; 150 } else { 151 LogUtil.i( 152 "TranscriptionService.onStartJob", 153 "transcription server address: " + configProvider.getServerAddress()); 154 jobParameters = params; 155 return checkForWork(); 156 } 157 } 158 159 @Override 160 @MainThread onStopJob(JobParameters params)161 public boolean onStopJob(JobParameters params) { 162 Assert.isMainThread(); 163 LogUtil.i("TranscriptionService.onStopJob", "params: " + params); 164 stopped = true; 165 Logger.get(this).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_JOB_STOPPED); 166 if (activeTask != null) { 167 LogUtil.i("TranscriptionService.onStopJob", "cancelling active task"); 168 activeTask.cancel(); 169 Logger.get(this).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_TASK_CANCELLED); 170 } 171 return true; 172 } 173 174 @Override 175 @MainThread onDestroy()176 public void onDestroy() { 177 Assert.isMainThread(); 178 LogUtil.enterBlock("TranscriptionService.onDestroy"); 179 cleanup(); 180 } 181 cleanup()182 private void cleanup() { 183 if (clientFactory != null) { 184 clientFactory.shutdown(); 185 clientFactory = null; 186 } 187 if (executorService != null) { 188 executorService.shutdownNow(); 189 executorService = null; 190 } 191 } 192 193 @MainThread checkForWork()194 private boolean checkForWork() { 195 Assert.isMainThread(); 196 if (stopped) { 197 LogUtil.i("TranscriptionService.checkForWork", "stopped"); 198 return false; 199 } 200 JobWorkItem workItem = jobParameters.dequeueWork(); 201 if (workItem != null) { 202 Assert.checkState(activeTask == null); 203 activeTask = 204 configProvider.shouldUseSyncApi() 205 ? new TranscriptionTaskSync( 206 this, new Callback(), workItem, getClientFactory(), configProvider) 207 : new TranscriptionTaskAsync( 208 this, new Callback(), workItem, getClientFactory(), configProvider); 209 getExecutorService().execute(activeTask); 210 return true; 211 } else { 212 return false; 213 } 214 } 215 getVoicemailUri(JobWorkItem workItem)216 static Uri getVoicemailUri(JobWorkItem workItem) { 217 return workItem.getIntent().getParcelableExtra(EXTRA_VOICEMAIL_URI); 218 } 219 getPhoneAccountHandle(JobWorkItem workItem)220 static PhoneAccountHandle getPhoneAccountHandle(JobWorkItem workItem) { 221 return workItem.getIntent().getParcelableExtra(EXTRA_ACCOUNT_HANDLE); 222 } 223 getExecutorService()224 private ExecutorService getExecutorService() { 225 if (executorService == null) { 226 // The common use case is transcribing a single voicemail so just use a single thread executor 227 // The reason we're not using DialerExecutor here is because the transcription task can be 228 // very long running (ie. multiple minutes). 229 executorService = Executors.newSingleThreadExecutor(); 230 } 231 return executorService; 232 } 233 234 private class Callback implements JobCallback { 235 @Override 236 @MainThread onWorkCompleted(JobWorkItem completedWorkItem)237 public void onWorkCompleted(JobWorkItem completedWorkItem) { 238 Assert.isMainThread(); 239 LogUtil.i("TranscriptionService.Callback.onWorkCompleted", completedWorkItem.toString()); 240 activeTask = null; 241 if (stopped) { 242 LogUtil.i("TranscriptionService.Callback.onWorkCompleted", "stopped"); 243 } else { 244 jobParameters.completeWork(completedWorkItem); 245 checkForWork(); 246 } 247 } 248 } 249 makeWorkItem(Uri voicemailUri, PhoneAccountHandle account)250 private static JobWorkItem makeWorkItem(Uri voicemailUri, PhoneAccountHandle account) { 251 Intent intent = new Intent(); 252 intent.putExtra(EXTRA_VOICEMAIL_URI, voicemailUri); 253 if (account != null) { 254 intent.putExtra(EXTRA_ACCOUNT_HANDLE, account); 255 } 256 return new JobWorkItem(intent); 257 } 258 getConfigProvider()259 private TranscriptionConfigProvider getConfigProvider() { 260 if (configProvider == null) { 261 configProvider = new TranscriptionConfigProvider(this); 262 } 263 return configProvider; 264 } 265 getClientFactory()266 private TranscriptionClientFactory getClientFactory() { 267 if (clientFactory == null) { 268 clientFactory = new TranscriptionClientFactory(this, getConfigProvider()); 269 } 270 return clientFactory; 271 } 272 } 273