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
diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java
index 47ef32d..4f78bc9 100644
--- a/java/com/android/dialer/app/calllog/CallLogAdapter.java
+++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java
@@ -960,6 +960,7 @@
     }
     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 ad931e8..3898d1f 100644
--- a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
+++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java
@@ -19,30 +19,39 @@
 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 @@
 
       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 @@
       }
 
       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 @@
             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 40c0894..8b7a92b 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 View transcriptionView;
   public final TextView voicemailTranscriptionView;
   public final TextView voicemailTranscriptionBrandingView;
+  public final View voicemailTranscriptionRatingView;
   public final TextView callAccountLabel;
 
   private PhoneCallDetailsViews(
@@ -42,6 +43,7 @@
       View transcriptionView,
       TextView voicemailTranscriptionView,
       TextView voicemailTranscriptionBrandingView,
+      View voicemailTranscriptionRatingView,
       TextView callAccountLabel) {
     this.nameView = nameView;
     this.callTypeView = callTypeView;
@@ -50,6 +52,7 @@
     this.transcriptionView = transcriptionView;
     this.voicemailTranscriptionView = voicemailTranscriptionView;
     this.voicemailTranscriptionBrandingView = voicemailTranscriptionBrandingView;
+    this.voicemailTranscriptionRatingView = voicemailTranscriptionRatingView;
     this.callAccountLabel = callAccountLabel;
   }
 
@@ -69,6 +72,7 @@
         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 @@
         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 e0f9e63..75c8fe6 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 f1f5002..84a381f 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 485bd89..01c4776 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 d464ca7..c268210 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 869a3d0..fe8bfde 100644
--- a/java/com/android/dialer/calllogutils/PhoneCallDetails.java
+++ b/java/com/android/dialer/calllogutils/PhoneCallDetails.java
@@ -134,6 +134,9 @@
   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 175ea5d..02eebb3 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 @@
    * <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 c0835b2..1a852d0 100644
--- a/java/com/android/dialer/constants/ScheduledJobIds.java
+++ b/java/com/android/dialer/constants/ScheduledJobIds.java
@@ -46,6 +46,7 @@
   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 4cad224..5363609 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 8fcbf3b..f6a00f6 100644
--- a/java/com/android/voicemail/impl/VoicemailTranscriptionServiceGrpc.java
+++ b/java/com/android/voicemail/impl/VoicemailTranscriptionServiceGrpc.java
@@ -73,6 +73,15 @@
               "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 @@
       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 @@
                 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 @@
       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 @@
       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 @@
       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 @@
           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 @@
     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 0000000..1caface
--- /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 0000000..cff2c6d
--- /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 f3b1d58..97cf89e 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.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 @@
   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 @@
     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 @@
       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 @@
     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 a001f17..36b1400 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 b18d956..381cb32 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 @@
 
 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 @@
       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 0000000..bc6155b
--- /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 c46fb21..8248b02 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 @@
 
   // 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) {
   }
 }