diff options
Diffstat (limited to 'java/com/android/voicemail/impl')
16 files changed, 1197 insertions, 627 deletions
diff --git a/java/com/android/voicemail/impl/AndroidManifest.xml b/java/com/android/voicemail/impl/AndroidManifest.xml index 95e6e8212..be7dac10d 100644 --- a/java/com/android/voicemail/impl/AndroidManifest.xml +++ b/java/com/android/voicemail/impl/AndroidManifest.xml @@ -97,6 +97,11 @@ android:exported="false"/> <service + android:name="com.android.voicemail.impl.transcribe.TranscriptionService" + android:permission="android.permission.BIND_JOB_SERVICE" + android:exported="false"/> + + <service android:name="com.android.voicemail.impl.OmtpService" android:permission="android.permission.BIND_VISUAL_VOICEMAIL_SERVICE" android:exported="true" diff --git a/java/com/android/voicemail/impl/OmtpReceiver.java b/java/com/android/voicemail/impl/OmtpReceiver.java deleted file mode 100644 index 9baf95415..000000000 --- a/java/com/android/voicemail/impl/OmtpReceiver.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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; - -import android.annotation.TargetApi; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.Build.VERSION_CODES; -import android.telecom.PhoneAccountHandle; -import android.telephony.VisualVoicemailSms; -import com.android.dialer.common.Assert; -import com.android.dialer.logging.DialerImpression; -import com.android.dialer.logging.Logger; -import com.android.voicemail.VoicemailComponent; -import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil; -import com.android.voicemail.impl.sync.VvmAccountManager; - -/** Listens to com.android.phone.vvm.ACTION_TEMP_VISUAL_VOICEMAIL_SERVICE_EVENT */ -@TargetApi(VERSION_CODES.O) -public class OmtpReceiver extends BroadcastReceiver { - - private static final String TAG = "VvmOmtpReceiver"; - - public static final String ACTION_SMS_RECEIVED = "com.android.vociemailomtp.sms.sms_received"; - - public static final String EXTRA_VOICEMAIL_SMS = "extra_voicemail_sms"; - - private static final String EXTRA_WHAT = "what"; - - private static final int MSG_ON_CELL_SERVICE_CONNECTED = 1; - - private static final int MSG_ON_SMS_RECEIVED = 2; - - private static final int MSG_ON_SIM_REMOVED = 3; - - private static final int MSG_TASK_STOPPED = 5; - - private static final String DATA_PHONE_ACCOUNT_HANDLE = "data_phone_account_handle"; - - private static final String DATA_SMS = "data_sms"; - - @Override - public void onReceive(Context context, Intent intent) { - // ACTION_VISUAL_VOICEMAIL_SERVICE_EVENT is not a protected broadcast pre-O. - if (!VoicemailComponent.get(context).getVoicemailClient().isVoicemailModuleEnabled()) { - VvmLog.e(TAG, "ACTION_VISUAL_VOICEMAIL_SERVICE_EVENT received when module is disabled"); - return; - } - - int what = intent.getIntExtra(EXTRA_WHAT, -1); - PhoneAccountHandle phoneAccountHandle = intent.getParcelableExtra(DATA_PHONE_ACCOUNT_HANDLE); - OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, phoneAccountHandle); - if (!config.isValid()) { - VvmLog.i(TAG, "VVM not supported on " + phoneAccountHandle); - return; - } - if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccountHandle) - && !config.isLegacyModeEnabled()) { - VvmLog.i(TAG, "VVM is disabled"); - return; - } - switch (what) { - case MSG_ON_CELL_SERVICE_CONNECTED: - VvmLog.i(TAG, "onCellServiceConnected"); - Logger.get(context).logImpression(DialerImpression.Type.VVM_UNBUNDLED_EVENT_RECEIVED); - ActivationTask.start(context, phoneAccountHandle, null); - break; - case MSG_ON_SMS_RECEIVED: - VvmLog.i(TAG, "onSmsReceived"); - Logger.get(context).logImpression(DialerImpression.Type.VVM_UNBUNDLED_EVENT_RECEIVED); - VisualVoicemailSms sms = intent.getParcelableExtra(DATA_SMS); - Intent receivedIntent = new Intent(ACTION_SMS_RECEIVED); - receivedIntent.setPackage(context.getPackageName()); - receivedIntent.putExtra(EXTRA_VOICEMAIL_SMS, sms); - context.sendBroadcast(receivedIntent); - break; - case MSG_ON_SIM_REMOVED: - VvmLog.i(TAG, "onSimRemoved"); - Logger.get(context).logImpression(DialerImpression.Type.VVM_UNBUNDLED_EVENT_RECEIVED); - VvmAccountManager.removeAccount(context, phoneAccountHandle); - break; - case MSG_TASK_STOPPED: - VvmLog.i(TAG, "onStopped"); - Logger.get(context).logImpression(DialerImpression.Type.VVM_UNBUNDLED_EVENT_RECEIVED); - break; - default: - throw Assert.createIllegalStateFailException("unexpected what: " + what); - } - } -} diff --git a/java/com/android/voicemail/impl/TelephonyManagerStub.java b/java/com/android/voicemail/impl/TelephonyManagerStub.java deleted file mode 100644 index 4762e9023..000000000 --- a/java/com/android/voicemail/impl/TelephonyManagerStub.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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; - -import android.annotation.TargetApi; -import android.os.Build.VERSION_CODES; - -/** - * Temporary stub for public APIs that should be added into telephony manager. - * - * <p>TODO(b/32637799) remove this. - */ -@TargetApi(VERSION_CODES.O) -public class TelephonyManagerStub { - - public static void showVoicemailNotification(int voicemailCount) {} - - /** - * Dismisses the message waiting (voicemail) indicator. - * - * @param subId the subscription id we should dismiss the notification for. - */ - public static void clearMwiIndicator(int subId) {} - - public static void setShouldCheckVisualVoicemailConfigurationForMwi(int subId, boolean enabled) {} -} diff --git a/java/com/android/voicemail/impl/VvmPackageInstallReceiver.java b/java/com/android/voicemail/impl/VvmPackageInstallReceiver.java deleted file mode 100644 index 1e2de6070..000000000 --- a/java/com/android/voicemail/impl/VvmPackageInstallReceiver.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2015 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; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.telecom.PhoneAccountHandle; -import android.telecom.TelecomManager; -import com.android.voicemail.VoicemailComponent; -import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil; - -/** - * When a new package is installed, check if it matches any of the vvm carrier apps of the currently - * enabled dialer VVM sources. The dialer VVM client will be disabled upon carrier VVM app - * installation, unless it was explicitly enabled by the user. - */ -public class VvmPackageInstallReceiver extends BroadcastReceiver { - - private static final String TAG = "VvmPkgInstallReceiver"; - - @Override - public void onReceive(Context context, Intent intent) { - if (!VoicemailComponent.get(context).getVoicemailClient().isVoicemailModuleEnabled()) { - return; - } - - if (intent.getData() == null) { - return; - } - - String packageName = intent.getData().getSchemeSpecificPart(); - if (packageName == null) { - return; - } - - // This get called every time an app is installed and will be noisy. Don't log until the app - // is identified as a carrier VVM app. - for (PhoneAccountHandle phoneAccount : - context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) { - OmtpVvmCarrierConfigHelper carrierConfigHelper = - new OmtpVvmCarrierConfigHelper(context, phoneAccount); - if (!carrierConfigHelper.isValid()) { - continue; - } - if (carrierConfigHelper.getCarrierVvmPackageNames() == null) { - continue; - } - if (!carrierConfigHelper.getCarrierVvmPackageNames().contains(packageName)) { - continue; - } - - VvmLog.i(TAG, "Carrier app installed"); - if (VisualVoicemailSettingsUtil.isEnabledUserSet(context, phoneAccount)) { - // Skip the check if this voicemail source's setting is overridden by the user. - VvmLog.i(TAG, "VVM enabled by user, not disabling"); - continue; - } - - // Force deactivate the client. The user can re-enable it in the settings. - // There is no need to update the settings for deactivation. At this point, if the - // default value is used it should be false because a carrier package is present. - VvmLog.i(TAG, "Carrier VVM package installed, disabling system VVM client"); - VisualVoicemailSettingsUtil.setEnabled(context, phoneAccount, false); - } - } -} diff --git a/java/com/android/voicemail/impl/com/google/internal/communications/voicemailtranscription/v1/VoicemailTranscriptionServiceGrpc.java b/java/com/android/voicemail/impl/com/google/internal/communications/voicemailtranscription/v1/VoicemailTranscriptionServiceGrpc.java new file mode 100644 index 000000000..448c69356 --- /dev/null +++ b/java/com/android/voicemail/impl/com/google/internal/communications/voicemailtranscription/v1/VoicemailTranscriptionServiceGrpc.java @@ -0,0 +1,254 @@ +/* + * 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.google.internal.communications.voicemailtranscription.v1; + +import static io.grpc.stub.ClientCalls.asyncUnaryCall; +import static io.grpc.stub.ClientCalls.asyncServerStreamingCall; +import static io.grpc.stub.ClientCalls.asyncClientStreamingCall; +import static io.grpc.stub.ClientCalls.asyncBidiStreamingCall; +import static io.grpc.stub.ClientCalls.blockingUnaryCall; +import static io.grpc.stub.ClientCalls.blockingServerStreamingCall; +import static io.grpc.stub.ClientCalls.futureUnaryCall; +import static io.grpc.MethodDescriptor.generateFullMethodName; +import static io.grpc.stub.ServerCalls.asyncUnaryCall; +import static io.grpc.stub.ServerCalls.asyncServerStreamingCall; +import static io.grpc.stub.ServerCalls.asyncClientStreamingCall; +import static io.grpc.stub.ServerCalls.asyncBidiStreamingCall; +import static io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall; +import static io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall; + +/** + * <pre> + * RPC service for transcribing voicemails. + * </pre> + */ +@javax.annotation.Generated( + value = "by gRPC proto compiler (version 1.0.3)", + comments = "Source: voicemail_transcription.proto") +public class VoicemailTranscriptionServiceGrpc { + + private VoicemailTranscriptionServiceGrpc() {} + + public static final String SERVICE_NAME = "google.internal.communications.voicemailtranscription.v1.VoicemailTranscriptionService"; + + // Static method descriptors that strictly reflect the proto. + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/1901") + public static final io.grpc.MethodDescriptor<com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailRequest, + com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailResponse> METHOD_TRANSCRIBE_VOICEMAIL = + io.grpc.MethodDescriptor.create( + io.grpc.MethodDescriptor.MethodType.UNARY, + generateFullMethodName( + "google.internal.communications.voicemailtranscription.v1.VoicemailTranscriptionService", "TranscribeVoicemail"), + io.grpc.protobuf.lite.ProtoLiteUtils.marshaller(com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailRequest.getDefaultInstance()), + io.grpc.protobuf.lite.ProtoLiteUtils.marshaller(com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailResponse.getDefaultInstance())); + + /** + * Creates a new async stub that supports all call types for the service + */ + public static VoicemailTranscriptionServiceStub newStub(io.grpc.Channel channel) { + return new VoicemailTranscriptionServiceStub(channel); + } + + /** + * Creates a new blocking-style stub that supports unary and streaming output calls on the service + */ + public static VoicemailTranscriptionServiceBlockingStub newBlockingStub( + io.grpc.Channel channel) { + return new VoicemailTranscriptionServiceBlockingStub(channel); + } + + /** + * Creates a new ListenableFuture-style stub that supports unary and streaming output calls on the service + */ + public static VoicemailTranscriptionServiceFutureStub newFutureStub( + io.grpc.Channel channel) { + return new VoicemailTranscriptionServiceFutureStub(channel); + } + + /** + * <pre> + * RPC service for transcribing voicemails. + * </pre> + */ + public static abstract class VoicemailTranscriptionServiceImplBase implements io.grpc.BindableService { + + /** + * <pre> + * Returns a transcript of the given voicemail. + * </pre> + */ + public void transcribeVoicemail(com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailRequest request, + io.grpc.stub.StreamObserver<com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailResponse> responseObserver) { + asyncUnimplementedUnaryCall(METHOD_TRANSCRIBE_VOICEMAIL, responseObserver); + } + + @java.lang.Override public io.grpc.ServerServiceDefinition bindService() { + return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor()) + .addMethod( + METHOD_TRANSCRIBE_VOICEMAIL, + asyncUnaryCall( + new MethodHandlers< + com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailRequest, + com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailResponse>( + this, METHODID_TRANSCRIBE_VOICEMAIL))) + .build(); + } + } + + /** + * <pre> + * RPC service for transcribing voicemails. + * </pre> + */ + public static final class VoicemailTranscriptionServiceStub extends io.grpc.stub.AbstractStub<VoicemailTranscriptionServiceStub> { + private VoicemailTranscriptionServiceStub(io.grpc.Channel channel) { + super(channel); + } + + private VoicemailTranscriptionServiceStub(io.grpc.Channel channel, + io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected VoicemailTranscriptionServiceStub build(io.grpc.Channel channel, + io.grpc.CallOptions callOptions) { + return new VoicemailTranscriptionServiceStub(channel, callOptions); + } + + /** + * <pre> + * Returns a transcript of the given voicemail. + * </pre> + */ + public void transcribeVoicemail(com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailRequest request, + io.grpc.stub.StreamObserver<com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailResponse> responseObserver) { + asyncUnaryCall( + getChannel().newCall(METHOD_TRANSCRIBE_VOICEMAIL, getCallOptions()), request, responseObserver); + } + } + + /** + * <pre> + * RPC service for transcribing voicemails. + * </pre> + */ + public static final class VoicemailTranscriptionServiceBlockingStub extends io.grpc.stub.AbstractStub<VoicemailTranscriptionServiceBlockingStub> { + private VoicemailTranscriptionServiceBlockingStub(io.grpc.Channel channel) { + super(channel); + } + + private VoicemailTranscriptionServiceBlockingStub(io.grpc.Channel channel, + io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected VoicemailTranscriptionServiceBlockingStub build(io.grpc.Channel channel, + io.grpc.CallOptions callOptions) { + return new VoicemailTranscriptionServiceBlockingStub(channel, callOptions); + } + + /** + * <pre> + * Returns a transcript of the given voicemail. + * </pre> + */ + public com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailResponse transcribeVoicemail(com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailRequest request) { + return blockingUnaryCall( + getChannel(), METHOD_TRANSCRIBE_VOICEMAIL, getCallOptions(), request); + } + } + + /** + * <pre> + * RPC service for transcribing voicemails. + * </pre> + */ + public static final class VoicemailTranscriptionServiceFutureStub extends io.grpc.stub.AbstractStub<VoicemailTranscriptionServiceFutureStub> { + private VoicemailTranscriptionServiceFutureStub(io.grpc.Channel channel) { + super(channel); + } + + private VoicemailTranscriptionServiceFutureStub(io.grpc.Channel channel, + io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected VoicemailTranscriptionServiceFutureStub build(io.grpc.Channel channel, + io.grpc.CallOptions callOptions) { + return new VoicemailTranscriptionServiceFutureStub(channel, callOptions); + } + + /** + * <pre> + * Returns a transcript of the given voicemail. + * </pre> + */ + public com.google.common.util.concurrent.ListenableFuture<com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailResponse> transcribeVoicemail( + com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailRequest request) { + return futureUnaryCall( + getChannel().newCall(METHOD_TRANSCRIBE_VOICEMAIL, getCallOptions()), request); + } + } + + private static final int METHODID_TRANSCRIBE_VOICEMAIL = 0; + + private static class MethodHandlers<Req, Resp> implements + io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>, + io.grpc.stub.ServerCalls.ServerStreamingMethod<Req, Resp>, + io.grpc.stub.ServerCalls.ClientStreamingMethod<Req, Resp>, + io.grpc.stub.ServerCalls.BidiStreamingMethod<Req, Resp> { + private final VoicemailTranscriptionServiceImplBase serviceImpl; + private final int methodId; + + public MethodHandlers(VoicemailTranscriptionServiceImplBase serviceImpl, int methodId) { + this.serviceImpl = serviceImpl; + this.methodId = methodId; + } + + @java.lang.Override + @java.lang.SuppressWarnings("unchecked") + public void invoke(Req request, io.grpc.stub.StreamObserver<Resp> responseObserver) { + switch (methodId) { + case METHODID_TRANSCRIBE_VOICEMAIL: + serviceImpl.transcribeVoicemail((com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailRequest) request, + (io.grpc.stub.StreamObserver<com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailResponse>) responseObserver); + break; + default: + throw new AssertionError(); + } + } + + @java.lang.Override + @java.lang.SuppressWarnings("unchecked") + public io.grpc.stub.StreamObserver<Req> invoke( + io.grpc.stub.StreamObserver<Resp> responseObserver) { + switch (methodId) { + default: + throw new AssertionError(); + } + } + } + + public static io.grpc.ServiceDescriptor getServiceDescriptor() { + return new io.grpc.ServiceDescriptor(SERVICE_NAME, + METHOD_TRANSCRIBE_VOICEMAIL); + } + +} diff --git a/java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java b/java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java index f386fce0e..d15ce12ef 100644 --- a/java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java +++ b/java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java @@ -23,9 +23,12 @@ import android.provider.VoicemailContract.Voicemails; import android.support.annotation.Nullable; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; +import com.android.dialer.common.Assert; +import com.android.dialer.common.concurrent.ThreadUtil; import com.android.voicemail.impl.R; import com.android.voicemail.impl.VvmLog; import com.android.voicemail.impl.imap.VoicemailPayload; +import com.android.voicemail.impl.transcribe.TranscriptionService; import java.io.IOException; import java.io.OutputStream; import org.apache.commons.io.IOUtils; @@ -56,6 +59,7 @@ public class VoicemailFetchedCallback { * @param voicemailPayload The object containing the content data for the voicemail */ public void setVoicemailContent(@Nullable VoicemailPayload voicemailPayload) { + Assert.isWorkerThread(); if (voicemailPayload == null) { VvmLog.i(TAG, "Payload not found, message has unsupported format"); ContentValues values = new ContentValues(); @@ -90,13 +94,23 @@ public class VoicemailFetchedCallback { ContentValues values = new ContentValues(); values.put(Voicemails.MIME_TYPE, voicemailPayload.getMimeType()); values.put(Voicemails.HAS_CONTENT, true); - updateVoicemail(values); + if (updateVoicemail(values)) { + ThreadUtil.postOnUiThread( + () -> { + if (!TranscriptionService.transcribeVoicemail(mContext, mUri)) { + VvmLog.w(TAG, String.format("Failed to schedule transcription for %s", mUri)); + } + }); + } } - private void updateVoicemail(ContentValues values) { + private boolean updateVoicemail(ContentValues values) { int updatedCount = mContentResolver.update(mUri, values, null, null); if (updatedCount != 1) { VvmLog.e(TAG, "Updating voicemail should have updated 1 row, was: " + updatedCount); + return false; + } else { + return true; } } } diff --git a/java/com/android/voicemail/impl/mail/MailTransport.java b/java/com/android/voicemail/impl/mail/MailTransport.java index 3df36d544..00339f03d 100644 --- a/java/com/android/voicemail/impl/mail/MailTransport.java +++ b/java/com/android/voicemail/impl/mail/MailTransport.java @@ -17,7 +17,9 @@ package com.android.voicemail.impl.mail; import android.content.Context; import android.net.Network; +import android.net.TrafficStats; import android.support.annotation.VisibleForTesting; +import com.android.dialer.constants.TrafficStatsTags; import com.android.voicemail.impl.OmtpEvents; import com.android.voicemail.impl.imap.ImapHelper; import com.android.voicemail.impl.mail.store.ImapStore; @@ -188,6 +190,7 @@ public class MailTransport { try { LogUtils.v(TAG, "createSocket: network specified"); + TrafficStats.setThreadStatsTag(TrafficStatsTags.VISUAL_VOICEMAIL_TAG); return mNetwork.getSocketFactory().createSocket(); } catch (IOException ioe) { LogUtils.d(TAG, ioe.toString()); diff --git a/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java b/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java deleted file mode 100644 index 5ad2447de..000000000 --- a/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java +++ /dev/null @@ -1,400 +0,0 @@ -/* - * Copyright (C) 2016 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.scheduling; - -import android.annotation.TargetApi; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.Binder; -import android.os.Build.VERSION_CODES; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.support.annotation.MainThread; -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; -import android.support.annotation.WorkerThread; -import com.android.voicemail.impl.Assert; -import com.android.voicemail.impl.NeededForTesting; -import com.android.voicemail.impl.VvmLog; -import com.android.voicemail.impl.scheduling.TaskQueue.NextTask; -import java.util.List; - -/** - * A service to queue and run {@link Task} with the {@link android.app.job.JobScheduler}. A task is - * queued using {@link Context#startService(Intent)}. The intent should contain enough information - * in {@link Intent#getExtras()} to construct the task (see {@link Tasks#createIntent(Context, - * Class)}). - * - * <p>All tasks are ran in the background with a wakelock being held by the {@link - * android.app.job.JobScheduler}, which is between {@link #onStartJob(Job, List)} and {@link - * #finishJob()}. The {@link TaskSchedulerJobService} also has a {@link TaskQueue}, but the data is - * stored in the {@link android.app.job.JobScheduler} instead of the process memory, so if the - * process is killed the queued tasks will be restored. If a new task is added, a new {@link - * TaskSchedulerJobService} will be scheduled to run the task. If the job is already scheduled, the - * new task will be pushed into the queue of the scheduled job. If the job is already running, the - * job will be queued in process memory. - * - * <p>Only one task will be ran at a time, and same task cannot exist in the queue at the same time. - * Refer to {@link TaskQueue} for queuing and execution order. - * - * <p>If there are still tasks in the queue but none are executable immediately, the service will - * enter a "sleep", pushing all remaining task into a new job and end the current job. - * - * <p>The service will be started when a intent is received, and stopped when there are no more - * tasks in the queue. - * - * <p>{@link android.app.job.JobScheduler} is not used directly due to: - * - * <ul> - * <li>The {@link android.telecom.PhoneAccountHandle} used to differentiate task can not be easily - * mapped into an integer for job id - * <li>A job cannot be mutated to store information such as retry count. - * </ul> - */ -@SuppressWarnings("AndroidApiChecker") /* stream() */ -@TargetApi(VERSION_CODES.O) -public class TaskSchedulerService extends Service { - - interface Job { - void finish(); - } - - private static final String TAG = "VvmTaskScheduler"; - - private static final int READY_TOLERANCE_MILLISECONDS = 100; - - /** - * Threshold to determine whether to do a short or long sleep when a task is scheduled in the - * future. - * - * <p>A short sleep will continue the job and use {@link Handler#postDelayed(Runnable, long)} to - * wait for the next task. - * - * <p>A long sleep will finish the job and schedule a new one. The exact execution time is - * subjected to {@link android.app.job.JobScheduler} battery optimization, and is not exact. - */ - private static final int SHORT_SLEEP_THRESHOLD_MILLISECONDS = 10_000; - /** - * When there are no more tasks to be run the service should be stopped. But when all tasks has - * finished there might still be more tasks in the message queue waiting to be processed, - * especially the ones submitted in {@link Task#onCompleted()}. Wait for a while before stopping - * the service to make sure there are no pending messages. - */ - private static final int STOP_DELAY_MILLISECONDS = 5_000; - - // The thread to run tasks on - private volatile WorkerThreadHandler mWorkerThreadHandler; - - /** - * Used by tests to turn task handling into a single threaded process by calling {@link - * Handler#handleMessage(Message)} directly - */ - private MessageSender mMessageSender = new MessageSender(); - - private MainThreadHandler mMainThreadHandler; - - // Binder given to clients - private final IBinder mBinder = new LocalBinder(); - - /** Main thread only, access through {@link #getTasks()} */ - private final TaskQueue mTasks = new TaskQueue(); - - private boolean mWorkerThreadIsBusy = false; - - private Job mJob; - - private final Runnable mStopServiceWithDelay = - new Runnable() { - @MainThread - @Override - public void run() { - VvmLog.i(TAG, "Stopping service"); - finishJob(); - stopSelf(); - } - }; - - /** Should attempt to run the next task when a task has finished or been added. */ - private boolean mTaskAutoRunDisabledForTesting = false; - - @VisibleForTesting - final class WorkerThreadHandler extends Handler { - - public WorkerThreadHandler(Looper looper) { - super(looper); - } - - @Override - @WorkerThread - public void handleMessage(Message msg) { - Assert.isNotMainThread(); - Task task = (Task) msg.obj; - try { - VvmLog.i(TAG, "executing task " + task); - task.onExecuteInBackgroundThread(); - } catch (Throwable throwable) { - VvmLog.e(TAG, "Exception while executing task " + task + ":", throwable); - } - - Message schedulerMessage = mMainThreadHandler.obtainMessage(); - schedulerMessage.obj = task; - mMessageSender.send(schedulerMessage); - } - } - - @VisibleForTesting - final class MainThreadHandler extends Handler { - - public MainThreadHandler(Looper looper) { - super(looper); - } - - @Override - @MainThread - public void handleMessage(Message msg) { - Assert.isMainThread(); - Task task = (Task) msg.obj; - getTasks().remove(task); - task.onCompleted(); - mWorkerThreadIsBusy = false; - maybeRunNextTask(); - } - } - - @Override - @MainThread - public void onCreate() { - super.onCreate(); - HandlerThread thread = new HandlerThread("VvmTaskSchedulerService"); - thread.start(); - - mWorkerThreadHandler = new WorkerThreadHandler(thread.getLooper()); - mMainThreadHandler = new MainThreadHandler(Looper.getMainLooper()); - } - - @Override - public void onDestroy() { - mWorkerThreadHandler.getLooper().quit(); - } - - @Override - @MainThread - public int onStartCommand(@Nullable Intent intent, int flags, int startId) { - Assert.isMainThread(); - if (intent == null) { - VvmLog.w(TAG, "null intent received"); - return START_NOT_STICKY; - } - Task task = Tasks.createTask(this, intent.getExtras()); - Assert.isTrue(task != null); - addTask(task); - - mMainThreadHandler.removeCallbacks(mStopServiceWithDelay); - VvmLog.i(TAG, "task added"); - if (mJob == null) { - scheduleJob(0, true); - } else { - maybeRunNextTask(); - } - // STICKY means the service will be automatically restarted will the last intent if it is - // killed. - return START_NOT_STICKY; - } - - @MainThread - @VisibleForTesting - void addTask(Task task) { - Assert.isMainThread(); - getTasks().add(task); - } - - @MainThread - @VisibleForTesting - TaskQueue getTasks() { - Assert.isMainThread(); - return mTasks; - } - - @MainThread - private void maybeRunNextTask() { - Assert.isMainThread(); - if (mWorkerThreadIsBusy) { - return; - } - if (mTaskAutoRunDisabledForTesting) { - // If mTaskAutoRunDisabledForTesting is true, runNextTask() must be explicitly called - // to run the next task. - return; - } - - runNextTask(); - } - - @VisibleForTesting - @MainThread - void runNextTask() { - Assert.isMainThread(); - if (getTasks().isEmpty()) { - prepareStop(); - return; - } - NextTask nextTask = getTasks().getNextTask(READY_TOLERANCE_MILLISECONDS); - - if (nextTask.task != null) { - nextTask.task.onBeforeExecute(); - Message message = mWorkerThreadHandler.obtainMessage(); - message.obj = nextTask.task; - mWorkerThreadIsBusy = true; - mMessageSender.send(message); - return; - } - VvmLog.i(TAG, "minimal wait time:" + nextTask.minimalWaitTimeMillis); - if (!mTaskAutoRunDisabledForTesting && nextTask.minimalWaitTimeMillis != null) { - // No tasks are currently ready. Sleep until the next one should be. - // If a new task is added during the sleep the service will wake immediately. - sleep(nextTask.minimalWaitTimeMillis); - } - } - - @MainThread - private void sleep(long timeMillis) { - VvmLog.i(TAG, "sleep for " + timeMillis + " millis"); - if (timeMillis < SHORT_SLEEP_THRESHOLD_MILLISECONDS) { - mMainThreadHandler.postDelayed( - new Runnable() { - @Override - public void run() { - maybeRunNextTask(); - } - }, - timeMillis); - return; - } - finishJob(); - mMainThreadHandler.post(() -> scheduleJob(timeMillis, false)); - } - - private List<Bundle> serializePendingTasks() { - return getTasks().toBundles(); - } - - private void prepareStop() { - VvmLog.i( - TAG, - "no more tasks, stopping service if no task are added in " - + STOP_DELAY_MILLISECONDS - + " millis"); - mMainThreadHandler.postDelayed(mStopServiceWithDelay, STOP_DELAY_MILLISECONDS); - } - - @NeededForTesting - static class MessageSender { - - public void send(Message message) { - message.sendToTarget(); - } - } - - @NeededForTesting - void setTaskAutoRunDisabledForTest(boolean value) { - mTaskAutoRunDisabledForTesting = value; - } - - @NeededForTesting - void setMessageSenderForTest(MessageSender sender) { - mMessageSender = sender; - } - - /** - * The {@link TaskSchedulerJobService} has started and all queued task should be executed in the - * worker thread. - */ - @MainThread - public void onStartJob(Job job, List<Bundle> pendingTasks) { - VvmLog.i(TAG, "onStartJob"); - mJob = job; - mTasks.fromBundles(this, pendingTasks); - maybeRunNextTask(); - } - - /** - * The {@link TaskSchedulerJobService} is being terminated by the system (timeout or network - * lost). A new job will be queued to resume all pending tasks. The current unfinished job may be - * ran again. - */ - @MainThread - public void onStopJob() { - VvmLog.e(TAG, "onStopJob"); - if (isJobRunning()) { - finishJob(); - mMainThreadHandler.post(() -> scheduleJob(0, true)); - } - } - - /** - * Serializes all pending tasks and schedule a new {@link TaskSchedulerJobService}. - * - * @param delayMillis the delay before stating the job, see {@link - * android.app.job.JobInfo.Builder#setMinimumLatency(long)}. This must be 0 if {@code - * isNewJob} is true. - * @param isNewJob a new job will be requested to run immediately, bypassing all requirements. - */ - @MainThread - private void scheduleJob(long delayMillis, boolean isNewJob) { - Assert.isMainThread(); - TaskSchedulerJobService.scheduleJob(this, serializePendingTasks(), delayMillis, isNewJob); - mTasks.clear(); - } - - /** - * Signals {@link TaskSchedulerJobService} the current session of tasks has finished, and the wake - * lock can be released. Note: this only takes effect after the main thread has been returned. If - * a new job need to be scheduled, it should be posted on the main thread handler instead of - * calling directly. - */ - @MainThread - private void finishJob() { - Assert.isMainThread(); - VvmLog.i(TAG, "finishing Job"); - mJob.finish(); - mJob = null; - } - - @Override - @Nullable - public IBinder onBind(Intent intent) { - return mBinder; - } - - @NeededForTesting - class LocalBinder extends Binder { - - @NeededForTesting - public TaskSchedulerService getService() { - return TaskSchedulerService.this; - } - } - - private boolean isJobRunning() { - return mJob != null; - } -} diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java b/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java new file mode 100644 index 000000000..17c9be73b --- /dev/null +++ b/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java @@ -0,0 +1,62 @@ +/* + * 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.content.Context; +import com.android.dialer.common.ConfigProviderBindings; + +/** Provides configuration values needed to connect to the transcription server. */ +public class TranscriptionConfigProvider { + private final Context context; + + public TranscriptionConfigProvider(Context context) { + this.context = context; + } + + public boolean isVoicemailTranscriptionEnabled() { + return ConfigProviderBindings.get(context).getBoolean("voicemail_transcription_enabled", false); + } + + public String getServerAddress() { + // Private voicemail transcription service + return ConfigProviderBindings.get(context) + .getString( + "voicemail_transcription_server_address", "voicemailtranscription-pa.googleapis.com"); + } + + public String getApiKey() { + // Android API key restricted to com.google.android.dialer + return ConfigProviderBindings.get(context) + .getString( + "voicemail_transcription_client_api_key", "AIzaSyAXdDnif6B7sBYxU8hzw9qAp3pRPVHs060"); + } + + public String getAuthToken() { + return null; + } + + public boolean shouldUsePlaintext() { + return ConfigProviderBindings.get(context) + .getBoolean("voicemail_transcription_server_use_plaintext", false); + } + + @Override + public String toString() { + return String.format( + "{ address: %s, api key: %s, auth token: %s, plaintext: %b }", + getServerAddress(), getApiKey(), getAuthToken(), shouldUsePlaintext()); + } +} diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionDbHelper.java b/java/com/android/voicemail/impl/transcribe/TranscriptionDbHelper.java new file mode 100644 index 000000000..cbc5cb8a0 --- /dev/null +++ b/java/com/android/voicemail/impl/transcribe/TranscriptionDbHelper.java @@ -0,0 +1,105 @@ +/* + * 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.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build.VERSION_CODES; +import android.provider.VoicemailContract.Voicemails; +import android.support.annotation.WorkerThread; +import android.support.v4.os.BuildCompat; +import android.util.Pair; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; + +/** Helper class for reading and writing transcription data in the database */ +@TargetApi(VERSION_CODES.O) +public class TranscriptionDbHelper { + private static final String[] PROJECTION = + new String[] { + Voicemails.TRANSCRIPTION, // 0 + VoicemailCompat.TRANSCRIPTION_STATE // 1 + }; + + public static final int TRANSCRIPTION = 0; + public static final int TRANSCRIPTION_STATE = 1; + + private final ContentResolver contentResolver; + private final Uri uri; + + TranscriptionDbHelper(Context context, Uri uri) { + Assert.isNotNull(uri); + this.contentResolver = context.getContentResolver(); + this.uri = uri; + } + + @WorkerThread + @TargetApi(VERSION_CODES.M) // used for try with resources + Pair<String, Integer> getTranscriptionAndState() { + Assert.checkArgument(BuildCompat.isAtLeastO()); + Assert.isWorkerThread(); + try (Cursor cursor = contentResolver.query(uri, PROJECTION, null, null, null)) { + if (cursor == null) { + LogUtil.e("TranscriptionDbHelper.getTranscriptionAndState", "query failed."); + return null; + } + + if (cursor.moveToFirst()) { + String transcription = cursor.getString(TRANSCRIPTION); + int transcriptionState = cursor.getInt(TRANSCRIPTION_STATE); + return new Pair<>(transcription, transcriptionState); + } + } + LogUtil.i("TranscriptionDbHelper.getTranscriptionAndState", "query returned no results"); + return null; + } + + @WorkerThread + void setTranscriptionState(int transcriptionState) { + Assert.isWorkerThread(); + LogUtil.i( + "TranscriptionDbHelper.setTranscriptionState", + "uri: " + uri + ", state: " + transcriptionState); + ContentValues values = new ContentValues(); + values.put(VoicemailCompat.TRANSCRIPTION_STATE, transcriptionState); + updateDatabase(values); + } + + @WorkerThread + void setTranscriptionAndState(String transcription, int transcriptionState) { + Assert.isWorkerThread(); + LogUtil.i( + "TranscriptionDbHelper.setTranscriptionAndState", + "uri: " + uri + ", state: " + transcriptionState); + ContentValues values = new ContentValues(); + values.put(Voicemails.TRANSCRIPTION, transcription); + values.put(VoicemailCompat.TRANSCRIPTION_STATE, transcriptionState); + updateDatabase(values); + } + + private void updateDatabase(ContentValues values) { + int updatedCount = contentResolver.update(uri, values, null, null); + if (updatedCount != 1) { + LogUtil.e( + "TranscriptionDbHelper.updateDatabase", + "Wrong row count, should have updated 1 row, was: " + updatedCount); + } + } +} diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionService.java b/java/com/android/voicemail/impl/transcribe/TranscriptionService.java new file mode 100644 index 000000000..3e80a7f59 --- /dev/null +++ b/java/com/android/voicemail/impl/transcribe/TranscriptionService.java @@ -0,0 +1,203 @@ +/* + * 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.os.StrictMode; +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.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; + private StrictMode.VmPolicy originalPolicy; + + /** 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 transcribeVoicemail(Context context, Uri voicemailUri) { + Assert.isMainThread(); + if (BuildCompat.isAtLeastO()) { + LogUtil.i("TranscriptionService.transcribeVoicemail", "scheduling transcription"); + ComponentName componentName = new ComponentName(context, TranscriptionService.class); + JobInfo.Builder builder = + new JobInfo.Builder(ScheduledJobIds.VVM_TRANSCRIPTION_JOB, componentName) + .setMinimumLatency(0) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + JobScheduler scheduler = context.getSystemService(JobScheduler.class); + JobWorkItem workItem = makeWorkItem(voicemailUri); + return scheduler.enqueue(builder.build(), workItem) == JobScheduler.RESULT_SUCCESS; + } else { + LogUtil.i("TranscriptionService.transcribeVoicemail", "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()); + originalPolicy = StrictMode.getVmPolicy(); + StrictMode.enableDefaults(); + 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; + } + if (originalPolicy != null) { + StrictMode.setVmPolicy(originalPolicy); + originalPolicy = null; + } + } + + @MainThread + private boolean checkForWork() { + Assert.isMainThread(); + JobWorkItem workItem = jobParameters.dequeueWork(); + if (workItem != null) { + getExecutorService() + .execute(new TranscriptionTask(this, new Callback(), workItem, getClientFactory())); + return true; + } else { + return false; + } + } + + 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; + } +} diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java b/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java new file mode 100644 index 000000000..0fbc33ad5 --- /dev/null +++ b/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java @@ -0,0 +1,191 @@ +/* + * 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.annotation.TargetApi; +import android.app.job.JobWorkItem; +import android.content.Context; +import android.net.Uri; +import android.text.TextUtils; +import com.android.dialer.common.concurrent.ThreadUtil; +import com.android.voicemail.impl.VvmLog; +import com.android.voicemail.impl.transcribe.TranscriptionService.JobCallback; +import com.android.voicemail.impl.transcribe.grpc.TranscriptionClient; +import com.android.voicemail.impl.transcribe.grpc.TranscriptionClientFactory; +import com.google.internal.communications.voicemailtranscription.v1.AudioFormat; +import com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailRequest; +import com.google.protobuf.ByteString; +import io.grpc.Status; +import java.io.IOException; +import java.io.InputStream; + +/** + * Background task to get a voicemail transcription and update the database. + * + * <pre> + * This task performs the following steps: + * 1. Update the transcription-state in the database to 'in-progress' + * 2. Create grpc client and transcription request + * 3. Make synchronous grpc transcription request to backend server + * 3a. On response + * Update the database with transcription (if successful) and new transcription-state + * 3b. On network error + * If retry-count < max then increment retry-count and retry the request + * Otherwise update the transcription-state in the database to 'transcription-failed' + * 4. Notify the callback that the work item is complete + * </pre> + */ +public class TranscriptionTask implements Runnable { + private static final String TAG = "TranscriptionTask"; + + private final Context context; + private final JobCallback callback; + private final JobWorkItem workItem; + private final TranscriptionClientFactory clientFactory; + private final Uri voicemailUri; + private final TranscriptionDbHelper databaseHelper; + private ByteString audioData; + private AudioFormat encoding; + + private static final int MAX_RETRIES = 2; + static final String AMR_PREFIX = "#!AMR\n"; + + public TranscriptionTask( + Context context, + JobCallback callback, + JobWorkItem workItem, + TranscriptionClientFactory clientFactory) { + this.context = context; + this.callback = callback; + this.workItem = workItem; + this.clientFactory = clientFactory; + this.voicemailUri = getVoicemailUri(workItem); + databaseHelper = new TranscriptionDbHelper(context, voicemailUri); + } + + @Override + public void run() { + VvmLog.i(TAG, "run"); + if (readAndValidateAudioFile()) { + updateTranscriptionState(VoicemailCompat.TRANSCRIPTION_IN_PROGRESS); + transcribeVoicemail(); + } else { + updateTranscriptionState(VoicemailCompat.TRANSCRIPTION_FAILED); + } + ThreadUtil.postOnUiThread( + () -> { + callback.onWorkCompleted(workItem); + }); + } + + private void transcribeVoicemail() { + VvmLog.i(TAG, "transcribeVoicemail"); + TranscribeVoicemailRequest request = makeRequest(); + TranscriptionClient client = clientFactory.getClient(); + String transcript = null; + for (int i = 0; transcript == null && i < MAX_RETRIES; i++) { + VvmLog.i(TAG, "transcribeVoicemail, try: " + (i + 1)); + TranscriptionClient.TranscriptionResponseWrapper responseWrapper = + client.transcribeVoicemail(request); + if (responseWrapper.status != null) { + VvmLog.i(TAG, "transcribeVoicemail, status: " + responseWrapper.status.getCode()); + if (shouldRetryRequest(responseWrapper.status)) { + backoff(i); + } else { + break; + } + } else if (responseWrapper.response != null) { + if (!TextUtils.isEmpty(responseWrapper.response.getTranscript())) { + VvmLog.i(TAG, "transcribeVoicemail, got response"); + transcript = responseWrapper.response.getTranscript(); + } else { + VvmLog.i(TAG, "transcribeVoicemail, empty transcription"); + } + } else { + VvmLog.w(TAG, "transcribeVoicemail, no response"); + } + } + + int newState = + (transcript == null) + ? VoicemailCompat.TRANSCRIPTION_FAILED + : VoicemailCompat.TRANSCRIPTION_AVAILABLE; + updateTranscriptionAndState(transcript, newState); + } + + private static boolean shouldRetryRequest(Status status) { + return status.getCode() == Status.Code.UNAVAILABLE; + } + + private static void backoff(int retryCount) { + VvmLog.i(TAG, "backoff, count: " + retryCount); + try { + long millis = (1 << retryCount) * 1000; + Thread.sleep(millis); + } catch (InterruptedException e) { + VvmLog.w(TAG, "interrupted"); + Thread.currentThread().interrupt(); + } + } + + private void updateTranscriptionAndState(String transcript, int newState) { + databaseHelper.setTranscriptionAndState(transcript, newState); + } + + private void updateTranscriptionState(int newState) { + databaseHelper.setTranscriptionState(newState); + } + + private TranscribeVoicemailRequest makeRequest() { + return TranscribeVoicemailRequest.newBuilder() + .setVoicemailData(audioData) + .setAudioFormat(encoding) + .build(); + } + + // Uses try-with-resource + @TargetApi(android.os.Build.VERSION_CODES.M) + private boolean readAndValidateAudioFile() { + if (voicemailUri == null) { + VvmLog.i(TAG, "Transcriber.readAndValidateAudioFile, file not found."); + return false; + } else { + VvmLog.i(TAG, "Transcriber.readAndValidateAudioFile, reading: " + voicemailUri); + } + + try (InputStream in = context.getContentResolver().openInputStream(voicemailUri)) { + audioData = ByteString.readFrom(in); + VvmLog.i(TAG, "Transcriber.readAndValidateAudioFile, read " + audioData.size() + " bytes"); + } catch (IOException e) { + VvmLog.e(TAG, "Transcriber.readAndValidateAudioFile", e); + return false; + } + + if (audioData.startsWith(ByteString.copyFromUtf8(AMR_PREFIX))) { + encoding = AudioFormat.AMR_NB_8KHZ; + } else { + VvmLog.i(TAG, "Transcriber.readAndValidateAudioFile, unknown encoding"); + encoding = AudioFormat.AUDIO_FORMAT_UNSPECIFIED; + return false; + } + + return true; + } + + private static Uri getVoicemailUri(JobWorkItem workItem) { + return workItem.getIntent().getParcelableExtra(TranscriptionService.EXTRA_VOICEMAIL_URI); + } +} diff --git a/java/com/android/voicemail/impl/transcribe/VoicemailCompat.java b/java/com/android/voicemail/impl/transcribe/VoicemailCompat.java new file mode 100644 index 000000000..c6e30c6de --- /dev/null +++ b/java/com/android/voicemail/impl/transcribe/VoicemailCompat.java @@ -0,0 +1,59 @@ +/* + * 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; + +/** + * Provide access to new API constants before they're publicly available + * + * <p>Copied from android.provider.VoicemailContract.Voicemails. These should become public in O-MR1 + * and these constants can be removed then. + */ +public class VoicemailCompat { + + /** + * The state of the voicemail transcription. + * + * <p>Possible values: {@link #TRANSCRIPTION_NOT_STARTED}, {@link #TRANSCRIPTION_IN_PROGRESS}, + * {@link #TRANSCRIPTION_FAILED}, {@link #TRANSCRIPTION_AVAILABLE}. + * + * <p>Type: INTEGER + */ + public static final String TRANSCRIPTION_STATE = "transcription_state"; + + /** + * Value of {@link #TRANSCRIPTION_STATE} when the voicemail transcription has not yet been + * attempted. + */ + public static final int TRANSCRIPTION_NOT_STARTED = 0; + + /** + * Value of {@link #TRANSCRIPTION_STATE} when the voicemail transcription has begun but is not yet + * complete. + */ + public static final int TRANSCRIPTION_IN_PROGRESS = 1; + + /** + * Value of {@link #TRANSCRIPTION_STATE} when the voicemail transcription has been attempted and + * failed. + */ + public static final int TRANSCRIPTION_FAILED = 2; + + /** + * Value of {@link #TRANSCRIPTION_STATE} when the voicemail transcription has completed and the + * result has been stored in the {@link #TRANSCRIPTION} column. + */ + public static final int TRANSCRIPTION_AVAILABLE = 3; +} diff --git a/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionClient.java b/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionClient.java new file mode 100644 index 000000000..27603d910 --- /dev/null +++ b/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionClient.java @@ -0,0 +1,61 @@ +/* + * 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.grpc; + +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; +import com.android.dialer.common.Assert; +import com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailRequest; +import com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailResponse; +import com.google.internal.communications.voicemailtranscription.v1.VoicemailTranscriptionServiceGrpc; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; + +/** Wrapper around Grpc transcription server stub */ +public class TranscriptionClient { + + private final VoicemailTranscriptionServiceGrpc.VoicemailTranscriptionServiceBlockingStub stub; + + /** Wraps the server response and status objects, either of which may be null. */ + public static class TranscriptionResponseWrapper { + public final TranscribeVoicemailResponse response; + public final Status status; + + public TranscriptionResponseWrapper( + @Nullable TranscribeVoicemailResponse response, @Nullable Status status) { + Assert.checkArgument(!(response == null && status == null)); + this.response = response; + this.status = status; + } + } + + TranscriptionClient( + VoicemailTranscriptionServiceGrpc.VoicemailTranscriptionServiceBlockingStub stub) { + this.stub = stub; + } + + @WorkerThread + public TranscriptionResponseWrapper transcribeVoicemail(TranscribeVoicemailRequest request) { + TranscribeVoicemailResponse response = null; + Status status = null; + try { + response = stub.transcribeVoicemail(request); + } catch (StatusRuntimeException e) { + status = e.getStatus(); + } + return new TranscriptionClient.TranscriptionResponseWrapper(response, status); + } +} diff --git a/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionClientFactory.java b/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionClientFactory.java new file mode 100644 index 000000000..6101ed516 --- /dev/null +++ b/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionClientFactory.java @@ -0,0 +1,194 @@ +/* + * 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.grpc; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.text.TextUtils; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.voicemail.impl.transcribe.TranscriptionConfigProvider; +import com.google.internal.communications.voicemailtranscription.v1.VoicemailTranscriptionServiceGrpc; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ClientInterceptors; +import io.grpc.ForwardingClientCall; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.okhttp.OkHttpChannelBuilder; +import java.security.MessageDigest; + +/** + * Factory for creating grpc clients that talk to the transcription server. This allows all clients + * to share the same channel, which is relatively expensive to create. + */ +public class TranscriptionClientFactory { + private static final String DIGEST_ALGORITHM_SHA1 = "SHA1"; + private static final char[] HEX_UPPERCASE = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + private final TranscriptionConfigProvider configProvider; + private final ManagedChannel originalChannel; + private final String packageName; + private final String cert; + + public TranscriptionClientFactory(Context context, TranscriptionConfigProvider configProvider) { + this(context, configProvider, getManagedChannel(configProvider)); + } + + public TranscriptionClientFactory( + Context context, TranscriptionConfigProvider configProvider, ManagedChannel managedChannel) { + this.configProvider = configProvider; + this.packageName = context.getPackageName(); + this.cert = getCertificateFingerprint(context); + originalChannel = managedChannel; + } + + public TranscriptionClient getClient() { + LogUtil.enterBlock("TranscriptionClientFactory.getClient"); + Assert.checkState(!originalChannel.isShutdown()); + Channel channel = + ClientInterceptors.intercept( + originalChannel, + new Interceptor( + packageName, cert, configProvider.getApiKey(), configProvider.getAuthToken())); + return new TranscriptionClient(VoicemailTranscriptionServiceGrpc.newBlockingStub(channel)); + } + + public void shutdown() { + LogUtil.enterBlock("TranscriptionClientFactory.shutdown"); + originalChannel.shutdown(); + } + + private static ManagedChannel getManagedChannel(TranscriptionConfigProvider configProvider) { + ManagedChannelBuilder<OkHttpChannelBuilder> builder = + OkHttpChannelBuilder.forTarget(configProvider.getServerAddress()); + // Only use plaintext for debugging + if (configProvider.shouldUsePlaintext()) { + // Just passing 'false' doesnt have the same effect as not setting this field + builder.usePlaintext(true); + } + return builder.build(); + } + + private static String getCertificateFingerprint(Context context) { + try { + PackageInfo packageInfo = + context + .getPackageManager() + .getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES); + if (packageInfo != null + && packageInfo.signatures != null + && packageInfo.signatures.length > 0) { + MessageDigest messageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM_SHA1); + if (messageDigest == null) { + LogUtil.w( + "TranscriptionClientFactory.getCertificateFingerprint", "error getting digest."); + return null; + } + byte[] bytes = messageDigest.digest(packageInfo.signatures[0].toByteArray()); + if (bytes == null) { + LogUtil.w( + "TranscriptionClientFactory.getCertificateFingerprint", "empty message digest."); + return null; + } + + int length = bytes.length; + StringBuilder out = new StringBuilder(length * 2); + for (int i = 0; i < length; i++) { + out.append(HEX_UPPERCASE[(bytes[i] & 0xf0) >>> 4]); + out.append(HEX_UPPERCASE[bytes[i] & 0x0f]); + } + return out.toString(); + } else { + LogUtil.w( + "TranscriptionClientFactory.getCertificateFingerprint", + "failed to get package signature."); + } + } catch (Exception e) { + LogUtil.e( + "TranscriptionClientFactory.getCertificateFingerprint", + "error getting certificate fingerprint.", + e); + } + + return null; + } + + private static final class Interceptor implements ClientInterceptor { + private final String packageName; + private final String cert; + private final String apiKey; + private final String authToken; + + private static final Metadata.Key<String> API_KEY_HEADER = + Metadata.Key.of("X-Goog-Api-Key", Metadata.ASCII_STRING_MARSHALLER); + private static final Metadata.Key<String> ANDROID_PACKAGE_HEADER = + Metadata.Key.of("X-Android-Package", Metadata.ASCII_STRING_MARSHALLER); + private static final Metadata.Key<String> ANDROID_CERT_HEADER = + Metadata.Key.of("X-Android-Cert", Metadata.ASCII_STRING_MARSHALLER); + private static final Metadata.Key<String> AUTHORIZATION_HEADER = + Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER); + + public Interceptor(String packageName, String cert, String apiKey, String authToken) { + this.packageName = packageName; + this.cert = cert; + this.apiKey = apiKey; + this.authToken = authToken; + } + + @Override + public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall( + MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) { + LogUtil.enterBlock( + "TranscriptionClientFactory.interceptCall, intercepted " + method.getFullMethodName()); + ClientCall<ReqT, RespT> call = next.newCall(method, callOptions); + + call = + new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(call) { + @Override + public void start(Listener<RespT> responseListener, Metadata headers) { + if (!TextUtils.isEmpty(packageName)) { + LogUtil.i( + "TranscriptionClientFactory.interceptCall", + "attaching package name: " + packageName); + headers.put(ANDROID_PACKAGE_HEADER, packageName); + } + if (!TextUtils.isEmpty(cert)) { + LogUtil.i("TranscriptionClientFactory.interceptCall", "attaching android cert"); + headers.put(ANDROID_CERT_HEADER, cert); + } + if (!TextUtils.isEmpty(apiKey)) { + LogUtil.i("TranscriptionClientFactory.interceptCall", "attaching API Key"); + headers.put(API_KEY_HEADER, apiKey); + } + if (!TextUtils.isEmpty(authToken)) { + LogUtil.i("TranscriptionClientFactory.interceptCall", "attaching auth token"); + headers.put(AUTHORIZATION_HEADER, "Bearer " + authToken); + } + super.start(responseListener, headers); + } + }; + return call; + } + } +} diff --git a/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto b/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto new file mode 100644 index 000000000..4b1e19b8a --- /dev/null +++ b/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto @@ -0,0 +1,44 @@ +// LINT.IfChange + +syntax = "proto2"; + +package google.internal.communications.voicemailtranscription.v1; + +option java_multiple_files = true; +option java_package = "com.google.internal.communications.voicemailtranscription.v1"; +option optimize_for = LITE_RUNTIME; + +// Enum that specifies supported audio formats. +enum AudioFormat { + // Default but invalid value. + AUDIO_FORMAT_UNSPECIFIED = 0; + + // Adaptive Multi-Rate Narrowband, 8kHz sampling frequency. + // https://en.wikipedia.org/wiki/Adaptive_Multi-Rate_audio_codec + AMR_NB_8KHZ = 1; +} + +// Request for synchronous voicemail transcription. +message TranscribeVoicemailRequest { + // Voicemail audio file containing the raw bytes we receive from the carrier. + optional bytes voicemail_data = 1; + + // Audio format of the voicemail file. + optional AudioFormat audio_format = 2; +} + +// Response for synchronous voicemail transcription. +message TranscribeVoicemailResponse { + // The transcribed text of the voicemail. + optional string transcript = 1; +} + +// RPC service for transcribing voicemails. +service VoicemailTranscriptionService { + // Returns a transcript of the given voicemail. + rpc TranscribeVoicemail(TranscribeVoicemailRequest) + returns (TranscribeVoicemailResponse) {} +} + +// LINT.ThenChange(//depot/google3/google/internal/communications/voicemailtranscription/v1/\ +// voicemail_transcription.proto) |