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.isVoicemailTranscriptionEnabled(context, account)) { 102 LogUtil.i("TranscriptionService.canTranscribeVoicemail", "transcription is not enabled"); 103 return false; 104 } 105 if (!client.hasAcceptedTos(context, account)) { 106 LogUtil.i("TranscriptionService.canTranscribeVoicemail", "hasn't accepted TOS"); 107 return false; 108 } 109 if (!Boolean.parseBoolean( 110 client.getCarrierConfigString( 111 context, account, CarrierConfigKeys.VVM_CARRIER_ALLOWS_OTT_TRANSCRIPTION_STRING))) { 112 LogUtil.i( 113 "TranscriptionService.canTranscribeVoicemail", "carrier doesn't allow transcription"); 114 return false; 115 } 116 return true; 117 } 118 119 // Cancel all transcription tasks 120 @MainThread cancelTranscriptions(Context context)121 public static void cancelTranscriptions(Context context) { 122 Assert.isMainThread(); 123 LogUtil.enterBlock("TranscriptionService.cancelTranscriptions"); 124 JobScheduler scheduler = context.getSystemService(JobScheduler.class); 125 scheduler.cancel(ScheduledJobIds.VVM_TRANSCRIPTION_JOB); 126 } 127 128 @MainThread TranscriptionService()129 public TranscriptionService() { 130 Assert.isMainThread(); 131 } 132 133 @VisibleForTesting TranscriptionService( ExecutorService executorService, TranscriptionClientFactory clientFactory, TranscriptionConfigProvider configProvider)134 TranscriptionService( 135 ExecutorService executorService, 136 TranscriptionClientFactory clientFactory, 137 TranscriptionConfigProvider configProvider) { 138 this.executorService = executorService; 139 this.clientFactory = clientFactory; 140 this.configProvider = configProvider; 141 } 142 143 @Override 144 @MainThread onStartJob(JobParameters params)145 public boolean onStartJob(JobParameters params) { 146 Assert.isMainThread(); 147 LogUtil.enterBlock("TranscriptionService.onStartJob"); 148 if (!getConfigProvider().isVoicemailTranscriptionAvailable()) { 149 LogUtil.i("TranscriptionService.onStartJob", "transcription not available, exiting."); 150 return false; 151 } else if (TextUtils.isEmpty(getConfigProvider().getServerAddress())) { 152 LogUtil.i("TranscriptionService.onStartJob", "transcription server not configured, exiting."); 153 return false; 154 } else { 155 LogUtil.i( 156 "TranscriptionService.onStartJob", 157 "transcription server address: " + configProvider.getServerAddress()); 158 jobParameters = params; 159 return checkForWork(); 160 } 161 } 162 163 @Override 164 @MainThread onStopJob(JobParameters params)165 public boolean onStopJob(JobParameters params) { 166 Assert.isMainThread(); 167 LogUtil.i("TranscriptionService.onStopJob", "params: " + params); 168 stopped = true; 169 Logger.get(this).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_JOB_STOPPED); 170 if (activeTask != null) { 171 LogUtil.i("TranscriptionService.onStopJob", "cancelling active task"); 172 activeTask.cancel(); 173 Logger.get(this).logImpression(DialerImpression.Type.VVM_TRANSCRIPTION_TASK_CANCELLED); 174 } 175 return true; 176 } 177 178 @Override 179 @MainThread onDestroy()180 public void onDestroy() { 181 Assert.isMainThread(); 182 LogUtil.enterBlock("TranscriptionService.onDestroy"); 183 cleanup(); 184 } 185 cleanup()186 private void cleanup() { 187 if (clientFactory != null) { 188 clientFactory.shutdown(); 189 clientFactory = null; 190 } 191 if (executorService != null) { 192 executorService.shutdownNow(); 193 executorService = null; 194 } 195 } 196 197 @MainThread checkForWork()198 private boolean checkForWork() { 199 Assert.isMainThread(); 200 if (stopped) { 201 LogUtil.i("TranscriptionService.checkForWork", "stopped"); 202 return false; 203 } 204 JobWorkItem workItem = jobParameters.dequeueWork(); 205 if (workItem != null) { 206 Assert.checkState(activeTask == null); 207 activeTask = 208 configProvider.shouldUseSyncApi() 209 ? new TranscriptionTaskSync( 210 this, new Callback(), workItem, getClientFactory(), configProvider) 211 : new TranscriptionTaskAsync( 212 this, new Callback(), workItem, getClientFactory(), configProvider); 213 getExecutorService().execute(activeTask); 214 return true; 215 } else { 216 return false; 217 } 218 } 219 getVoicemailUri(JobWorkItem workItem)220 static Uri getVoicemailUri(JobWorkItem workItem) { 221 return workItem.getIntent().getParcelableExtra(EXTRA_VOICEMAIL_URI); 222 } 223 getPhoneAccountHandle(JobWorkItem workItem)224 static PhoneAccountHandle getPhoneAccountHandle(JobWorkItem workItem) { 225 return workItem.getIntent().getParcelableExtra(EXTRA_ACCOUNT_HANDLE); 226 } 227 getExecutorService()228 private ExecutorService getExecutorService() { 229 if (executorService == null) { 230 // The common use case is transcribing a single voicemail so just use a single thread executor 231 // The reason we're not using DialerExecutor here is because the transcription task can be 232 // very long running (ie. multiple minutes). 233 executorService = Executors.newSingleThreadExecutor(); 234 } 235 return executorService; 236 } 237 238 private class Callback implements JobCallback { 239 @Override 240 @MainThread onWorkCompleted(JobWorkItem completedWorkItem)241 public void onWorkCompleted(JobWorkItem completedWorkItem) { 242 Assert.isMainThread(); 243 LogUtil.i("TranscriptionService.Callback.onWorkCompleted", completedWorkItem.toString()); 244 activeTask = null; 245 if (stopped) { 246 LogUtil.i("TranscriptionService.Callback.onWorkCompleted", "stopped"); 247 } else { 248 jobParameters.completeWork(completedWorkItem); 249 checkForWork(); 250 } 251 } 252 } 253 makeWorkItem(Uri voicemailUri, PhoneAccountHandle account)254 private static JobWorkItem makeWorkItem(Uri voicemailUri, PhoneAccountHandle account) { 255 Intent intent = new Intent(); 256 intent.putExtra(EXTRA_VOICEMAIL_URI, voicemailUri); 257 if (account != null) { 258 intent.putExtra(EXTRA_ACCOUNT_HANDLE, account); 259 } 260 return new JobWorkItem(intent); 261 } 262 getConfigProvider()263 private TranscriptionConfigProvider getConfigProvider() { 264 if (configProvider == null) { 265 configProvider = new TranscriptionConfigProvider(this); 266 } 267 return configProvider; 268 } 269 getClientFactory()270 private TranscriptionClientFactory getClientFactory() { 271 if (clientFactory == null) { 272 clientFactory = new TranscriptionClientFactory(this, getConfigProvider()); 273 } 274 return clientFactory; 275 } 276 } 277