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();
+ }
+ }
+ }
}