diff options
Diffstat (limited to 'java/com/android/voicemail')
17 files changed, 627 insertions, 1212 deletions
diff --git a/java/com/android/voicemail/impl/AndroidManifest.xml b/java/com/android/voicemail/impl/AndroidManifest.xml index be7dac10d..95e6e8212 100644 --- a/java/com/android/voicemail/impl/AndroidManifest.xml +++ b/java/com/android/voicemail/impl/AndroidManifest.xml @@ -97,11 +97,6 @@ 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 new file mode 100644 index 000000000..9baf95415 --- /dev/null +++ b/java/com/android/voicemail/impl/OmtpReceiver.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; + +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 new file mode 100644 index 000000000..4762e9023 --- /dev/null +++ b/java/com/android/voicemail/impl/TelephonyManagerStub.java @@ -0,0 +1,40 @@ +/* + * 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 new file mode 100644 index 000000000..1e2de6070 --- /dev/null +++ b/java/com/android/voicemail/impl/VvmPackageInstallReceiver.java @@ -0,0 +1,80 @@ +/* + * 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 deleted file mode 100644 index 448c69356..000000000 --- a/java/com/android/voicemail/impl/com/google/internal/communications/voicemailtranscription/v1/VoicemailTranscriptionServiceGrpc.java +++ /dev/null @@ -1,254 +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.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 d15ce12ef..f386fce0e 100644 --- a/java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java +++ b/java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java @@ -23,12 +23,9 @@ 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; @@ -59,7 +56,6 @@ 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(); @@ -94,23 +90,13 @@ public class VoicemailFetchedCallback { ContentValues values = new ContentValues(); values.put(Voicemails.MIME_TYPE, voicemailPayload.getMimeType()); values.put(Voicemails.HAS_CONTENT, true); - if (updateVoicemail(values)) { - ThreadUtil.postOnUiThread( - () -> { - if (!TranscriptionService.transcribeVoicemail(mContext, mUri)) { - VvmLog.w(TAG, String.format("Failed to schedule transcription for %s", mUri)); - } - }); - } + updateVoicemail(values); } - private boolean updateVoicemail(ContentValues values) { + private void 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 00339f03d..3df36d544 100644 --- a/java/com/android/voicemail/impl/mail/MailTransport.java +++ b/java/com/android/voicemail/impl/mail/MailTransport.java @@ -17,9 +17,7 @@ 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; @@ -190,7 +188,6 @@ 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 new file mode 100644 index 000000000..5ad2447de --- /dev/null +++ b/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java @@ -0,0 +1,400 @@ +/* + * 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 deleted file mode 100644 index 17c9be73b..000000000 --- a/java/com/android/voicemail/impl/transcribe/TranscriptionConfigProvider.java +++ /dev/null @@ -1,62 +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.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 deleted file mode 100644 index cbc5cb8a0..000000000 --- a/java/com/android/voicemail/impl/transcribe/TranscriptionDbHelper.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.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 deleted file mode 100644 index 3e80a7f59..000000000 --- a/java/com/android/voicemail/impl/transcribe/TranscriptionService.java +++ /dev/null @@ -1,203 +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.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 deleted file mode 100644 index 0fbc33ad5..000000000 --- a/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java +++ /dev/null @@ -1,191 +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.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 deleted file mode 100644 index c6e30c6de..000000000 --- a/java/com/android/voicemail/impl/transcribe/VoicemailCompat.java +++ /dev/null @@ -1,59 +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.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 deleted file mode 100644 index 27603d910..000000000 --- a/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionClient.java +++ /dev/null @@ -1,61 +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.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 deleted file mode 100644 index 6101ed516..000000000 --- a/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionClientFactory.java +++ /dev/null @@ -1,194 +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.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 deleted file mode 100644 index 4b1e19b8a..000000000 --- a/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto +++ /dev/null @@ -1,44 +0,0 @@ -// 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) diff --git a/java/com/android/voicemail/AndroidManifest.xml b/java/com/android/voicemail/permissions.xml index aedfd6fbf..bedf06898 100644 --- a/java/com/android/voicemail/AndroidManifest.xml +++ b/java/com/android/voicemail/permissions.xml @@ -1,18 +1,3 @@ -<!-- - ~ 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 - --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.voicemailomtp"> |