Enhanced TextClassifier APIs to support OTP detection use cases
1) Added SystemApi method getClassifier
2) Added TYPE enums that are requred to use the new method
3) A new TYPE_OTP enum has been added to support OTP detection use cases
Note: It would have been ideal to expose the existing method `getTextClassifier(int type)` as a SystemAPI, however this method is annotated as `UnsupportedAppUsage`. While it is acceptable to expose it as SystemApi, in our case we will need to Flag and Guard with a permission. go/UnsupportedAppUsage#can-i-add-an-annotated-method-to-the-public-sdk-systemapi
Bug: 377229653
Test: atest FrameworksCoreTests:TextClassificationManagerTest CtsTextClassifierTestCases:TextClassificationManagerTest CtsPermissionPolicyTestCases:PermissionPolicyTest
Flag: android.permission.flags.text_classifier_choice_api_enabled
Change-Id: I513e06729e6de244a925c4e169bbdcaedfafc8e0
diff --git a/core/api/current.txt b/core/api/current.txt
index 612d0b7..c6e53e8 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -58318,6 +58318,7 @@
method @NonNull @WorkerThread public default android.view.textclassifier.TextSelection suggestSelection(@NonNull android.view.textclassifier.TextSelection.Request);
method @NonNull @WorkerThread public default android.view.textclassifier.TextSelection suggestSelection(@NonNull CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @Nullable android.os.LocaleList);
field public static final String EXTRA_FROM_TEXT_CLASSIFIER = "android.view.textclassifier.extra.FROM_TEXT_CLASSIFIER";
+ field @FlaggedApi("android.permission.flags.text_classifier_choice_api_enabled") public static final String EXTRA_TEXT_ORIGIN_PACKAGE = "android.view.textclassifier.extra.TEXT_ORIGIN_PACKAGE";
field public static final String HINT_TEXT_IS_EDITABLE = "android.text_is_editable";
field public static final String HINT_TEXT_IS_NOT_EDITABLE = "android.text_is_not_editable";
field public static final android.view.textclassifier.TextClassifier NO_OP;
@@ -58327,6 +58328,7 @@
field public static final String TYPE_EMAIL = "email";
field public static final String TYPE_FLIGHT_NUMBER = "flight";
field public static final String TYPE_OTHER = "other";
+ field @FlaggedApi("android.permission.flags.text_classifier_choice_api_enabled") public static final String TYPE_OTP = "otp";
field public static final String TYPE_PHONE = "phone";
field public static final String TYPE_UNKNOWN = "";
field public static final String TYPE_URL = "url";
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index edb30bd..b8d272a 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -26,6 +26,7 @@
field public static final String ACCESS_SHORTCUTS = "android.permission.ACCESS_SHORTCUTS";
field @FlaggedApi("android.app.smartspace.flags.access_smartspace") public static final String ACCESS_SMARTSPACE = "android.permission.ACCESS_SMARTSPACE";
field public static final String ACCESS_SURFACE_FLINGER = "android.permission.ACCESS_SURFACE_FLINGER";
+ field @FlaggedApi("android.permission.flags.text_classifier_choice_api_enabled") public static final String ACCESS_TEXT_CLASSIFIER_BY_TYPE = "android.permission.ACCESS_TEXT_CLASSIFIER_BY_TYPE";
field public static final String ACCESS_TUNED_INFO = "android.permission.ACCESS_TUNED_INFO";
field public static final String ACCESS_TV_DESCRAMBLER = "android.permission.ACCESS_TV_DESCRAMBLER";
field public static final String ACCESS_TV_SHARED_FILTER = "android.permission.ACCESS_TV_SHARED_FILTER";
@@ -19293,6 +19294,20 @@
}
+package android.view.textclassifier {
+
+ public final class TextClassificationManager {
+ method @FlaggedApi("android.permission.flags.text_classifier_choice_api_enabled") @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_TEXT_CLASSIFIER_BY_TYPE) public android.view.textclassifier.TextClassifier getClassifier(int);
+ }
+
+ public interface TextClassifier {
+ field @FlaggedApi("android.permission.flags.text_classifier_choice_api_enabled") public static final int CLASSIFIER_TYPE_ANDROID_DEFAULT = 2; // 0x2
+ field @FlaggedApi("android.permission.flags.text_classifier_choice_api_enabled") public static final int CLASSIFIER_TYPE_DEVICE_DEFAULT = 1; // 0x1
+ field @FlaggedApi("android.permission.flags.text_classifier_choice_api_enabled") public static final int CLASSIFIER_TYPE_SELF_PROVIDED = 0; // 0x0
+ }
+
+}
+
package android.view.translation {
public final class TranslationCapability implements android.os.Parcelable {
diff --git a/core/api/system-lint-baseline.txt b/core/api/system-lint-baseline.txt
index 7c43891..3b9ef959 100644
--- a/core/api/system-lint-baseline.txt
+++ b/core/api/system-lint-baseline.txt
@@ -505,6 +505,8 @@
FlaggedApiLiteral: android.Manifest.permission#ACCESS_LAST_KNOWN_CELL_ID:
@FlaggedApi contains a string literal, but should reference the field generated by aconfig (com.android.server.telecom.flags.Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES).
+FlaggedApiLiteral: android.Manifest.permission#ACCESS_TEXT_CLASSIFIER_BY_TYPE:
+ @FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.permission.flags.Flags.FLAG_TEXT_CLASSIFIER_CHOICE_API_ENABLED).
FlaggedApiLiteral: android.Manifest.permission#BACKUP_HEALTH_CONNECT_DATA_AND_SETTINGS:
@FlaggedApi contains a string literal, but should reference the field generated by aconfig (android.permission.flags.Flags.FLAG_HEALTH_CONNECT_BACKUP_RESTORE_PERMISSION_ENABLED).
FlaggedApiLiteral: android.Manifest.permission#BIND_VERIFICATION_AGENT:
diff --git a/core/java/android/view/textclassifier/TextClassificationManager.java b/core/java/android/view/textclassifier/TextClassificationManager.java
index b606340..b929324 100644
--- a/core/java/android/view/textclassifier/TextClassificationManager.java
+++ b/core/java/android/view/textclassifier/TextClassificationManager.java
@@ -16,13 +16,20 @@
package android.view.textclassifier;
+import static android.Manifest.permission.ACCESS_TEXT_CLASSIFIER_BY_TYPE;
+
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.os.Build;
import android.os.ServiceManager;
+import android.permission.flags.Flags;
import android.view.textclassifier.TextClassifier.TextClassifierType;
import com.android.internal.annotations.GuardedBy;
@@ -115,6 +122,29 @@
}
}
+ /**
+ * Returns a specific type of text classifier.
+ * If the specified text classifier cannot be found, this returns {@link TextClassifier#NO_OP}.
+ * <p>
+ *
+ * @see TextClassifier#CLASSIFIER_TYPE_SELF_PROVIDED
+ * @see TextClassifier#CLASSIFIER_TYPE_DEVICE_DEFAULT
+ * @see TextClassifier#CLASSIFIER_TYPE_ANDROID_DEFAULT
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ @FlaggedApi(Flags.FLAG_TEXT_CLASSIFIER_CHOICE_API_ENABLED)
+ @RequiresPermission(ACCESS_TEXT_CLASSIFIER_BY_TYPE)
+ public TextClassifier getClassifier(@TextClassifierType int type) {
+ if (mContext.checkCallingOrSelfPermission(ACCESS_TEXT_CLASSIFIER_BY_TYPE)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException(
+ "Caller does not have permission " + ACCESS_TEXT_CLASSIFIER_BY_TYPE);
+ }
+ return getTextClassifier(type);
+ }
+
private TextClassificationConstants getSettings() {
synchronized (mLock) {
if (mSettings == null) {
diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java
index ef50045..59afdac 100644
--- a/core/java/android/view/textclassifier/TextClassifier.java
+++ b/core/java/android/view/textclassifier/TextClassifier.java
@@ -16,16 +16,20 @@
package android.view.textclassifier;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringDef;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
import android.annotation.WorkerThread;
import android.os.LocaleList;
import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
+import android.permission.flags.Flags;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.URLSpan;
@@ -63,11 +67,6 @@
/** @hide */
String LOG_TAG = "androidtc";
-
- /** @hide */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef(value = {LOCAL, SYSTEM, DEFAULT_SYSTEM})
- @interface TextClassifierType {} // TODO: Expose as system APIs.
/** Specifies a TextClassifier that runs locally in the app's process. @hide */
int LOCAL = 0;
/** Specifies a TextClassifier that runs in the system process and serves all apps. @hide */
@@ -76,8 +75,33 @@
int DEFAULT_SYSTEM = 2;
/** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {CLASSIFIER_TYPE_SELF_PROVIDED, CLASSIFIER_TYPE_DEVICE_DEFAULT,
+ CLASSIFIER_TYPE_ANDROID_DEFAULT})
+ @interface TextClassifierType {
+ }
+ /** Specifies a TextClassifier that runs locally in the app's process. @hide */
+ @FlaggedApi(Flags.FLAG_TEXT_CLASSIFIER_CHOICE_API_ENABLED)
+ @SystemApi
+ int CLASSIFIER_TYPE_SELF_PROVIDED = LOCAL;
+ /**
+ * Specifies a TextClassifier that is set as the default on this particular device. This may be
+ * the same as CLASSIFIER_TYPE_DEVICE_DEFAULT, unless set otherwise by the device manufacturer.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_TEXT_CLASSIFIER_CHOICE_API_ENABLED)
+ @SystemApi
+ int CLASSIFIER_TYPE_DEVICE_DEFAULT = SYSTEM;
+ /** Specifies the TextClassifier that is provided by Android. @hide */
+ @FlaggedApi(Flags.FLAG_TEXT_CLASSIFIER_CHOICE_API_ENABLED)
+ @SystemApi
+ int CLASSIFIER_TYPE_ANDROID_DEFAULT = DEFAULT_SYSTEM;
+
+ /** @hide */
+ @SuppressLint("SwitchIntDef")
static String typeToString(@TextClassifierType int type) {
- switch (type) {
+ int unflaggedType = type;
+ switch (unflaggedType) {
case LOCAL:
return "Local";
case SYSTEM:
@@ -108,6 +132,9 @@
String TYPE_DATE_TIME = "datetime";
/** Flight number in IATA format. */
String TYPE_FLIGHT_NUMBER = "flight";
+ /** Onetime password. */
+ @FlaggedApi(Flags.FLAG_TEXT_CLASSIFIER_CHOICE_API_ENABLED)
+ String TYPE_OTP = "otp";
/**
* Word that users may be interested to look up for meaning.
* @hide
@@ -126,7 +153,8 @@
TYPE_DATE,
TYPE_DATE_TIME,
TYPE_FLIGHT_NUMBER,
- TYPE_DICTIONARY
+ TYPE_DICTIONARY,
+ TYPE_OTP
})
@interface EntityType {}
@@ -198,6 +226,16 @@
String EXTRA_FROM_TEXT_CLASSIFIER = "android.view.textclassifier.extra.FROM_TEXT_CLASSIFIER";
/**
+ * Extra specifying the package name of the app from which the text to be classified originated.
+ *
+ * For example, a notification assistant might use TextClassifier, but the notification
+ * content could originate from a different app. This key allows you to provide
+ * the package name of that source app.
+ */
+ @FlaggedApi(Flags.FLAG_TEXT_CLASSIFIER_CHOICE_API_ENABLED)
+ String EXTRA_TEXT_ORIGIN_PACKAGE = "android.view.textclassifier.extra.TEXT_ORIGIN_PACKAGE";
+
+ /**
* Returns suggested text selection start and end indices, recognized entity types, and their
* associated confidence scores. The entity types are ordered from highest to lowest scoring.
*
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index d0a5318..f175e93 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -8785,6 +8785,17 @@
android:protectionLevel="signature|privileged|vendorPrivileged"
android:featureFlag="android.media.tv.flags.kids_mode_tvdb_sharing"/>
+ <!-- @SystemApi
+ @FlaggedApi("android.permission.flags.text_classifier_choice_api_enabled")
+ This permission is required to access the specific text classifier you need from the
+ TextClassificationManager.
+ <p>Protection level: signature|role
+ @hide
+ -->
+ <permission android:name="android.permission.ACCESS_TEXT_CLASSIFIER_BY_TYPE"
+ android:protectionLevel="signature|role"
+ android:featureFlag="android.permission.flags.text_classifier_choice_api_enabled"/>
+
<!-- Attribution for Geofencing service. -->
<attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
<!-- Attribution for Country Detector. -->
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java
index 402b92a..26806b1 100644
--- a/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java
@@ -16,16 +16,23 @@
package android.view.textclassifier;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
+import android.Manifest;
import android.content.Context;
+import android.permission.flags.Flags;
+import android.platform.test.annotations.RequiresFlagsEnabled;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -61,4 +68,28 @@
assertThat(mTcm.getTextClassifier(TextClassifier.SYSTEM))
.isInstanceOf(SystemTextClassifier.class);
}
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_TEXT_CLASSIFIER_CHOICE_API_ENABLED)
+ public void testGetClassifier() {
+ Assume.assumeTrue(Flags.textClassifierChoiceApiEnabled());
+ assertThrows(SecurityException.class,
+ () -> mTcm.getClassifier(TextClassifier.CLASSIFIER_TYPE_DEVICE_DEFAULT));
+ assertThrows(SecurityException.class,
+ () -> mTcm.getClassifier(TextClassifier.CLASSIFIER_TYPE_ANDROID_DEFAULT));
+ assertThrows(SecurityException.class,
+ () -> mTcm.getClassifier(TextClassifier.CLASSIFIER_TYPE_SELF_PROVIDED));
+
+ runWithShellPermissionIdentity(() -> {
+ assertThat(
+ mTcm.getClassifier(TextClassifier.CLASSIFIER_TYPE_DEVICE_DEFAULT)).isInstanceOf(
+ SystemTextClassifier.class);
+ assertThat(mTcm.getClassifier(
+ TextClassifier.CLASSIFIER_TYPE_ANDROID_DEFAULT)).isInstanceOf(
+ SystemTextClassifier.class);
+ assertThat(mTcm.getClassifier(
+ TextClassifier.CLASSIFIER_TYPE_SELF_PROVIDED)).isSameInstanceAs(
+ TextClassifier.NO_OP);
+ }, Manifest.permission.ACCESS_TEXT_CLASSIFIER_BY_TYPE);
+ }
}
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index baf829a..3cb74ee 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -992,6 +992,10 @@
<uses-permission android:name="android.permission.health.READ_SKIN_TEMPERATURE"
android:featureFlag="android.permission.flags.replace_body_sensor_permission_enabled"/>
+ <!-- Permission for TestClassifier tests to get access to classifier by type -->
+ <uses-permission android:name="android.permission.ACCESS_TEXT_CLASSIFIER_BY_TYPE"
+ android:featureFlag="android.permission.flags.text_classifier_choice_api_enabled"/>
+
<application
android:label="@string/app_label"
android:theme="@android:style/Theme.DeviceDefault.DayNight"