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