diff options
author | mdooley <mdooley@google.com> | 2017-11-23 08:31:05 -0800 |
---|---|---|
committer | Eric Erfanian <erfanian@google.com> | 2017-11-28 14:34:47 -0800 |
commit | 70fedf8d6caee1177ee891bbfff404dc48867c16 (patch) | |
tree | c59375133ee2f7236c17758706e88a492db81259 | |
parent | 5b6d823a45fe56cf9c36e5b00908831049c1d827 (diff) |
Adding transcription rating feedback
Allow users who have agreed to donate their voicemails to also provide
transcription quality feedback.
screenshot:
https://drive.google.com/open?id=0B9o_KvtLkcuIajVtdFN3Y0Qydmx2NXJYN2N3OVA3N0h5UEdR
Bug: 68712148
Test: manual and new unit tests
PiperOrigin-RevId: 176774942
Change-Id: I08b9afbbefaedfb0de5199038a1d2769bd983855
19 files changed, 534 insertions, 32 deletions
diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java index 47ef32d42..4f78bc97f 100644 --- a/java/com/android/dialer/app/calllog/CallLogAdapter.java +++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java @@ -960,6 +960,7 @@ public class CallLogAdapter extends GroupingListAdapter } views.callType = cursor.getInt(CallLogQuery.CALL_TYPE); views.voicemailUri = cursor.getString(CallLogQuery.VOICEMAIL_URI); + details.voicemailUri = views.voicemailUri; return details; } diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java index ad931e87a..3898d1f24 100644 --- a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java +++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java @@ -19,30 +19,39 @@ package com.android.dialer.app.calllog; import android.content.Context; import android.content.res.Resources; import android.graphics.Typeface; +import android.net.Uri; import android.provider.CallLog.Calls; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.support.v4.content.ContextCompat; import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; import android.text.TextUtils; import android.text.format.DateUtils; import android.text.util.Linkify; +import android.view.Gravity; import android.view.View; import android.widget.TextView; +import android.widget.Toast; import com.android.dialer.app.R; import com.android.dialer.app.calllog.calllogcache.CallLogCache; import com.android.dialer.calllogutils.PhoneCallDetails; +import com.android.dialer.common.LogUtil; import com.android.dialer.compat.android.provider.VoicemailCompat; import com.android.dialer.logging.ContactSource; import com.android.dialer.oem.MotorolaUtils; import com.android.dialer.phonenumberutil.PhoneNumberHelper; import com.android.dialer.util.DialerUtils; +import com.android.voicemail.VoicemailComponent; +import com.android.voicemail.impl.transcribe.TranscriptionRatingHelper; +import com.google.internal.communications.voicemailtranscription.v1.TranscriptionRatingValue; import java.util.ArrayList; import java.util.Calendar; import java.util.concurrent.TimeUnit; /** Helper class to fill in the views in {@link PhoneCallDetailsViews}. */ -public class PhoneCallDetailsHelper { - +public class PhoneCallDetailsHelper + implements TranscriptionRatingHelper.SuccessListener, + TranscriptionRatingHelper.FailureListener { /** The maximum number of icons will be shown to represent the call types in a group. */ private static final int MAX_CALL_TYPE_ICONS = 3; @@ -152,13 +161,16 @@ public class PhoneCallDetailsHelper { String transcript = ""; String branding = ""; + boolean showRatingPrompt = false; if (!TextUtils.isEmpty(details.transcription)) { transcript = details.transcription; - // Set the branding text if the voicemail was transcribed by google - // TODO(mdooley): the transcription state is only set by the google transcription code, - // but a better solution would be to check the SOURCE_PACKAGE - if (details.transcriptionState == VoicemailCompat.TRANSCRIPTION_AVAILABLE) { + // Show a transcription quality rating prompt or set the branding text if the voicemail was + // transcribed by google + if (shouldShowTranscriptionRating(details.transcriptionState, details.accountHandle)) { + showRatingPrompt = true; + } else if (details.transcriptionState == VoicemailCompat.TRANSCRIPTION_AVAILABLE + || details.transcriptionState == VoicemailCompat.TRANSCRIPTION_AVAILABLE_AND_RATED) { branding = mResources.getString(R.string.voicemail_transcription_branding_text); } } else { @@ -183,7 +195,28 @@ public class PhoneCallDetailsHelper { } views.voicemailTranscriptionView.setText(transcript); - views.voicemailTranscriptionBrandingView.setText(branding); + if (showRatingPrompt) { + views.voicemailTranscriptionBrandingView.setVisibility(View.GONE); + + View ratingView = views.voicemailTranscriptionRatingView; + ratingView.setVisibility(View.VISIBLE); + ratingView + .findViewById(R.id.voicemail_transcription_rating_good) + .setOnClickListener( + view -> + recordTranscriptionRating( + TranscriptionRatingValue.GOOD_TRANSCRIPTION, details)); + ratingView + .findViewById(R.id.voicemail_transcription_rating_bad) + .setOnClickListener( + view -> + recordTranscriptionRating(TranscriptionRatingValue.BAD_TRANSCRIPTION, details)); + } else { + views.voicemailTranscriptionRatingView.setVisibility(View.GONE); + + views.voicemailTranscriptionBrandingView.setVisibility(View.VISIBLE); + views.voicemailTranscriptionBrandingView.setText(branding); + } } // Bold if not read @@ -198,6 +231,40 @@ public class PhoneCallDetailsHelper { details.isRead ? R.color.call_log_detail_color : R.color.call_log_unread_text_color)); } + private boolean shouldShowTranscriptionRating( + int transcriptionState, PhoneAccountHandle account) { + // TODO(mdooley): add a configurable random element here? + return transcriptionState == VoicemailCompat.TRANSCRIPTION_AVAILABLE + && VoicemailComponent.get(mContext) + .getVoicemailClient() + .isVoicemailDonationEnabled(mContext, account); + } + + private void recordTranscriptionRating( + TranscriptionRatingValue ratingValue, PhoneCallDetails details) { + LogUtil.enterBlock("PhoneCallDetailsHelper.recordTranscriptionRating"); + TranscriptionRatingHelper.sendRating( + mContext, + ratingValue, + Uri.parse(details.voicemailUri), + this::onRatingSuccess, + this::onRatingFailure); + } + + @Override + public void onRatingSuccess(Uri voicemailUri) { + LogUtil.enterBlock("PhoneCallDetailsHelper.onRatingSuccess"); + Toast toast = + Toast.makeText(mContext, R.string.voicemail_transcription_rating_thanks, Toast.LENGTH_LONG); + toast.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 50); + toast.show(); + } + + @Override + public void onRatingFailure(Throwable t) { + LogUtil.e("PhoneCallDetailsHelper.onRatingFailure", "failed to send rating", t); + } + /** * Builds a string containing the call location and date. For voicemail logs only the call date is * returned because location information is displayed in the call action button diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java index 40c0894f0..8b7a92bd4 100644 --- a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java +++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java @@ -32,6 +32,7 @@ public final class PhoneCallDetailsViews { public final View transcriptionView; public final TextView voicemailTranscriptionView; public final TextView voicemailTranscriptionBrandingView; + public final View voicemailTranscriptionRatingView; public final TextView callAccountLabel; private PhoneCallDetailsViews( @@ -42,6 +43,7 @@ public final class PhoneCallDetailsViews { View transcriptionView, TextView voicemailTranscriptionView, TextView voicemailTranscriptionBrandingView, + View voicemailTranscriptionRatingView, TextView callAccountLabel) { this.nameView = nameView; this.callTypeView = callTypeView; @@ -50,6 +52,7 @@ public final class PhoneCallDetailsViews { this.transcriptionView = transcriptionView; this.voicemailTranscriptionView = voicemailTranscriptionView; this.voicemailTranscriptionBrandingView = voicemailTranscriptionBrandingView; + this.voicemailTranscriptionRatingView = voicemailTranscriptionRatingView; this.callAccountLabel = callAccountLabel; } @@ -69,6 +72,7 @@ public final class PhoneCallDetailsViews { view.findViewById(R.id.transcription), (TextView) view.findViewById(R.id.voicemail_transcription), (TextView) view.findViewById(R.id.voicemail_transcription_branding), + view.findViewById(R.id.voicemail_transcription_rating), (TextView) view.findViewById(R.id.call_account_label)); } @@ -81,6 +85,7 @@ public final class PhoneCallDetailsViews { new View(context), new TextView(context), new TextView(context), + new View(context), new TextView(context)); } } diff --git a/java/com/android/dialer/app/res/layout/call_log_list_item.xml b/java/com/android/dialer/app/res/layout/call_log_list_item.xml index e0f9e63b4..75c8fe6e2 100644 --- a/java/com/android/dialer/app/res/layout/call_log_list_item.xml +++ b/java/com/android/dialer/app/res/layout/call_log_list_item.xml @@ -150,7 +150,7 @@ <LinearLayout android:id="@+id/transcription" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/call_log_icon_margin" android:visibility="gone" @@ -174,8 +174,54 @@ android:textSize="@dimen/call_log_voicemail_transcription_text_size" android:focusable="true" android:nextFocusUp="@id/voicemail_transcription" + android:nextFocusDown="@+id/voicemail_transcription_rating" android:paddingTop="2dp"/> + <LinearLayout + android:id="@+id/voicemail_transcription_rating" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/call_log_icon_margin" + android:layout_gravity="center_vertical" + android:visibility="gone" + android:paddingTop="2dp" + android:orientation="horizontal"> + + <TextView + style="@style/TranscriptionQualityRating" + android:id="@+id/voicemail_transcription_rating_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:gravity="start" + android:text="@string/voicemail_transcription_rating"/> + + <TextView + style="@style/TranscriptionQualityRatingLink" + android:id="@+id/voicemail_transcription_rating_good" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="end" + android:text="@string/voicemail_transcription_rating_good"/> + + <TextView + style="@style/TranscriptionQualityRating" + android:id="@+id/voicemail_transcription_rating_separator" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="end" + android:text="@string/voicemail_transcription_rating_separator"/> + + <TextView + style="@style/TranscriptionQualityRatingLink" + android:id="@+id/voicemail_transcription_rating_bad" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="end" + android:text="@string/voicemail_transcription_rating_bad"/> + + </LinearLayout> + </LinearLayout> </LinearLayout> diff --git a/java/com/android/dialer/app/res/values/colors.xml b/java/com/android/dialer/app/res/values/colors.xml index f1f5002ed..84a381f21 100644 --- a/java/com/android/dialer/app/res/values/colors.xml +++ b/java/com/android/dialer/app/res/values/colors.xml @@ -34,6 +34,8 @@ <color name="call_log_voicemail_transcript_color">#de000000</color> <!-- 54% black --> <color name="call_log_voicemail_transcript_branding_color">#8a000000</color> + <!-- 100% blue --> + <color name="call_log_voicemail_transcript_rating_color">#ff2a56c6</color> <!-- 70% black --> <color name="call_log_action_color">#b3000000</color> <!-- 54% black --> diff --git a/java/com/android/dialer/app/res/values/strings.xml b/java/com/android/dialer/app/res/values/strings.xml index 485bd8994..01c477680 100644 --- a/java/com/android/dialer/app/res/values/strings.xml +++ b/java/com/android/dialer/app/res/values/strings.xml @@ -665,6 +665,37 @@ [CHAR LIMIT=64] --> <string name="voicemail_transcription_failed_no_speech">Transcript not available. No speech detected.</string> + <!-- Prompt asking the user to rate the quality of the voicemail transcription [CHAR LIMIT=30] + voicemail_transcription_rating, voicemail_transcription_rating_good, + voicemail_transcription_rating_separator and voicemail_transcription_rating_bad are + used together to form the rating prompt: 'Rate transcription quality Good or Bad' + where 'Good' and 'Bad' are clickable links. --> + <string name="voicemail_transcription_rating">Rate transcription quality</string> + + <!-- Rating choice indicating that the voicemail transcription was good [CHAR LIMIT=10] + voicemail_transcription_rating, voicemail_transcription_rating_good, + voicemail_transcription_rating_separator and voicemail_transcription_rating_bad are + used together to form the rating prompt: 'Rate transcription quality Good or Bad' + where 'Good' and 'Bad' are clickable links. --> + <string name="voicemail_transcription_rating_good">Good</string> + + <!-- Rating choice indicating that the voicemail transcription was bad [CHAR LIMIT=10] + voicemail_transcription_rating, voicemail_transcription_rating_good, + voicemail_transcription_rating_separator and voicemail_transcription_rating_bad are + used together to form the rating prompt: 'Rate transcription quality Good or Bad' + where 'Good' and 'Bad' are clickable links. --> + <string name="voicemail_transcription_rating_bad">Bad</string> + + <!-- Separator between the good and bad transcription rating choices [CHAR LIMIT=10] + voicemail_transcription_rating, voicemail_transcription_rating_good, + voicemail_transcription_rating_separator and voicemail_transcription_rating_bad are + used together to form the rating prompt: 'Rate transcription quality Good or Bad' + where 'Good' and 'Bad' are clickable links. --> + <string name="voicemail_transcription_rating_separator"> or </string> + + <!-- Message displayed after user has rated a voicemail transcription [CHAR LIMIT=30] --> + <string name="voicemail_transcription_rating_thanks">Thanks for your feedback</string> + <!-- Button text to prompt a user to open an sms conversation [CHAR LIMIT=NONE] --> <string name="view_conversation">View</string> diff --git a/java/com/android/dialer/app/res/values/styles.xml b/java/com/android/dialer/app/res/values/styles.xml index d464ca7f2..c26821023 100644 --- a/java/com/android/dialer/app/res/values/styles.xml +++ b/java/com/android/dialer/app/res/values/styles.xml @@ -251,4 +251,19 @@ <item name="android:layout_height">1dp</item> <item name="android:background">?android:attr/listDivider</item> </style> + + <style name="TranscriptionQualityRating"> + <item name="android:textColor">@color/call_log_voicemail_transcript_branding_color</item> + <item name="android:textSize">@dimen/call_log_voicemail_transcription_text_size</item> + </style> + + <style name="TranscriptionQualityRatingLink"> + <item name="android:textColor">@color/call_log_voicemail_transcript_rating_color</item> + <item name="android:textSize">@dimen/call_log_voicemail_transcription_text_size</item> + <item name="android:paddingTop">8dp</item> + <item name="android:paddingBottom">8dp</item> + <item name="android:paddingLeft">4dp</item> + <item name="android:paddingRight">4dp</item> + <item name="android:minHeight">48dp</item> + </style> </resources> diff --git a/java/com/android/dialer/calllogutils/PhoneCallDetails.java b/java/com/android/dialer/calllogutils/PhoneCallDetails.java index 869a3d099..fe8bfde43 100644 --- a/java/com/android/dialer/calllogutils/PhoneCallDetails.java +++ b/java/com/android/dialer/calllogutils/PhoneCallDetails.java @@ -134,6 +134,9 @@ public class PhoneCallDetails { public int voicemailId; public int previousGroup; + // The URI of the voicemail associated with this phone call, if this call went to voicemail. + public String voicemailUri; + /** * Constructor with required fields for the details of a call with a number associated with a * contact. diff --git a/java/com/android/dialer/compat/android/provider/VoicemailCompat.java b/java/com/android/dialer/compat/android/provider/VoicemailCompat.java index 175ea5d95..02eebb32c 100644 --- a/java/com/android/dialer/compat/android/provider/VoicemailCompat.java +++ b/java/com/android/dialer/compat/android/provider/VoicemailCompat.java @@ -72,4 +72,21 @@ public class VoicemailCompat { * <p>Internal dialer use only, not part of the public SDK. */ public static final int TRANSCRIPTION_FAILED_LANGUAGE_NOT_SUPPORTED = -2; + + /** + * Value of {@link #TRANSCRIPTION_STATE} when the voicemail transcription has completed and the + * result has been stored in the {@link #TRANSCRIPTION} column of the database, and the user has + * provided a quality rating for the transcription. + */ + public static final int TRANSCRIPTION_AVAILABLE_AND_RATED = -3; + + /** + * Voicemail transcription quality rating value sent to the server indicating a good transcription + */ + public static final int TRANSCRIPTION_QUALITY_RATING_GOOD = 1; + + /** + * Voicemail transcription quality rating value sent to the server indicating a bad transcription + */ + public static final int TRANSCRIPTION_QUALITY_RATING_BAD = 2; } diff --git a/java/com/android/dialer/constants/ScheduledJobIds.java b/java/com/android/dialer/constants/ScheduledJobIds.java index c0835b261..1a852d0e7 100644 --- a/java/com/android/dialer/constants/ScheduledJobIds.java +++ b/java/com/android/dialer/constants/ScheduledJobIds.java @@ -46,6 +46,7 @@ public final class ScheduledJobIds { public static final int VVM_TRANSCRIPTION_JOB = 203; public static final int VVM_TRANSCRIPTION_BACKFILL_JOB = 204; public static final int VVM_NOTIFICATION_JOB = 205; + public static final int VVM_TRANSCRIPTION_RATING_JOB = 206; public static final int VOIP_REGISTRATION = 300; diff --git a/java/com/android/voicemail/impl/AndroidManifest.xml b/java/com/android/voicemail/impl/AndroidManifest.xml index 4cad2247c..53636092a 100644 --- a/java/com/android/voicemail/impl/AndroidManifest.xml +++ b/java/com/android/voicemail/impl/AndroidManifest.xml @@ -102,6 +102,11 @@ android:exported="false"/> <service + android:name="com.android.voicemail.impl.transcribe.TranscriptionRatingService" + 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/VoicemailTranscriptionServiceGrpc.java b/java/com/android/voicemail/impl/VoicemailTranscriptionServiceGrpc.java index 8fcbf3b97..f6a00f6d6 100644 --- a/java/com/android/voicemail/impl/VoicemailTranscriptionServiceGrpc.java +++ b/java/com/android/voicemail/impl/VoicemailTranscriptionServiceGrpc.java @@ -73,6 +73,15 @@ public class VoicemailTranscriptionServiceGrpc { "google.internal.communications.voicemailtranscription.v1.VoicemailTranscriptionService", "GetTranscript"), io.grpc.protobuf.lite.ProtoLiteUtils.marshaller(com.google.internal.communications.voicemailtranscription.v1.GetTranscriptRequest.getDefaultInstance()), io.grpc.protobuf.lite.ProtoLiteUtils.marshaller(com.google.internal.communications.voicemailtranscription.v1.GetTranscriptResponse.getDefaultInstance())); + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/1901") + public static final io.grpc.MethodDescriptor<com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackRequest, + com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackResponse> METHOD_SEND_TRANSCRIPTION_FEEDBACK = + io.grpc.MethodDescriptor.create( + io.grpc.MethodDescriptor.MethodType.UNARY, + generateFullMethodName( + "google.internal.communications.voicemailtranscription.v1.VoicemailTranscriptionService", "SendTranscriptionFeedback"), + io.grpc.protobuf.lite.ProtoLiteUtils.marshaller(com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackRequest.getDefaultInstance()), + io.grpc.protobuf.lite.ProtoLiteUtils.marshaller(com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackResponse.getDefaultInstance())); /** * Creates a new async stub that supports all call types for the service @@ -136,6 +145,17 @@ public class VoicemailTranscriptionServiceGrpc { asyncUnimplementedUnaryCall(METHOD_GET_TRANSCRIPT, responseObserver); } + /** + * <pre> + * Uploads user's transcription feedback. Feedback will only be collected from + * user's who have consented to donate their voicemails. + * </pre> + */ + public void sendTranscriptionFeedback(com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackRequest request, + io.grpc.stub.StreamObserver<com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackResponse> responseObserver) { + asyncUnimplementedUnaryCall(METHOD_SEND_TRANSCRIPTION_FEEDBACK, responseObserver); + } + @java.lang.Override public io.grpc.ServerServiceDefinition bindService() { return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor()) .addMethod( @@ -159,6 +179,13 @@ public class VoicemailTranscriptionServiceGrpc { com.google.internal.communications.voicemailtranscription.v1.GetTranscriptRequest, com.google.internal.communications.voicemailtranscription.v1.GetTranscriptResponse>( this, METHODID_GET_TRANSCRIPT))) + .addMethod( + METHOD_SEND_TRANSCRIPTION_FEEDBACK, + asyncUnaryCall( + new MethodHandlers< + com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackRequest, + com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackResponse>( + this, METHODID_SEND_TRANSCRIPTION_FEEDBACK))) .build(); } } @@ -218,6 +245,18 @@ public class VoicemailTranscriptionServiceGrpc { asyncUnaryCall( getChannel().newCall(METHOD_GET_TRANSCRIPT, getCallOptions()), request, responseObserver); } + + /** + * <pre> + * Uploads user's transcription feedback. Feedback will only be collected from + * user's who have consented to donate their voicemails. + * </pre> + */ + public void sendTranscriptionFeedback(com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackRequest request, + io.grpc.stub.StreamObserver<com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackResponse> responseObserver) { + asyncUnaryCall( + getChannel().newCall(METHOD_SEND_TRANSCRIPTION_FEEDBACK, getCallOptions()), request, responseObserver); + } } /** @@ -272,6 +311,17 @@ public class VoicemailTranscriptionServiceGrpc { return blockingUnaryCall( getChannel(), METHOD_GET_TRANSCRIPT, getCallOptions(), request); } + + /** + * <pre> + * Uploads user's transcription feedback. Feedback will only be collected from + * user's who have consented to donate their voicemails. + * </pre> + */ + public com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackResponse sendTranscriptionFeedback(com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackRequest request) { + return blockingUnaryCall( + getChannel(), METHOD_SEND_TRANSCRIPTION_FEEDBACK, getCallOptions(), request); + } } /** @@ -329,11 +379,24 @@ public class VoicemailTranscriptionServiceGrpc { return futureUnaryCall( getChannel().newCall(METHOD_GET_TRANSCRIPT, getCallOptions()), request); } + + /** + * <pre> + * Uploads user's transcription feedback. Feedback will only be collected from + * user's who have consented to donate their voicemails. + * </pre> + */ + public com.google.common.util.concurrent.ListenableFuture<com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackResponse> sendTranscriptionFeedback( + com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackRequest request) { + return futureUnaryCall( + getChannel().newCall(METHOD_SEND_TRANSCRIPTION_FEEDBACK, getCallOptions()), request); + } } private static final int METHODID_TRANSCRIBE_VOICEMAIL = 0; private static final int METHODID_TRANSCRIBE_VOICEMAIL_ASYNC = 1; private static final int METHODID_GET_TRANSCRIPT = 2; + private static final int METHODID_SEND_TRANSCRIPTION_FEEDBACK = 3; private static class MethodHandlers<Req, Resp> implements io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>, @@ -364,6 +427,10 @@ public class VoicemailTranscriptionServiceGrpc { serviceImpl.getTranscript((com.google.internal.communications.voicemailtranscription.v1.GetTranscriptRequest) request, (io.grpc.stub.StreamObserver<com.google.internal.communications.voicemailtranscription.v1.GetTranscriptResponse>) responseObserver); break; + case METHODID_SEND_TRANSCRIPTION_FEEDBACK: + serviceImpl.sendTranscriptionFeedback((com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackRequest) request, + (io.grpc.stub.StreamObserver<com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackResponse>) responseObserver); + break; default: throw new AssertionError(); } @@ -384,7 +451,8 @@ public class VoicemailTranscriptionServiceGrpc { return new io.grpc.ServiceDescriptor(SERVICE_NAME, METHOD_TRANSCRIBE_VOICEMAIL, METHOD_TRANSCRIBE_VOICEMAIL_ASYNC, - METHOD_GET_TRANSCRIPT); + METHOD_GET_TRANSCRIPT, + METHOD_SEND_TRANSCRIPTION_FEEDBACK); } } diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionRatingHelper.java b/java/com/android/voicemail/impl/transcribe/TranscriptionRatingHelper.java new file mode 100644 index 000000000..1cafacecf --- /dev/null +++ b/java/com/android/voicemail/impl/transcribe/TranscriptionRatingHelper.java @@ -0,0 +1,97 @@ +/* + * 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 android.net.Uri; +import com.android.dialer.common.concurrent.DialerExecutor; +import com.android.dialer.common.concurrent.DialerExecutorComponent; +import com.android.dialer.compat.android.provider.VoicemailCompat; +import com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackRequest; +import com.google.internal.communications.voicemailtranscription.v1.TranscriptionRating; +import com.google.internal.communications.voicemailtranscription.v1.TranscriptionRatingValue; +import com.google.protobuf.ByteString; + +/** + * Send voicemail transcription rating feedback to the server and record the fact that feedback was + * provided in the local database. + */ +public class TranscriptionRatingHelper { + + /** Callback invoked after the feedback has been recorded locally */ + public interface SuccessListener { + void onRatingSuccess(Uri voicemailUri); + } + + /** Callback invoked if there was an error recording the feedback */ + public interface FailureListener { + void onRatingFailure(Throwable t); + } + + /** + * Method for sending a user voicemail transcription feedback rating to the server and recording + * the fact that the voicemail was rated in the local database. + */ + public static void sendRating( + Context context, + TranscriptionRatingValue ratingValue, + Uri voicemailUri, + SuccessListener successListener, + FailureListener failureListener) { + DialerExecutorComponent.get(context) + .dialerExecutorFactory() + .createNonUiTaskBuilder(new RatingWorker(context, ratingValue, voicemailUri)) + .onSuccess(output -> successListener.onRatingSuccess(voicemailUri)) + .onFailure(e -> failureListener.onRatingFailure(e)) + .build() + .executeParallel(null); + } + + /** Worker class used to record a user's quality rating of a voicemail transcription. */ + private static class RatingWorker implements DialerExecutor.Worker<Void, Void> { + private final Context context; + private final TranscriptionRatingValue ratingValue; + private final Uri voicemailUri; + + private RatingWorker(Context context, TranscriptionRatingValue ratingValue, Uri voicemailUri) { + this.context = context; + this.ratingValue = ratingValue; + this.voicemailUri = voicemailUri; + } + + @Override + public Void doInBackground(Void input) { + // Schedule a task to upload the feedback (requires network connectivity) + TranscriptionRatingService.scheduleTask(context, getFeedbackRequest()); + + // Record the fact that the transcription has been rated + TranscriptionDbHelper dbHelper = new TranscriptionDbHelper(context, voicemailUri); + dbHelper.setTranscriptionState(VoicemailCompat.TRANSCRIPTION_AVAILABLE_AND_RATED); + return null; + } + + private SendTranscriptionFeedbackRequest getFeedbackRequest() { + ByteString audioData = TranscriptionUtils.getAudioData(context, voicemailUri); + String voicemailId = TranscriptionUtils.getFingerprintFor(audioData); + TranscriptionRating rating = + TranscriptionRating.newBuilder() + .setTranscriptionId(voicemailId) + .setRatingValue(ratingValue) + .build(); + return SendTranscriptionFeedbackRequest.newBuilder().addRating(rating).build(); + } + } +} diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionRatingService.java b/java/com/android/voicemail/impl/transcribe/TranscriptionRatingService.java new file mode 100644 index 000000000..cff2c6d84 --- /dev/null +++ b/java/com/android/voicemail/impl/transcribe/TranscriptionRatingService.java @@ -0,0 +1,90 @@ +/* + * 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.JobScheduler; +import android.app.job.JobWorkItem; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.support.annotation.WorkerThread; +import android.support.v4.app.JobIntentService; +import com.android.dialer.common.LogUtil; +import com.android.dialer.constants.ScheduledJobIds; +import com.android.voicemail.impl.transcribe.grpc.TranscriptionClientFactory; +import com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackRequest; +import com.google.protobuf.InvalidProtocolBufferException; + +/** + * JobScheduler service for uploading transcription feedback. This service requires a network + * connection. + */ +public class TranscriptionRatingService extends JobIntentService { + private static final String FEEDBACK_REQUEST_EXTRA = "feedback_request_extra"; + + /** Schedule a task to upload transcription rating feedback */ + public static boolean scheduleTask(Context context, SendTranscriptionFeedbackRequest request) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + LogUtil.enterBlock("TranscriptionRatingService.scheduleTask"); + ComponentName componentName = new ComponentName(context, TranscriptionRatingService.class); + JobInfo.Builder builder = + new JobInfo.Builder(ScheduledJobIds.VVM_TRANSCRIPTION_RATING_JOB, componentName) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + JobScheduler scheduler = context.getSystemService(JobScheduler.class); + return scheduler.enqueue(builder.build(), makeWorkItem(request)) + == JobScheduler.RESULT_SUCCESS; + } else { + LogUtil.i("TranscriptionRatingService.scheduleTask", "not supported"); + return false; + } + } + + public TranscriptionRatingService() {} + + private static JobWorkItem makeWorkItem(SendTranscriptionFeedbackRequest request) { + Intent intent = new Intent(); + intent.putExtra(FEEDBACK_REQUEST_EXTRA, request.toByteArray()); + return new JobWorkItem(intent); + } + + @Override + @WorkerThread + protected void onHandleWork(Intent intent) { + LogUtil.enterBlock("TranscriptionRatingService.onHandleWork"); + + TranscriptionConfigProvider configProvider = new TranscriptionConfigProvider(this); + TranscriptionClientFactory factory = new TranscriptionClientFactory(this, configProvider); + try { + // Send rating to server + SendTranscriptionFeedbackRequest request = + SendTranscriptionFeedbackRequest.parseFrom( + intent.getByteArrayExtra(FEEDBACK_REQUEST_EXTRA)); + factory.getClient().sendTranscriptFeedbackRequest(request); + } catch (InvalidProtocolBufferException e) { + LogUtil.e("TranscriptionRatingService.onHandleWork", "failed to send feedback", e); + } finally { + factory.shutdown(); + } + } + + @Override + public void onDestroy() { + LogUtil.enterBlock("TranscriptionRatingService.onDestroy"); + super.onDestroy(); + } +} diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java b/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java index f3b1d587f..97cf89eef 100644 --- a/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java +++ b/java/com/android/voicemail/impl/transcribe/TranscriptionTask.java @@ -15,7 +15,6 @@ */ package com.android.voicemail.impl.transcribe; -import android.annotation.TargetApi; import android.app.job.JobWorkItem; import android.content.Context; import android.net.Uri; @@ -37,8 +36,6 @@ import com.android.voicemail.impl.transcribe.grpc.TranscriptionResponse; import com.google.internal.communications.voicemailtranscription.v1.AudioFormat; import com.google.internal.communications.voicemailtranscription.v1.TranscriptionStatus; import com.google.protobuf.ByteString; -import java.io.IOException; -import java.io.InputStream; /** * Background task to get a voicemail transcription and update the database. @@ -71,8 +68,6 @@ public abstract class TranscriptionTask implements Runnable { protected AudioFormat encoding; protected volatile boolean cancelled; - static final String AMR_PREFIX = "#!AMR\n"; - /** Functional interface for sending requests to the transcription server */ public interface Request { TranscriptionResponse getResponse(TranscriptionClient client); @@ -226,8 +221,6 @@ public abstract class TranscriptionTask implements Runnable { databaseHelper.setTranscriptionState(newState); } - // 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."); @@ -236,15 +229,15 @@ public abstract class TranscriptionTask implements Runnable { 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); + audioData = TranscriptionUtils.getAudioData(context, voicemailUri); + if (audioData != null) { + VvmLog.i(TAG, "readAndValidateAudioFile, read " + audioData.size() + " bytes"); + } else { + VvmLog.i(TAG, "readAndValidateAudioFile, unable to read audio data for " + voicemailUri); return false; } - encoding = getAudioFormat(audioData); + encoding = TranscriptionUtils.getAudioFormat(audioData); if (encoding == AudioFormat.AUDIO_FORMAT_UNSPECIFIED) { VvmLog.i(TAG, "Transcriber.readAndValidateAudioFile, unknown encoding"); return false; @@ -253,15 +246,9 @@ public abstract class TranscriptionTask implements Runnable { return true; } - private static AudioFormat getAudioFormat(ByteString audioData) { - return audioData != null && audioData.startsWith(ByteString.copyFromUtf8(AMR_PREFIX)) - ? AudioFormat.AMR_NB_8KHZ - : AudioFormat.AUDIO_FORMAT_UNSPECIFIED; - } - @VisibleForTesting void setAudioDataForTesting(ByteString audioData) { this.audioData = audioData; - encoding = getAudioFormat(audioData); + encoding = TranscriptionUtils.getAudioFormat(audioData); } } diff --git a/java/com/android/voicemail/impl/transcribe/TranscriptionUtils.java b/java/com/android/voicemail/impl/transcribe/TranscriptionUtils.java index a001f179a..36b1400be 100644 --- a/java/com/android/voicemail/impl/transcribe/TranscriptionUtils.java +++ b/java/com/android/voicemail/impl/transcribe/TranscriptionUtils.java @@ -16,17 +16,38 @@ package com.android.voicemail.impl.transcribe; import android.annotation.TargetApi; -import android.os.Build.VERSION_CODES; +import android.content.Context; +import android.net.Uri; import android.util.Base64; import com.android.dialer.common.Assert; +import com.google.internal.communications.voicemailtranscription.v1.AudioFormat; import com.google.protobuf.ByteString; +import java.io.IOException; +import java.io.InputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** Utility methods used by this transcription package. */ public class TranscriptionUtils { + static final String AMR_PREFIX = "#!AMR\n"; - @TargetApi(VERSION_CODES.O) + // Uses try-with-resource + @TargetApi(android.os.Build.VERSION_CODES.M) + static ByteString getAudioData(Context context, Uri voicemailUri) { + try (InputStream in = context.getContentResolver().openInputStream(voicemailUri)) { + return ByteString.readFrom(in); + } catch (IOException e) { + return null; + } + } + + static AudioFormat getAudioFormat(ByteString audioData) { + return audioData != null && audioData.startsWith(ByteString.copyFromUtf8(AMR_PREFIX)) + ? AudioFormat.AMR_NB_8KHZ + : AudioFormat.AUDIO_FORMAT_UNSPECIFIED; + } + + @TargetApi(android.os.Build.VERSION_CODES.O) static String getFingerprintFor(ByteString data) { Assert.checkArgument(data != null); try { diff --git a/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionClient.java b/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionClient.java index b18d95627..381cb3268 100644 --- a/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionClient.java +++ b/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionClient.java @@ -17,6 +17,7 @@ package com.android.voicemail.impl.transcribe.grpc; import android.support.annotation.WorkerThread; import com.google.internal.communications.voicemailtranscription.v1.GetTranscriptRequest; +import com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackRequest; import com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailAsyncRequest; import com.google.internal.communications.voicemailtranscription.v1.TranscribeVoicemailRequest; import com.google.internal.communications.voicemailtranscription.v1.VoicemailTranscriptionServiceGrpc; @@ -58,4 +59,14 @@ public class TranscriptionClient { return new GetTranscriptResponseAsync(e.getStatus()); } } + + @WorkerThread + public TranscriptionFeedbackResponseAsync sendTranscriptFeedbackRequest( + SendTranscriptionFeedbackRequest request) { + try { + return new TranscriptionFeedbackResponseAsync(stub.sendTranscriptionFeedback(request)); + } catch (StatusRuntimeException e) { + return new TranscriptionFeedbackResponseAsync(e.getStatus()); + } + } } diff --git a/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionFeedbackResponseAsync.java b/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionFeedbackResponseAsync.java new file mode 100644 index 000000000..bc6155bbd --- /dev/null +++ b/java/com/android/voicemail/impl/transcribe/grpc/TranscriptionFeedbackResponseAsync.java @@ -0,0 +1,35 @@ +/* + * 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.VisibleForTesting; +import com.android.dialer.common.Assert; +import com.google.internal.communications.voicemailtranscription.v1.SendTranscriptionFeedbackResponse; +import io.grpc.Status; + +/** Container for response and status objects for an asynchronous transcription feedback request */ +public class TranscriptionFeedbackResponseAsync extends TranscriptionResponse { + + @VisibleForTesting + public TranscriptionFeedbackResponseAsync(SendTranscriptionFeedbackResponse response) { + Assert.checkArgument(response != null); + } + + @VisibleForTesting + public TranscriptionFeedbackResponseAsync(Status status) { + super(status); + } +} diff --git a/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto b/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto index c46fb2176..8248b02a7 100644 --- a/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto +++ b/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto @@ -184,7 +184,7 @@ service VoicemailTranscriptionService { // Uploads user's transcription feedback. Feedback will only be collected from // user's who have consented to donate their voicemails. - rpc SendTranscriptionFeedback(SendTranscriptionFeedbackResponse) + rpc SendTranscriptionFeedback(SendTranscriptionFeedbackRequest) returns (SendTranscriptionFeedbackResponse) { } } |