Merge "Add theme to the inline suggestion UI, which allows the host app (IME) to customize it"
diff --git a/api/current.txt b/api/current.txt
index d012a8b..63cf1c6 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -333,6 +333,9 @@
     field public static final int autoUrlDetect = 16843404; // 0x101028c
     field public static final int autoVerify = 16844014; // 0x10104ee
     field public static final int autofillHints = 16844118; // 0x1010556
+    field public static final int autofillInlineSuggestionChip = 16844307; // 0x1010613
+    field public static final int autofillInlineSuggestionSubtitle = 16844309; // 0x1010615
+    field public static final int autofillInlineSuggestionTitle = 16844308; // 0x1010614
     field public static final int autofilledHighlight = 16844136; // 0x1010568
     field public static final int background = 16842964; // 0x10100d4
     field public static final int backgroundDimAmount = 16842802; // 0x1010032
@@ -2253,6 +2256,7 @@
     field public static final int ThemeOverlay_Material_Dialog = 16974550; // 0x10302d6
     field public static final int ThemeOverlay_Material_Dialog_Alert = 16974551; // 0x10302d7
     field public static final int ThemeOverlay_Material_Light = 16974410; // 0x103024a
+    field public static final int Theme_AutofillInlineSuggestion = 16974565; // 0x10302e5
     field public static final int Theme_Black = 16973832; // 0x1030008
     field public static final int Theme_Black_NoTitleBar = 16973833; // 0x1030009
     field public static final int Theme_Black_NoTitleBar_Fullscreen = 16973834; // 0x103000a
diff --git a/api/system-current.txt b/api/system-current.txt
index b50ba29..a90909c 100755
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -250,6 +250,7 @@
 
   public static final class R.attr {
     field public static final int allowClearUserDataOnFailedRestore = 16844288; // 0x1010600
+    field public static final int isAutofillInlineSuggestionTheme = 16844310; // 0x1010616
     field public static final int isVrOnly = 16844152; // 0x1010578
     field public static final int minExtensionVersion = 16844306; // 0x1010612
     field public static final int requiredSystemPropertyName = 16844133; // 0x1010565
diff --git a/core/res/res/layout/autofill_inline_suggestion.xml b/core/res/res/layout/autofill_inline_suggestion.xml
index f7ac164..27faea4 100644
--- a/core/res/res/layout/autofill_inline_suggestion.xml
+++ b/core/res/res/layout/autofill_inline_suggestion.xml
@@ -16,19 +16,20 @@
 
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
+    style="?android:attr/autofillInlineSuggestionChip"
     android:layout_width="wrap_content"
-    android:layout_height="56dp"
-    android:background="@color/white"
-    android:orientation="horizontal"
-    android:paddingStart="12dp"
-    android:paddingEnd="12dp">
+    android:layout_height="wrap_content"
+    android:gravity="center"
+    android:paddingTop="4dp"
+    android:paddingBottom="4dp"
+    android:orientation="horizontal">
 
     <ImageView
         android:id="@+id/autofill_inline_suggestion_start_icon"
-        android:layout_width="24dp"
-        android:layout_height="24dp"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
         android:layout_gravity="center"
+        android:scaleType="fitCenter"
         android:contentDescription="autofill_inline_suggestion_start_icon" />
 
     <LinearLayout
@@ -36,32 +37,33 @@
         android:layout_height="match_parent"
         android:layout_gravity="center"
         android:layout_weight="1"
-        android:layout_marginStart="12dp"
-        android:layout_marginEnd="12dp"
+        android:paddingStart="4dp"
+        android:paddingEnd="4dp"
         android:orientation="vertical"
-        android:gravity="center_vertical">
+        android:gravity="center">
 
         <TextView
+            style="?android:attr/autofillInlineSuggestionTitle"
             android:id="@+id/autofill_inline_suggestion_title"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:ellipsize="end"
-            android:maxLines="1"
-            tools:text="username1"/>
+            android:maxLines="1"/>
 
         <TextView
+            style="?android:attr/autofillInlineSuggestionSubtitle"
             android:id="@+id/autofill_inline_suggestion_subtitle"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:ellipsize="end"
-            android:maxLines="1"
-            tools:text="inline fill service"/>
+            android:maxLines="1"/>
     </LinearLayout>
 
     <ImageView
         android:id="@+id/autofill_inline_suggestion_end_icon"
-        android:layout_width="24dp"
-        android:layout_height="24dp"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
         android:layout_gravity="center"
+        android:scaleType="fitCenter"
         android:contentDescription="autofill_inline_suggestion_end_icon" />
 </LinearLayout>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 940e9f1..c9c47b9 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -9197,4 +9197,12 @@
     </declare-styleable>
 
     <attr name="autoSizePresetSizes" />
+
+    <declare-styleable name="AutofillInlineSuggestion">
+        <!-- @hide @SystemApi -->
+        <attr name="isAutofillInlineSuggestionTheme" format="boolean" />
+        <attr name="autofillInlineSuggestionChip" format="reference" />
+        <attr name="autofillInlineSuggestionTitle" format="reference" />
+        <attr name="autofillInlineSuggestionSubtitle" format="reference" />
+    </declare-styleable>
 </resources>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 36dbcbd..d43c9fd 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -3012,12 +3012,18 @@
       <public name="sdkVersion" />
       <!-- @hide @SystemApi -->
       <public name="minExtensionVersion" />
+      <public name="autofillInlineSuggestionChip" />
+      <public name="autofillInlineSuggestionTitle" />
+      <public name="autofillInlineSuggestionSubtitle" />
+      <!-- @hide @SystemApi -->
+      <public name="isAutofillInlineSuggestionTheme" />
     </public-group>
 
     <public-group type="drawable" first-id="0x010800b5">
     </public-group>
 
     <public-group type="style" first-id="0x010302e5">
+      <public name="Theme.AutofillInlineSuggestion" />
     </public-group>
 
     <public-group type="id" first-id="0x0102004a">
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index bcce1f0..751eca0 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -1482,6 +1482,22 @@
         <item name="android:windowExitAnimation">@anim/slide_out_down</item>
     </style>
 
+    <!-- The style for the Autofill inline suggestion chip. -->
+    <!-- @hide -->
+    <style name="AutofillInlineSuggestionChip">
+        <item name="background">@drawable/autofill_dataset_picker_background</item>
+    </style>
+
+    <!-- @hide -->
+    <style name="AutofillInlineSuggestionTitle">
+        <item name="android:textAppearance">@style/TextAppearance</item>
+    </style>
+
+    <!-- @hide -->
+    <style name="AutofillInlineSuggestionSubtitle">
+        <item name="android:textAppearance">@style/TextAppearance.Small</item>
+    </style>
+
     <!-- The style for the container of media actions in a notification. -->
     <!-- @hide -->
     <style name="NotificationMediaActionContainer">
diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml
index ad38f3d..5e6dd82 100644
--- a/core/res/res/values/themes.xml
+++ b/core/res/res/values/themes.xml
@@ -893,4 +893,11 @@
         <item name="windowActivityTransitions">false</item>
     </style>
 
+    <!-- Theme for the Autofill inline suggestion on IME -->
+    <style name="Theme.AutofillInlineSuggestion" parent="Theme.DeviceDefault">
+        <item name="isAutofillInlineSuggestionTheme">true</item>
+        <item name="autofillInlineSuggestionChip">@style/AutofillInlineSuggestionChip</item>
+        <item name="autofillInlineSuggestionTitle">@style/AutofillInlineSuggestionTitle</item>
+        <item name="autofillInlineSuggestionSubtitle">@style/AutofillInlineSuggestionSubtitle</item>
+    </style>
 </resources>
diff --git a/services/autofill/java/com/android/server/autofill/InlineSuggestionFactory.java b/services/autofill/java/com/android/server/autofill/InlineSuggestionFactory.java
index 5e6f97e..331eb37 100644
--- a/services/autofill/java/com/android/server/autofill/InlineSuggestionFactory.java
+++ b/services/autofill/java/com/android/server/autofill/InlineSuggestionFactory.java
@@ -20,7 +20,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.slice.Slice;
 import android.content.Context;
 import android.os.RemoteException;
 import android.service.autofill.Dataset;
@@ -132,7 +131,7 @@
             }
         };
         final InlineSuggestion inlineSuggestion = new InlineSuggestion(inlineSuggestionInfo,
-                createInlineContentProvider(inlinePresentation.getSlice(), inlineSuggestionUi,
+                createInlineContentProvider(inlinePresentation, inlineSuggestionUi,
                         onClickListener));
         return inlineSuggestion;
     }
@@ -151,20 +150,22 @@
             client.fill(requestId, fieldIndex, dataset);
         };
         final InlineSuggestion inlineSuggestion = new InlineSuggestion(inlineSuggestionInfo,
-                createInlineContentProvider(inlinePresentation.getSlice(), inlineSuggestionUi,
+                createInlineContentProvider(inlinePresentation, inlineSuggestionUi,
                         onClickListener));
         return inlineSuggestion;
     }
 
     private static IInlineContentProvider.Stub createInlineContentProvider(
-            @NonNull Slice slice, @NonNull InlineSuggestionUi inlineSuggestionUi,
+            @NonNull InlinePresentation inlinePresentation,
+            @NonNull InlineSuggestionUi inlineSuggestionUi,
             @Nullable View.OnClickListener onClickListener) {
         return new IInlineContentProvider.Stub() {
             @Override
             public void provideContent(int width, int height,
                     IInlineContentCallback callback) {
                 UiThread.getHandler().post(() -> {
-                    SurfaceControl sc = inlineSuggestionUi.inflate(slice, width, height,
+                    SurfaceControl sc = inlineSuggestionUi.inflate(inlinePresentation, width,
+                            height,
                             onClickListener);
                     try {
                         callback.onContent(sc);
diff --git a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionUi.java b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionUi.java
index 2460732..2adefea 100644
--- a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionUi.java
+++ b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionUi.java
@@ -25,10 +25,17 @@
 import android.app.slice.Slice;
 import android.app.slice.SliceItem;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.graphics.PixelFormat;
 import android.graphics.drawable.Icon;
+import android.graphics.fonts.SystemFonts;
 import android.os.IBinder;
+import android.service.autofill.InlinePresentation;
+import android.text.TextUtils;
 import android.util.Log;
+import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
@@ -41,6 +48,8 @@
 import com.android.internal.R;
 
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * This is a temporary inline suggestion UI inflater which will be replaced by the ExtServices
@@ -54,6 +63,10 @@
 
     private static final String TAG = "InlineSuggestionUi";
 
+    // The pattern to match the value can be obtained by calling {@code Resources#getResourceName
+    // (int)}. This name is a single string of the form "package:type/entry".
+    private static final Pattern RESOURCE_NAME_PATTERN = Pattern.compile("([^:]+):([^/]+)/(\\S+)");
+
     private final Context mContext;
 
     public InlineSuggestionUi(Context context) {
@@ -65,28 +78,36 @@
      */
     @MainThread
     @Nullable
-    public SurfaceControl inflate(@NonNull Slice slice, int width, int height,
-            @Nullable View.OnClickListener onClickListener) {
+    public SurfaceControl inflate(@NonNull InlinePresentation inlinePresentation, int width,
+            int height, @Nullable View.OnClickListener onClickListener) {
         Log.d(TAG, "Inflating the inline suggestion UI");
 
         //TODO(b/137800469): Pass in inputToken from IME.
         final SurfaceControlViewHost wvr = new SurfaceControlViewHost(mContext,
                 mContext.getDisplay(), (IBinder) null);
         final SurfaceControl sc = wvr.getSurfacePackage().getSurfaceControl();
-        final ViewGroup suggestionView = (ViewGroup) renderSlice(slice);
+
+        Context contextThemeWrapper = getContextThemeWrapper(mContext,
+                inlinePresentation.getInlinePresentationSpec().getStyle());
+        if (contextThemeWrapper == null) {
+            contextThemeWrapper = getDefaultContextThemeWrapper(mContext);
+        }
+        final View suggestionView = renderSlice(inlinePresentation.getSlice(),
+                contextThemeWrapper);
         if (onClickListener != null) {
             suggestionView.setOnClickListener(onClickListener);
         }
 
         WindowManager.LayoutParams lp =
                 new WindowManager.LayoutParams(width, height,
-                        WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.TRANSPARENT);
+                        WindowManager.LayoutParams.TYPE_APPLICATION, 0,
+                        PixelFormat.TRANSPARENT);
         wvr.addView(suggestionView, lp);
         return sc;
     }
 
-    private View renderSlice(Slice slice) {
-        final LayoutInflater inflater = LayoutInflater.from(mContext);
+    private static View renderSlice(Slice slice, Context context) {
+        final LayoutInflater inflater = LayoutInflater.from(context);
         final ViewGroup suggestionView =
                 (ViewGroup) inflater.inflate(R.layout.autofill_inline_suggestion, null);
 
@@ -137,4 +158,96 @@
 
         return suggestionView;
     }
+
+    private Context getDefaultContextThemeWrapper(@NonNull Context context) {
+        Resources.Theme theme = context.getResources().newTheme();
+        theme.applyStyle(android.R.style.Theme_AutofillInlineSuggestion, true);
+        return new ContextThemeWrapper(context, theme);
+    }
+
+    /**
+     * Returns a context wrapping the theme in the provided {@code style}, or null if {@code
+     * style} doesn't pass validation.
+     */
+    @Nullable
+    private static Context getContextThemeWrapper(@NonNull Context context,
+            @Nullable String style) {
+        if (style == null) {
+            return null;
+        }
+        Matcher matcher = RESOURCE_NAME_PATTERN.matcher(style);
+        if (!matcher.matches()) {
+            Log.d(TAG, "Can not parse the style=" + style);
+            return null;
+        }
+        String packageName = matcher.group(1);
+        String type = matcher.group(2);
+        String entry = matcher.group(3);
+        if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(type) || TextUtils.isEmpty(entry)) {
+            Log.d(TAG, "Can not proceed with empty field values in the style=" + style);
+            return null;
+        }
+        Resources resources = null;
+        try {
+            resources = context.getPackageManager().getResourcesForApplication(
+                    packageName);
+        } catch (PackageManager.NameNotFoundException e) {
+            return null;
+        }
+        int resId = resources.getIdentifier(entry, type, packageName);
+        if (resId == Resources.ID_NULL) {
+            return null;
+        }
+        Resources.Theme theme = resources.newTheme();
+        theme.applyStyle(resId, true);
+        if (!validateBaseTheme(theme, resId)) {
+            Log.d(TAG, "Provided theme is not a child of Theme.InlineSuggestion, ignoring it.");
+            return null;
+        }
+        if (!validateFontFamilyForTextViewStyles(theme)) {
+            Log.d(TAG,
+                    "Provided theme specifies a font family that is not system font, ignoring it.");
+            return null;
+        }
+        return new ContextThemeWrapper(context, theme);
+    }
+
+    private static boolean validateFontFamilyForTextViewStyles(Resources.Theme theme) {
+        return validateFontFamily(theme, android.R.attr.autofillInlineSuggestionTitle)
+                && validateFontFamily(theme, android.R.attr.autofillInlineSuggestionSubtitle);
+    }
+
+    private static boolean validateFontFamily(Resources.Theme theme, int styleAttr) {
+        TypedArray ta = null;
+        try {
+            ta = theme.obtainStyledAttributes(null, new int[]{android.R.attr.fontFamily},
+                    styleAttr,
+                    0);
+            if (ta.getIndexCount() == 0) {
+                return true;
+            }
+            String fontFamily = ta.getString(ta.getIndex(0));
+            return SystemFonts.getRawSystemFallbackMap().containsKey(fontFamily);
+        } finally {
+            if (ta != null) {
+                ta.recycle();
+            }
+        }
+    }
+
+    private static boolean validateBaseTheme(Resources.Theme theme, int styleAttr) {
+        TypedArray ta = null;
+        try {
+            ta = theme.obtainStyledAttributes(null,
+                    new int[]{android.R.attr.isAutofillInlineSuggestionTheme}, styleAttr, 0);
+            if (ta.getIndexCount() == 0) {
+                return false;
+            }
+            return ta.getBoolean(ta.getIndex(0), false);
+        } finally {
+            if (ta != null) {
+                ta.recycle();
+            }
+        }
+    }
 }