diff options
Diffstat (limited to 'android')
210 files changed, 10944 insertions, 4284 deletions
diff --git a/android/accessibilityservice/AccessibilityService.java b/android/accessibilityservice/AccessibilityService.java index 0a4541ba..829a9444 100644 --- a/android/accessibilityservice/AccessibilityService.java +++ b/android/accessibilityservice/AccessibilityService.java @@ -537,7 +537,7 @@ public abstract class AccessibilityService extends Service { * anything behind it, then only the modal window will be reported * (assuming it is the top one). For convenience the returned windows * are ordered in a descending layer order, which is the windows that - * are higher in the Z-order are reported first. Since the user can always + * are on top are reported first. Since the user can always * interact with the window that has input focus by typing, the focused * window is always returned (even if covered by a modal window). * <p> diff --git a/android/app/ActivityOptions.java b/android/app/ActivityOptions.java index 09dcbf21..ecd99a7b 100644 --- a/android/app/ActivityOptions.java +++ b/android/app/ActivityOptions.java @@ -1139,7 +1139,8 @@ public class ActivityOptions { * {@link android.app.admin.DevicePolicyManager} can run in LockTask mode. Therefore, if * {@link android.app.admin.DevicePolicyManager#isLockTaskPermitted(String)} returns * {@code false} for the package of the target activity, a {@link SecurityException} will be - * thrown during {@link Context#startActivity(Intent, Bundle)}. + * thrown during {@link Context#startActivity(Intent, Bundle)}. This method doesn't affect + * activities that are already running — relaunch the activity to run in lock task mode. * * Defaults to {@code false} if not set. * diff --git a/android/app/ActivityThread.java b/android/app/ActivityThread.java index 50a43989..82c3383d 100644 --- a/android/app/ActivityThread.java +++ b/android/app/ActivityThread.java @@ -5873,7 +5873,7 @@ public final class ActivityThread extends ClientTransactionHandler { } finally { // If the app targets < O-MR1, or doesn't change the thread policy // during startup, clobber the policy to maintain behavior of b/36951662 - if (data.appInfo.targetSdkVersion <= Build.VERSION_CODES.O + if (data.appInfo.targetSdkVersion < Build.VERSION_CODES.O_MR1 || StrictMode.getThreadPolicy().equals(writesAllowedPolicy)) { StrictMode.setThreadPolicy(savedPolicy); } diff --git a/android/app/ApplicationPackageManager.java b/android/app/ApplicationPackageManager.java index a68136b5..1084b425 100644 --- a/android/app/ApplicationPackageManager.java +++ b/android/app/ApplicationPackageManager.java @@ -2155,40 +2155,28 @@ public class ApplicationPackageManager extends PackageManager { public String[] setPackagesSuspended(String[] packageNames, boolean suspended, PersistableBundle appExtras, PersistableBundle launcherExtras, String dialogMessage) { - // TODO (b/75332201): Pass in the dialogMessage and use it in the interceptor dialog try { return mPM.setPackagesSuspendedAsUser(packageNames, suspended, appExtras, - launcherExtras, mContext.getOpPackageName(), mContext.getUserId()); + launcherExtras, dialogMessage, mContext.getOpPackageName(), + mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } @Override - public PersistableBundle getSuspendedPackageAppExtras(String packageName) { + public Bundle getSuspendedPackageAppExtras() { + final PersistableBundle extras; try { - return mPM.getSuspendedPackageAppExtras(packageName, mContext.getUserId()); + extras = mPM.getSuspendedPackageAppExtras(mContext.getOpPackageName(), + mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } - } - - @Override - public Bundle getSuspendedPackageAppExtras() { - final PersistableBundle extras = getSuspendedPackageAppExtras(mContext.getOpPackageName()); return extras != null ? new Bundle(extras.deepCopy()) : null; } @Override - public void setSuspendedPackageAppExtras(String packageName, PersistableBundle appExtras) { - try { - mPM.setSuspendedPackageAppExtras(packageName, appExtras, mContext.getUserId()); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } - } - - @Override public boolean isPackageSuspendedForUser(String packageName, int userId) { try { return mPM.isPackageSuspendedForUser(packageName, userId); @@ -2199,8 +2187,12 @@ public class ApplicationPackageManager extends PackageManager { /** @hide */ @Override - public boolean isPackageSuspended(String packageName) { - return isPackageSuspendedForUser(packageName, mContext.getUserId()); + public boolean isPackageSuspended(String packageName) throws NameNotFoundException { + try { + return isPackageSuspendedForUser(packageName, mContext.getUserId()); + } catch (IllegalArgumentException ie) { + throw new NameNotFoundException(packageName); + } } @Override diff --git a/android/app/BroadcastOptions.java b/android/app/BroadcastOptions.java index b6cff385..69c3632b 100644 --- a/android/app/BroadcastOptions.java +++ b/android/app/BroadcastOptions.java @@ -32,6 +32,7 @@ public class BroadcastOptions { private long mTemporaryAppWhitelistDuration; private int mMinManifestReceiverApiLevel = 0; private int mMaxManifestReceiverApiLevel = Build.VERSION_CODES.CUR_DEVELOPMENT; + private boolean mDontSendToRestrictedApps = false; /** * How long to temporarily put an app on the power whitelist when executing this broadcast @@ -52,6 +53,12 @@ public class BroadcastOptions { static final String KEY_MAX_MANIFEST_RECEIVER_API_LEVEL = "android:broadcast.maxManifestReceiverApiLevel"; + /** + * Corresponds to {@link #setMaxManifestReceiverApiLevel}. + */ + static final String KEY_DONT_SEND_TO_RESTRICTED_APPS = + "android:broadcast.dontSendToRestrictedApps"; + public static BroadcastOptions makeBasic() { BroadcastOptions opts = new BroadcastOptions(); return opts; @@ -66,6 +73,7 @@ public class BroadcastOptions { mMinManifestReceiverApiLevel = opts.getInt(KEY_MIN_MANIFEST_RECEIVER_API_LEVEL, 0); mMaxManifestReceiverApiLevel = opts.getInt(KEY_MAX_MANIFEST_RECEIVER_API_LEVEL, Build.VERSION_CODES.CUR_DEVELOPMENT); + mDontSendToRestrictedApps = opts.getBoolean(KEY_DONT_SEND_TO_RESTRICTED_APPS, false); } /** @@ -123,6 +131,23 @@ public class BroadcastOptions { } /** + * Sets whether pending intent can be sent for an application with background restrictions + * @param dontSendToRestrictedApps if true, pending intent will not be sent for an application + * with background restrictions. Default value is {@code false} + */ + public void setDontSendToRestrictedApps(boolean dontSendToRestrictedApps) { + mDontSendToRestrictedApps = dontSendToRestrictedApps; + } + + /** + * @hide + * @return #setDontSendToRestrictedApps + */ + public boolean isDontSendToRestrictedApps() { + return mDontSendToRestrictedApps; + } + + /** * Returns the created options as a Bundle, which can be passed to * {@link android.content.Context#sendBroadcast(android.content.Intent) * Context.sendBroadcast(Intent)} and related methods. @@ -141,6 +166,9 @@ public class BroadcastOptions { if (mMaxManifestReceiverApiLevel != Build.VERSION_CODES.CUR_DEVELOPMENT) { b.putInt(KEY_MAX_MANIFEST_RECEIVER_API_LEVEL, mMaxManifestReceiverApiLevel); } + if (mDontSendToRestrictedApps) { + b.putBoolean(KEY_DONT_SEND_TO_RESTRICTED_APPS, true); + } return b.isEmpty() ? null : b; } } diff --git a/android/app/ContextImpl.java b/android/app/ContextImpl.java index 71b88fa4..9a491bc3 100644 --- a/android/app/ContextImpl.java +++ b/android/app/ContextImpl.java @@ -16,6 +16,7 @@ package android.app; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; @@ -88,11 +89,12 @@ import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Objects; import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicInteger; class ReceiverRestrictedContext extends ContextWrapper { ReceiverRestrictedContext(Context base) { @@ -212,13 +214,24 @@ class ContextImpl extends Context { static final int STATE_UNINITIALIZED = 0; static final int STATE_INITIALIZING = 1; static final int STATE_READY = 2; + static final int STATE_NOT_FOUND = 3; + + /** @hide */ + @IntDef(prefix = { "STATE_" }, value = { + STATE_UNINITIALIZED, + STATE_INITIALIZING, + STATE_READY, + STATE_NOT_FOUND, + }) + @Retention(RetentionPolicy.SOURCE) + @interface ServiceInitializationState {} /** * Initialization state for each service. Any of {@link #STATE_UNINITIALIZED}, * {@link #STATE_INITIALIZING} or {@link #STATE_READY}, */ - final AtomicInteger[] mServiceInitializationStateArray = - SystemServiceRegistry.createServiceInitializationStateArray(); + @ServiceInitializationState + final int[] mServiceInitializationStateArray = new int[mServiceCache.length]; static ContextImpl getImpl(Context context) { Context nextContext; diff --git a/android/app/Notification.java b/android/app/Notification.java index 4326ee3e..2b4f4206 100644 --- a/android/app/Notification.java +++ b/android/app/Notification.java @@ -3902,7 +3902,7 @@ public class Notification implements Parcelable * @deprecated use {@link #addPerson(Person)} */ public Builder addPerson(String uri) { - addPerson(new Person().setUri(uri)); + addPerson(new Person.Builder().setUri(uri).build()); return this; } @@ -4588,10 +4588,18 @@ public class Notification implements Parcelable bindHeaderChronometerAndTime(contentView); bindProfileBadge(contentView); } + bindActivePermissions(contentView); bindExpandButton(contentView); mN.mUsesStandardHeader = true; } + private void bindActivePermissions(RemoteViews contentView) { + int color = isColorized() ? getPrimaryTextColor() : getSecondaryTextColor(); + contentView.setDrawableTint(R.id.camera, false, color, PorterDuff.Mode.SRC_ATOP); + contentView.setDrawableTint(R.id.mic, false, color, PorterDuff.Mode.SRC_ATOP); + contentView.setDrawableTint(R.id.overlay, false, color, PorterDuff.Mode.SRC_ATOP); + } + private void bindExpandButton(RemoteViews contentView) { int color = isColorized() ? getPrimaryTextColor() : getSecondaryTextColor(); contentView.setDrawableTint(R.id.expand_button, false, color, @@ -6384,7 +6392,7 @@ public class Notification implements Parcelable * @deprecated use {@code MessagingStyle(Person)} */ public MessagingStyle(@NonNull CharSequence userDisplayName) { - this(new Person().setName(userDisplayName)); + this(new Person.Builder().setName(userDisplayName).build()); } /** @@ -6431,6 +6439,7 @@ public class Notification implements Parcelable /** * @return the user to be displayed for any replies sent by the user */ + @NonNull public Person getUser() { return mUser; } @@ -6489,7 +6498,7 @@ public class Notification implements Parcelable */ public MessagingStyle addMessage(CharSequence text, long timestamp, CharSequence sender) { return addMessage(text, timestamp, - sender == null ? null : new Person().setName(sender)); + sender == null ? null : new Person.Builder().setName(sender).build()); } /** @@ -6505,7 +6514,8 @@ public class Notification implements Parcelable * * @return this object for method chaining */ - public MessagingStyle addMessage(CharSequence text, long timestamp, Person sender) { + public MessagingStyle addMessage(@NonNull CharSequence text, long timestamp, + @Nullable Person sender) { return addMessage(new Message(text, timestamp, sender)); } @@ -6661,7 +6671,7 @@ public class Notification implements Parcelable mUser = extras.getParcelable(EXTRA_MESSAGING_PERSON); if (mUser == null) { CharSequence displayName = extras.getCharSequence(EXTRA_SELF_DISPLAY_NAME); - mUser = new Person().setName(displayName); + mUser = new Person.Builder().setName(displayName).build(); } mConversationTitle = extras.getCharSequence(EXTRA_CONVERSATION_TITLE); Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES); @@ -6678,7 +6688,8 @@ public class Notification implements Parcelable public RemoteViews makeContentView(boolean increasedHeight) { mBuilder.mOriginalActions = mBuilder.mActions; mBuilder.mActions = new ArrayList<>(); - RemoteViews remoteViews = makeMessagingView(true /* isCollapsed */); + RemoteViews remoteViews = makeMessagingView(true /* displayImagesAtEnd */, + true /* showReplyIcon */); mBuilder.mActions = mBuilder.mOriginalActions; mBuilder.mOriginalActions = null; return remoteViews; @@ -6765,11 +6776,19 @@ public class Notification implements Parcelable */ @Override public RemoteViews makeBigContentView() { - return makeMessagingView(false /* isCollapsed */); + return makeMessagingView(false /* displayImagesAtEnd */, false /* showReplyIcon */); } + /** + * Create a messaging layout. + * + * @param displayImagesAtEnd should images be displayed at the end of the content instead + * of inline. + * @param showReplyIcon Should the reply affordance be shown at the end of the notification + * @return the created remoteView. + */ @NonNull - private RemoteViews makeMessagingView(boolean isCollapsed) { + private RemoteViews makeMessagingView(boolean displayImagesAtEnd, boolean showReplyIcon) { CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle) ? super.mBigContentTitle : mConversationTitle; @@ -6780,24 +6799,24 @@ public class Notification implements Parcelable nameReplacement = conversationTitle; conversationTitle = null; } - boolean hideLargeIcon = !isCollapsed || isOneToOne; + boolean hideLargeIcon = !showReplyIcon || isOneToOne; RemoteViews contentView = mBuilder.applyStandardTemplateWithActions( mBuilder.getMessagingLayoutResource(), mBuilder.mParams.reset().hasProgress(false).title(conversationTitle).text(null) .hideLargeIcon(hideLargeIcon) .headerTextSecondary(conversationTitle) - .alwaysShowReply(isCollapsed)); + .alwaysShowReply(showReplyIcon)); addExtras(mBuilder.mN.extras); // also update the end margin if there is an image int endMargin = R.dimen.notification_content_margin_end; - if (isCollapsed) { + if (showReplyIcon) { endMargin = R.dimen.notification_content_plus_picture_margin_end; } contentView.setViewLayoutMarginEndDimen(R.id.notification_main_column, endMargin); contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor", mBuilder.resolveContrastColor()); - contentView.setBoolean(R.id.status_bar_latest_event_content, "setIsCollapsed", - isCollapsed); + contentView.setBoolean(R.id.status_bar_latest_event_content, "setDisplayImagesAtEnd", + displayImagesAtEnd); contentView.setIcon(R.id.status_bar_latest_event_content, "setLargeIcon", mBuilder.mN.mLargeIcon); contentView.setCharSequence(R.id.status_bar_latest_event_content, "setNameReplacement", @@ -6864,7 +6883,8 @@ public class Notification implements Parcelable */ @Override public RemoteViews makeHeadsUpContentView(boolean increasedHeight) { - RemoteViews remoteViews = makeMessagingView(true /* isCollapsed */); + RemoteViews remoteViews = makeMessagingView(true /* displayImagesAtEnd */, + false /* showReplyIcon */); remoteViews.setInt(R.id.notification_messaging, "setMaxDisplayedLines", 1); return remoteViews; } @@ -6906,7 +6926,8 @@ public class Notification implements Parcelable * @deprecated use {@code Message(CharSequence, long, Person)} */ public Message(CharSequence text, long timestamp, CharSequence sender){ - this(text, timestamp, sender == null ? null : new Person().setName(sender)); + this(text, timestamp, sender == null ? null + : new Person.Builder().setName(sender).build()); } /** @@ -6917,13 +6938,14 @@ public class Notification implements Parcelable * Should be <code>null</code> for messages by the current user, in which case * the platform will insert the user set in {@code MessagingStyle(Person)}. * <p> - * The person provided should contain an Icon, set with {@link Person#setIcon(Icon)} - * and also have a name provided with {@link Person#setName(CharSequence)}. If multiple - * users have the same name, consider providing a key with {@link Person#setKey(String)} - * in order to differentiate between the different users. + * The person provided should contain an Icon, set with + * {@link Person.Builder#setIcon(Icon)} and also have a name provided + * with {@link Person.Builder#setName(CharSequence)}. If multiple users have the same + * name, consider providing a key with {@link Person.Builder#setKey(String)} in order + * to differentiate between the different users. * </p> */ - public Message(CharSequence text, long timestamp, @Nullable Person sender){ + public Message(@NonNull CharSequence text, long timestamp, @Nullable Person sender) { mText = text; mTimestamp = timestamp; mSender = sender; @@ -7082,7 +7104,7 @@ public class Notification implements Parcelable // the native api instead CharSequence senderName = bundle.getCharSequence(KEY_SENDER); if (senderName != null) { - senderPerson = new Person().setName(senderName); + senderPerson = new Person.Builder().setName(senderName).build(); } } Message message = new Message(bundle.getCharSequence(KEY_TEXT), @@ -7777,217 +7799,6 @@ public class Notification implements Parcelable } } - /** - * A Person associated with this Notification. - */ - public static final class Person implements Parcelable { - @Nullable private CharSequence mName; - @Nullable private Icon mIcon; - @Nullable private String mUri; - @Nullable private String mKey; - private boolean mBot; - private boolean mImportant; - - protected Person(Parcel in) { - mName = in.readCharSequence(); - if (in.readInt() != 0) { - mIcon = Icon.CREATOR.createFromParcel(in); - } - mUri = in.readString(); - mKey = in.readString(); - mImportant = in.readBoolean(); - mBot = in.readBoolean(); - } - - /** - * Create a new person. - */ - public Person() { - } - - /** - * Give this person a name. - * - * @param name the name of this person - */ - public Person setName(@Nullable CharSequence name) { - this.mName = name; - return this; - } - - /** - * Add an icon for this person. - * <br /> - * This is currently only used for {@link MessagingStyle} notifications and should not be - * provided otherwise, in order to save memory. The system will prefer this icon over any - * images that are resolved from the URI. - * - * @param icon the icon of the person - */ - public Person setIcon(@Nullable Icon icon) { - this.mIcon = icon; - return this; - } - - /** - * Set a URI associated with this person. - * - * <P> - * Depending on user preferences, adding a URI to a Person may allow the notification to - * pass through interruption filters, if this notification is of - * category {@link #CATEGORY_CALL} or {@link #CATEGORY_MESSAGE}. - * The addition of people may also cause this notification to appear more prominently in - * the user interface. - * </P> - * - * <P> - * The person should be specified by the {@code String} representation of a - * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}. - * </P> - * - * <P>The system will also attempt to resolve {@code mailto:} and {@code tel:} schema - * URIs. The path part of these URIs must exist in the contacts database, in the - * appropriate column, or the reference will be discarded as invalid. Telephone schema - * URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}. - * </P> - * - * @param uri a URI for the person - */ - public Person setUri(@Nullable String uri) { - mUri = uri; - return this; - } - - /** - * Add a key to this person in order to uniquely identify it. - * This is especially useful if the name doesn't uniquely identify this person or if the - * display name is a short handle of the actual name. - * - * <P>If no key is provided, the name serves as as the key for the purpose of - * identification.</P> - * - * @param key the key that uniquely identifies this person - */ - public Person setKey(@Nullable String key) { - mKey = key; - return this; - } - - /** - * Sets whether this is an important person. Use this method to denote users who frequently - * interact with the user of this device, when it is not possible to refer to the user - * by {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}. - * - * @param isImportant {@code true} if this is an important person, {@code false} otherwise. - */ - public Person setImportant(boolean isImportant) { - mImportant = isImportant; - return this; - } - - /** - * Sets whether this person is a machine rather than a human. - * - * @param isBot {@code true} if this person is a machine, {@code false} otherwise. - */ - public Person setBot(boolean isBot) { - mBot = isBot; - return this; - } - - /** - * @return the uri provided for this person or {@code null} if no Uri was provided - */ - @Nullable - public String getUri() { - return mUri; - } - - /** - * @return the name provided for this person or {@code null} if no name was provided - */ - @Nullable - public CharSequence getName() { - return mName; - } - - /** - * @return the icon provided for this person or {@code null} if no icon was provided - */ - @Nullable - public Icon getIcon() { - return mIcon; - } - - /** - * @return the key provided for this person or {@code null} if no key was provided - */ - @Nullable - public String getKey() { - return mKey; - } - - /** - * @return whether this Person is a machine. - */ - public boolean isBot() { - return mBot; - } - - /** - * @return whether this Person is important. - */ - public boolean isImportant() { - return mImportant; - } - - /** - * @return the URI associated with this person, or "name:mName" otherwise - * @hide - */ - public String resolveToLegacyUri() { - if (mUri != null) { - return mUri; - } - if (mName != null) { - return "name:" + mName; - } - return ""; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, @WriteFlags int flags) { - dest.writeCharSequence(mName); - if (mIcon != null) { - dest.writeInt(1); - mIcon.writeToParcel(dest, 0); - } else { - dest.writeInt(0); - } - dest.writeString(mUri); - dest.writeString(mKey); - dest.writeBoolean(mImportant); - dest.writeBoolean(mBot); - } - - public static final Creator<Person> CREATOR = new Creator<Person>() { - @Override - public Person createFromParcel(Parcel in) { - return new Person(in); - } - - @Override - public Person[] newArray(int size) { - return new Person[size]; - } - }; - } - // When adding a new Style subclass here, don't forget to update // Builder.getNotificationStyleClass. diff --git a/android/app/NotificationChannel.java b/android/app/NotificationChannel.java index 4a7cf623..9e47ced4 100644 --- a/android/app/NotificationChannel.java +++ b/android/app/NotificationChannel.java @@ -328,7 +328,8 @@ public final class NotificationChannel implements Parcelable { * Group information is only used for presentation, not for behavior. * * Only modifiable before the channel is submitted to - * {@link NotificationManager#notify(String, int, Notification)}. + * {@link NotificationManager#createNotificationChannel(NotificationChannel)}, unless the + * channel is not currently part of a group. * * @param groupId the id of a group created by * {@link NotificationManager#createNotificationChannelGroup(NotificationChannelGroup)}. @@ -341,6 +342,9 @@ public final class NotificationChannel implements Parcelable { * Sets whether notifications posted to this channel can appear as application icon badges * in a Launcher. * + * Only modifiable before the channel is submitted to + * {@link NotificationManager#createNotificationChannel(NotificationChannel)}. + * * @param showBadge true if badges should be allowed to be shown. */ public void setShowBadge(boolean showBadge) { @@ -353,7 +357,7 @@ public final class NotificationChannel implements Parcelable { * least {@link NotificationManager#IMPORTANCE_DEFAULT} should have a sound. * * Only modifiable before the channel is submitted to - * {@link NotificationManager#notify(String, int, Notification)}. + * {@link NotificationManager#createNotificationChannel(NotificationChannel)}. */ public void setSound(Uri sound, AudioAttributes audioAttributes) { this.mSound = sound; @@ -365,7 +369,7 @@ public final class NotificationChannel implements Parcelable { * on devices that support that feature. * * Only modifiable before the channel is submitted to - * {@link NotificationManager#notify(String, int, Notification)}. + * {@link NotificationManager#createNotificationChannel(NotificationChannel)}. */ public void enableLights(boolean lights) { this.mLights = lights; @@ -376,7 +380,7 @@ public final class NotificationChannel implements Parcelable { * {@link #enableLights(boolean) enabled} on this channel and the device supports that feature. * * Only modifiable before the channel is submitted to - * {@link NotificationManager#notify(String, int, Notification)}. + * {@link NotificationManager#createNotificationChannel(NotificationChannel)}. */ public void setLightColor(int argb) { this.mLightColor = argb; @@ -387,7 +391,7 @@ public final class NotificationChannel implements Parcelable { * be set with {@link #setVibrationPattern(long[])}. * * Only modifiable before the channel is submitted to - * {@link NotificationManager#notify(String, int, Notification)}. + * {@link NotificationManager#createNotificationChannel(NotificationChannel)}. */ public void enableVibration(boolean vibration) { this.mVibrationEnabled = vibration; @@ -399,7 +403,7 @@ public final class NotificationChannel implements Parcelable { * vibration} as well. Otherwise, vibration will be disabled. * * Only modifiable before the channel is submitted to - * {@link NotificationManager#notify(String, int, Notification)}. + * {@link NotificationManager#createNotificationChannel(NotificationChannel)}. */ public void setVibrationPattern(long[] vibrationPattern) { this.mVibrationEnabled = vibrationPattern != null && vibrationPattern.length > 0; @@ -407,9 +411,10 @@ public final class NotificationChannel implements Parcelable { } /** - * Sets the level of interruption of this notification channel. Only - * modifiable before the channel is submitted to - * {@link NotificationManager#notify(String, int, Notification)}. + * Sets the level of interruption of this notification channel. + * + * Only modifiable before the channel is submitted to + * {@link NotificationManager#createNotificationChannel(NotificationChannel)}. * * @param importance the amount the user should be interrupted by * notifications from this channel. diff --git a/android/app/NotificationManager.java b/android/app/NotificationManager.java index 46d1264f..757fc643 100644 --- a/android/app/NotificationManager.java +++ b/android/app/NotificationManager.java @@ -1145,6 +1145,21 @@ public class NotificationManager { SUPPRESSED_EFFECT_NOTIFICATION_LIST }; + private static final int[] SCREEN_OFF_SUPPRESSED_EFFECTS = { + SUPPRESSED_EFFECT_SCREEN_OFF, + SUPPRESSED_EFFECT_FULL_SCREEN_INTENT, + SUPPRESSED_EFFECT_LIGHTS, + SUPPRESSED_EFFECT_AMBIENT, + }; + + private static final int[] SCREEN_ON_SUPPRESSED_EFFECTS = { + SUPPRESSED_EFFECT_SCREEN_ON, + SUPPRESSED_EFFECT_PEEK, + SUPPRESSED_EFFECT_STATUS_BAR, + SUPPRESSED_EFFECT_BADGE, + SUPPRESSED_EFFECT_NOTIFICATION_LIST + }; + /** * Visual effects to suppress for a notification that is filtered by Do Not Disturb mode. * Bitmask of SUPPRESSED_EFFECT_* constants. @@ -1297,6 +1312,58 @@ public class NotificationManager { return true; } + /** + * @hide + */ + public static boolean areAnyScreenOffEffectsSuppressed(int effects) { + for (int i = 0; i < SCREEN_OFF_SUPPRESSED_EFFECTS.length; i++) { + final int effect = SCREEN_OFF_SUPPRESSED_EFFECTS[i]; + if ((effects & effect) != 0) { + return true; + } + } + return false; + } + + /** + * @hide + */ + public static boolean areAnyScreenOnEffectsSuppressed(int effects) { + for (int i = 0; i < SCREEN_ON_SUPPRESSED_EFFECTS.length; i++) { + final int effect = SCREEN_ON_SUPPRESSED_EFFECTS[i]; + if ((effects & effect) != 0) { + return true; + } + } + return false; + } + + /** + * @hide + */ + public static int toggleScreenOffEffectsSuppressed(int currentEffects, boolean suppress) { + return toggleEffects(currentEffects, SCREEN_OFF_SUPPRESSED_EFFECTS, suppress); + } + + /** + * @hide + */ + public static int toggleScreenOnEffectsSuppressed(int currentEffects, boolean suppress) { + return toggleEffects(currentEffects, SCREEN_ON_SUPPRESSED_EFFECTS, suppress); + } + + private static int toggleEffects(int currentEffects, int[] effects, boolean suppress) { + for (int i = 0; i < effects.length; i++) { + final int effect = effects[i]; + if (suppress) { + currentEffects |= effect; + } else { + currentEffects &= ~effect; + } + } + return currentEffects; + } + public static String suppressedEffectsToString(int effects) { if (effects <= 0) return ""; final StringBuilder sb = new StringBuilder(); diff --git a/android/app/Person.java b/android/app/Person.java new file mode 100644 index 00000000..3884a8d4 --- /dev/null +++ b/android/app/Person.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2018 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 android.app; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.drawable.Icon; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Provides an immutable reference to an entity that appears repeatedly on different surfaces of the + * platform. For example, this could represent the sender of a message. + */ +public final class Person implements Parcelable { + + @Nullable private CharSequence mName; + @Nullable private Icon mIcon; + @Nullable private String mUri; + @Nullable private String mKey; + private boolean mIsBot; + private boolean mIsImportant; + + private Person(Parcel in) { + mName = in.readCharSequence(); + if (in.readInt() != 0) { + mIcon = Icon.CREATOR.createFromParcel(in); + } + mUri = in.readString(); + mKey = in.readString(); + mIsImportant = in.readBoolean(); + mIsBot = in.readBoolean(); + } + + private Person(Builder builder) { + mName = builder.mName; + mIcon = builder.mIcon; + mUri = builder.mUri; + mKey = builder.mKey; + mIsBot = builder.mIsBot; + mIsImportant = builder.mIsImportant; + } + + /** Creates and returns a new {@link Builder} initialized with this Person's data. */ + public Builder toBuilder() { + return new Builder(this); + } + + /** + * @return the uri provided for this person or {@code null} if no Uri was provided. + */ + @Nullable + public String getUri() { + return mUri; + } + + /** + * @return the name provided for this person or {@code null} if no name was provided. + */ + @Nullable + public CharSequence getName() { + return mName; + } + + /** + * @return the icon provided for this person or {@code null} if no icon was provided. + */ + @Nullable + public Icon getIcon() { + return mIcon; + } + + /** + * @return the key provided for this person or {@code null} if no key was provided. + */ + @Nullable + public String getKey() { + return mKey; + } + + /** + * @return whether this Person is a machine. + */ + public boolean isBot() { + return mIsBot; + } + + /** + * @return whether this Person is important. + */ + public boolean isImportant() { + return mIsImportant; + } + + /** + * @return the URI associated with this person, or "name:mName" otherwise + * @hide + */ + public String resolveToLegacyUri() { + if (mUri != null) { + return mUri; + } + if (mName != null) { + return "name:" + mName; + } + return ""; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, @WriteFlags int flags) { + dest.writeCharSequence(mName); + if (mIcon != null) { + dest.writeInt(1); + mIcon.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + dest.writeString(mUri); + dest.writeString(mKey); + dest.writeBoolean(mIsImportant); + dest.writeBoolean(mIsBot); + } + + /** Builder for the immutable {@link Person} class. */ + public static class Builder { + @Nullable private CharSequence mName; + @Nullable private Icon mIcon; + @Nullable private String mUri; + @Nullable private String mKey; + private boolean mIsBot; + private boolean mIsImportant; + + /** Creates a new, empty {@link Builder}. */ + public Builder() { + } + + private Builder(Person person) { + mName = person.mName; + mIcon = person.mIcon; + mUri = person.mUri; + mKey = person.mKey; + mIsBot = person.mIsBot; + mIsImportant = person.mIsImportant; + } + + /** + * Give this person a name. + * + * @param name the name of this person. + */ + @NonNull + public Person.Builder setName(@Nullable CharSequence name) { + this.mName = name; + return this; + } + + /** + * Add an icon for this person. + * <br /> + * The system will prefer this icon over any images that are resolved from the URI. + * + * @param icon the icon of the person. + */ + @NonNull + public Person.Builder setIcon(@Nullable Icon icon) { + this.mIcon = icon; + return this; + } + + /** + * Set a URI associated with this person. + * + * <P> + * The person should be specified by the {@code String} representation of a + * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}. + * </P> + * + * <P>The system will also attempt to resolve {@code mailto:} and {@code tel:} schema + * URIs. The path part of these URIs must exist in the contacts database, in the + * appropriate column, or the reference will be discarded as invalid. Telephone schema + * URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}. + * </P> + * + * @param uri a URI for the person. + */ + @NonNull + public Person.Builder setUri(@Nullable String uri) { + mUri = uri; + return this; + } + + /** + * Add a key to this person in order to uniquely identify it. + * This is especially useful if the name doesn't uniquely identify this person or if the + * display name is a short handle of the actual name. + * + * <P>If no key is provided, the name serves as the key for the purpose of + * identification.</P> + * + * @param key the key that uniquely identifies this person. + */ + @NonNull + public Person.Builder setKey(@Nullable String key) { + mKey = key; + return this; + } + + /** + * Sets whether this is an important person. Use this method to denote users who frequently + * interact with the user of this device when {@link #setUri(String)} isn't provided with + * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}, and instead with + * the {@code mailto:} or {@code tel:} schemas. + * + * @param isImportant {@code true} if this is an important person, {@code false} otherwise. + */ + @NonNull + public Person.Builder setImportant(boolean isImportant) { + mIsImportant = isImportant; + return this; + } + + /** + * Sets whether this person is a machine rather than a human. + * + * @param isBot {@code true} if this person is a machine, {@code false} otherwise. + */ + @NonNull + public Person.Builder setBot(boolean isBot) { + mIsBot = isBot; + return this; + } + + /** Creates and returns the {@link Person} this builder represents. */ + @NonNull + public Person build() { + return new Person(this); + } + } + + public static final Creator<Person> CREATOR = new Creator<Person>() { + @Override + public Person createFromParcel(Parcel in) { + return new Person(in); + } + + @Override + public Person[] newArray(int size) { + return new Person[size]; + } + }; +} diff --git a/android/app/RemoteAction.java b/android/app/RemoteAction.java index 47741c02..c1746655 100644 --- a/android/app/RemoteAction.java +++ b/android/app/RemoteAction.java @@ -122,6 +122,7 @@ public final class RemoteAction implements Parcelable { public RemoteAction clone() { RemoteAction action = new RemoteAction(mIcon, mTitle, mContentDescription, mActionIntent); action.setEnabled(mEnabled); + action.setShouldShowIcon(mShouldShowIcon); return action; } diff --git a/android/app/StatsManager.java b/android/app/StatsManager.java index 4a6fa8c2..8783d945 100644 --- a/android/app/StatsManager.java +++ b/android/app/StatsManager.java @@ -23,6 +23,7 @@ import android.os.IBinder; import android.os.IStatsManager; import android.os.RemoteException; import android.os.ServiceManager; +import android.util.AndroidException; import android.util.Slog; /** @@ -82,55 +83,74 @@ public final class StatsManager { } /** - * Clients can send a configuration and simultaneously registers the name of a broadcast - * receiver that listens for when it should request data. + * Adds the given configuration and associates it with the given configKey. If a config with the + * given configKey already exists for the caller's uid, it is replaced with the new one. * * @param configKey An arbitrary integer that allows clients to track the configuration. - * @param config Wire-encoded StatsDConfig proto that specifies metrics (and all + * @param config Wire-encoded StatsdConfig proto that specifies metrics (and all * dependencies eg, conditions and matchers). - * @return true if successful + * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service + * @throws IllegalArgumentException if config is not a wire-encoded StatsdConfig proto */ @RequiresPermission(Manifest.permission.DUMP) - public boolean addConfiguration(long configKey, byte[] config) { + public void addConfig(long configKey, byte[] config) throws StatsUnavailableException { synchronized (this) { try { IStatsManager service = getIStatsManagerLocked(); - if (service == null) { - Slog.e(TAG, "Failed to find statsd when adding configuration"); - return false; - } - return service.addConfiguration(configKey, config); + service.addConfiguration(configKey, config); // can throw IllegalArgumentException } catch (RemoteException e) { Slog.e(TAG, "Failed to connect to statsd when adding configuration"); - return false; + throw new StatsUnavailableException("could not connect", e); } } } /** + * TODO: Temporary for backwards compatibility. Remove. + */ + @RequiresPermission(Manifest.permission.DUMP) + public boolean addConfiguration(long configKey, byte[] config) { + try { + addConfig(configKey, config); + return true; + } catch (StatsUnavailableException | IllegalArgumentException e) { + return false; + } + } + + /** * Remove a configuration from logging. * * @param configKey Configuration key to remove. - * @return true if successful + * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service */ @RequiresPermission(Manifest.permission.DUMP) - public boolean removeConfiguration(long configKey) { + public void removeConfig(long configKey) throws StatsUnavailableException { synchronized (this) { try { IStatsManager service = getIStatsManagerLocked(); - if (service == null) { - Slog.e(TAG, "Failed to find statsd when removing configuration"); - return false; - } - return service.removeConfiguration(configKey); + service.removeConfiguration(configKey); } catch (RemoteException e) { Slog.e(TAG, "Failed to connect to statsd when removing configuration"); - return false; + throw new StatsUnavailableException("could not connect", e); } } } /** + * TODO: Temporary for backwards compatibility. Remove. + */ + @RequiresPermission(Manifest.permission.DUMP) + public boolean removeConfiguration(long configKey) { + try { + removeConfig(configKey); + return true; + } catch (StatsUnavailableException e) { + return false; + } + } + + /** * Set the PendingIntent to be used when broadcasting subscriber information to the given * subscriberId within the given config. * <p> @@ -150,126 +170,168 @@ public final class StatsManager { * {@link #EXTRA_STATS_DIMENSIONS_VALUE}. * <p> * This function can only be called by the owner (uid) of the config. It must be called each - * time statsd starts. The config must have been added first (via addConfiguration()). + * time statsd starts. The config must have been added first (via {@link #addConfig}). * - * @param configKey The integer naming the config to which this subscriber is attached. - * @param subscriberId ID of the subscriber, as used in the config. * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber * associated with the given subscriberId. May be null, in which case * it undoes any previous setting of this subscriberId. - * @return true if successful + * @param configKey The integer naming the config to which this subscriber is attached. + * @param subscriberId ID of the subscriber, as used in the config. + * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service */ @RequiresPermission(Manifest.permission.DUMP) - public boolean setBroadcastSubscriber( - long configKey, long subscriberId, PendingIntent pendingIntent) { + public void setBroadcastSubscriber( + PendingIntent pendingIntent, long configKey, long subscriberId) + throws StatsUnavailableException { synchronized (this) { try { IStatsManager service = getIStatsManagerLocked(); - if (service == null) { - Slog.e(TAG, "Failed to find statsd when adding broadcast subscriber"); - return false; - } if (pendingIntent != null) { // Extracts IIntentSender from the PendingIntent and turns it into an IBinder. IBinder intentSender = pendingIntent.getTarget().asBinder(); - return service.setBroadcastSubscriber(configKey, subscriberId, intentSender); + service.setBroadcastSubscriber(configKey, subscriberId, intentSender); } else { - return service.unsetBroadcastSubscriber(configKey, subscriberId); + service.unsetBroadcastSubscriber(configKey, subscriberId); } } catch (RemoteException e) { Slog.e(TAG, "Failed to connect to statsd when adding broadcast subscriber", e); - return false; + throw new StatsUnavailableException("could not connect", e); } } } /** + * TODO: Temporary for backwards compatibility. Remove. + */ + @RequiresPermission(Manifest.permission.DUMP) + public boolean setBroadcastSubscriber( + long configKey, long subscriberId, PendingIntent pendingIntent) { + try { + setBroadcastSubscriber(pendingIntent, configKey, subscriberId); + return true; + } catch (StatsUnavailableException e) { + return false; + } + } + + /** * Registers the operation that is called to retrieve the metrics data. This must be called - * each time statsd starts. The config must have been added first (via addConfiguration(), - * although addConfiguration could have been called on a previous boot). This operation allows + * each time statsd starts. The config must have been added first (via {@link #addConfig}, + * although addConfig could have been called on a previous boot). This operation allows * statsd to send metrics data whenever statsd determines that the metrics in memory are - * approaching the memory limits. The fetch operation should call {@link #getData} to fetch the - * data, which also deletes the retrieved metrics from statsd's memory. + * approaching the memory limits. The fetch operation should call {@link #getReports} to fetch + * the data, which also deletes the retrieved metrics from statsd's memory. * - * @param configKey The integer naming the config to which this operation is attached. * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber * associated with the given subscriberId. May be null, in which case * it removes any associated pending intent with this configKey. - * @return true if successful + * @param configKey The integer naming the config to which this operation is attached. + * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service */ @RequiresPermission(Manifest.permission.DUMP) - public boolean setDataFetchOperation(long configKey, PendingIntent pendingIntent) { + public void setFetchReportsOperation(PendingIntent pendingIntent, long configKey) + throws StatsUnavailableException { synchronized (this) { try { IStatsManager service = getIStatsManagerLocked(); - if (service == null) { - Slog.e(TAG, "Failed to find statsd when registering data listener."); - return false; - } if (pendingIntent == null) { - return service.removeDataFetchOperation(configKey); + service.removeDataFetchOperation(configKey); } else { // Extracts IIntentSender from the PendingIntent and turns it into an IBinder. IBinder intentSender = pendingIntent.getTarget().asBinder(); - return service.setDataFetchOperation(configKey, intentSender); + service.setDataFetchOperation(configKey, intentSender); } } catch (RemoteException e) { Slog.e(TAG, "Failed to connect to statsd when registering data listener."); - return false; + throw new StatsUnavailableException("could not connect", e); } } } /** - * Clients can request data with a binder call. This getter is destructive and also clears - * the retrieved metrics from statsd memory. + * TODO: Temporary for backwards compatibility. Remove. + */ + @RequiresPermission(Manifest.permission.DUMP) + public boolean setDataFetchOperation(long configKey, PendingIntent pendingIntent) { + try { + setFetchReportsOperation(pendingIntent, configKey); + return true; + } catch (StatsUnavailableException e) { + return false; + } + } + + /** + * Request the data collected for the given configKey. + * This getter is destructive - it also clears the retrieved metrics from statsd's memory. * * @param configKey Configuration key to retrieve data from. - * @return Serialized ConfigMetricsReportList proto. Returns null on failure (eg, if statsd - * crashed). + * @return Serialized ConfigMetricsReportList proto. + * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service */ @RequiresPermission(Manifest.permission.DUMP) - public @Nullable byte[] getData(long configKey) { + public byte[] getReports(long configKey) throws StatsUnavailableException { synchronized (this) { try { IStatsManager service = getIStatsManagerLocked(); - if (service == null) { - Slog.e(TAG, "Failed to find statsd when getting data"); - return null; - } return service.getData(configKey); } catch (RemoteException e) { Slog.e(TAG, "Failed to connect to statsd when getting data"); - return null; + throw new StatsUnavailableException("could not connect", e); } } } /** + * TODO: Temporary for backwards compatibility. Remove. + */ + @RequiresPermission(Manifest.permission.DUMP) + public @Nullable byte[] getData(long configKey) { + try { + return getReports(configKey); + } catch (StatsUnavailableException e) { + return null; + } + } + + /** * Clients can request metadata for statsd. Will contain stats across all configurations but not - * the actual metrics themselves (metrics must be collected via {@link #getData(String)}. + * the actual metrics themselves (metrics must be collected via {@link #getReports(long)}. * This getter is not destructive and will not reset any metrics/counters. * - * @return Serialized StatsdStatsReport proto. Returns null on failure (eg, if statsd crashed). + * @return Serialized StatsdStatsReport proto. + * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service */ @RequiresPermission(Manifest.permission.DUMP) - public @Nullable byte[] getMetadata() { + public byte[] getStatsMetadata() throws StatsUnavailableException { synchronized (this) { try { IStatsManager service = getIStatsManagerLocked(); - if (service == null) { - Slog.e(TAG, "Failed to find statsd when getting metadata"); - return null; - } return service.getMetadata(); } catch (RemoteException e) { Slog.e(TAG, "Failed to connect to statsd when getting metadata"); - return null; + throw new StatsUnavailableException("could not connect", e); } } } + /** + * Clients can request metadata for statsd. Will contain stats across all configurations but not + * the actual metrics themselves (metrics must be collected via {@link #getReports(long)}. + * This getter is not destructive and will not reset any metrics/counters. + * + * @return Serialized StatsdStatsReport proto. Returns null on failure (eg, if statsd crashed). + */ + @RequiresPermission(Manifest.permission.DUMP) + public @Nullable byte[] getMetadata() { + try { + return getStatsMetadata(); + } catch (StatsUnavailableException e) { + return null; + } + } + private class StatsdDeathRecipient implements IBinder.DeathRecipient { @Override public void binderDied() { @@ -279,14 +341,33 @@ public final class StatsManager { } } - private IStatsManager getIStatsManagerLocked() throws RemoteException { + private IStatsManager getIStatsManagerLocked() throws StatsUnavailableException { if (mService != null) { return mService; } mService = IStatsManager.Stub.asInterface(ServiceManager.getService("stats")); - if (mService != null) { + if (mService == null) { + throw new StatsUnavailableException("could not be found"); + } + try { mService.asBinder().linkToDeath(new StatsdDeathRecipient(), 0); + } catch (RemoteException e) { + throw new StatsUnavailableException("could not connect when linkToDeath", e); } return mService; } + + /** + * Exception thrown when communication with the stats service fails (eg if it is not available). + * This might be thrown early during boot before the stats service has started or if it crashed. + */ + public static class StatsUnavailableException extends AndroidException { + public StatsUnavailableException(String reason) { + super("Failed to connect to statsd: " + reason); + } + + public StatsUnavailableException(String reason, Throwable e) { + super("Failed to connect to statsd: " + reason, e); + } + } } diff --git a/android/app/SystemServiceRegistry.java b/android/app/SystemServiceRegistry.java index 1776eace..246d4a37 100644 --- a/android/app/SystemServiceRegistry.java +++ b/android/app/SystemServiceRegistry.java @@ -18,6 +18,7 @@ package android.app; import android.accounts.AccountManager; import android.accounts.IAccountManager; +import android.app.ContextImpl.ServiceInitializationState; import android.app.admin.DevicePolicyManager; import android.app.admin.IDevicePolicyManager; import android.app.job.IJobScheduler; @@ -104,10 +105,12 @@ import android.nfc.NfcManager; import android.os.BatteryManager; import android.os.BatteryStats; import android.os.Build; +import android.os.DeviceIdleManager; import android.os.DropBoxManager; import android.os.HardwarePropertiesManager; import android.os.IBatteryPropertiesRegistrar; import android.os.IBinder; +import android.os.IDeviceIdleController; import android.os.IHardwarePropertiesManager; import android.os.IPowerManager; import android.os.IRecoverySystem; @@ -160,7 +163,6 @@ import com.android.internal.os.IDropBoxManagerService; import com.android.internal.policy.PhoneLayoutInflater; import java.util.HashMap; -import java.util.concurrent.atomic.AtomicInteger; /** * Manages all of the system services that can be returned by {@link Context#getSystemService}. @@ -279,12 +281,12 @@ final class SystemServiceRegistry { }}); registerService(Context.IPSEC_SERVICE, IpSecManager.class, - new StaticServiceFetcher<IpSecManager>() { + new CachedServiceFetcher<IpSecManager>() { @Override - public IpSecManager createService() { + public IpSecManager createService(ContextImpl ctx) throws ServiceNotFoundException { IBinder b = ServiceManager.getService(Context.IPSEC_SERVICE); IIpSecService service = IIpSecService.Stub.asInterface(b); - return new IpSecManager(service); + return new IpSecManager(ctx, service); }}); registerService(Context.COUNTRY_DETECTOR, CountryDetector.class, @@ -984,6 +986,17 @@ final class SystemServiceRegistry { ctx.mMainThread.getHandler()); } }); + + registerService(Context.DEVICE_IDLE_CONTROLLER, DeviceIdleManager.class, + new CachedServiceFetcher<DeviceIdleManager>() { + @Override + public DeviceIdleManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IDeviceIdleController service = IDeviceIdleController.Stub.asInterface( + ServiceManager.getServiceOrThrow( + Context.DEVICE_IDLE_CONTROLLER)); + return new DeviceIdleManager(ctx.getOuterContext(), service); + }}); } /** @@ -993,10 +1006,6 @@ final class SystemServiceRegistry { return new Object[sServiceCacheSize]; } - public static AtomicInteger[] createServiceInitializationStateArray() { - return new AtomicInteger[sServiceCacheSize]; - } - /** * Gets a system service from a given context. */ @@ -1037,7 +1046,10 @@ final class SystemServiceRegistry { static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> { private final int mCacheIndex; - public CachedServiceFetcher() { + CachedServiceFetcher() { + // Note this class must be instantiated only by the static initializer of the + // outer class (SystemServiceRegistry), which already does the synchronization, + // so bare access to sServiceCacheSize is okay here. mCacheIndex = sServiceCacheSize++; } @@ -1045,95 +1057,73 @@ final class SystemServiceRegistry { @SuppressWarnings("unchecked") public final T getService(ContextImpl ctx) { final Object[] cache = ctx.mServiceCache; + final int[] gates = ctx.mServiceInitializationStateArray; + + for (;;) { + boolean doInitialize = false; + synchronized (cache) { + // Return it if we already have a cached instance. + T service = (T) cache[mCacheIndex]; + if (service != null || gates[mCacheIndex] == ContextImpl.STATE_NOT_FOUND) { + return service; + } - // Fast path. If it's already cached, just return it. - Object service = cache[mCacheIndex]; - if (service != null) { - return (T) service; - } + // If we get here, there's no cached instance. - // Slow path. - final AtomicInteger[] gates = ctx.mServiceInitializationStateArray; - final AtomicInteger gate; + // Grr... if gate is STATE_READY, then this means we initialized the service + // once but someone cleared it. + // We start over from STATE_UNINITIALIZED. + if (gates[mCacheIndex] == ContextImpl.STATE_READY) { + gates[mCacheIndex] = ContextImpl.STATE_UNINITIALIZED; + } - synchronized (cache) { - // See if it's cached or not again, with the lock held this time. - service = cache[mCacheIndex]; - if (service != null) { - return (T) service; - } + // It's possible for multiple threads to get here at the same time, so + // use the "gate" to make sure only the first thread will call createService(). - // Not initialized yet. Create an atomic boolean to control which thread should - // instantiate the service. - if (gates[mCacheIndex] != null) { - gate = gates[mCacheIndex]; - } else { - gate = new AtomicInteger(ContextImpl.STATE_UNINITIALIZED); - gates[mCacheIndex] = gate; + // At this point, the gate must be either UNINITIALIZED or INITIALIZING. + if (gates[mCacheIndex] == ContextImpl.STATE_UNINITIALIZED) { + doInitialize = true; + gates[mCacheIndex] = ContextImpl.STATE_INITIALIZING; + } } - } - // Not cached yet. - // - // Note multiple threads can reach here for the same service on the same context - // concurrently. - // - // Now we're going to instantiate the service, but do so without the cache held; - // otherwise it could deadlock. (b/71882178) - // - // However we still don't want to instantiate the same service multiple times, so - // use the atomic integer to ensure only one thread will call createService(). - - if (gate.compareAndSet( - ContextImpl.STATE_UNINITIALIZED, ContextImpl.STATE_INITIALIZING)) { - try { - // This thread is the first one to get here. Instantiate the service - // *without* the cache lock held. + if (doInitialize) { + // Only the first thread gets here. + + T service = null; + @ServiceInitializationState int newState = ContextImpl.STATE_NOT_FOUND; try { + // This thread is the first one to get here. Instantiate the service + // *without* the cache lock held. service = createService(ctx); + newState = ContextImpl.STATE_READY; + + } catch (ServiceNotFoundException e) { + onServiceNotFound(e); + } finally { synchronized (cache) { cache[mCacheIndex] = service; + gates[mCacheIndex] = newState; + cache.notifyAll(); } - } catch (ServiceNotFoundException e) { - onServiceNotFound(e); - } - } finally { - // Tell the all other threads that the cache is ready now. - // (But it's still be null in case of ServiceNotFoundException.) - synchronized (gate) { - gate.set(ContextImpl.STATE_READY); - gate.notifyAll(); } + return service; } - return (T) service; - } - // Other threads will wait on the gate lock. - synchronized (gate) { - boolean interrupted = false; - - // Note: We check whether "state == STATE_READY", not - // "cache[mCacheIndex] != null", because "cache[mCacheIndex] == null" - // is still a valid outcome in the ServiceNotFoundException case. - while (gate.get() != ContextImpl.STATE_READY) { - try { - gate.wait(); - } catch (InterruptedException e) { - Log.w(TAG, "getService() interrupted"); - interrupted = true; + // The other threads will wait for the first thread to call notifyAll(), + // and go back to the top and retry. + synchronized (cache) { + while (gates[mCacheIndex] < ContextImpl.STATE_READY) { + try { + cache.wait(); + } catch (InterruptedException e) { + Log.w(TAG, "getService() interrupted"); + Thread.currentThread().interrupt(); + return null; + } } } - if (interrupted) { - Thread.currentThread().interrupt(); - } - } - // Now the first thread has initialized it. - // It may still be null if ServiceNotFoundException was thrown, but that shouldn't - // happen, so we'll just return null here in that case. - synchronized (cache) { - service = cache[mCacheIndex]; } - return (T) service; } public abstract T createService(ContextImpl ctx) throws ServiceNotFoundException; diff --git a/android/app/UiAutomation.java b/android/app/UiAutomation.java index bd4933a2..c03340e8 100644 --- a/android/app/UiAutomation.java +++ b/android/app/UiAutomation.java @@ -580,6 +580,8 @@ public final class UiAutomation { // Execute the command *without* the lock being held. command.run(); + List<AccessibilityEvent> receivedEvents = new ArrayList<>(); + // Acquire the lock and wait for the event. try { // Wait for the event. @@ -600,14 +602,14 @@ public final class UiAutomation { if (filter.accept(event)) { return event; } - event.recycle(); + receivedEvents.add(event); } // Check if timed out and if not wait. final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis; if (remainingTimeMillis <= 0) { throw new TimeoutException("Expected event not received within: " - + timeoutMillis + " ms."); + + timeoutMillis + " ms among: " + receivedEvents); } synchronized (mLock) { if (mEventQueue.isEmpty()) { @@ -620,6 +622,11 @@ public final class UiAutomation { } } } finally { + int size = receivedEvents.size(); + for (int i = 0; i < size; i++) { + receivedEvents.get(i).recycle(); + } + synchronized (mLock) { mWaitingForEventDelivery = false; mEventQueue.clear(); diff --git a/android/app/WallpaperManager.java b/android/app/WallpaperManager.java index 465340f6..6c2fb2df 100644 --- a/android/app/WallpaperManager.java +++ b/android/app/WallpaperManager.java @@ -401,7 +401,8 @@ public class WallpaperManager { } } synchronized (this) { - if (mCachedWallpaper != null && mCachedWallpaperUserId == userId) { + if (mCachedWallpaper != null && mCachedWallpaperUserId == userId + && !mCachedWallpaper.isRecycled()) { return mCachedWallpaper; } mCachedWallpaper = null; @@ -412,7 +413,7 @@ public class WallpaperManager { } catch (OutOfMemoryError e) { Log.w(TAG, "Out of memory loading the current wallpaper: " + e); } catch (SecurityException e) { - if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.O) { + if (context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O_MR1) { Log.w(TAG, "No permission to access wallpaper, suppressing" + " exception to avoid crashing legacy app."); } else { @@ -976,7 +977,7 @@ public class WallpaperManager { } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (SecurityException e) { - if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.O) { + if (mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O_MR1) { Log.w(TAG, "No permission to access wallpaper, suppressing" + " exception to avoid crashing legacy app."); return null; diff --git a/android/app/admin/DevicePolicyManager.java b/android/app/admin/DevicePolicyManager.java index b64aae52..c491dccb 100644 --- a/android/app/admin/DevicePolicyManager.java +++ b/android/app/admin/DevicePolicyManager.java @@ -1169,10 +1169,18 @@ public class DevicePolicyManager { * Constant to indicate the feature of mandatory backups. Used as argument to * {@link #createAdminSupportIntent(String)}. * @see #setMandatoryBackupTransport(ComponentName, ComponentName) + * @hide */ public static final String POLICY_MANDATORY_BACKUPS = "policy_mandatory_backups"; /** + * Constant to indicate the feature of suspending app. Use it as the value of + * {@link #EXTRA_RESTRICTION}. + * @hide + */ + public static final String POLICY_SUSPEND_PACKAGES = "policy_suspend_packages"; + + /** * A String indicating a specific restricted feature. Can be a user restriction from the * {@link UserManager}, e.g. {@link UserManager#DISALLOW_ADJUST_VOLUME}, or one of the values * {@link #POLICY_DISABLE_CAMERA}, {@link #POLICY_DISABLE_SCREEN_CAPTURE} or @@ -4211,6 +4219,15 @@ public class DevicePolicyManager { return null; } + /** + * Returns {@code true} if the device supports attestation of device identifiers in addition + * to key attestation. + * @return {@code true} if Device ID attestation is supported. + */ + public boolean isDeviceIdAttestationSupported() { + PackageManager pm = mContext.getPackageManager(); + return pm.hasSystemFeature(PackageManager.FEATURE_DEVICE_ID_ATTESTATION); + } /** * Called by a device or profile owner, or delegated certificate installer, to associate @@ -6182,6 +6199,7 @@ public class DevicePolicyManager { * @hide */ @SystemApi + @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public @Nullable List<String> getPermittedAccessibilityServices(int userId) { throwIfParentInstance("getPermittedAccessibilityServices"); if (mService != null) { @@ -6826,8 +6844,7 @@ public class DevicePolicyManager { * @param restriction Indicates for which feature the dialog should be displayed. Can be a * user restriction from {@link UserManager}, e.g. * {@link UserManager#DISALLOW_ADJUST_VOLUME}, or one of the constants - * {@link #POLICY_DISABLE_CAMERA}, {@link #POLICY_DISABLE_SCREEN_CAPTURE} or - * {@link #POLICY_MANDATORY_BACKUPS}. + * {@link #POLICY_DISABLE_CAMERA}, {@link #POLICY_DISABLE_SCREEN_CAPTURE}. * @return Intent An intent to be used to start the dialog-activity if the restriction is * set by an admin, or null if the restriction does not exist or no admin set it. */ @@ -8774,13 +8791,6 @@ public class DevicePolicyManager { * * <p> Backup service is off by default when device owner is present. * - * <p> If backups are made mandatory by specifying a non-null mandatory backup transport using - * the {@link DevicePolicyManager#setMandatoryBackupTransport} method, the backup service is - * automatically enabled. - * - * <p> If the backup service is disabled using this method after the mandatory backup transport - * has been set, the mandatory backup transport is cleared. - * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param enabled {@code true} to enable the backup service, {@code false} to disable it. * @throws SecurityException if {@code admin} is not a device owner. @@ -8818,6 +8828,8 @@ public class DevicePolicyManager { * <p>Only device owner can call this method. * <p>If backups were disabled and a non-null backup transport {@link ComponentName} is * specified, backups will be enabled. + * <p> If the backup service is disabled after the mandatory backup transport has been set, the + * mandatory backup transport is cleared. * * <p>NOTE: The method shouldn't be called on the main thread. * @@ -8825,6 +8837,7 @@ public class DevicePolicyManager { * @param backupTransportComponent The backup transport layer to be used for mandatory backups. * @return {@code true} if the backup transport was successfully set; {@code false} otherwise. * @throws SecurityException if {@code admin} is not a device owner. + * @hide */ @WorkerThread public boolean setMandatoryBackupTransport( @@ -8844,6 +8857,7 @@ public class DevicePolicyManager { * * @return a {@link ComponentName} of the backup transport layer to be used if backups are * mandatory or {@code null} if backups are not mandatory. + * @hide */ public ComponentName getMandatoryBackupTransport() { throwIfParentInstance("getMandatoryBackupTransport"); diff --git a/android/app/admin/FreezeInterval.java b/android/app/admin/FreezePeriod.java index de5e21ac..657f0177 100644 --- a/android/app/admin/FreezeInterval.java +++ b/android/app/admin/FreezePeriod.java @@ -20,49 +20,88 @@ import android.util.Log; import android.util.Pair; import java.time.LocalDate; +import java.time.MonthDay; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; /** - * An interval representing one freeze period which repeats annually. We use the number of days - * since the start of (non-leap) year to define the start and end dates of an interval, both - * inclusive. If the end date is smaller than the start date, the interval is considered wrapped - * around the year-end. As far as an interval is concerned, February 29th should be treated as - * if it were February 28th: so an interval starting or ending on February 28th are not - * distinguishable from an interval on February 29th. When calulating interval length or - * distance between two dates, February 29th is also disregarded. + * A class that represents one freeze period which repeats <em>annually</em>. A freeze period has + * two {@link java.time#MonthDay} values that define the start and end dates of the period, both + * inclusive. If the end date is earlier than the start date, the period is considered wrapped + * around the year-end. As far as freeze period is concerned, leap year is disregarded and February + * 29th should be treated as if it were February 28th: so a freeze starting or ending on February + * 28th is identical to a freeze starting or ending on February 29th. When calulating the length of + * a freeze or the distance bewteen two freee periods, February 29th is also ignored. * * @see SystemUpdatePolicy#setFreezePeriods - * @hide */ -public class FreezeInterval { - private static final String TAG = "FreezeInterval"; +public class FreezePeriod { + private static final String TAG = "FreezePeriod"; private static final int DUMMY_YEAR = 2001; static final int DAYS_IN_YEAR = 365; // 365 since DUMMY_YEAR is not a leap year - final int mStartDay; // [1,365] - final int mEndDay; // [1,365] + private final MonthDay mStart; + private final MonthDay mEnd; - FreezeInterval(int startDay, int endDay) { - if (startDay < 1 || startDay > 365 || endDay < 1 || endDay > 365) { - throw new RuntimeException("Bad dates for Interval: " + startDay + "," + endDay); - } + /* + * Start and end dates represented by number of days since the beginning of the year. + * They are internal representations of mStart and mEnd with normalized Leap year days + * (Feb 29 == Feb 28 == 59th day of year). All internal calclations are based on + * these two values so that leap year days are disregarded. + */ + private final int mStartDay; // [1, 365] + private final int mEndDay; // [1, 365] + + /** + * Creates a freeze period by its start and end dates. If the end date is earlier than the start + * date, the freeze period is considered wrapping year-end. + */ + public FreezePeriod(MonthDay start, MonthDay end) { + mStart = start; + mStartDay = mStart.atYear(DUMMY_YEAR).getDayOfYear(); + mEnd = end; + mEndDay = mEnd.atYear(DUMMY_YEAR).getDayOfYear(); + } + + /** + * Returns the start date (inclusive) of this freeze period. + */ + public MonthDay getStart() { + return mStart; + } + + /** + * Returns the end date (inclusive) of this freeze period. + */ + public MonthDay getEnd() { + return mEnd; + } + + /** + * @hide + */ + private FreezePeriod(int startDay, int endDay) { mStartDay = startDay; + mStart = dayOfYearToMonthDay(startDay); mEndDay = endDay; + mEnd = dayOfYearToMonthDay(endDay); } + /** @hide */ int getLength() { return getEffectiveEndDay() - mStartDay + 1; } + /** @hide */ boolean isWrapped() { return mEndDay < mStartDay; } /** * Returns the effective end day, taking wrapping around year-end into consideration + * @hide */ int getEffectiveEndDay() { if (!isWrapped()) { @@ -72,6 +111,7 @@ public class FreezeInterval { } } + /** @hide */ boolean contains(LocalDate localDate) { final int daysOfYear = dayOfYearDisregardLeapYear(localDate); if (!isWrapped()) { @@ -84,6 +124,7 @@ public class FreezeInterval { } } + /** @hide */ boolean after(LocalDate localDate) { return mStartDay > dayOfYearDisregardLeapYear(localDate); } @@ -95,6 +136,7 @@ public class FreezeInterval { * include now, the returned dates represents the next future interval. * The result will always have the same month and dayOfMonth value as the non-instantiated * interval itself. + * @hide */ Pair<LocalDate, LocalDate> toCurrentOrFutureRealDates(LocalDate now) { final int nowDays = dayOfYearDisregardLeapYear(now); @@ -138,14 +180,24 @@ public class FreezeInterval { + LocalDate.ofYearDay(DUMMY_YEAR, mEndDay).format(formatter); } - // Treat the supplied date as in a non-leap year and return its day of year. - static int dayOfYearDisregardLeapYear(LocalDate date) { + /** @hide */ + private static MonthDay dayOfYearToMonthDay(int dayOfYear) { + LocalDate date = LocalDate.ofYearDay(DUMMY_YEAR, dayOfYear); + return MonthDay.of(date.getMonth(), date.getDayOfMonth()); + } + + /** + * Treat the supplied date as in a non-leap year and return its day of year. + * @hide + */ + private static int dayOfYearDisregardLeapYear(LocalDate date) { return date.withYear(DUMMY_YEAR).getDayOfYear(); } /** * Compute the number of days between first (inclusive) and second (exclusive), * treating all years in between as non-leap. + * @hide */ public static int distanceWithoutLeapYear(LocalDate first, LocalDate second) { return dayOfYearDisregardLeapYear(first) - dayOfYearDisregardLeapYear(second) @@ -165,16 +217,16 @@ public class FreezeInterval { * 3. At most one wrapped Interval remains, and it will be at the end of the list * @hide */ - protected static List<FreezeInterval> canonicalizeIntervals(List<FreezeInterval> intervals) { + static List<FreezePeriod> canonicalizePeriods(List<FreezePeriod> intervals) { boolean[] taken = new boolean[DAYS_IN_YEAR]; // First convert the intervals into flat array - for (FreezeInterval interval : intervals) { + for (FreezePeriod interval : intervals) { for (int i = interval.mStartDay; i <= interval.getEffectiveEndDay(); i++) { taken[(i - 1) % DAYS_IN_YEAR] = true; } } // Then reconstruct intervals from the array - List<FreezeInterval> result = new ArrayList<>(); + List<FreezePeriod> result = new ArrayList<>(); int i = 0; while (i < DAYS_IN_YEAR) { if (!taken[i]) { @@ -183,14 +235,14 @@ public class FreezeInterval { } final int intervalStart = i + 1; while (i < DAYS_IN_YEAR && taken[i]) i++; - result.add(new FreezeInterval(intervalStart, i)); + result.add(new FreezePeriod(intervalStart, i)); } // Check if the last entry can be merged to the first entry to become one single // wrapped interval final int lastIndex = result.size() - 1; if (lastIndex > 0 && result.get(lastIndex).mEndDay == DAYS_IN_YEAR && result.get(0).mStartDay == 1) { - FreezeInterval wrappedInterval = new FreezeInterval(result.get(lastIndex).mStartDay, + FreezePeriod wrappedInterval = new FreezePeriod(result.get(lastIndex).mStartDay, result.get(0).mEndDay); result.set(lastIndex, wrappedInterval); result.remove(0); @@ -207,18 +259,18 @@ public class FreezeInterval { * * @hide */ - protected static void validatePeriods(List<FreezeInterval> periods) { - List<FreezeInterval> allPeriods = FreezeInterval.canonicalizeIntervals(periods); + static void validatePeriods(List<FreezePeriod> periods) { + List<FreezePeriod> allPeriods = FreezePeriod.canonicalizePeriods(periods); if (allPeriods.size() != periods.size()) { throw SystemUpdatePolicy.ValidationFailedException.duplicateOrOverlapPeriods(); } for (int i = 0; i < allPeriods.size(); i++) { - FreezeInterval current = allPeriods.get(i); + FreezePeriod current = allPeriods.get(i); if (current.getLength() > SystemUpdatePolicy.FREEZE_PERIOD_MAX_LENGTH) { throw SystemUpdatePolicy.ValidationFailedException.freezePeriodTooLong("Freeze " + "period " + current + " is too long: " + current.getLength() + " days"); } - FreezeInterval previous = i > 0 ? allPeriods.get(i - 1) + FreezePeriod previous = i > 0 ? allPeriods.get(i - 1) : allPeriods.get(allPeriods.size() - 1); if (previous != current) { final int separation; @@ -247,7 +299,7 @@ public class FreezeInterval { * * @hide */ - protected static void validateAgainstPreviousFreezePeriod(List<FreezeInterval> periods, + static void validateAgainstPreviousFreezePeriod(List<FreezePeriod> periods, LocalDate prevPeriodStart, LocalDate prevPeriodEnd, LocalDate now) { if (periods.size() == 0 || prevPeriodStart == null || prevPeriodEnd == null) { return; @@ -258,14 +310,14 @@ public class FreezeInterval { // Clock was adjusted backwards. We can continue execution though, the separation // and length validation below still works under this condition. } - List<FreezeInterval> allPeriods = FreezeInterval.canonicalizeIntervals(periods); + List<FreezePeriod> allPeriods = FreezePeriod.canonicalizePeriods(periods); // Given current time now, find the freeze period that's either current, or the one // that's immediately afterwards. For the later case, it might be after the year-end, // but this can only happen if there is only one freeze period. - FreezeInterval curOrNextFreezePeriod = allPeriods.get(0); - for (FreezeInterval interval : allPeriods) { + FreezePeriod curOrNextFreezePeriod = allPeriods.get(0); + for (FreezePeriod interval : allPeriods) { if (interval.contains(now) - || interval.mStartDay > FreezeInterval.dayOfYearDisregardLeapYear(now)) { + || interval.mStartDay > FreezePeriod.dayOfYearDisregardLeapYear(now)) { curOrNextFreezePeriod = interval; break; } @@ -282,7 +334,7 @@ public class FreezeInterval { // Now validate [prevPeriodStart, prevPeriodEnd] against curOrNextFreezeDates final String periodsDescription = "Prev: " + prevPeriodStart + "," + prevPeriodEnd + "; cur: " + curOrNextFreezeDates.first + "," + curOrNextFreezeDates.second; - long separation = FreezeInterval.distanceWithoutLeapYear(curOrNextFreezeDates.first, + long separation = FreezePeriod.distanceWithoutLeapYear(curOrNextFreezeDates.first, prevPeriodEnd) - 1; if (separation > 0) { // Two intervals do not overlap, check separation @@ -292,7 +344,7 @@ public class FreezeInterval { } } else { // Two intervals overlap, check combined length - long length = FreezeInterval.distanceWithoutLeapYear(curOrNextFreezeDates.second, + long length = FreezePeriod.distanceWithoutLeapYear(curOrNextFreezeDates.second, prevPeriodStart) + 1; if (length > SystemUpdatePolicy.FREEZE_PERIOD_MAX_LENGTH) { throw ValidationFailedException.combinedPeriodTooLong("Combined freeze period " diff --git a/android/app/admin/SystemUpdatePolicy.java b/android/app/admin/SystemUpdatePolicy.java index 47b3a81d..20eef6cc 100644 --- a/android/app/admin/SystemUpdatePolicy.java +++ b/android/app/admin/SystemUpdatePolicy.java @@ -38,9 +38,11 @@ import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.MonthDay; import java.time.ZoneId; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -51,7 +53,7 @@ import java.util.stream.Collectors; * @see DevicePolicyManager#setSystemUpdatePolicy * @see DevicePolicyManager#getSystemUpdatePolicy */ -public class SystemUpdatePolicy implements Parcelable { +public final class SystemUpdatePolicy implements Parcelable { private static final String TAG = "SystemUpdatePolicy"; /** @hide */ @@ -163,6 +165,7 @@ public class SystemUpdatePolicy implements Parcelable { ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE, ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG, ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE, + ERROR_UNKNOWN, }) @Retention(RetentionPolicy.SOURCE) @interface ValidationFailureType {} @@ -171,33 +174,38 @@ public class SystemUpdatePolicy implements Parcelable { public static final int ERROR_NONE = 0; /** + * Validation failed with unknown error. + */ + public static final int ERROR_UNKNOWN = 1; + + /** * The freeze periods contains duplicates, periods that overlap with each * other or periods whose start and end joins. */ - public static final int ERROR_DUPLICATE_OR_OVERLAP = 1; + public static final int ERROR_DUPLICATE_OR_OVERLAP = 2; /** * There exists at least one freeze period whose length exceeds 90 days. */ - public static final int ERROR_NEW_FREEZE_PERIOD_TOO_LONG = 2; + public static final int ERROR_NEW_FREEZE_PERIOD_TOO_LONG = 3; /** * There exists some freeze period which starts within 60 days of the preceding period's * end time. */ - public static final int ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE = 3; + public static final int ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE = 4; /** * The device has been in a freeze period and when combining with the new freeze period * to be set, it will result in the total freeze period being longer than 90 days. */ - public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG = 4; + public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG = 5; /** * The device has been in a freeze period and some new freeze period to be set is less * than 60 days from the end of the last freeze period the device went through. */ - public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE = 5; + public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE = 6; @ValidationFailureType private final int mErrorCode; @@ -272,7 +280,7 @@ public class SystemUpdatePolicy implements Parcelable { private int mMaintenanceWindowStart; private int mMaintenanceWindowEnd; - private final ArrayList<FreezeInterval> mFreezePeriods; + private final ArrayList<FreezePeriod> mFreezePeriods; private SystemUpdatePolicy() { mPolicyType = TYPE_UNKNOWN; @@ -444,12 +452,10 @@ public class SystemUpdatePolicy implements Parcelable { * requirement set above * @return this instance */ - public SystemUpdatePolicy setFreezePeriods(List<Pair<Integer, Integer>> freezePeriods) { - List<FreezeInterval> newPeriods = freezePeriods.stream().map( - p -> new FreezeInterval(p.first, p.second)).collect(Collectors.toList()); - FreezeInterval.validatePeriods(newPeriods); + public SystemUpdatePolicy setFreezePeriods(List<FreezePeriod> freezePeriods) { + FreezePeriod.validatePeriods(freezePeriods); mFreezePeriods.clear(); - mFreezePeriods.addAll(newPeriods); + mFreezePeriods.addAll(freezePeriods); return this; } @@ -458,12 +464,8 @@ public class SystemUpdatePolicy implements Parcelable { * * @return the list of freeze periods, or an empty list if none was set. */ - public List<Pair<Integer, Integer>> getFreezePeriods() { - List<Pair<Integer, Integer>> result = new ArrayList<>(mFreezePeriods.size()); - for (FreezeInterval interval : mFreezePeriods) { - result.add(new Pair<>(interval.mStartDay, interval.mEndDay)); - } - return result; + public List<FreezePeriod> getFreezePeriods() { + return Collections.unmodifiableList(mFreezePeriods); } /** @@ -472,7 +474,7 @@ public class SystemUpdatePolicy implements Parcelable { * @hide */ public Pair<LocalDate, LocalDate> getCurrentFreezePeriod(LocalDate now) { - for (FreezeInterval interval : mFreezePeriods) { + for (FreezePeriod interval : mFreezePeriods) { if (interval.contains(now)) { return interval.toCurrentOrFutureRealDates(now); } @@ -485,10 +487,10 @@ public class SystemUpdatePolicy implements Parcelable { * is not within a freeze period. */ private long timeUntilNextFreezePeriod(long now) { - List<FreezeInterval> sortedPeriods = FreezeInterval.canonicalizeIntervals(mFreezePeriods); + List<FreezePeriod> sortedPeriods = FreezePeriod.canonicalizePeriods(mFreezePeriods); LocalDate nowDate = millisToDate(now); LocalDate nextFreezeStart = null; - for (FreezeInterval interval : sortedPeriods) { + for (FreezePeriod interval : sortedPeriods) { if (interval.after(nowDate)) { nextFreezeStart = interval.toCurrentOrFutureRealDates(nowDate).first; break; @@ -506,13 +508,13 @@ public class SystemUpdatePolicy implements Parcelable { /** @hide */ public void validateFreezePeriods() { - FreezeInterval.validatePeriods(mFreezePeriods); + FreezePeriod.validatePeriods(mFreezePeriods); } /** @hide */ public void validateAgainstPreviousFreezePeriod(LocalDate prevPeriodStart, LocalDate prevPeriodEnd, LocalDate now) { - FreezeInterval.validateAgainstPreviousFreezePeriod(mFreezePeriods, prevPeriodStart, + FreezePeriod.validateAgainstPreviousFreezePeriod(mFreezePeriods, prevPeriodStart, prevPeriodEnd, now); } @@ -521,10 +523,10 @@ public class SystemUpdatePolicy implements Parcelable { * updates and how long this action is valid for, given the current system update policy. Its * action could be one of the following * <ul> - * <li> {@code TYPE_INSTALL_AUTOMATIC} system updates should be installed immedately and without - * user intervention as soon as they become available. - * <li> {@code TYPE_POSTPONE} system updates should be postponed for a maximum of 30 days - * <li> {@code TYPE_PAUSE} system updates should be postponed indefinitely until further notice + * <li> {@link #TYPE_INSTALL_AUTOMATIC} system updates should be installed immedately and + * without user intervention as soon as they become available. + * <li> {@link #TYPE_POSTPONE} system updates should be postponed for a maximum of 30 days + * <li> {@link #TYPE_PAUSE} system updates should be postponed indefinitely until further notice * </ul> * * The effective time measures how long this installation option is valid for from the queried @@ -535,18 +537,38 @@ public class SystemUpdatePolicy implements Parcelable { */ @SystemApi public static class InstallationOption { + /** @hide */ + @IntDef(prefix = { "TYPE_" }, value = { + TYPE_INSTALL_AUTOMATIC, + TYPE_PAUSE, + TYPE_POSTPONE + }) + @Retention(RetentionPolicy.SOURCE) + @interface InstallationOptionType {} + + @InstallationOptionType private final int mType; private long mEffectiveTime; - InstallationOption(int type, long effectiveTime) { + InstallationOption(@InstallationOptionType int type, long effectiveTime) { this.mType = type; this.mEffectiveTime = effectiveTime; } - public int getType() { + /** + * Returns the type of the current installation option, could be one of + * {@link #TYPE_INSTALL_AUTOMATIC}, {@link #TYPE_POSTPONE} and {@link #TYPE_PAUSE}. + * @return type of installation option. + */ + public @InstallationOptionType int getType() { return mType; } + /** + * Returns how long the current installation option in effective for, starting from the time + * of query. + * @return the effective time in milliseconds. + */ public long getEffectiveTime() { return mEffectiveTime; } @@ -667,9 +689,11 @@ public class SystemUpdatePolicy implements Parcelable { int freezeCount = mFreezePeriods.size(); dest.writeInt(freezeCount); for (int i = 0; i < freezeCount; i++) { - FreezeInterval interval = mFreezePeriods.get(i); - dest.writeInt(interval.mStartDay); - dest.writeInt(interval.mEndDay); + FreezePeriod interval = mFreezePeriods.get(i); + dest.writeInt(interval.getStart().getMonthValue()); + dest.writeInt(interval.getStart().getDayOfMonth()); + dest.writeInt(interval.getEnd().getMonthValue()); + dest.writeInt(interval.getEnd().getDayOfMonth()); } } @@ -686,8 +710,9 @@ public class SystemUpdatePolicy implements Parcelable { int freezeCount = source.readInt(); policy.mFreezePeriods.ensureCapacity(freezeCount); for (int i = 0; i < freezeCount; i++) { - policy.mFreezePeriods.add( - new FreezeInterval(source.readInt(), source.readInt())); + MonthDay start = MonthDay.of(source.readInt(), source.readInt()); + MonthDay end = MonthDay.of(source.readInt(), source.readInt()); + policy.mFreezePeriods.add(new FreezePeriod(start, end)); } return policy; } @@ -730,9 +755,9 @@ public class SystemUpdatePolicy implements Parcelable { if (!parser.getName().equals(KEY_FREEZE_TAG)) { continue; } - policy.mFreezePeriods.add(new FreezeInterval( - Integer.parseInt(parser.getAttributeValue(null, KEY_FREEZE_START)), - Integer.parseInt(parser.getAttributeValue(null, KEY_FREEZE_END)))); + policy.mFreezePeriods.add(new FreezePeriod( + MonthDay.parse(parser.getAttributeValue(null, KEY_FREEZE_START)), + MonthDay.parse(parser.getAttributeValue(null, KEY_FREEZE_END)))); } return policy; } @@ -751,10 +776,10 @@ public class SystemUpdatePolicy implements Parcelable { out.attribute(null, KEY_INSTALL_WINDOW_START, Integer.toString(mMaintenanceWindowStart)); out.attribute(null, KEY_INSTALL_WINDOW_END, Integer.toString(mMaintenanceWindowEnd)); for (int i = 0; i < mFreezePeriods.size(); i++) { - FreezeInterval interval = mFreezePeriods.get(i); + FreezePeriod interval = mFreezePeriods.get(i); out.startTag(null, KEY_FREEZE_TAG); - out.attribute(null, KEY_FREEZE_START, Integer.toString(interval.mStartDay)); - out.attribute(null, KEY_FREEZE_END, Integer.toString(interval.mEndDay)); + out.attribute(null, KEY_FREEZE_START, interval.getStart().toString()); + out.attribute(null, KEY_FREEZE_END, interval.getEnd().toString()); out.endTag(null, KEY_FREEZE_TAG); } } diff --git a/android/app/slice/Slice.java b/android/app/slice/Slice.java index bf3398ad..4336f184 100644 --- a/android/app/slice/Slice.java +++ b/android/app/slice/Slice.java @@ -21,19 +21,13 @@ import android.annotation.Nullable; import android.annotation.StringDef; import android.app.PendingIntent; import android.app.RemoteInput; -import android.content.ContentResolver; -import android.content.Context; -import android.content.IContentProvider; -import android.content.Intent; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import android.os.RemoteException; import com.android.internal.util.ArrayUtils; -import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -575,45 +569,4 @@ public final class Slice implements Parcelable { } return sb.toString(); } - - /** - * @deprecated TO BE REMOVED. - */ - @Deprecated - public static @Nullable Slice bindSlice(ContentResolver resolver, - @NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) { - Preconditions.checkNotNull(uri, "uri"); - IContentProvider provider = resolver.acquireProvider(uri); - if (provider == null) { - throw new IllegalArgumentException("Unknown URI " + uri); - } - try { - Bundle extras = new Bundle(); - extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri); - extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS, - new ArrayList<>(supportedSpecs)); - final Bundle res = provider.call(resolver.getPackageName(), SliceProvider.METHOD_SLICE, - null, extras); - Bundle.setDefusable(res, true); - if (res == null) { - return null; - } - return res.getParcelable(SliceProvider.EXTRA_SLICE); - } catch (RemoteException e) { - // Arbitrary and not worth documenting, as Activity - // Manager will kill this process shortly anyway. - return null; - } finally { - resolver.releaseProvider(provider); - } - } - - /** - * @deprecated TO BE REMOVED. - */ - @Deprecated - public static @Nullable Slice bindSlice(Context context, @NonNull Intent intent, - @NonNull List<SliceSpec> supportedSpecs) { - return context.getSystemService(SliceManager.class).bindSlice(intent, supportedSpecs); - } } diff --git a/android/app/slice/SliceManager.java b/android/app/slice/SliceManager.java index 0285e9f9..ad49437f 100644 --- a/android/app/slice/SliceManager.java +++ b/android/app/slice/SliceManager.java @@ -16,6 +16,8 @@ package android.app.slice; +import static android.content.pm.PackageManager.PERMISSION_DENIED; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; @@ -38,6 +40,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; import android.os.UserHandle; +import android.util.ArraySet; import android.util.Log; import com.android.internal.util.Preconditions; @@ -47,6 +50,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Set; /** * Class to handle interactions with {@link Slice}s. @@ -101,22 +105,6 @@ public class SliceManager { private final IBinder mToken = new Binder(); /** - * Permission denied. - * @hide - */ - public static final int PERMISSION_DENIED = -1; - /** - * Permission granted. - * @hide - */ - public static final int PERMISSION_GRANTED = 0; - /** - * Permission just granted by the user, and should be granted uri permission as well. - * @hide - */ - public static final int PERMISSION_USER_GRANTED = 1; - - /** * @hide */ public SliceManager(Context context, Handler handler) throws ServiceNotFoundException { @@ -140,7 +128,7 @@ public class SliceManager { * @see Intent#ACTION_ASSIST * @see Intent#CATEGORY_HOME */ - public void pinSlice(@NonNull Uri uri, @NonNull List<SliceSpec> specs) { + public void pinSlice(@NonNull Uri uri, @NonNull Set<SliceSpec> specs) { try { mService.pinSlice(mContext.getPackageName(), uri, specs.toArray(new SliceSpec[specs.size()]), mToken); @@ -150,6 +138,14 @@ public class SliceManager { } /** + * @deprecated TO BE REMOVED + */ + @Deprecated + public void pinSlice(@NonNull Uri uri, @NonNull List<SliceSpec> specs) { + pinSlice(uri, new ArraySet<>(specs)); + } + + /** * Remove a pin for a slice. * <p> * If the slice has no other pins/callbacks then the slice will be unpinned. @@ -189,9 +185,10 @@ public class SliceManager { * into account all clients and returns only specs supported by all. * @see SliceSpec */ - public @NonNull List<SliceSpec> getPinnedSpecs(Uri uri) { + public @NonNull Set<SliceSpec> getPinnedSpecs(Uri uri) { try { - return Arrays.asList(mService.getPinnedSpecs(uri, mContext.getPackageName())); + return new ArraySet<>(Arrays.asList(mService.getPinnedSpecs(uri, + mContext.getPackageName()))); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -240,7 +237,7 @@ public class SliceManager { * @return The Slice provided by the app or null if none is given. * @see Slice */ - public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) { + public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull Set<SliceSpec> supportedSpecs) { Preconditions.checkNotNull(uri, "uri"); ContentResolver resolver = mContext.getContentResolver(); try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) { @@ -265,6 +262,14 @@ public class SliceManager { } /** + * @deprecated TO BE REMOVED + */ + @Deprecated + public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) { + return bindSlice(uri, new ArraySet<>(supportedSpecs)); + } + + /** * Turns a slice intent into a slice uri. Expects an explicit intent. * <p> * This goes through a several stage resolution process to determine if any slice @@ -272,12 +277,12 @@ public class SliceManager { * <ol> * <li> If the intent contains data that {@link ContentResolver#getType} is * {@link SliceProvider#SLICE_TYPE} then the data will be returned.</li> - * <li>If the intent with {@link #CATEGORY_SLICE} added resolves to a provider, then - * the provider will be asked to {@link SliceProvider#onMapIntentToUri} and that result - * will be returned.</li> - * <li>Lastly, if the intent explicitly points at an activity, and that activity has + * <li>If the intent explicitly points at an activity, and that activity has * meta-data for key {@link #SLICE_METADATA_KEY}, then the Uri specified there will be * returned.</li> + * <li>Lastly, if the intent with {@link #CATEGORY_SLICE} added resolves to a provider, then + * the provider will be asked to {@link SliceProvider#onMapIntentToUri} and that result + * will be returned.</li> * <li>If no slice is found, then {@code null} is returned.</li> * </ol> * @param intent The intent associated with a slice. @@ -287,37 +292,12 @@ public class SliceManager { * @see Intent */ public @Nullable Uri mapIntentToUri(@NonNull Intent intent) { - Preconditions.checkNotNull(intent, "intent"); - Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null - || intent.getData() != null, - "Slice intent must be explicit %s", intent); ContentResolver resolver = mContext.getContentResolver(); - - // Check if the intent has data for the slice uri on it and use that - final Uri intentData = intent.getData(); - if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) { - return intentData; - } + final Uri staticUri = resolveStatic(intent, resolver); + if (staticUri != null) return staticUri; // Otherwise ask the app - Intent queryIntent = new Intent(intent); - if (!queryIntent.hasCategory(CATEGORY_SLICE)) { - queryIntent.addCategory(CATEGORY_SLICE); - } - List<ResolveInfo> providers = - mContext.getPackageManager().queryIntentContentProviders(queryIntent, 0); - if (providers == null || providers.isEmpty()) { - // There are no providers, see if this activity has a direct link. - ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent, - PackageManager.GET_META_DATA); - if (resolve != null && resolve.activityInfo != null - && resolve.activityInfo.metaData != null - && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) { - return Uri.parse( - resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY)); - } - return null; - } - String authority = providers.get(0).providerInfo.authority; + String authority = getAuthority(intent); + if (authority == null) return null; Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) .authority(authority).build(); try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) { @@ -338,10 +318,43 @@ public class SliceManager { } } + private String getAuthority(Intent intent) { + Intent queryIntent = new Intent(intent); + if (!queryIntent.hasCategory(CATEGORY_SLICE)) { + queryIntent.addCategory(CATEGORY_SLICE); + } + List<ResolveInfo> providers = + mContext.getPackageManager().queryIntentContentProviders(queryIntent, 0); + return providers != null && !providers.isEmpty() ? providers.get(0).providerInfo.authority + : null; + } + + private Uri resolveStatic(@NonNull Intent intent, ContentResolver resolver) { + Preconditions.checkNotNull(intent, "intent"); + Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null + || intent.getData() != null, + "Slice intent must be explicit %s", intent); + + // Check if the intent has data for the slice uri on it and use that + final Uri intentData = intent.getData(); + if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) { + return intentData; + } + // There are no providers, see if this activity has a direct link. + ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent, + PackageManager.GET_META_DATA); + if (resolve != null && resolve.activityInfo != null + && resolve.activityInfo.metaData != null + && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) { + return Uri.parse( + resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY)); + } + return null; + } + /** - * Turns a slice intent into slice content. Expects an explicit intent. If there is no - * {@link android.content.ContentProvider} associated with the given intent this will throw - * {@link IllegalArgumentException}. + * Turns a slice intent into slice content. Is a shortcut to perform the action + * of both {@link #mapIntentToUri(Intent)} and {@link #bindSlice(Uri, List)} at once. * * @param intent The intent associated with a slice. * @param supportedSpecs List of supported specs. @@ -351,34 +364,17 @@ public class SliceManager { * @see Intent */ public @Nullable Slice bindSlice(@NonNull Intent intent, - @NonNull List<SliceSpec> supportedSpecs) { + @NonNull Set<SliceSpec> supportedSpecs) { Preconditions.checkNotNull(intent, "intent"); Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null || intent.getData() != null, "Slice intent must be explicit %s", intent); ContentResolver resolver = mContext.getContentResolver(); - - // Check if the intent has data for the slice uri on it and use that - final Uri intentData = intent.getData(); - if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) { - return bindSlice(intentData, supportedSpecs); - } + final Uri staticUri = resolveStatic(intent, resolver); + if (staticUri != null) return bindSlice(staticUri, supportedSpecs); // Otherwise ask the app - List<ResolveInfo> providers = - mContext.getPackageManager().queryIntentContentProviders(intent, 0); - if (providers == null || providers.isEmpty()) { - // There are no providers, see if this activity has a direct link. - ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent, - PackageManager.GET_META_DATA); - if (resolve != null && resolve.activityInfo != null - && resolve.activityInfo.metaData != null - && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) { - return bindSlice(Uri.parse(resolve.activityInfo.metaData - .getString(SLICE_METADATA_KEY)), supportedSpecs); - } - return null; - } - String authority = providers.get(0).providerInfo.authority; + String authority = getAuthority(intent); + if (authority == null) return null; Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) .authority(authority).build(); try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) { @@ -387,8 +383,6 @@ public class SliceManager { } Bundle extras = new Bundle(); extras.putParcelable(SliceProvider.EXTRA_INTENT, intent); - extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS, - new ArrayList<>(supportedSpecs)); final Bundle res = provider.call(SliceProvider.METHOD_MAP_INTENT, null, extras); if (res == null) { return null; @@ -402,6 +396,16 @@ public class SliceManager { } /** + * @deprecated TO BE REMOVED. + */ + @Deprecated + @Nullable + public Slice bindSlice(@NonNull Intent intent, + @NonNull List<SliceSpec> supportedSpecs) { + return bindSlice(intent, new ArraySet<>(supportedSpecs)); + } + + /** * Determine whether a particular process and user ID has been granted * permission to access a specific slice URI. * @@ -417,9 +421,11 @@ public class SliceManager { * @see #grantSlicePermission(String, Uri) */ public @PermissionResult int checkSlicePermission(@NonNull Uri uri, int pid, int uid) { - // TODO: Switch off Uri permissions. - return mContext.checkUriPermission(uri, pid, uid, - Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + try { + return mService.checkSlicePermission(uri, null, pid, uid, null); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** @@ -431,11 +437,11 @@ public class SliceManager { * @see #revokeSlicePermission */ public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) { - // TODO: Switch off Uri permissions. - mContext.grantUriPermission(toPackage, uri, - Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); + try { + mService.grantSlicePermission(mContext.getPackageName(), toPackage, uri); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** @@ -453,11 +459,11 @@ public class SliceManager { * @see #grantSlicePermission */ public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) { - // TODO: Switch off Uri permissions. - mContext.revokeUriPermission(toPackage, uri, - Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); + try { + mService.revokeSlicePermission(mContext.getPackageName(), toPackage, uri); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** @@ -478,16 +484,6 @@ public class SliceManager { throw new SecurityException("User " + uid + " does not have slice permission for " + uri + "."); } - if (result == PERMISSION_USER_GRANTED) { - // We just had a user grant of this permission and need to grant this to the app - // permanently. - mContext.grantUriPermission(pkg, uri.buildUpon().path("").build(), - Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); - // Notify a change has happened because we just granted a permission. - mContext.getContentResolver().notifyChange(uri, null); - } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/android/app/slice/SliceProvider.java b/android/app/slice/SliceProvider.java index fe5742d6..d369272d 100644 --- a/android/app/slice/SliceProvider.java +++ b/android/app/slice/SliceProvider.java @@ -44,6 +44,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Set; /** * A SliceProvider allows an app to provide content to be displayed in system spaces. This content @@ -197,6 +198,14 @@ public abstract class SliceProvider extends ContentProvider { * @see {@link Slice}. * @see {@link Slice#HINT_PARTIAL} */ + public Slice onBindSlice(Uri sliceUri, Set<SliceSpec> supportedSpecs) { + return onBindSlice(sliceUri, new ArrayList<>(supportedSpecs)); + } + + /** + * @deprecated TO BE REMOVED + */ + @Deprecated public Slice onBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs) { return null; } diff --git a/android/app/usage/NetworkStats.java b/android/app/usage/NetworkStats.java index 7252f028..216a4a09 100644 --- a/android/app/usage/NetworkStats.java +++ b/android/app/usage/NetworkStats.java @@ -237,20 +237,26 @@ public final class NetworkStats implements AutoCloseable { DEFAULT_NETWORK_YES }) @Retention(RetentionPolicy.SOURCE) - public @interface DefaultNetwork {} + public @interface DefaultNetworkStatus {} /** - * Combined usage for this network regardless of whether it was the active default network. + * Combined usage for this network regardless of default network status. */ public static final int DEFAULT_NETWORK_ALL = -1; /** - * Usage that occurs while this network is not the active default network. + * Usage that occurs while this network is not a default network. + * + * <p>This implies that the app responsible for this usage requested that it occur on a + * specific network different from the one(s) the system would have selected for it. */ public static final int DEFAULT_NETWORK_NO = 0x1; /** - * Usage that occurs while this network is the active default network. + * Usage that occurs while this network is a default network. + * + * <p>This implies that the app either did not select a specific network for this usage, + * or it selected a network that the system could have selected for app traffic. */ public static final int DEFAULT_NETWORK_YES = 0x2; @@ -262,7 +268,7 @@ public final class NetworkStats implements AutoCloseable { private int mUid; private int mTag; private int mState; - private int mDefaultNetwork; + private int mDefaultNetworkStatus; private int mMetered; private int mRoaming; private long mBeginTimeStamp; @@ -323,8 +329,9 @@ public final class NetworkStats implements AutoCloseable { return 0; } - private static @DefaultNetwork int convertDefaultNetwork(int defaultNetwork) { - switch (defaultNetwork) { + private static @DefaultNetworkStatus int convertDefaultNetworkStatus( + int defaultNetworkStatus) { + switch (defaultNetworkStatus) { case android.net.NetworkStats.DEFAULT_NETWORK_ALL : return DEFAULT_NETWORK_ALL; case android.net.NetworkStats.DEFAULT_NETWORK_NO: return DEFAULT_NETWORK_NO; case android.net.NetworkStats.DEFAULT_NETWORK_YES: return DEFAULT_NETWORK_YES; @@ -397,18 +404,15 @@ public final class NetworkStats implements AutoCloseable { } /** - * Default network state. One of the following values:<p/> + * Default network status. One of the following values:<p/> * <ul> * <li>{@link #DEFAULT_NETWORK_ALL}</li> * <li>{@link #DEFAULT_NETWORK_NO}</li> * <li>{@link #DEFAULT_NETWORK_YES}</li> * </ul> - * <p>Indicates whether the network usage occurred on the system default network for this - * type of traffic, or whether the application chose to send this traffic on a network that - * was not the one selected by the system. */ - public @DefaultNetwork int getDefaultNetwork() { - return mDefaultNetwork; + public @DefaultNetworkStatus int getDefaultNetworkStatus() { + return mDefaultNetworkStatus; } /** @@ -605,7 +609,7 @@ public final class NetworkStats implements AutoCloseable { bucketOut.mUid = Bucket.convertUid(mRecycledSummaryEntry.uid); bucketOut.mTag = Bucket.convertTag(mRecycledSummaryEntry.tag); bucketOut.mState = Bucket.convertState(mRecycledSummaryEntry.set); - bucketOut.mDefaultNetwork = Bucket.convertDefaultNetwork( + bucketOut.mDefaultNetworkStatus = Bucket.convertDefaultNetworkStatus( mRecycledSummaryEntry.defaultNetwork); bucketOut.mMetered = Bucket.convertMetered(mRecycledSummaryEntry.metered); bucketOut.mRoaming = Bucket.convertRoaming(mRecycledSummaryEntry.roaming); @@ -657,7 +661,7 @@ public final class NetworkStats implements AutoCloseable { bucketOut.mUid = Bucket.convertUid(getUid()); bucketOut.mTag = Bucket.convertTag(mTag); bucketOut.mState = mState; - bucketOut.mDefaultNetwork = Bucket.DEFAULT_NETWORK_ALL; + bucketOut.mDefaultNetworkStatus = Bucket.DEFAULT_NETWORK_ALL; bucketOut.mMetered = Bucket.METERED_ALL; bucketOut.mRoaming = Bucket.ROAMING_ALL; bucketOut.mBeginTimeStamp = mRecycledHistoryEntry.bucketStart; diff --git a/android/app/usage/NetworkStatsManager.java b/android/app/usage/NetworkStatsManager.java index b2fe9586..0b21196f 100644 --- a/android/app/usage/NetworkStatsManager.java +++ b/android/app/usage/NetworkStatsManager.java @@ -35,6 +35,7 @@ import android.os.Messenger; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; +import android.util.DataUnit; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; @@ -95,6 +96,15 @@ public class NetworkStatsManager { /** @hide */ public static final int CALLBACK_RELEASED = 1; + /** + * Minimum data usage threshold for registering usage callbacks. + * + * Requests registered with a threshold lower than this will only be triggered once this minimum + * is reached. + * @hide + */ + public static final long MIN_THRESHOLD_BYTES = DataUnit.MEBIBYTES.toBytes(2); + private final Context mContext; private final INetworkStatsService mService; @@ -305,6 +315,8 @@ public class NetworkStatsManager { * {@link java.lang.System#currentTimeMillis}. * @param uid UID of app * @param tag TAG of interest. Use {@link NetworkStats.Bucket#TAG_NONE} for no tags. + * @param state state of interest. Use {@link NetworkStats.Bucket#STATE_ALL} to aggregate + * traffic from all states. * @return Statistics object or null if an error happened during statistics collection. * @throws SecurityException if permissions are insufficient to read network statistics. */ diff --git a/android/app/usage/TimeSparseArray.java b/android/app/usage/TimeSparseArray.java index 9ef88e41..4ec0e9e4 100644 --- a/android/app/usage/TimeSparseArray.java +++ b/android/app/usage/TimeSparseArray.java @@ -88,7 +88,7 @@ public class TimeSparseArray<E> extends LongSparseArray<E> { key++; keyIndex++; } - if (key >= origKey + 10) { + if (key >= origKey + 100) { Slog.w(TAG, "Value " + value + " supposed to be inserted at " + origKey + " displaced to " + key); } diff --git a/android/app/usage/UsageEvents.java b/android/app/usage/UsageEvents.java index 84f57a30..503ca6c3 100644 --- a/android/app/usage/UsageEvents.java +++ b/android/app/usage/UsageEvents.java @@ -111,7 +111,7 @@ public final class UsageEvents implements Parcelable { /** * An event type denoting a change in App Standby Bucket. The new bucket can be - * retrieved by calling {@link #getStandbyBucket()}. + * retrieved by calling {@link #getAppStandbyBucket()}. * * @see UsageStatsManager#getAppStandbyBucket() */ @@ -326,13 +326,23 @@ public final class UsageEvents implements Parcelable { * Returns the standby bucket of the app, if the event is of type * {@link #STANDBY_BUCKET_CHANGED}, otherwise returns 0. * @return the standby bucket associated with the event. - * + * @hide */ public int getStandbyBucket() { return (mBucketAndReason & 0xFFFF0000) >>> 16; } /** + * Returns the standby bucket of the app, if the event is of type + * {@link #STANDBY_BUCKET_CHANGED}, otherwise returns 0. + * @return the standby bucket associated with the event. + * + */ + public int getAppStandbyBucket() { + return (mBucketAndReason & 0xFFFF0000) >>> 16; + } + + /** * Returns the reason for the bucketing, if the event is of type * {@link #STANDBY_BUCKET_CHANGED}, otherwise returns 0. Reason values include * the main reason which is one of REASON_MAIN_*, OR'ed with REASON_SUB_*, if there diff --git a/android/appwidget/AppWidgetHost.java b/android/appwidget/AppWidgetHost.java index 37360bad..49cc498c 100644 --- a/android/appwidget/AppWidgetHost.java +++ b/android/appwidget/AppWidgetHost.java @@ -37,6 +37,7 @@ import android.util.SparseArray; import android.widget.RemoteViews; import android.widget.RemoteViews.OnClickHandler; +import com.android.internal.R; import com.android.internal.appwidget.IAppWidgetHost; import com.android.internal.appwidget.IAppWidgetService; @@ -171,8 +172,9 @@ public class AppWidgetHost { return; } sServiceInitialized = true; - if (!context.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_APP_WIDGETS)) { + PackageManager packageManager = context.getPackageManager(); + if (!packageManager.hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS) + && !context.getResources().getBoolean(R.bool.config_enableAppWidgetService)) { return; } IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE); diff --git a/android/bluetooth/BluetoothHearingAid.java b/android/bluetooth/BluetoothHearingAid.java index 8f8083ed..159e165d 100644 --- a/android/bluetooth/BluetoothHearingAid.java +++ b/android/bluetooth/BluetoothHearingAid.java @@ -421,29 +421,29 @@ public final class BluetoothHearingAid implements BluetoothProfile { } /** - * Check whether the device is active. + * Get the connected physical Hearing Aid devices that are active * * <p>Requires {@link android.Manifest.permission#BLUETOOTH} * permission. * - * @return the connected device that is active or null if no device - * is active + * @return the list of active devices. The first element is the left active + * device; the second element is the right active device. If either or both side + * is not active, it will be null on that position. Returns empty list on error. * @hide */ @RequiresPermission(Manifest.permission.BLUETOOTH) - public boolean isActiveDevice(@Nullable BluetoothDevice device) { - if (VDBG) log("isActiveDevice()"); + public List<BluetoothDevice> getActiveDevices() { + if (VDBG) log("getActiveDevices()"); try { mServiceLock.readLock().lock(); - if (mService != null && isEnabled() - && ((device == null) || isValidDevice(device))) { - return mService.isActiveDevice(device); + if (mService != null && isEnabled()) { + return mService.getActiveDevices(); } if (mService == null) Log.w(TAG, "Proxy not attached to service"); - return false; + return new ArrayList<>(); } catch (RemoteException e) { Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); - return false; + return new ArrayList<>(); } finally { mServiceLock.readLock().unlock(); } diff --git a/android/bluetooth/BluetoothHidDevice.java b/android/bluetooth/BluetoothHidDevice.java index af99bf7d..3bc8544e 100644 --- a/android/bluetooth/BluetoothHidDevice.java +++ b/android/bluetooth/BluetoothHidDevice.java @@ -701,6 +701,28 @@ public final class BluetoothHidDevice implements BluetoothProfile { } /** + * Gets the application name of the current HidDeviceService user. + * + * @return the current user name, or empty string if cannot get the name + * {@hide} + */ + public String getUserAppName() { + final IBluetoothHidDevice service = mService; + + if (service != null) { + try { + return service.getUserAppName(); + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + } else { + Log.w(TAG, "Proxy not attached to service"); + } + + return ""; + } + + /** * Initiates connection to host which is currently paired with this device. If the application * is not registered, #connect(BluetoothDevice) will fail. The connection state should be * tracked by the application by handling callback from Callback#onConnectionStateChanged. The diff --git a/android/content/ContentResolver.java b/android/content/ContentResolver.java index 9f3df377..f7908b69 100644 --- a/android/content/ContentResolver.java +++ b/android/content/ContentResolver.java @@ -51,7 +51,6 @@ import android.text.TextUtils; import android.util.EventLog; import android.util.Log; -import com.android.internal.util.ArrayUtils; import com.android.internal.util.MimeIconUtils; import com.android.internal.util.Preconditions; @@ -602,6 +601,8 @@ public abstract class ContentResolver { try { return provider.getType(url); } catch (RemoteException e) { + // Arbitrary and not worth documenting, as Activity + // Manager will kill this process shortly anyway. return null; } catch (java.lang.Exception e) { Log.w(TAG, "Failed to get type for: " + url + " (" + e.getMessage() + ")"); @@ -620,9 +621,7 @@ public abstract class ContentResolver { ContentProvider.getUriWithoutUserId(url), resolveUserId(url)); return type; } catch (RemoteException e) { - // Arbitrary and not worth documenting, as Activity - // Manager will kill this process shortly anyway. - return null; + throw e.rethrowFromSystemServer(); } catch (java.lang.Exception e) { Log.w(TAG, "Failed to get type for: " + url + " (" + e.getMessage() + ")"); return null; @@ -1964,6 +1963,7 @@ public abstract class ContentResolver { getContentService().registerContentObserver(uri, notifyForDescendents, observer.getContentObserver(), userHandle, mTargetSdkVersion); } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } } @@ -1982,6 +1982,7 @@ public abstract class ContentResolver { contentObserver); } } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } } @@ -2089,6 +2090,7 @@ public abstract class ContentResolver { syncToNetwork ? NOTIFY_SYNC_TO_NETWORK : 0, userHandle, mTargetSdkVersion); } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } } @@ -2105,6 +2107,7 @@ public abstract class ContentResolver { observer != null && observer.deliverSelfNotifications(), flags, userHandle, mTargetSdkVersion); } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } } @@ -2126,6 +2129,7 @@ public abstract class ContentResolver { ContentProvider.getUriWithoutUserId(uri), modeFlags, /* toPackage= */ null, resolveUserId(uri)); } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } } @@ -2141,6 +2145,7 @@ public abstract class ContentResolver { ContentProvider.getUriWithoutUserId(uri), modeFlags, toPackage, resolveUserId(uri)); } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } } @@ -2160,6 +2165,7 @@ public abstract class ContentResolver { ContentProvider.getUriWithoutUserId(uri), modeFlags, /* toPackage= */ null, resolveUserId(uri)); } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } } @@ -2178,7 +2184,7 @@ public abstract class ContentResolver { return ActivityManager.getService() .getPersistedUriPermissions(mPackageName, true).getList(); } catch (RemoteException e) { - throw new RuntimeException("Activity manager has died", e); + throw e.rethrowFromSystemServer(); } } @@ -2194,7 +2200,7 @@ public abstract class ContentResolver { return ActivityManager.getService() .getPersistedUriPermissions(mPackageName, false).getList(); } catch (RemoteException e) { - throw new RuntimeException("Activity manager has died", e); + throw e.rethrowFromSystemServer(); } } @@ -2273,7 +2279,7 @@ public abstract class ContentResolver { try { getContentService().syncAsUser(request, userId); } catch(RemoteException e) { - // Shouldn't happen. + throw e.rethrowFromSystemServer(); } } @@ -2285,7 +2291,7 @@ public abstract class ContentResolver { try { getContentService().sync(request); } catch(RemoteException e) { - // Shouldn't happen. + throw e.rethrowFromSystemServer(); } } @@ -2349,6 +2355,7 @@ public abstract class ContentResolver { try { getContentService().cancelSync(account, authority, null); } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } } @@ -2360,6 +2367,7 @@ public abstract class ContentResolver { try { getContentService().cancelSyncAsUser(account, authority, null, userId); } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } } @@ -2371,7 +2379,7 @@ public abstract class ContentResolver { try { return getContentService().getSyncAdapterTypes(); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2383,7 +2391,7 @@ public abstract class ContentResolver { try { return getContentService().getSyncAdapterTypesAsUser(userId); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2397,8 +2405,8 @@ public abstract class ContentResolver { try { return getContentService().getSyncAdapterPackagesForAuthorityAsUser(authority, userId); } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } - return ArrayUtils.emptyArray(String.class); } /** @@ -2414,7 +2422,7 @@ public abstract class ContentResolver { try { return getContentService().getSyncAutomatically(account, authority); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2427,7 +2435,7 @@ public abstract class ContentResolver { try { return getContentService().getSyncAutomaticallyAsUser(account, authority, userId); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2453,8 +2461,7 @@ public abstract class ContentResolver { try { getContentService().setSyncAutomaticallyAsUser(account, authority, sync, userId); } catch (RemoteException e) { - // exception ignored; if this is thrown then it means the runtime is in the midst of - // being restarted + throw e.rethrowFromSystemServer(); } } @@ -2500,8 +2507,7 @@ public abstract class ContentResolver { try { getContentService().addPeriodicSync(account, authority, extras, pollFrequency); } catch (RemoteException e) { - // exception ignored; if this is thrown then it means the runtime is in the midst of - // being restarted + throw e.rethrowFromSystemServer(); } } @@ -2540,7 +2546,7 @@ public abstract class ContentResolver { try { getContentService().removePeriodicSync(account, authority, extras); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2564,8 +2570,7 @@ public abstract class ContentResolver { try { getContentService().cancelRequest(request); } catch (RemoteException e) { - // exception ignored; if this is thrown then it means the runtime is in the midst of - // being restarted + throw e.rethrowFromSystemServer(); } } @@ -2582,7 +2587,7 @@ public abstract class ContentResolver { try { return getContentService().getPeriodicSyncs(account, authority, null); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2596,7 +2601,7 @@ public abstract class ContentResolver { try { return getContentService().getIsSyncable(account, authority); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2609,7 +2614,7 @@ public abstract class ContentResolver { try { return getContentService().getIsSyncableAsUser(account, authority, userId); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2623,8 +2628,7 @@ public abstract class ContentResolver { try { getContentService().setIsSyncable(account, authority, syncable); } catch (RemoteException e) { - // exception ignored; if this is thrown then it means the runtime is in the midst of - // being restarted + throw e.rethrowFromSystemServer(); } } @@ -2640,7 +2644,7 @@ public abstract class ContentResolver { try { return getContentService().getMasterSyncAutomatically(); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2652,7 +2656,7 @@ public abstract class ContentResolver { try { return getContentService().getMasterSyncAutomaticallyAsUser(userId); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2676,8 +2680,7 @@ public abstract class ContentResolver { try { getContentService().setMasterSyncAutomaticallyAsUser(sync, userId); } catch (RemoteException e) { - // exception ignored; if this is thrown then it means the runtime is in the midst of - // being restarted + throw e.rethrowFromSystemServer(); } } @@ -2701,7 +2704,7 @@ public abstract class ContentResolver { try { return getContentService().isSyncActive(account, authority, null); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2727,7 +2730,7 @@ public abstract class ContentResolver { } return syncs.get(0); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2744,7 +2747,7 @@ public abstract class ContentResolver { try { return getContentService().getCurrentSyncs(); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2756,7 +2759,7 @@ public abstract class ContentResolver { try { return getContentService().getCurrentSyncsAsUser(userId); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2771,7 +2774,7 @@ public abstract class ContentResolver { try { return getContentService().getSyncStatus(account, authority, null); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2784,7 +2787,7 @@ public abstract class ContentResolver { try { return getContentService().getSyncStatusAsUser(account, authority, null, userId); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2809,7 +2812,7 @@ public abstract class ContentResolver { try { return getContentService().isSyncPendingAsUser(account, authority, null, userId); } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2841,7 +2844,7 @@ public abstract class ContentResolver { getContentService().addStatusChangeListener(mask, observer); return observer; } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); + throw e.rethrowFromSystemServer(); } } @@ -2856,8 +2859,7 @@ public abstract class ContentResolver { try { getContentService().removeStatusChangeListener((ISyncStatusObserver.Stub) handle); } catch (RemoteException e) { - // exception ignored; if this is thrown then it means the runtime is in the midst of - // being restarted + throw e.rethrowFromSystemServer(); } } @@ -3027,9 +3029,7 @@ public abstract class ContentResolver { return sContentService; } IBinder b = ServiceManager.getService(CONTENT_SERVICE_NAME); - if (false) Log.v("ContentService", "default service binder = " + b); sContentService = IContentService.Stub.asInterface(b); - if (false) Log.v("ContentService", "default service = " + sContentService); return sContentService; } @@ -3038,7 +3038,7 @@ public abstract class ContentResolver { return mPackageName; } - private static IContentService sContentService; + private static volatile IContentService sContentService; private final Context mContext; final String mPackageName; diff --git a/android/content/Context.java b/android/content/Context.java index 920056a8..ede7ee4b 100644 --- a/android/content/Context.java +++ b/android/content/Context.java @@ -3780,7 +3780,7 @@ public abstract class Context { public static final String DROPBOX_SERVICE = "dropbox"; /** - * System service name for the DeviceIdleController. There is no Java API for this. + * System service name for the DeviceIdleManager. * @see #getSystemService(String) * @hide */ diff --git a/android/content/Intent.java b/android/content/Intent.java index 000912cd..f608fcb1 100644 --- a/android/content/Intent.java +++ b/android/content/Intent.java @@ -40,6 +40,7 @@ import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; +import android.os.PersistableBundle; import android.os.Process; import android.os.ResultReceiver; import android.os.ShellCommand; @@ -1814,8 +1815,12 @@ public class Intent implements Parcelable, Cloneable { public static final String EXTRA_PACKAGE_NAME = "android.intent.extra.PACKAGE_NAME"; /** - * Intent extra: A {@link Bundle} of extras for a package being suspended. Will be sent with - * {@link #ACTION_MY_PACKAGE_SUSPENDED}. + * Intent extra: A {@link Bundle} of extras for a package being suspended. Will be sent as an + * extra with {@link #ACTION_MY_PACKAGE_SUSPENDED}. + * + * <p>The contents of this {@link Bundle} are a contract between the suspended app and the + * suspending app, i.e. any app with the permission {@code android.permission.SUSPEND_APPS}. + * This is meant to enable the suspended app to better handle the state of being suspended. * * @see #ACTION_MY_PACKAGE_SUSPENDED * @see #ACTION_MY_PACKAGE_UNSUSPENDED @@ -2282,6 +2287,34 @@ public class Intent implements Parcelable, Cloneable { public static final String ACTION_MY_PACKAGE_SUSPENDED = "android.intent.action.MY_PACKAGE_SUSPENDED"; /** + * Activity Action: Started to show more details about why an application was suspended. + * + * <p>Whenever the system detects an activity launch for a suspended app, it shows a dialog to + * the user to inform them of the state and present them an affordance to start this activity + * action to show more details about the reason for suspension. + * + * <p>Apps holding {@link android.Manifest.permission#SUSPEND_APPS} must declare an activity + * handling this intent and protect it with + * {@link android.Manifest.permission#SEND_SHOW_SUSPENDED_APP_DETAILS}. + * + * <p>Includes an extra {@link #EXTRA_PACKAGE_NAME} which is the name of the suspended package. + * + * <p class="note">This is a protected intent that can only be sent + * by the system. + * + * @see PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle, + * PersistableBundle, String) + * @see PackageManager#isPackageSuspended() + * @see #ACTION_PACKAGES_SUSPENDED + * + * @hide + */ + @SystemApi + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SHOW_SUSPENDED_APP_DETAILS = + "android.intent.action.SHOW_SUSPENDED_APP_DETAILS"; + + /** * Broadcast Action: Sent to a package that has been unsuspended. * * <p class="note">This is a protected intent that can only be sent @@ -6788,6 +6821,9 @@ public class Intent implements Parcelable, Cloneable { case "--activity-task-on-home": intent.addFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME); break; + case "--activity-match-external": + intent.addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL); + break; case "--receiver-registered-only": intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); break; @@ -6924,7 +6960,7 @@ public class Intent implements Parcelable, Cloneable { " [--activity-no-user-action] [--activity-previous-is-top]", " [--activity-reorder-to-front] [--activity-reset-task-if-needed]", " [--activity-single-top] [--activity-clear-task]", - " [--activity-task-on-home]", + " [--activity-task-on-home] [--activity-match-external]", " [--receiver-registered-only] [--receiver-replace-pending]", " [--receiver-foreground] [--receiver-no-abort]", " [--receiver-include-background]", diff --git a/android/content/QuickViewConstants.java b/android/content/QuickViewConstants.java index a25513de..132d43f2 100644 --- a/android/content/QuickViewConstants.java +++ b/android/content/QuickViewConstants.java @@ -45,8 +45,8 @@ public class QuickViewConstants { * Feature to delete an individual document. Quick viewer implementations must use * Storage Access Framework to both verify delete permission and to delete content. * - * @see DocumentsContract#Document#FLAG_SUPPORTS_DELETE - * @see DocumentsContract#deleteDocument(ContentResolver resolver, Uri documentUri) + * @see DocumentsContract.Document#FLAG_SUPPORTS_DELETE + * @see DocumentsContract#deleteDocument(ContentResolver, Uri) */ public static final String FEATURE_DELETE = "android:delete"; diff --git a/android/content/pm/ApplicationInfo.java b/android/content/pm/ApplicationInfo.java index e85058df..d65e051b 100644 --- a/android/content/pm/ApplicationInfo.java +++ b/android/content/pm/ApplicationInfo.java @@ -26,6 +26,7 @@ import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Environment; import android.os.Parcel; import android.os.Parcelable; @@ -590,26 +591,33 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { public static final int PRIVATE_FLAG_VIRTUAL_PRELOAD = 1 << 16; /** - * Value for {@linl #privateFlags}: whether this app is pre-installed on the + * Value for {@link #privateFlags}: whether this app is pre-installed on the * OEM partition of the system image. * @hide */ public static final int PRIVATE_FLAG_OEM = 1 << 17; /** - * Value for {@linl #privateFlags}: whether this app is pre-installed on the + * Value for {@link #privateFlags}: whether this app is pre-installed on the * vendor partition of the system image. * @hide */ public static final int PRIVATE_FLAG_VENDOR = 1 << 18; /** - * Value for {@linl #privateFlags}: whether this app is pre-installed on the + * Value for {@link #privateFlags}: whether this app is pre-installed on the * product partition of the system image. * @hide */ public static final int PRIVATE_FLAG_PRODUCT = 1 << 19; + /** + * Value for {@link #privateFlags}: whether this app is signed with the + * platform key. + * @hide + */ + public static final int PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY = 1 << 20; + /** @hide */ @IntDef(flag = true, prefix = { "PRIVATE_FLAG_" }, value = { PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE, @@ -629,6 +637,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { PRIVATE_FLAG_PRIVILEGED, PRIVATE_FLAG_PRODUCT, PRIVATE_FLAG_REQUIRED_FOR_SYSTEM_USER, + PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY, PRIVATE_FLAG_STATIC_SHARED_LIBRARY, PRIVATE_FLAG_VENDOR, PRIVATE_FLAG_VIRTUAL_PRELOAD, @@ -904,7 +913,17 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * The app's declared version code. * @hide */ - public long versionCode; + public long longVersionCode; + + /** + * An integer representation of the app's declared version code. This is being left in place as + * some apps were using reflection to access it before the move to long in + * {@link android.os.Build.VERSION_CODES#P} + * @deprecated Use {@link #longVersionCode} instead. + * @hide + */ + @Deprecated + public int versionCode; /** * The user-visible SDK version (ex. 26) of the framework against which the application claims @@ -1114,11 +1133,12 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { */ public static final int HIDDEN_API_ENFORCEMENT_NONE = 0; /** - * Light grey list enforcement, the strictest option. Enforces the light grey, dark grey and - * black lists. + * No API enforcement, but enable the detection logic and warnings. Observed behaviour is the + * same as {@link #HIDDEN_API_ENFORCEMENT_NONE} but you may see warnings in the log when APIs + * are accessed. * @hide * */ - public static final int HIDDEN_API_ENFORCEMENT_ALL_LISTS = 1; + public static final int HIDDEN_API_ENFORCEMENT_JUST_WARN = 1; /** * Dark grey list enforcement. Enforces the dark grey and black lists * @hide @@ -1140,14 +1160,15 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { @IntDef(prefix = { "HIDDEN_API_ENFORCEMENT_" }, value = { HIDDEN_API_ENFORCEMENT_DEFAULT, HIDDEN_API_ENFORCEMENT_NONE, - HIDDEN_API_ENFORCEMENT_ALL_LISTS, + HIDDEN_API_ENFORCEMENT_JUST_WARN, HIDDEN_API_ENFORCEMENT_DARK_GREY_AND_BLACK, HIDDEN_API_ENFORCEMENT_BLACK, }) @Retention(RetentionPolicy.SOURCE) public @interface HiddenApiEnforcementPolicy {} - private boolean isValidHiddenApiEnforcementPolicy(int policy) { + /** @hide */ + public static boolean isValidHiddenApiEnforcementPolicy(int policy) { return policy >= HIDDEN_API_ENFORCEMENT_DEFAULT && policy <= HIDDEN_API_ENFORCEMENT_MAX; } @@ -1214,7 +1235,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { pw.println(prefix + "enabled=" + enabled + " minSdkVersion=" + minSdkVersion + " targetSdkVersion=" + targetSdkVersion - + " versionCode=" + versionCode + + " versionCode=" + longVersionCode + " targetSandboxVersion=" + targetSandboxVersion); if ((dumpFlags & DUMP_FLAG_DETAILS) != 0) { if (manageSpaceActivityName != null) { @@ -1287,7 +1308,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { proto.write(ApplicationInfoProto.Version.ENABLED, enabled); proto.write(ApplicationInfoProto.Version.MIN_SDK_VERSION, minSdkVersion); proto.write(ApplicationInfoProto.Version.TARGET_SDK_VERSION, targetSdkVersion); - proto.write(ApplicationInfoProto.Version.VERSION_CODE, versionCode); + proto.write(ApplicationInfoProto.Version.VERSION_CODE, longVersionCode); proto.write(ApplicationInfoProto.Version.TARGET_SANDBOX_VERSION, targetSandboxVersion); proto.end(versionToken); @@ -1421,7 +1442,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { uid = orig.uid; minSdkVersion = orig.minSdkVersion; targetSdkVersion = orig.targetSdkVersion; - versionCode = orig.versionCode; + setVersionCode(orig.longVersionCode); enabled = orig.enabled; enabledSetting = orig.enabledSetting; installLocation = orig.installLocation; @@ -1495,7 +1516,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeInt(uid); dest.writeInt(minSdkVersion); dest.writeInt(targetSdkVersion); - dest.writeLong(versionCode); + dest.writeLong(longVersionCode); dest.writeInt(enabled ? 1 : 0); dest.writeInt(enabledSetting); dest.writeInt(installLocation); @@ -1566,7 +1587,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { uid = source.readInt(); minSdkVersion = source.readInt(); targetSdkVersion = source.readInt(); - versionCode = source.readLong(); + setVersionCode(source.readLong()); enabled = source.readInt() != 0; enabledSetting = source.readInt(); installLocation = source.readInt(); @@ -1658,17 +1679,26 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { return SystemConfig.getInstance().getHiddenApiWhitelistedApps().contains(packageName); } + private boolean isAllowedToUseHiddenApis() { + return isSignedWithPlatformKey() + || (isPackageWhitelistedForHiddenApis() && (isSystemApp() || isUpdatedSystemApp())); + } + /** * @hide */ public @HiddenApiEnforcementPolicy int getHiddenApiEnforcementPolicy() { + if (isAllowedToUseHiddenApis()) { + return HIDDEN_API_ENFORCEMENT_NONE; + } if (mHiddenApiPolicy != HIDDEN_API_ENFORCEMENT_DEFAULT) { return mHiddenApiPolicy; } - if (isPackageWhitelistedForHiddenApis() && (isSystemApp() || isUpdatedSystemApp())) { - return HIDDEN_API_ENFORCEMENT_NONE; + if (targetSdkVersion < Build.VERSION_CODES.P) { + return HIDDEN_API_ENFORCEMENT_BLACK; + } else { + return HIDDEN_API_ENFORCEMENT_DARK_GREY_AND_BLACK; } - return HIDDEN_API_ENFORCEMENT_BLACK; } /** @@ -1682,6 +1712,39 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { } /** + * Updates the hidden API enforcement policy for this app from the given values, if appropriate. + * + * This will have no effect if this app is not subject to hidden API enforcement, i.e. if it + * is on the package whitelist. + * + * @param policyPreP configured policy for pre-P apps, or {@link + * #HIDDEN_API_ENFORCEMENT_DEFAULT} if nothing configured. + * @param policyP configured policy for apps targeting P or later, or {@link + * #HIDDEN_API_ENFORCEMENT_DEFAULT} if nothing configured. + * @hide + */ + public void maybeUpdateHiddenApiEnforcementPolicy( + @HiddenApiEnforcementPolicy int policyPreP, @HiddenApiEnforcementPolicy int policyP) { + if (isPackageWhitelistedForHiddenApis()) { + return; + } + if (targetSdkVersion < Build.VERSION_CODES.P) { + setHiddenApiEnforcementPolicy(policyPreP); + } else if (targetSdkVersion >= Build.VERSION_CODES.P) { + setHiddenApiEnforcementPolicy(policyP); + } + + } + + /** + * @hide + */ + public void setVersionCode(long newVersionCode) { + longVersionCode = newVersionCode; + versionCode = (int) newVersionCode; + } + + /** * @hide */ @Override @@ -1758,6 +1821,11 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { } /** @hide */ + public boolean isSignedWithPlatformKey() { + return (privateFlags & ApplicationInfo.PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY) != 0; + } + + /** @hide */ @TestApi public boolean isPrivilegedApp() { return (privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0; diff --git a/android/content/pm/LauncherApps.java b/android/content/pm/LauncherApps.java index 9aace2e7..8223363a 100644 --- a/android/content/pm/LauncherApps.java +++ b/android/content/pm/LauncherApps.java @@ -212,7 +212,7 @@ public class LauncherApps { * an applicaton. * * <p>Note: On devices running {@link android.os.Build.VERSION_CODES#P Android P} or higher, - * any apps that override {@link #onPackagesSuspended(String[], Bundle, UserHandle)} will + * any apps that override {@link #onPackagesSuspended(String[], UserHandle, Bundle)} will * not receive this callback. * * @param packageNames The names of the packages that have just been @@ -226,15 +226,20 @@ public class LauncherApps { * Indicates that one or more packages have been suspended. A device administrator or an app * with {@code android.permission.SUSPEND_APPS} can do this. * + * <p>A suspending app with the permission {@code android.permission.SUSPEND_APPS} can + * optionally provide a {@link Bundle} of extra information that it deems helpful for the + * launcher to handle the suspended state of these packages. The contents of this + * {@link Bundle} supposed to be a contract between the suspending app and the launcher. + * * @param packageNames The names of the packages that have just been suspended. - * @param launcherExtras A {@link Bundle} of extras for the launcher. * @param user the user for which the given packages were suspended. - * + * @param launcherExtras A {@link Bundle} of extras for the launcher, if provided to the + * system, {@code null} otherwise. * @see PackageManager#isPackageSuspended() * @see #getSuspendedPackageLauncherExtras(String, UserHandle) */ - public void onPackagesSuspended(String[] packageNames, @Nullable Bundle launcherExtras, - UserHandle user) { + public void onPackagesSuspended(String[] packageNames, UserHandle user, + @Nullable Bundle launcherExtras) { onPackagesSuspended(packageNames, user); } @@ -662,6 +667,9 @@ public class LauncherApps { * {@code PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle, * PersistableBundle, String)}. * + * <p>The contents of this {@link Bundle} are supposed to be a contract between the suspending + * app and the launcher. + * * <p>Note: This just returns whatever extras were provided to the system, <em>which might * even be {@code null}.</em> * @@ -670,7 +678,7 @@ public class LauncherApps { * @return A {@link Bundle} of launcher extras. Or {@code null} if the package is not currently * suspended. * - * @see Callback#onPackagesSuspended(String[], Bundle, UserHandle) + * @see Callback#onPackagesSuspended(String[], UserHandle, Bundle) * @see PackageManager#isPackageSuspended() */ public @Nullable Bundle getSuspendedPackageLauncherExtras(String packageName, UserHandle user) { @@ -1298,8 +1306,8 @@ public class LauncherApps { mCallback.onPackagesUnavailable(info.packageNames, info.user, info.replacing); break; case MSG_SUSPENDED: - mCallback.onPackagesSuspended(info.packageNames, info.launcherExtras, - info.user); + mCallback.onPackagesSuspended(info.packageNames, info.user, info.launcherExtras + ); break; case MSG_UNSUSPENDED: mCallback.onPackagesUnsuspended(info.packageNames, info.user); diff --git a/android/content/pm/PackageInfo.java b/android/content/pm/PackageInfo.java index 627ceb78..5f9f8f1f 100644 --- a/android/content/pm/PackageInfo.java +++ b/android/content/pm/PackageInfo.java @@ -244,7 +244,7 @@ public class PackageInfo implements Parcelable { * the first position to be the same across updates. * * <strong>Deprecated</strong> This has been replaced by the - * {@link PackageInfo#signingCertificateHistory} field, which takes into + * {@link PackageInfo#signingInfo} field, which takes into * account signing certificate rotation. For backwards compatibility in * the event of signing certificate rotation, this will return the oldest * reported signing certificate, so that an application will appear to @@ -256,29 +256,15 @@ public class PackageInfo implements Parcelable { public Signature[] signatures; /** - * Array of all signatures arrays read from the package file, potentially + * Signing information read from the package file, potentially * including past signing certificates no longer used after signing - * certificate rotation. Though signing certificate rotation is only - * available for apps with a single signing certificate, this provides an - * array of arrays so that packages signed with multiple signing - * certificates can still return all signers. This is only filled in if + * certificate rotation. This is only filled in if * the flag {@link PackageManager#GET_SIGNING_CERTIFICATES} was set. * - * A package must be singed with at least one certificate, which is at - * position zero in the array. An application may be signed by multiple - * certificates, which would be in the array at position zero in an - * indeterminate order. A package may also have a history of certificates - * due to signing certificate rotation. In this case, the array will be - * populated by a series of single-entry arrays corresponding to a signing - * certificate of the package. - * - * <strong>Note:</strong> Signature ordering is not guaranteed to be - * stable which means that a package signed with certificates A and B is - * equivalent to being signed with certificates B and A. This means that - * in case multiple signatures are reported you cannot assume the one at - * the first position will be the same across updates. + * Use this field instead of the deprecated {@code signatures} field. + * See {@link SigningInfo} for more information on its contents. */ - public Signature[][] signingCertificateHistory; + public SigningInfo signingInfo; /** * Application specified preferred configuration @@ -476,17 +462,11 @@ public class PackageInfo implements Parcelable { dest.writeBoolean(mOverlayIsStatic); dest.writeInt(compileSdkVersion); dest.writeString(compileSdkVersionCodename); - writeSigningCertificateHistoryToParcel(dest, parcelableFlags); - } - - private void writeSigningCertificateHistoryToParcel(Parcel dest, int parcelableFlags) { - if (signingCertificateHistory != null) { - dest.writeInt(signingCertificateHistory.length); - for (int i = 0; i < signingCertificateHistory.length; i++) { - dest.writeTypedArray(signingCertificateHistory[i], parcelableFlags); - } + if (signingInfo != null) { + dest.writeInt(1); + signingInfo.writeToParcel(dest, parcelableFlags); } else { - dest.writeInt(-1); + dest.writeInt(0); } } @@ -544,7 +524,10 @@ public class PackageInfo implements Parcelable { mOverlayIsStatic = source.readBoolean(); compileSdkVersion = source.readInt(); compileSdkVersionCodename = source.readString(); - readSigningCertificateHistoryFromParcel(source); + int hasSigningInfo = source.readInt(); + if (hasSigningInfo != 0) { + signingInfo = SigningInfo.CREATOR.createFromParcel(source); + } // The component lists were flattened with the redundant ApplicationInfo // instances omitted. Distribute the canonical one here as appropriate. @@ -556,16 +539,6 @@ public class PackageInfo implements Parcelable { } } - private void readSigningCertificateHistoryFromParcel(Parcel source) { - int len = source.readInt(); - if (len != -1) { - signingCertificateHistory = new Signature[len][]; - for (int i = 0; i < len; i++) { - signingCertificateHistory[i] = source.createTypedArray(Signature.CREATOR); - } - } - } - private void propagateApplicationInfo(ApplicationInfo appInfo, ComponentInfo[] components) { if (components != null) { for (ComponentInfo ci : components) { diff --git a/android/content/pm/PackageManager.java b/android/content/pm/PackageManager.java index 491f0af2..9d3b53f2 100644 --- a/android/content/pm/PackageManager.java +++ b/android/content/pm/PackageManager.java @@ -2629,6 +2629,17 @@ public abstract class PackageManager { "android.hardware.strongbox_keystore"; /** + * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: + * The device has a Keymaster implementation that supports Device ID attestation. + * + * @see DevicePolicyManager#isDeviceIdAttestationSupported + * @hide + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_DEVICE_ID_ATTESTATION = + "android.software.device_id_attestation"; + + /** * Action to external storage service to clean out removed apps. * @hide */ @@ -3941,6 +3952,7 @@ public abstract class PackageManager { * * @hide */ + @TestApi public abstract @NonNull String getServicesSystemSharedLibraryPackageName(); /** @@ -3950,6 +3962,7 @@ public abstract class PackageManager { * * @hide */ + @TestApi public abstract @NonNull String getSharedSystemSharedLibraryPackageName(); /** @@ -5558,7 +5571,8 @@ public abstract class PackageManager { * @param packageName The name of the package to get the suspended status of. * @param userId The user id. * @return {@code true} if the package is suspended or {@code false} if the package is not - * suspended or could not be found. + * suspended. + * @throws IllegalArgumentException if the package was not found. * @hide */ public abstract boolean isPackageSuspendedForUser(String packageName, int userId); @@ -5567,12 +5581,13 @@ public abstract class PackageManager { * Query if an app is currently suspended. * * @return {@code true} if the given package is suspended, {@code false} otherwise + * @throws NameNotFoundException if the package could not be found. * * @see #setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle, String) * @hide */ @SystemApi - public boolean isPackageSuspended(String packageName) { + public boolean isPackageSuspended(String packageName) throws NameNotFoundException { throw new UnsupportedOperationException("isPackageSuspended not implemented"); } @@ -5603,51 +5618,16 @@ public abstract class PackageManager { } /** - * Retrieve the {@link PersistableBundle} that was passed as {@code appExtras} when the given - * package was suspended. - * - * <p> The caller must hold permission {@link Manifest.permission#SUSPEND_APPS} to use this - * api.</p> - * - * @param packageName The package to retrieve extras for. - * @return The {@code appExtras} for the suspended package. - * - * @see #setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle, String) - * @hide - */ - @SystemApi - @RequiresPermission(Manifest.permission.SUSPEND_APPS) - public @Nullable PersistableBundle getSuspendedPackageAppExtras(String packageName) { - throw new UnsupportedOperationException("getSuspendedPackageAppExtras not implemented"); - } - - /** - * Set the app extras for a suspended package. This method can be used to update the appExtras - * for a package that was earlier suspended using - * {@link #setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle, - * String)} - * Does nothing if the given package is not already in a suspended state. + * Returns a {@link Bundle} of extras that was meant to be sent to the calling app when it was + * suspended. An app with the permission {@code android.permission.SUSPEND_APPS} can supply this + * to the system at the time of suspending an app. * - * @param packageName The package for which the appExtras need to be updated - * @param appExtras The new appExtras for the given package + * <p>This is the same {@link Bundle} that is sent along with the broadcast + * {@link Intent#ACTION_MY_PACKAGE_SUSPENDED}, whenever the app is suspended. The contents of + * this {@link Bundle} are a contract between the suspended app and the suspending app. * - * @see #setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle, String) - * - * @hide - */ - @SystemApi - @RequiresPermission(Manifest.permission.SUSPEND_APPS) - public void setSuspendedPackageAppExtras(String packageName, - @Nullable PersistableBundle appExtras) { - throw new UnsupportedOperationException("setSuspendedPackageAppExtras not implemented"); - } - - /** - * Returns any extra information supplied as {@code appExtras} to the system when the calling - * app was suspended. - * - * <p>Note: If no extras were supplied to the system, this method will return {@code null}, even - * when the calling app has been suspended.</p> + * <p>Note: These extras are optional, so if no extras were supplied to the system, this method + * will return {@code null}, even when the calling app has been suspended. * * @return A {@link Bundle} containing the extras for the app, or {@code null} if the * package is not currently suspended. @@ -5655,6 +5635,7 @@ public abstract class PackageManager { * @see #isPackageSuspended() * @see Intent#ACTION_MY_PACKAGE_UNSUSPENDED * @see Intent#ACTION_MY_PACKAGE_SUSPENDED + * @see Intent#EXTRA_SUSPENDED_PACKAGE_EXTRAS */ public @Nullable Bundle getSuspendedPackageAppExtras() { throw new UnsupportedOperationException("getSuspendedPackageAppExtras not implemented"); @@ -6118,7 +6099,9 @@ public abstract class PackageManager { * signed. This should be used instead of {@code getPackageInfo} with {@code GET_SIGNATURES} * since it takes into account the possibility of signing certificate rotation, except in the * case of packages that are signed by multiple certificates, for which signing certificate - * rotation is not supported. + * rotation is not supported. This method is analogous to using {@code getPackageInfo} with + * {@code GET_SIGNING_CERTIFICATES} and then searching through the resulting {@code + * signingCertificateHistory} field to see if the desired certificate is present. * * @param packageName package whose signing certificates to check * @param certificate signing certificate for which to search @@ -6132,13 +6115,19 @@ public abstract class PackageManager { } /** - * Searches the set of signing certificates by which the given uid has proven to have been - * signed. This should be used instead of {@code getPackageInfo} with {@code GET_SIGNATURES} + * Searches the set of signing certificates by which the package(s) for the given uid has proven + * to have been signed. For multiple packages sharing the same uid, this will return the + * signing certificates found in the signing history of the "newest" package, where "newest" + * indicates the package with the newest signing certificate in the shared uid group. This + * method should be used instead of {@code getPackageInfo} with {@code GET_SIGNATURES} * since it takes into account the possibility of signing certificate rotation, except in the * case of packages that are signed by multiple certificates, for which signing certificate - * rotation is not supported. + * rotation is not supported. This method is analogous to using {@code getPackagesForUid} + * followed by {@code getPackageInfo} with {@code GET_SIGNING_CERTIFICATES}, selecting the + * {@code PackageInfo} of the newest-signed bpackage , and finally searching through the + * resulting {@code signingCertificateHistory} field to see if the desired certificate is there. * - * @param uid package whose signing certificates to check + * @param uid uid whose signing certificates to check * @param certificate signing certificate for which to search * @param type representation of the {@code certificate} * @return true if this package was or is signed by exactly the certificate {@code certificate} diff --git a/android/content/pm/PackageManagerInternal.java b/android/content/pm/PackageManagerInternal.java index c9b78c08..a9d09110 100644 --- a/android/content/pm/PackageManagerInternal.java +++ b/android/content/pm/PackageManagerInternal.java @@ -191,10 +191,10 @@ public abstract class PackageManagerInternal { /** * Retrieve launcher extras for a suspended package provided to the system in * {@link PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle, - * PersistableBundle, String)} + * PersistableBundle, String)}. * * @param packageName The package for which to return launcher extras. - * @param userId The user for which to check, + * @param userId The user for which to check. * @return The launcher extras. * * @see PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle, @@ -205,6 +205,38 @@ public abstract class PackageManagerInternal { int userId); /** + * Internal api to query the suspended state of a package. + * @param packageName The package to check. + * @param userId The user id to check for. + * @return {@code true} if the package is suspended, {@code false} otherwise. + * @see PackageManager#isPackageSuspended(String) + */ + public abstract boolean isPackageSuspended(String packageName, int userId); + + /** + * Get the name of the package that suspended the given package. Packages can be suspended by + * device administrators or apps holding {@link android.Manifest.permission#MANAGE_USERS} or + * {@link android.Manifest.permission#SUSPEND_APPS}. + * + * @param suspendedPackage The package that has been suspended. + * @param userId The user for which to check. + * @return Name of the package that suspended the given package. Returns {@code null} if the + * given package is not currently suspended and the platform package name - i.e. + * {@code "android"} - if the package was suspended by a device admin. + */ + public abstract String getSuspendingPackage(String suspendedPackage, int userId); + + /** + * Get the dialog message to be shown to the user when they try to launch a suspended + * application. + * + * @param suspendedPackage The package that has been suspended. + * @param userId The user for which to check. + * @return The dialog message to be shown to the user. + */ + public abstract String getSuspendedDialogMessage(String suspendedPackage, int userId); + + /** * Do a straight uid lookup for the given package/application in the given user. * @see PackageManager#getPackageUidAsUser(String, int, int) * @return The app's uid, or < 0 if the package was not found in that user @@ -429,7 +461,7 @@ public abstract class PackageManagerInternal { * Resolves an activity intent, allowing instant apps to be resolved. */ public abstract ResolveInfo resolveIntent(Intent intent, String resolvedType, - int flags, int userId, boolean resolveForStart); + int flags, int userId, boolean resolveForStart, int filterCallingUid); /** * Resolves a service intent, allowing instant apps to be resolved. diff --git a/android/content/pm/PackageParser.java b/android/content/pm/PackageParser.java index 2f0faf25..453a74aa 100644 --- a/android/content/pm/PackageParser.java +++ b/android/content/pm/PackageParser.java @@ -810,21 +810,11 @@ public class PackageParser { // replacement for GET_SIGNATURES if ((flags & PackageManager.GET_SIGNING_CERTIFICATES) != 0) { - if (p.mSigningDetails.hasPastSigningCertificates()) { - // Package has included signing certificate rotation information. Convert each - // entry to an array - int numberOfSigs = p.mSigningDetails.pastSigningCertificates.length; - pi.signingCertificateHistory = new Signature[numberOfSigs][]; - for (int i = 0; i < numberOfSigs; i++) { - pi.signingCertificateHistory[i] = - new Signature[] { p.mSigningDetails.pastSigningCertificates[i] }; - } - } else if (p.mSigningDetails.hasSignatures()) { - // otherwise keep old behavior - int numberOfSigs = p.mSigningDetails.signatures.length; - pi.signingCertificateHistory = new Signature[1][numberOfSigs]; - System.arraycopy(p.mSigningDetails.signatures, 0, - pi.signingCertificateHistory[0], 0, numberOfSigs); + if (p.mSigningDetails != SigningDetails.UNKNOWN) { + // only return a valid SigningInfo if there is signing information to report + pi.signingInfo = new SigningInfo(p.mSigningDetails); + } else { + pi.signingInfo = null; } } return pi; @@ -1918,7 +1908,7 @@ public class PackageParser { com.android.internal.R.styleable.AndroidManifest_versionCode, 0); pkg.mVersionCodeMajor = sa.getInteger( com.android.internal.R.styleable.AndroidManifest_versionCodeMajor, 0); - pkg.applicationInfo.versionCode = pkg.getLongVersionCode(); + pkg.applicationInfo.setVersionCode(pkg.getLongVersionCode()); pkg.baseRevisionCode = sa.getInteger( com.android.internal.R.styleable.AndroidManifest_revisionCode, 0); pkg.mVersionName = sa.getNonConfigurationString( @@ -2726,7 +2716,7 @@ public class PackageParser { // Fot apps targeting O-MR1 we require explicit enumeration of all certs. String[] additionalCertSha256Digests = EmptyArray.STRING; - if (pkg.applicationInfo.targetSdkVersion > Build.VERSION_CODES.O) { + if (pkg.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.O_MR1) { additionalCertSha256Digests = parseAdditionalCertificates(res, parser, outError); if (additionalCertSha256Digests == null) { return false; diff --git a/android/content/pm/PackageSharedLibraryUpdater.java b/android/content/pm/PackageSharedLibraryUpdater.java index fa894320..b14b321a 100644 --- a/android/content/pm/PackageSharedLibraryUpdater.java +++ b/android/content/pm/PackageSharedLibraryUpdater.java @@ -62,7 +62,7 @@ public abstract class PackageSharedLibraryUpdater { static boolean apkTargetsApiLevelLessThanOrEqualToOMR1(PackageParser.Package pkg) { int targetSdkVersion = pkg.applicationInfo.targetSdkVersion; - return targetSdkVersion <= Build.VERSION_CODES.O_MR1; + return targetSdkVersion < Build.VERSION_CODES.P; } /** diff --git a/android/content/pm/PackageUserState.java b/android/content/pm/PackageUserState.java index f7b6e091..f471a1d9 100644 --- a/android/content/pm/PackageUserState.java +++ b/android/content/pm/PackageUserState.java @@ -34,6 +34,7 @@ import android.util.ArraySet; import com.android.internal.util.ArrayUtils; import java.util.Arrays; +import java.util.Objects; /** * Per-user state information about a package. @@ -47,6 +48,7 @@ public class PackageUserState { public boolean hidden; // Is the app restricted by owner / admin public boolean suspended; public String suspendingPackage; + public String dialogMessage; // Message to show when a suspended package launch attempt is made public PersistableBundle suspendedAppExtras; public PersistableBundle suspendedLauncherExtras; public boolean instantApp; @@ -82,6 +84,7 @@ public class PackageUserState { hidden = o.hidden; suspended = o.suspended; suspendingPackage = o.suspendingPackage; + dialogMessage = o.dialogMessage; suspendedAppExtras = o.suspendedAppExtras; suspendedLauncherExtras = o.suspendedLauncherExtras; instantApp = o.instantApp; @@ -208,6 +211,9 @@ public class PackageUserState { || !suspendingPackage.equals(oldState.suspendingPackage)) { return false; } + if (!Objects.equals(dialogMessage, oldState.dialogMessage)) { + return false; + } if (!BaseBundle.kindofEquals(suspendedAppExtras, oldState.suspendedAppExtras)) { return false; diff --git a/android/content/pm/SigningInfo.java b/android/content/pm/SigningInfo.java new file mode 100644 index 00000000..ef874035 --- /dev/null +++ b/android/content/pm/SigningInfo.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2018 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 android.content.pm; + + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Information pertaining to the signing certificates used to sign a package. + */ +public final class SigningInfo implements Parcelable { + + @NonNull + private final PackageParser.SigningDetails mSigningDetails; + + public SigningInfo() { + mSigningDetails = PackageParser.SigningDetails.UNKNOWN; + } + + /** + * @hide only packagemanager should be populating this + */ + public SigningInfo(PackageParser.SigningDetails signingDetails) { + mSigningDetails = new PackageParser.SigningDetails(signingDetails); + } + + public SigningInfo(SigningInfo orig) { + mSigningDetails = new PackageParser.SigningDetails(orig.mSigningDetails); + } + + private SigningInfo(Parcel source) { + mSigningDetails = PackageParser.SigningDetails.CREATOR.createFromParcel(source); + } + + /** + * Although relatively uncommon, packages may be signed by more than one signer, in which case + * their identity is viewed as being the set of all signers, not just any one. + */ + public boolean hasMultipleSigners() { + return mSigningDetails.signatures != null && mSigningDetails.signatures.length > 1; + } + + /** + * APK Signature Scheme v3 enables packages to provide a proof-of-rotation record that the + * platform verifies, and uses, to allow the use of new signing certificates. This is only + * available to packages that are not signed by multiple signers. In the event of a change to a + * new signing certificate, the package's past signing certificates are presented as well. Any + * check of a package's signing certificate should also include a search through its entire + * signing history, since it could change to a new signing certificate at any time. + */ + public boolean hasPastSigningCertificates() { + return mSigningDetails.signatures != null + && mSigningDetails.pastSigningCertificates != null; + } + + /** + * Returns the signing certificates this package has proven it is authorized to use. This + * includes both the signing certificate associated with the signer of the package and the past + * signing certificates it included as its proof of signing certificate rotation. This method + * is the preferred replacement for the {@code GET_SIGNATURES} flag used with {@link + * PackageManager#getPackageInfo(String, int)}. When determining if a package is signed by a + * desired certificate, the returned array should be checked to determine if it is one of the + * entries. + * + * <note> + * This method returns null if the package is signed by multiple signing certificates, as + * opposed to being signed by one current signer and also providing the history of past + * signing certificates. {@link #hasMultipleSigners()} may be used to determine if this + * package is signed by multiple signers. Packages which are signed by multiple signers + * cannot change their signing certificates and their {@code Signature} array should be + * checked to make sure that every entry matches the looked-for signing certificates. + * </note> + */ + public Signature[] getSigningCertificateHistory() { + if (hasMultipleSigners()) { + return null; + } else if (!hasPastSigningCertificates()) { + + // this package is only signed by one signer with no history, return it + return mSigningDetails.signatures; + } else { + + // this package has provided proof of past signing certificates, include them + return mSigningDetails.pastSigningCertificates; + } + } + + /** + * Returns the signing certificates used to sign the APK contents of this application. Not + * including any past signing certificates the package proved it is authorized to use. + * <note> + * This method should not be used unless {@link #hasMultipleSigners()} returns true, + * indicating that {@link #getSigningCertificateHistory()} cannot be used, otherwise {@link + * #getSigningCertificateHistory()} should be preferred. + * </note> + */ + public Signature[] getApkContentsSigners() { + return mSigningDetails.signatures; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int parcelableFlags) { + mSigningDetails.writeToParcel(dest, parcelableFlags); + } + + public static final Parcelable.Creator<SigningInfo> CREATOR = + new Parcelable.Creator<SigningInfo>() { + @Override + public SigningInfo createFromParcel(Parcel source) { + return new SigningInfo(source); + } + + @Override + public SigningInfo[] newArray(int size) { + return new SigningInfo[size]; + } + }; +} diff --git a/android/graphics/ImageDecoder.java b/android/graphics/ImageDecoder.java index 506eab5c..098f1000 100644 --- a/android/graphics/ImageDecoder.java +++ b/android/graphics/ImageDecoder.java @@ -16,36 +16,177 @@ package android.graphics; +import static android.system.OsConstants.SEEK_CUR; +import static android.system.OsConstants.SEEK_SET; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.annotation.AnyThread; +import android.annotation.IntDef; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.Px; +import android.annotation.TestApi; +import android.annotation.WorkerThread; import android.content.ContentResolver; +import android.content.res.AssetFileDescriptor; +import android.content.res.AssetManager; import android.content.res.AssetManager.AssetInputStream; import android.content.res.Resources; import android.graphics.drawable.AnimatedImageDrawable; -import android.graphics.drawable.Drawable; import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.NinePatchDrawable; import android.net.Uri; +import android.os.Build; +import android.system.ErrnoException; +import android.system.Os; import android.util.DisplayMetrics; import android.util.Size; import android.util.TypedValue; -import java.nio.ByteBuffer; +import dalvik.system.CloseGuard; + +import libcore.io.IoUtils; + import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.lang.ArrayIndexOutOfBoundsException; -import java.lang.AutoCloseable; -import java.lang.NullPointerException; import java.lang.annotation.Retention; -import static java.lang.annotation.RetentionPolicy.SOURCE; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicBoolean; /** - * Class for decoding images as {@link Bitmap}s or {@link Drawable}s. + * <p>A class for converting encoded images (like {@code PNG}, {@code JPEG}, + * {@code WEBP}, {@code GIF}, or {@code HEIF}) into {@link Drawable} or + * {@link Bitmap} objects. + * + * <p>To use it, first create a {@link Source Source} using one of the + * {@code createSource} overloads. For example, to decode from a {@link File}, call + * {@link #createSource(File)} and pass the result to {@link #decodeDrawable(Source)} + * or {@link #decodeBitmap(Source)}: + * + * <pre class="prettyprint"> + * File file = new File(...); + * ImageDecoder.Source source = ImageDecoder.createSource(file); + * Drawable drawable = ImageDecoder.decodeDrawable(source); + * </pre> + * + * <p>To change the default settings, pass the {@link Source Source} and an + * {@link OnHeaderDecodedListener OnHeaderDecodedListener} to + * {@link #decodeDrawable(Source, OnHeaderDecodedListener)} or + * {@link #decodeBitmap(Source, OnHeaderDecodedListener)}. For example, to + * create a sampled image with half the width and height of the original image, + * call {@link #setTargetSampleSize setTargetSampleSize(2)} inside + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}: + * + * <pre class="prettyprint"> + * OnHeaderDecodedListener listener = new OnHeaderDecodedListener() { + * public void onHeaderDecoded(ImageDecoder decoder, ImageInfo info, Source source) { + * decoder.setTargetSampleSize(2); + * } + * }; + * Drawable drawable = ImageDecoder.decodeDrawable(source, listener); + * </pre> + * + * <p>The {@link ImageInfo ImageInfo} contains information about the encoded image, like + * its width and height, and the {@link Source Source} can be used to match to a particular + * {@link Source Source} if a single {@link OnHeaderDecodedListener OnHeaderDecodedListener} + * is used with multiple {@link Source Source} objects. + * + * <p>The {@link OnHeaderDecodedListener OnHeaderDecodedListener} can also be implemented + * as a lambda: + * + * <pre class="prettyprint"> + * Drawable drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -> { + * decoder.setTargetSampleSize(2); + * }); + * </pre> + * + * <p>If the encoded image is an animated {@code GIF} or {@code WEBP}, + * {@link #decodeDrawable decodeDrawable} will return an {@link AnimatedImageDrawable}. To + * start its animation, call {@link AnimatedImageDrawable#start AnimatedImageDrawable.start()}: + * + * <pre class="prettyprint"> + * Drawable drawable = ImageDecoder.decodeDrawable(source); + * if (drawable instanceof AnimatedImageDrawable) { + * ((AnimatedImageDrawable) drawable).start(); + * } + * </pre> + * + * <p>By default, a {@link Bitmap} created by {@link ImageDecoder} (including + * one that is inside a {@link Drawable}) will be immutable (i.e. + * {@link Bitmap#isMutable Bitmap.isMutable()} returns {@code false}), and it + * will typically have {@code Config} {@link Bitmap.Config#HARDWARE}. Although + * these properties can be changed with {@link #setMutableRequired setMutableRequired(true)} + * (which is only compatible with {@link #decodeBitmap(Source)} and + * {@link #decodeBitmap(Source, OnHeaderDecodedListener)}) and {@link #setAllocator}, + * it is also possible to apply custom effects regardless of the mutability of + * the final returned object by passing a {@link PostProcessor} to + * {@link #setPostProcessor setPostProcessor}. A {@link PostProcessor} can also be a lambda: + * + * <pre class="prettyprint"> + * Drawable drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -> { + * decoder.setPostProcessor((canvas) -> { + * // This will create rounded corners. + * Path path = new Path(); + * path.setFillType(Path.FillType.INVERSE_EVEN_ODD); + * int width = canvas.getWidth(); + * int height = canvas.getHeight(); + * path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW); + * Paint paint = new Paint(); + * paint.setAntiAlias(true); + * paint.setColor(Color.TRANSPARENT); + * paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + * canvas.drawPath(path, paint); + * return PixelFormat.TRANSLUCENT; + * }); + * }); + * </pre> + * + * <p>If the encoded image is incomplete or contains an error, or if an + * {@link Exception} occurs during decoding, a {@link DecodeException DecodeException} + * will be thrown. In some cases, the {@link ImageDecoder} may have decoded part of + * the image. In order to display the partial image, an + * {@link OnPartialImageListener OnPartialImageListener} must be passed to + * {@link #setOnPartialImageListener setOnPartialImageListener}. For example: + * + * <pre class="prettyprint"> + * Drawable drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -> { + * decoder.setOnPartialImageListener((DecodeException e) -> { + * // Returning true indicates to create a Drawable or Bitmap even + * // if the whole image could not be decoded. Any remaining lines + * // will be blank. + * return true; + * }); + * }); + * </pre> */ public final class ImageDecoder implements AutoCloseable { + /** @hide **/ + public static int sApiLevel; /** - * Source of the encoded image data. + * Source of encoded image data. + * + * <p>References the data that will be used to decode a {@link Drawable} + * or {@link Bitmap} in {@link #decodeDrawable decodeDrawable} or + * {@link #decodeBitmap decodeBitmap}. Constructing a {@code Source} (with + * one of the overloads of {@code createSource}) can be done on any thread + * because the construction simply captures values. The real work is done + * in {@link #decodeDrawable decodeDrawable} or {@link #decodeBitmap decodeBitmap}. + * + * <p>A {@code Source} object can be reused to create multiple versions of the + * same image. For example, to decode a full size image and its thumbnail, + * the same {@code Source} can be used once with no + * {@link OnHeaderDecodedListener OnHeaderDecodedListener} and once with an + * implementation of {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded} + * that calls {@link #setTargetSize} with smaller dimensions. One {@code Source} + * even used simultaneously in multiple threads.</p> */ public static abstract class Source { private Source() {} @@ -58,7 +199,7 @@ public final class ImageDecoder implements AutoCloseable { int getDensity() { return Bitmap.DENSITY_NONE; } /* @hide */ - int computeDstDensity() { + final int computeDstDensity() { Resources res = getResources(); if (res == null) { return Bitmap.getDefaultDensity(); @@ -84,7 +225,7 @@ public final class ImageDecoder implements AutoCloseable { @Override public ImageDecoder createImageDecoder() throws IOException { - return new ImageDecoder(); + return nCreate(mData, mOffset, mLength, this); } } @@ -96,27 +237,126 @@ public final class ImageDecoder implements AutoCloseable { @Override public ImageDecoder createImageDecoder() throws IOException { - return new ImageDecoder(); + if (!mBuffer.isDirect() && mBuffer.hasArray()) { + int offset = mBuffer.arrayOffset() + mBuffer.position(); + int length = mBuffer.limit() - mBuffer.position(); + return nCreate(mBuffer.array(), offset, length, this); + } + ByteBuffer buffer = mBuffer.slice(); + return nCreate(buffer, buffer.position(), buffer.limit(), this); } } private static class ContentResolverSource extends Source { - ContentResolverSource(@NonNull ContentResolver resolver, @NonNull Uri uri) { + ContentResolverSource(@NonNull ContentResolver resolver, @NonNull Uri uri, + @Nullable Resources res) { mResolver = resolver; mUri = uri; + mResources = res; } private final ContentResolver mResolver; private final Uri mUri; + private final Resources mResources; + + @Nullable + Resources getResources() { return mResources; } @Override public ImageDecoder createImageDecoder() throws IOException { - return new ImageDecoder(); + AssetFileDescriptor assetFd = null; + try { + if (mUri.getScheme() == ContentResolver.SCHEME_CONTENT) { + assetFd = mResolver.openTypedAssetFileDescriptor(mUri, + "image/*", null); + } else { + assetFd = mResolver.openAssetFileDescriptor(mUri, "r"); + } + } catch (FileNotFoundException e) { + // Some images cannot be opened as AssetFileDescriptors (e.g. + // bmp, ico). Open them as InputStreams. + InputStream is = mResolver.openInputStream(mUri); + if (is == null) { + throw new FileNotFoundException(mUri.toString()); + } + + return createFromStream(is, true, this); + } + + final FileDescriptor fd = assetFd.getFileDescriptor(); + final long offset = assetFd.getStartOffset(); + + ImageDecoder decoder = null; + try { + try { + Os.lseek(fd, offset, SEEK_SET); + decoder = nCreate(fd, this); + } catch (ErrnoException e) { + decoder = createFromStream(new FileInputStream(fd), true, this); + } + } finally { + if (decoder == null) { + IoUtils.closeQuietly(assetFd); + } else { + decoder.mAssetFd = assetFd; + } + } + return decoder; + } + } + + @NonNull + private static ImageDecoder createFromFile(@NonNull File file, + @NonNull Source source) throws IOException { + FileInputStream stream = new FileInputStream(file); + FileDescriptor fd = stream.getFD(); + try { + Os.lseek(fd, 0, SEEK_CUR); + } catch (ErrnoException e) { + return createFromStream(stream, true, source); } + + ImageDecoder decoder = null; + try { + decoder = nCreate(fd, source); + } finally { + if (decoder == null) { + IoUtils.closeQuietly(stream); + } else { + decoder.mInputStream = stream; + decoder.mOwnsInputStream = true; + } + } + return decoder; + } + + @NonNull + private static ImageDecoder createFromStream(@NonNull InputStream is, + boolean closeInputStream, Source source) throws IOException { + // Arbitrary size matches BitmapFactory. + byte[] storage = new byte[16 * 1024]; + ImageDecoder decoder = null; + try { + decoder = nCreate(is, storage, source); + } finally { + if (decoder == null) { + if (closeInputStream) { + IoUtils.closeQuietly(is); + } + } else { + decoder.mInputStream = is; + decoder.mOwnsInputStream = closeInputStream; + decoder.mTempStorage = storage; + } + } + + return decoder; } /** * For backwards compatibility, this does *not* close the InputStream. + * + * Further, unlike other Sources, this one is not reusable. */ private static class InputStreamSource extends Source { InputStreamSource(Resources res, InputStream is, int inputDensity) { @@ -140,7 +380,15 @@ public final class ImageDecoder implements AutoCloseable { @Override public ImageDecoder createImageDecoder() throws IOException { - return new ImageDecoder(); + + synchronized (this) { + if (mInputStream == null) { + throw new IOException("Cannot reuse InputStreamSource"); + } + InputStream is = mInputStream; + mInputStream = null; + return createFromStream(is, false, this); + } } } @@ -178,7 +426,14 @@ public final class ImageDecoder implements AutoCloseable { @Override public ImageDecoder createImageDecoder() throws IOException { - return new ImageDecoder(); + synchronized (this) { + if (mAssetInputStream == null) { + throw new IOException("Cannot reuse AssetInputStreamSource"); + } + AssetInputStream ais = mAssetInputStream; + mAssetInputStream = null; + return createFromAsset(ais, this); + } } } @@ -192,16 +447,70 @@ public final class ImageDecoder implements AutoCloseable { final Resources mResources; final int mResId; int mResDensity; + private Object mLock = new Object(); @Override public Resources getResources() { return mResources; } @Override - public int getDensity() { return mResDensity; } + public int getDensity() { + synchronized (mLock) { + return mResDensity; + } + } + + @Override + public ImageDecoder createImageDecoder() throws IOException { + TypedValue value = new TypedValue(); + // This is just used in order to access the underlying Asset and + // keep it alive. + InputStream is = mResources.openRawResource(mResId, value); + + synchronized (mLock) { + if (value.density == TypedValue.DENSITY_DEFAULT) { + mResDensity = DisplayMetrics.DENSITY_DEFAULT; + } else if (value.density != TypedValue.DENSITY_NONE) { + mResDensity = value.density; + } + } + + return createFromAsset((AssetInputStream) is, this); + } + } + + /** + * ImageDecoder will own the AssetInputStream. + */ + private static ImageDecoder createFromAsset(AssetInputStream ais, + Source source) throws IOException { + ImageDecoder decoder = null; + try { + long asset = ais.getNativeAsset(); + decoder = nCreate(asset, source); + } finally { + if (decoder == null) { + IoUtils.closeQuietly(ais); + } else { + decoder.mInputStream = ais; + decoder.mOwnsInputStream = true; + } + } + return decoder; + } + + private static class AssetSource extends Source { + AssetSource(@NonNull AssetManager assets, @NonNull String fileName) { + mAssets = assets; + mFileName = fileName; + } + + private final AssetManager mAssets; + private final String mFileName; @Override public ImageDecoder createImageDecoder() throws IOException { - return new ImageDecoder(); + InputStream is = mAssets.open(mFileName); + return createFromAsset((AssetInputStream) is, this); } } @@ -214,17 +523,19 @@ public final class ImageDecoder implements AutoCloseable { @Override public ImageDecoder createImageDecoder() throws IOException { - return new ImageDecoder(); + return createFromFile(mFile, this); } } /** - * Contains information about the encoded image. + * Information about an encoded image. */ public static class ImageInfo { + private final Size mSize; private ImageDecoder mDecoder; private ImageInfo(@NonNull ImageDecoder decoder) { + mSize = new Size(decoder.mWidth, decoder.mHeight); mDecoder = decoder; } @@ -233,7 +544,7 @@ public final class ImageDecoder implements AutoCloseable { */ @NonNull public Size getSize() { - return new Size(0, 0); + return mSize; } /** @@ -241,100 +552,271 @@ public final class ImageDecoder implements AutoCloseable { */ @NonNull public String getMimeType() { - return ""; + return mDecoder.getMimeType(); } /** * Whether the image is animated. * - * <p>Calling {@link #decodeDrawable} will return an - * {@link AnimatedImageDrawable}.</p> + * <p>If {@code true}, {@link #decodeDrawable decodeDrawable} will + * return an {@link AnimatedImageDrawable}.</p> */ public boolean isAnimated() { return mDecoder.mAnimated; } + + /** + * If known, the color space the decoded bitmap will have. Note that the + * output color space is not guaranteed to be the color space the bitmap + * is encoded with. If not known (when the config is + * {@link Bitmap.Config#ALPHA_8} for instance), or there is an error, + * it is set to null. + */ + @Nullable + public ColorSpace getColorSpace() { + return mDecoder.getColorSpace(); + } }; - /** - * Thrown if the provided data is incomplete. + /** @removed + * @deprecated Subsumed by {@link #DecodeException}. */ + @Deprecated public static class IncompleteException extends IOException {}; /** - * Optional listener supplied to {@link #decodeDrawable} or - * {@link #decodeBitmap}. + * Interface for changing the default settings of a decode. + * + * <p>Supply an instance to + * {@link #decodeDrawable(Source, OnHeaderDecodedListener) decodeDrawable} + * or {@link #decodeBitmap(Source, OnHeaderDecodedListener) decodeBitmap}, + * which will call {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded} + * (in the same thread) once the size is known. The implementation of + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded} can then + * change the decode settings as desired. */ - public interface OnHeaderDecodedListener { + public static interface OnHeaderDecodedListener { /** - * Called when the header is decoded and the size is known. + * Called by {@link ImageDecoder} when the header has been decoded and + * the image size is known. * - * @param decoder allows changing the default settings of the decode. - * @param info Information about the encoded image. - * @param source that created the decoder. + * @param decoder the object performing the decode, for changing + * its default settings. + * @param info information about the encoded image. + * @param source object that created {@code decoder}. */ - void onHeaderDecoded(@NonNull ImageDecoder decoder, + public void onHeaderDecoded(@NonNull ImageDecoder decoder, @NonNull ImageInfo info, @NonNull Source source); }; - /** - * An Exception was thrown reading the {@link Source}. + /** @removed + * @deprecated Replaced by {@link #DecodeException#SOURCE_EXCEPTION}. */ + @Deprecated public static final int ERROR_SOURCE_EXCEPTION = 1; - /** - * The encoded data was incomplete. + /** @removed + * @deprecated Replaced by {@link #DecodeException#SOURCE_INCOMPLETE}. */ + @Deprecated public static final int ERROR_SOURCE_INCOMPLETE = 2; - /** - * The encoded data contained an error. + /** @removed + * @deprecated Replaced by {@link #DecodeException#SOURCE_MALFORMED_DATA}. */ + @Deprecated public static final int ERROR_SOURCE_ERROR = 3; - @Retention(SOURCE) - public @interface Error {} + /** + * Information about an interrupted decode. + */ + public static final class DecodeException extends IOException { + /** + * An Exception was thrown reading the {@link Source}. + */ + public static final int SOURCE_EXCEPTION = 1; + + /** + * The encoded data was incomplete. + */ + public static final int SOURCE_INCOMPLETE = 2; + + /** + * The encoded data contained an error. + */ + public static final int SOURCE_MALFORMED_DATA = 3; + + /** @hide **/ + @Retention(SOURCE) + @IntDef(value = { SOURCE_EXCEPTION, SOURCE_INCOMPLETE, SOURCE_MALFORMED_DATA }, + prefix = {"SOURCE_"}) + public @interface Error {}; + + @Error final int mError; + @NonNull final Source mSource; + + DecodeException(@Error int error, @Nullable Throwable cause, @NonNull Source source) { + super(errorMessage(error, cause), cause); + mError = error; + mSource = source; + } + + /** + * Private method called by JNI. + */ + @SuppressWarnings("unused") + DecodeException(@Error int error, @Nullable String msg, @Nullable Throwable cause, + @NonNull Source source) { + super(msg + errorMessage(error, cause), cause); + mError = error; + mSource = source; + } + + /** + * Retrieve the reason that decoding was interrupted. + * + * <p>If the error is {@link #SOURCE_EXCEPTION}, the underlying + * {@link java.lang.Throwable} can be retrieved with + * {@link java.lang.Throwable#getCause}.</p> + */ + @Error + public int getError() { + return mError; + } + + /** + * Retrieve the {@link Source Source} that was interrupted. + * + * <p>This can be used for equality checking to find the Source which + * failed to completely decode.</p> + */ + @NonNull + public Source getSource() { + return mSource; + } + + private static String errorMessage(@Error int error, @Nullable Throwable cause) { + switch (error) { + case SOURCE_EXCEPTION: + return "Exception in input: " + cause; + case SOURCE_INCOMPLETE: + return "Input was incomplete."; + case SOURCE_MALFORMED_DATA: + return "Input contained an error."; + default: + return ""; + } + } + } /** - * Optional listener supplied to the ImageDecoder. + * Interface for inspecting a {@link DecodeException DecodeException} + * and potentially preventing it from being thrown. * - * Without this listener, errors will throw {@link java.io.IOException}. + * <p>If an instance is passed to + * {@link #setOnPartialImageListener setOnPartialImageListener}, a + * {@link DecodeException DecodeException} that would otherwise have been + * thrown can be inspected inside + * {@link OnPartialImageListener#onPartialImage onPartialImage}. + * If {@link OnPartialImageListener#onPartialImage onPartialImage} returns + * {@code true}, a partial image will be created. */ - public interface OnPartialImageListener { + public static interface OnPartialImageListener { /** - * Called when there is only a partial image to display. + * Called by {@link ImageDecoder} when there is only a partial image to + * display. * - * If decoding is interrupted after having decoded a partial image, - * this listener lets the client know that and allows them to - * optionally finish the rest of the decode/creation process to create - * a partial {@link Drawable}/{@link Bitmap}. + * <p>If decoding is interrupted after having decoded a partial image, + * this method will be called. The implementation can inspect the + * {@link DecodeException DecodeException} and optionally finish the + * rest of the decode creation process to create a partial {@link Drawable} + * or {@link Bitmap}. * - * @param error indicating what interrupted the decode. - * @param source that had the error. - * @return True to create and return a {@link Drawable}/{@link Bitmap} - * with partial data. False (which is the default) to abort the - * decode and throw {@link java.io.IOException}. + * @param exception exception containing information about the + * decode interruption. + * @return {@code true} to create and return a {@link Drawable} or + * {@link Bitmap} with partial data. {@code false} (which is the + * default) to abort the decode and throw {@code e}. Any undecoded + * lines in the image will be blank. */ - boolean onPartialImage(@Error int error, @NonNull Source source); + boolean onPartialImage(@NonNull DecodeException exception); + }; + + // Fields + private long mNativePtr; + private final int mWidth; + private final int mHeight; + private final boolean mAnimated; + private final boolean mIsNinePatch; + + private int mDesiredWidth; + private int mDesiredHeight; + private int mAllocator = ALLOCATOR_DEFAULT; + private boolean mUnpremultipliedRequired = false; + private boolean mMutable = false; + private boolean mConserveMemory = false; + private boolean mDecodeAsAlphaMask = false; + private ColorSpace mDesiredColorSpace = null; + private Rect mCropRect; + private Rect mOutPaddingRect; + private Source mSource; + + private PostProcessor mPostProcessor; + private OnPartialImageListener mOnPartialImageListener; + + // Objects for interacting with the input. + private InputStream mInputStream; + private boolean mOwnsInputStream; + private byte[] mTempStorage; + private AssetFileDescriptor mAssetFd; + private final AtomicBoolean mClosed = new AtomicBoolean(); + private final CloseGuard mCloseGuard = CloseGuard.get(); + + /** + * Private constructor called by JNI. {@link #close} must be + * called after decoding to delete native resources. + */ + @SuppressWarnings("unused") + private ImageDecoder(long nativePtr, int width, int height, + boolean animated, boolean isNinePatch) { + mNativePtr = nativePtr; + mWidth = width; + mHeight = height; + mDesiredWidth = width; + mDesiredHeight = height; + mAnimated = animated; + mIsNinePatch = isNinePatch; + mCloseGuard.open("close"); } - private boolean mAnimated; - private Rect mOutPaddingRect; + @Override + protected void finalize() throws Throwable { + try { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + + // Avoid closing these in finalizer. + mInputStream = null; + mAssetFd = null; - public ImageDecoder() { - mAnimated = true; // This is too avoid throwing an exception in AnimatedImageDrawable + close(); + } finally { + super.finalize(); + } } /** - * Create a new {@link Source} from an asset. - * @hide + * Create a new {@link Source Source} from a resource. * * @param res the {@link Resources} object containing the image data. * @param resId resource ID of the image data. - * // FIXME: Can be an @DrawableRes? * @return a new Source object, which can be passed to - * {@link #decodeDrawable} or {@link #decodeBitmap}. + * {@link #decodeDrawable decodeDrawable} or + * {@link #decodeBitmap decodeBitmap}. */ + @AnyThread @NonNull public static Source createSource(@NonNull Resources res, int resId) { @@ -342,34 +824,71 @@ public final class ImageDecoder implements AutoCloseable { } /** - * Create a new {@link Source} from a {@link android.net.Uri}. + * Create a new {@link Source Source} from a {@link android.net.Uri}. + * + * <h5>Accepts the following URI schemes:</h5> + * <ul> + * <li>content ({@link ContentResolver#SCHEME_CONTENT})</li> + * <li>android.resource ({@link ContentResolver#SCHEME_ANDROID_RESOURCE})</li> + * <li>file ({@link ContentResolver#SCHEME_FILE})</li> + * </ul> * * @param cr to retrieve from. * @param uri of the image file. * @return a new Source object, which can be passed to - * {@link #decodeDrawable} or {@link #decodeBitmap}. + * {@link #decodeDrawable decodeDrawable} or + * {@link #decodeBitmap decodeBitmap}. */ + @AnyThread @NonNull public static Source createSource(@NonNull ContentResolver cr, @NonNull Uri uri) { - return new ContentResolverSource(cr, uri); + return new ContentResolverSource(cr, uri, null); + } + + /** + * Provide Resources for density scaling. + * + * @hide + */ + @AnyThread + @NonNull + public static Source createSource(@NonNull ContentResolver cr, + @NonNull Uri uri, @Nullable Resources res) { + return new ContentResolverSource(cr, uri, res); + } + + /** + * Create a new {@link Source Source} from a file in the "assets" directory. + */ + @AnyThread + @NonNull + public static Source createSource(@NonNull AssetManager assets, @NonNull String fileName) { + return new AssetSource(assets, fileName); } /** - * Create a new {@link Source} from a byte array. + * Create a new {@link Source Source} from a byte array. * * @param data byte array of compressed image data. * @param offset offset into data for where the decoder should begin * parsing. * @param length number of bytes, beginning at offset, to parse. + * @return a new Source object, which can be passed to + * {@link #decodeDrawable decodeDrawable} or + * {@link #decodeBitmap decodeBitmap}. * @throws NullPointerException if data is null. * @throws ArrayIndexOutOfBoundsException if offset and length are * not within data. * @hide */ + @AnyThread @NonNull public static Source createSource(@NonNull byte[] data, int offset, int length) throws ArrayIndexOutOfBoundsException { + if (data == null) { + throw new NullPointerException("null byte[] in createSource!"); + } if (offset < 0 || length < 0 || offset >= data.length || offset + length > data.length) { throw new ArrayIndexOutOfBoundsException( @@ -382,21 +901,29 @@ public final class ImageDecoder implements AutoCloseable { * See {@link #createSource(byte[], int, int). * @hide */ + @AnyThread @NonNull public static Source createSource(@NonNull byte[] data) { return createSource(data, 0, data.length); } /** - * Create a new {@link Source} from a {@link java.nio.ByteBuffer}. + * Create a new {@link Source Source} from a {@link java.nio.ByteBuffer}. + * + * <p>Decoding will start from {@link java.nio.ByteBuffer#position() buffer.position()}. + * The position of {@code buffer} will not be affected.</p> * - * <p>The returned {@link Source} effectively takes ownership of the - * {@link java.nio.ByteBuffer}; i.e. no other code should modify it after - * this call.</p> + * <p>Note: If this {@code Source} is passed to {@link #decodeDrawable decodeDrawable}, + * and the encoded image is animated, the returned {@link AnimatedImageDrawable} + * will continue reading from the {@code buffer}, so its contents must not + * be modified, even after the {@code AnimatedImageDrawable} is returned. + * {@code buffer}'s contents should never be modified during decode.</p> * - * Decoding will start from {@link java.nio.ByteBuffer#position()}. The - * position after decoding is undefined. + * @return a new Source object, which can be passed to + * {@link #decodeDrawable decodeDrawable} or + * {@link #decodeBitmap decodeBitmap}. */ + @AnyThread @NonNull public static Source createSource(@NonNull ByteBuffer buffer) { return new ByteBufferSource(buffer); @@ -404,23 +931,39 @@ public final class ImageDecoder implements AutoCloseable { /** * Internal API used to generate bitmaps for use by Drawables (i.e. BitmapDrawable) + * + * <p>Unlike other Sources, this one cannot be reused.</p> + * * @hide */ + @AnyThread + @NonNull public static Source createSource(Resources res, InputStream is) { return new InputStreamSource(res, is, Bitmap.getDefaultDensity()); } /** * Internal API used to generate bitmaps for use by Drawables (i.e. BitmapDrawable) + * + * <p>Unlike other Sources, this one cannot be reused.</p> + * * @hide */ + @AnyThread + @TestApi + @NonNull public static Source createSource(Resources res, InputStream is, int density) { return new InputStreamSource(res, is, density); } /** - * Create a new {@link Source} from a {@link java.io.File}. + * Create a new {@link Source Source} from a {@link java.io.File}. + * + * @return a new Source object, which can be passed to + * {@link #decodeDrawable decodeDrawable} or + * {@link #decodeBitmap decodeBitmap}. */ + @AnyThread @NonNull public static Source createSource(@NonNull File file) { return new FileSource(file); @@ -431,39 +974,142 @@ public final class ImageDecoder implements AutoCloseable { * * <p>This takes an input that functions like * {@link BitmapFactory.Options#inSampleSize}. It returns a width and - * height that can be acheived by sampling the encoded image. Other widths + * height that can be achieved by sampling the encoded image. Other widths * and heights may be supported, but will require an additional (internal) * scaling step. Such internal scaling is *not* supported with - * {@link #setRequireUnpremultiplied} set to {@code true}.</p> + * {@link #setUnpremultipliedRequired} set to {@code true}.</p> * * @param sampleSize Sampling rate of the encoded image. * @return {@link android.util.Size} of the width and height after * sampling. + * + * @hide */ @NonNull public Size getSampledSize(int sampleSize) { - return new Size(0, 0); + if (sampleSize <= 0) { + throw new IllegalArgumentException("sampleSize must be positive! " + + "provided " + sampleSize); + } + if (mNativePtr == 0) { + throw new IllegalStateException("ImageDecoder is closed!"); + } + + return nGetSampledSize(mNativePtr, sampleSize); } // Modifiers + /** @removed + * @deprecated Renamed to {@link #setTargetSize}. + */ + @Deprecated + public ImageDecoder setResize(int width, int height) { + this.setTargetSize(width, height); + return this; + } + /** - * Resize the output to have the following size. + * Specify the size of the output {@link Drawable} or {@link Bitmap}. + * + * <p>By default, the output size will match the size of the encoded + * image, which can be retrieved from the {@link ImageInfo ImageInfo} in + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> + * + * <p>This will sample or scale the output to an arbitrary size that may + * be smaller or larger than the encoded size.</p> + * + * <p>Only the last call to this or {@link #setTargetSampleSize} is + * respected.</p> * - * @param width must be greater than 0. - * @param height must be greater than 0. + * <p>Like all setters on ImageDecoder, this must be called inside + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> + * + * @param width width in pixels of the output, must be greater than 0 + * @param height height in pixels of the output, must be greater than 0 + */ + public void setTargetSize(@Px @IntRange(from = 1) int width, + @Px @IntRange(from = 1) int height) { + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Dimensions must be positive! " + + "provided (" + width + ", " + height + ")"); + } + + mDesiredWidth = width; + mDesiredHeight = height; + } + + /** @removed + * @deprecated Renamed to {@link #setTargetSampleSize}. */ - public void setResize(int width, int height) { + @Deprecated + public ImageDecoder setResize(int sampleSize) { + this.setTargetSampleSize(sampleSize); + return this; + } + + private int getTargetDimension(int original, int sampleSize, int computed) { + // Sampling will never result in a smaller size than 1. + if (sampleSize >= original) { + return 1; + } + + // Use integer divide to find the desired size. If that is what + // getSampledSize computed, that is the size to use. + int target = original / sampleSize; + if (computed == target) { + return computed; + } + + // If sampleSize does not divide evenly into original, the decoder + // may round in either direction. It just needs to get a result that + // is close. + int reverse = computed * sampleSize; + if (Math.abs(reverse - original) < sampleSize) { + // This is the size that can be decoded most efficiently. + return computed; + } + + // The decoder could not get close (e.g. it is a DNG image). + return target; } /** - * Resize based on a sample size. + * Set the target size with a sampleSize. * - * <p>This has the same effect as passing the result of - * {@link #getSampledSize} to {@link #setResize(int, int)}.</p> + * <p>By default, the output size will match the size of the encoded + * image, which can be retrieved from the {@link ImageInfo ImageInfo} in + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> * - * @param sampleSize Sampling rate of the encoded image. + * <p>Requests the decoder to subsample the original image, returning a + * smaller image to save memory. The {@code sampleSize} is the number of pixels + * in either dimension that correspond to a single pixel in the output. + * For example, {@code sampleSize == 4} returns an image that is 1/4 the + * width/height of the original, and 1/16 the number of pixels.</p> + * + * <p>Must be greater than or equal to 1.</p> + * + * <p>This has the same effect as calling {@link #setTargetSize} with + * dimensions based on the {@code sampleSize}. Unlike dividing the original + * width and height by the {@code sampleSize} manually, calling this method + * allows {@code ImageDecoder} to round in the direction that it can do most + * efficiently.</p> + * + * <p>Only the last call to this or {@link #setTargetSize} is respected.</p> + * + * <p>Like all setters on ImageDecoder, this must be called inside + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> + * + * @param sampleSize sampling rate of the encoded image. */ - public void setResize(int sampleSize) { + public void setTargetSampleSize(@IntRange(from = 1) int sampleSize) { + Size size = this.getSampledSize(sampleSize); + int targetWidth = getTargetDimension(mWidth, sampleSize, size.getWidth()); + int targetHeight = getTargetDimension(mHeight, sampleSize, size.getHeight()); + this.setTargetSize(targetWidth, targetHeight); + } + + private boolean requestedResize() { + return mWidth != mDesiredWidth || mHeight != mDesiredHeight; } // These need to stay in sync with ImageDecoder.cpp's Allocator enum. @@ -473,14 +1119,15 @@ public final class ImageDecoder implements AutoCloseable { * Will typically result in a {@link Bitmap.Config#HARDWARE} * allocation, but may be software for small images. In addition, this will * switch to software when HARDWARE is incompatible, e.g. - * {@link #setMutable}, {@link #setAsAlphaMask}. + * {@link #setMutableRequired setMutableRequired(true)} or + * {@link #setDecodeAsAlphaMaskEnabled setDecodeAsAlphaMaskEnabled(true)}. */ public static final int ALLOCATOR_DEFAULT = 0; /** * Use a software allocation for the pixel memory. * - * Useful for drawing to a software {@link Canvas} or for + * <p>Useful for drawing to a software {@link Canvas} or for * accessing the pixels on the final output. */ public static final int ALLOCATOR_SOFTWARE = 1; @@ -488,92 +1135,177 @@ public final class ImageDecoder implements AutoCloseable { /** * Use shared memory for the pixel memory. * - * Useful for sharing across processes. + * <p>Useful for sharing across processes. */ public static final int ALLOCATOR_SHARED_MEMORY = 2; /** * Require a {@link Bitmap.Config#HARDWARE} {@link Bitmap}. * - * When this is combined with incompatible options, like - * {@link #setMutable} or {@link #setAsAlphaMask}, {@link #decodeDrawable} - * / {@link #decodeBitmap} will throw an - * {@link java.lang.IllegalStateException}. + * <p>When this is combined with incompatible options, like + * {@link #setMutableRequired setMutableRequired(true)} or + * {@link #setDecodeAsAlphaMaskEnabled setDecodeAsAlphaMaskEnabled(true)}, + * {@link #decodeDrawable decodeDrawable} or {@link #decodeBitmap decodeBitmap} + * will throw an {@link java.lang.IllegalStateException}. */ public static final int ALLOCATOR_HARDWARE = 3; /** @hide **/ @Retention(SOURCE) + @IntDef(value = { ALLOCATOR_DEFAULT, ALLOCATOR_SOFTWARE, + ALLOCATOR_SHARED_MEMORY, ALLOCATOR_HARDWARE }, + prefix = {"ALLOCATOR_"}) public @interface Allocator {}; /** * Choose the backing for the pixel memory. * - * This is ignored for animated drawables. + * <p>This is ignored for animated drawables.</p> + * + * <p>Like all setters on ImageDecoder, this must be called inside + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> * * @param allocator Type of allocator to use. */ - public ImageDecoder setAllocator(@Allocator int allocator) { - return this; + public void setAllocator(@Allocator int allocator) { + if (allocator < ALLOCATOR_DEFAULT || allocator > ALLOCATOR_HARDWARE) { + throw new IllegalArgumentException("invalid allocator " + allocator); + } + mAllocator = allocator; + } + + /** + * Return the allocator for the pixel memory. + */ + @Allocator + public int getAllocator() { + return mAllocator; } /** * Specify whether the {@link Bitmap} should have unpremultiplied pixels. * - * By default, ImageDecoder will create a {@link Bitmap} with + * <p>By default, ImageDecoder will create a {@link Bitmap} with * premultiplied pixels, which is required for drawing with the * {@link android.view.View} system (i.e. to a {@link Canvas}). Calling * this method with a value of {@code true} will result in * {@link #decodeBitmap} returning a {@link Bitmap} with unpremultiplied - * pixels. See {@link Bitmap#isPremultiplied}. This is incompatible with - * {@link #decodeDrawable}; attempting to decode an unpremultiplied - * {@link Drawable} will throw an {@link java.lang.IllegalStateException}. + * pixels. See {@link Bitmap#isPremultiplied Bitmap.isPremultiplied()}. + * This is incompatible with {@link #decodeDrawable decodeDrawable}; + * attempting to decode an unpremultiplied {@link Drawable} will throw an + * {@link java.lang.IllegalStateException}. </p> + * + * <p>Like all setters on ImageDecoder, this must be called inside + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> + */ + public void setUnpremultipliedRequired(boolean unpremultipliedRequired) { + mUnpremultipliedRequired = unpremultipliedRequired; + } + + /** @removed + * @deprecated Renamed to {@link #setUnpremultipliedRequired}. */ - public ImageDecoder setRequireUnpremultiplied(boolean requireUnpremultiplied) { + @Deprecated + public ImageDecoder setRequireUnpremultiplied(boolean unpremultipliedRequired) { + this.setUnpremultipliedRequired(unpremultipliedRequired); return this; } /** + * Return whether the {@link Bitmap} will have unpremultiplied pixels. + */ + public boolean isUnpremultipliedRequired() { + return mUnpremultipliedRequired; + } + + /** @removed + * @deprecated Renamed to {@link #isUnpremultipliedRequired}. + */ + @Deprecated + public boolean getRequireUnpremultiplied() { + return this.isUnpremultipliedRequired(); + } + + /** * Modify the image after decoding and scaling. * * <p>This allows adding effects prior to returning a {@link Drawable} or * {@link Bitmap}. For a {@code Drawable} or an immutable {@code Bitmap}, * this is the only way to process the image after decoding.</p> * + * <p>If combined with {@link #setTargetSize} and/or {@link #setCrop}, + * {@link PostProcessor#onPostProcess} occurs last.</p> + * * <p>If set on a nine-patch image, the nine-patch data is ignored.</p> * * <p>For an animated image, the drawing commands drawn on the * {@link Canvas} will be recorded immediately and then applied to each * frame.</p> + * + * <p>Like all setters on ImageDecoder, this must be called inside + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> + * */ - public ImageDecoder setPostProcessor(@Nullable PostProcessor p) { - return this; + public void setPostProcessor(@Nullable PostProcessor postProcessor) { + mPostProcessor = postProcessor; + } + + /** + * Return the {@link PostProcessor} currently set. + */ + @Nullable + public PostProcessor getPostProcessor() { + return mPostProcessor; } /** * Set (replace) the {@link OnPartialImageListener} on this object. * - * Will be called if there is an error in the input. Without one, a - * partial {@link Bitmap} will be created. + * <p>Will be called if there is an error in the input. Without one, an + * error will result in an {@code Exception} being thrown.</p> + * + * <p>Like all setters on ImageDecoder, this must be called inside + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> + * */ - public ImageDecoder setOnPartialImageListener(@Nullable OnPartialImageListener l) { - return this; + public void setOnPartialImageListener(@Nullable OnPartialImageListener listener) { + mOnPartialImageListener = listener; + } + + /** + * Return the {@link OnPartialImageListener OnPartialImageListener} currently set. + */ + @Nullable + public OnPartialImageListener getOnPartialImageListener() { + return mOnPartialImageListener; } /** * Crop the output to {@code subset} of the (possibly) scaled image. * * <p>{@code subset} must be contained within the size set by - * {@link #setResize} or the bounds of the image if setResize was not - * called. Otherwise an {@link IllegalStateException} will be thrown by - * {@link #decodeDrawable}/{@link #decodeBitmap}.</p> + * {@link #setTargetSize} or the bounds of the image if setTargetSize was + * not called. Otherwise an {@link IllegalStateException} will be thrown by + * {@link #decodeDrawable decodeDrawable}/{@link #decodeBitmap decodeBitmap}.</p> * * <p>NOT intended as a replacement for - * {@link BitmapRegionDecoder#decodeRegion}. This supports all formats, - * but merely crops the output.</p> + * {@link BitmapRegionDecoder#decodeRegion BitmapRegionDecoder.decodeRegion()}. + * This supports all formats, but merely crops the output.</p> + * + * <p>Like all setters on ImageDecoder, this must be called inside + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> + * */ - public ImageDecoder setCrop(@Nullable Rect subset) { - return this; + public void setCrop(@Nullable Rect subset) { + mCropRect = subset; + } + + /** + * Return the cropping rectangle, if set. + */ + @Nullable + public Rect getCrop() { + return mCropRect; } /** @@ -582,43 +1314,122 @@ public final class ImageDecoder implements AutoCloseable { * If the image is a nine patch, this Rect will be set to the padding * rectangle during decode. Otherwise it will not be modified. * + * <p>Like all setters on ImageDecoder, this must be called inside + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> + * * @hide */ - public ImageDecoder setOutPaddingRect(@NonNull Rect outPadding) { + public void setOutPaddingRect(@NonNull Rect outPadding) { mOutPaddingRect = outPadding; - return this; } /** * Specify whether the {@link Bitmap} should be mutable. * - * <p>By default, a {@link Bitmap} created will be immutable, but that can - * be changed with this call.</p> + * <p>By default, a {@link Bitmap} created by {@link #decodeBitmap decodeBitmap} + * will be immutable i.e. {@link Bitmap#isMutable() Bitmap.isMutable()} returns + * {@code false}. This can be changed with {@code setMutableRequired(true)}. * * <p>Mutable Bitmaps are incompatible with {@link #ALLOCATOR_HARDWARE}, * because {@link Bitmap.Config#HARDWARE} Bitmaps cannot be mutable. * Attempting to combine them will throw an * {@link java.lang.IllegalStateException}.</p> * - * <p>Mutable Bitmaps are also incompatible with {@link #decodeDrawable}, + * <p>Mutable Bitmaps are also incompatible with {@link #decodeDrawable decodeDrawable}, * which would require retrieving the Bitmap from the returned Drawable in * order to modify. Attempting to decode a mutable {@link Drawable} will * throw an {@link java.lang.IllegalStateException}.</p> + * + * <p>Like all setters on ImageDecoder, this must be called inside + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> + */ + public void setMutableRequired(boolean mutable) { + mMutable = mutable; + } + + /** @removed + * @deprecated Renamed to {@link #setMutableRequired}. */ + @Deprecated public ImageDecoder setMutable(boolean mutable) { + this.setMutableRequired(mutable); return this; } /** - * Specify whether to potentially save RAM at the expense of quality. + * Return whether the decoded {@link Bitmap} will be mutable. + */ + public boolean isMutableRequired() { + return mMutable; + } + + /** @removed + * @deprecated Renamed to {@link #isMutableRequired}. + */ + @Deprecated + public boolean getMutable() { + return this.isMutableRequired(); + } + + /** + * Save memory if possible by using a denser {@link Bitmap.Config} at the + * cost of some image quality. * - * Setting this to {@code true} may result in a {@link Bitmap} with a - * denser {@link Bitmap.Config}, depending on the image. For example, for - * an opaque {@link Bitmap}, this may result in a {@link Bitmap.Config} - * with no alpha information. + * <p>For example an opaque 8-bit image may be compressed into an + * {@link Bitmap.Config#RGB_565} configuration, sacrificing image + * quality to save memory. */ - public ImageDecoder setPreferRamOverQuality(boolean preferRamOverQuality) { - return this; + public static final int MEMORY_POLICY_LOW_RAM = 0; + + /** + * Use the most natural {@link Bitmap.Config} for the internal {@link Bitmap}. + * + * <p>This is the recommended default for most applications and usages. This + * will use the closest {@link Bitmap.Config} for the encoded source. If the + * encoded source does not exactly match any {@link Bitmap.Config}, the next + * highest quality {@link Bitmap.Config} will be used avoiding any loss in + * image quality. + */ + public static final int MEMORY_POLICY_DEFAULT = 1; + + /** @hide **/ + @Retention(SOURCE) + @IntDef(value = { MEMORY_POLICY_DEFAULT, MEMORY_POLICY_LOW_RAM }, + prefix = {"MEMORY_POLICY_"}) + public @interface MemoryPolicy {}; + + /** + * Specify the memory policy for the decoded {@link Bitmap}. + * + * <p>Like all setters on ImageDecoder, this must be called inside + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> + */ + public void setMemorySizePolicy(@MemoryPolicy int policy) { + mConserveMemory = (policy == MEMORY_POLICY_LOW_RAM); + } + + /** + * Retrieve the memory policy for the decoded {@link Bitmap}. + */ + @MemoryPolicy + public int getMemorySizePolicy() { + return mConserveMemory ? MEMORY_POLICY_LOW_RAM : MEMORY_POLICY_DEFAULT; + } + + /** @removed + * @deprecated Replaced by {@link #setMemorySizePolicy}. + */ + @Deprecated + public void setConserveMemory(boolean conserveMemory) { + mConserveMemory = conserveMemory; + } + + /** @removed + * @deprecated Replaced by {@link #getMemorySizePolicy}. + */ + @Deprecated + public boolean getConserveMemory() { + return mConserveMemory; } /** @@ -628,77 +1439,467 @@ public final class ImageDecoder implements AutoCloseable { * with only one channel, treat that channel as alpha. Otherwise this call has * no effect.</p> * - * <p>setAsAlphaMask is incompatible with {@link #ALLOCATOR_HARDWARE}. Trying to - * combine them will result in {@link #decodeDrawable}/ - * {@link #decodeBitmap} throwing an + * <p>This is incompatible with {@link #ALLOCATOR_HARDWARE}. Trying to + * combine them will result in {@link #decodeDrawable decodeDrawable}/ + * {@link #decodeBitmap decodeBitmap} throwing an * {@link java.lang.IllegalStateException}.</p> + * + * <p>Like all setters on ImageDecoder, this must be called inside + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> + */ + public void setDecodeAsAlphaMaskEnabled(boolean enabled) { + mDecodeAsAlphaMask = enabled; + } + + /** @removed + * @deprecated Renamed to {@link #setDecodeAsAlphaMaskEnabled}. + */ + @Deprecated + public ImageDecoder setDecodeAsAlphaMask(boolean enabled) { + this.setDecodeAsAlphaMaskEnabled(enabled); + return this; + } + + /** @removed + * @deprecated Renamed to {@link #setDecodeAsAlphaMaskEnabled}. */ + @Deprecated public ImageDecoder setAsAlphaMask(boolean asAlphaMask) { + this.setDecodeAsAlphaMask(asAlphaMask); return this; } + /** + * Return whether to treat single channel input as alpha. + * + * <p>This returns whether {@link #setDecodeAsAlphaMaskEnabled} was set to + * {@code true}. It may still return {@code true} even if the image has + * more than one channel and therefore will not be treated as an alpha + * mask.</p> + */ + public boolean isDecodeAsAlphaMaskEnabled() { + return mDecodeAsAlphaMask; + } + + /** @removed + * @deprecated Renamed to {@link #isDecodeAsAlphaMaskEnabled}. + */ + @Deprecated + public boolean getDecodeAsAlphaMask() { + return mDecodeAsAlphaMask; + } + + /** @removed + * @deprecated Renamed to {@link #isDecodeAsAlphaMaskEnabled}. + */ + @Deprecated + public boolean getAsAlphaMask() { + return this.getDecodeAsAlphaMask(); + } + + /** + * Specify the desired {@link ColorSpace} for the output. + * + * <p>If non-null, the decoder will try to decode into {@code colorSpace}. + * If it is null, which is the default, or the request cannot be met, the + * decoder will pick either the color space embedded in the image or the + * {@link ColorSpace} best suited for the requested image configuration + * (for instance {@link ColorSpace.Named#SRGB sRGB} for the + * {@link Bitmap.Config#ARGB_8888} configuration).</p> + * + * <p>{@link Bitmap.Config#RGBA_F16} always uses the + * {@link ColorSpace.Named#LINEAR_EXTENDED_SRGB scRGB} color space. + * Bitmaps in other configurations without an embedded color space are + * assumed to be in the {@link ColorSpace.Named#SRGB sRGB} color space.</p> + * + * <p class="note">Only {@link ColorSpace.Model#RGB} color spaces are + * currently supported. An <code>IllegalArgumentException</code> will + * be thrown by {@link #decodeDrawable decodeDrawable}/ + * {@link #decodeBitmap decodeBitmap} when setting a non-RGB color space + * such as {@link ColorSpace.Named#CIE_LAB Lab}.</p> + * + * <p class="note">The specified color space's transfer function must be + * an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. An + * <code>IllegalArgumentException</code> will be thrown by the decode methods + * if calling {@link ColorSpace.Rgb#getTransferParameters()} on the + * specified color space returns null.</p> + * + * <p>Like all setters on ImageDecoder, this must be called inside + * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> + */ + public void setTargetColorSpace(ColorSpace colorSpace) { + mDesiredColorSpace = colorSpace; + } + + /** + * Closes this resource, relinquishing any underlying resources. This method + * is invoked automatically on objects managed by the try-with-resources + * statement. + * + * <p>This is an implementation detail of {@link ImageDecoder}, and should + * never be called manually.</p> + */ @Override public void close() { + mCloseGuard.close(); + if (!mClosed.compareAndSet(false, true)) { + return; + } + nClose(mNativePtr); + mNativePtr = 0; + + if (mOwnsInputStream) { + IoUtils.closeQuietly(mInputStream); + } + IoUtils.closeQuietly(mAssetFd); + + mInputStream = null; + mAssetFd = null; + mTempStorage = null; + } + + private void checkState() { + if (mNativePtr == 0) { + throw new IllegalStateException("Cannot use closed ImageDecoder!"); + } + + checkSubset(mDesiredWidth, mDesiredHeight, mCropRect); + + if (mAllocator == ALLOCATOR_HARDWARE) { + if (mMutable) { + throw new IllegalStateException("Cannot make mutable HARDWARE Bitmap!"); + } + if (mDecodeAsAlphaMask) { + throw new IllegalStateException("Cannot make HARDWARE Alpha mask Bitmap!"); + } + } + + if (mPostProcessor != null && mUnpremultipliedRequired) { + throw new IllegalStateException("Cannot draw to unpremultiplied pixels!"); + } + + if (mDesiredColorSpace != null) { + if (!(mDesiredColorSpace instanceof ColorSpace.Rgb)) { + throw new IllegalArgumentException("The target color space must use the " + + "RGB color model - provided: " + mDesiredColorSpace); + } + if (((ColorSpace.Rgb) mDesiredColorSpace).getTransferParameters() == null) { + throw new IllegalArgumentException("The target color space must use an " + + "ICC parametric transfer function - provided: " + mDesiredColorSpace); + } + } + } + + private static void checkSubset(int width, int height, Rect r) { + if (r == null) { + return; + } + if (r.left < 0 || r.top < 0 || r.right > width || r.bottom > height) { + throw new IllegalStateException("Subset " + r + " not contained by " + + "scaled image bounds: (" + width + " x " + height + ")"); + } + } + + @WorkerThread + @NonNull + private Bitmap decodeBitmapInternal() throws IOException { + checkState(); + return nDecodeBitmap(mNativePtr, this, mPostProcessor != null, + mDesiredWidth, mDesiredHeight, mCropRect, + mMutable, mAllocator, mUnpremultipliedRequired, + mConserveMemory, mDecodeAsAlphaMask, mDesiredColorSpace); + } + + private void callHeaderDecoded(@Nullable OnHeaderDecodedListener listener, + @NonNull Source src) { + if (listener != null) { + ImageInfo info = new ImageInfo(this); + try { + listener.onHeaderDecoded(this, info, src); + } finally { + info.mDecoder = null; + } + } } /** * Create a {@link Drawable} from a {@code Source}. * * @param src representing the encoded image. - * @param listener for learning the {@link ImageInfo} and changing any - * default settings on the {@code ImageDecoder}. If not {@code null}, - * this will be called on the same thread as {@code decodeDrawable} - * before that method returns. + * @param listener for learning the {@link ImageInfo ImageInfo} and changing any + * default settings on the {@code ImageDecoder}. This will be called on + * the same thread as {@code decodeDrawable} before that method returns. + * This is required in order to change any of the default settings. * @return Drawable for displaying the image. * @throws IOException if {@code src} is not found, is an unsupported * format, or cannot be decoded for any reason. */ + @WorkerThread @NonNull public static Drawable decodeDrawable(@NonNull Source src, + @NonNull OnHeaderDecodedListener listener) throws IOException { + if (listener == null) { + throw new IllegalArgumentException("listener cannot be null! " + + "Use decodeDrawable(Source) to not have a listener"); + } + return decodeDrawableImpl(src, listener); + } + + @WorkerThread + @NonNull + private static Drawable decodeDrawableImpl(@NonNull Source src, @Nullable OnHeaderDecodedListener listener) throws IOException { - Bitmap bitmap = decodeBitmap(src, listener); - return new BitmapDrawable(src.getResources(), bitmap); + try (ImageDecoder decoder = src.createImageDecoder()) { + decoder.mSource = src; + decoder.callHeaderDecoded(listener, src); + + if (decoder.mUnpremultipliedRequired) { + // Though this could be supported (ignored) for opaque images, + // it seems better to always report this error. + throw new IllegalStateException("Cannot decode a Drawable " + + "with unpremultiplied pixels!"); + } + + if (decoder.mMutable) { + throw new IllegalStateException("Cannot decode a mutable " + + "Drawable!"); + } + + // this call potentially manipulates the decoder so it must be performed prior to + // decoding the bitmap and after decode set the density on the resulting bitmap + final int srcDensity = decoder.computeDensity(src); + if (decoder.mAnimated) { + // AnimatedImageDrawable calls postProcessAndRelease only if + // mPostProcessor exists. + ImageDecoder postProcessPtr = decoder.mPostProcessor == null ? + null : decoder; + Drawable d = new AnimatedImageDrawable(decoder.mNativePtr, + postProcessPtr, decoder.mDesiredWidth, + decoder.mDesiredHeight, srcDensity, + src.computeDstDensity(), decoder.mCropRect, + decoder.mInputStream, decoder.mAssetFd); + // d has taken ownership of these objects. + decoder.mInputStream = null; + decoder.mAssetFd = null; + return d; + } + + Bitmap bm = decoder.decodeBitmapInternal(); + bm.setDensity(srcDensity); + + Resources res = src.getResources(); + byte[] np = bm.getNinePatchChunk(); + if (np != null && NinePatch.isNinePatchChunk(np)) { + Rect opticalInsets = new Rect(); + bm.getOpticalInsets(opticalInsets); + Rect padding = decoder.mOutPaddingRect; + if (padding == null) { + padding = new Rect(); + } + nGetPadding(decoder.mNativePtr, padding); + return new NinePatchDrawable(res, bm, np, padding, + opticalInsets, null); + } + + return new BitmapDrawable(res, bm); + } } /** - * See {@link #decodeDrawable(Source, OnHeaderDecodedListener)}. + * Create a {@link Drawable} from a {@code Source}. + * + * <p>Since there is no {@link OnHeaderDecodedListener OnHeaderDecodedListener}, + * the default settings will be used. In order to change any settings, call + * {@link #decodeDrawable(Source, OnHeaderDecodedListener)} instead.</p> + * + * @param src representing the encoded image. + * @return Drawable for displaying the image. + * @throws IOException if {@code src} is not found, is an unsupported + * format, or cannot be decoded for any reason. */ + @WorkerThread @NonNull public static Drawable decodeDrawable(@NonNull Source src) throws IOException { - return decodeDrawable(src, null); + return decodeDrawableImpl(src, null); } /** * Create a {@link Bitmap} from a {@code Source}. * * @param src representing the encoded image. - * @param listener for learning the {@link ImageInfo} and changing any - * default settings on the {@code ImageDecoder}. If not {@code null}, - * this will be called on the same thread as {@code decodeBitmap} - * before that method returns. + * @param listener for learning the {@link ImageInfo ImageInfo} and changing any + * default settings on the {@code ImageDecoder}. This will be called on + * the same thread as {@code decodeBitmap} before that method returns. + * This is required in order to change any of the default settings. * @return Bitmap containing the image. * @throws IOException if {@code src} is not found, is an unsupported * format, or cannot be decoded for any reason. */ + @WorkerThread @NonNull public static Bitmap decodeBitmap(@NonNull Source src, + @NonNull OnHeaderDecodedListener listener) throws IOException { + if (listener == null) { + throw new IllegalArgumentException("listener cannot be null! " + + "Use decodeBitmap(Source) to not have a listener"); + } + return decodeBitmapImpl(src, listener); + } + + @WorkerThread + @NonNull + private static Bitmap decodeBitmapImpl(@NonNull Source src, @Nullable OnHeaderDecodedListener listener) throws IOException { - TypedValue value = new TypedValue(); - value.density = src.getDensity(); - ImageDecoder decoder = src.createImageDecoder(); - if (listener != null) { - listener.onHeaderDecoded(decoder, new ImageInfo(decoder), src); + try (ImageDecoder decoder = src.createImageDecoder()) { + decoder.mSource = src; + decoder.callHeaderDecoded(listener, src); + + // this call potentially manipulates the decoder so it must be performed prior to + // decoding the bitmap + final int srcDensity = decoder.computeDensity(src); + Bitmap bm = decoder.decodeBitmapInternal(); + bm.setDensity(srcDensity); + + Rect padding = decoder.mOutPaddingRect; + if (padding != null) { + byte[] np = bm.getNinePatchChunk(); + if (np != null && NinePatch.isNinePatchChunk(np)) { + nGetPadding(decoder.mNativePtr, padding); + } + } + + return bm; + } + } + + // This method may modify the decoder so it must be called prior to performing the decode + private int computeDensity(@NonNull Source src) { + // if the caller changed the size then we treat the density as unknown + if (this.requestedResize()) { + return Bitmap.DENSITY_NONE; + } + + final int srcDensity = src.getDensity(); + if (srcDensity == Bitmap.DENSITY_NONE) { + return srcDensity; + } + + // Scaling up nine-patch divs is imprecise and is better handled + // at draw time. An app won't be relying on the internal Bitmap's + // size, so it is safe to let NinePatchDrawable handle scaling. + // mPostProcessor disables nine-patching, so behave normally if + // it is present. + if (mIsNinePatch && mPostProcessor == null) { + return srcDensity; + } + + // Special stuff for compatibility mode: if the target density is not + // the same as the display density, but the resource -is- the same as + // the display density, then don't scale it down to the target density. + // This allows us to load the system's density-correct resources into + // an application in compatibility mode, without scaling those down + // to the compatibility density only to have them scaled back up when + // drawn to the screen. + Resources res = src.getResources(); + if (res != null && res.getDisplayMetrics().noncompatDensityDpi == srcDensity) { + return srcDensity; + } + + final int dstDensity = src.computeDstDensity(); + if (srcDensity == dstDensity) { + return srcDensity; } - return BitmapFactory.decodeResourceStream(src.getResources(), value, - ((InputStreamSource) src).mInputStream, decoder.mOutPaddingRect, null); + + // For P and above, only resize if it would be a downscale. Scale up prior + // to P in case the app relies on the Bitmap's size without considering density. + if (srcDensity < dstDensity && sApiLevel >= Build.VERSION_CODES.P) { + return srcDensity; + } + + float scale = (float) dstDensity / srcDensity; + int scaledWidth = (int) (mWidth * scale + 0.5f); + int scaledHeight = (int) (mHeight * scale + 0.5f); + this.setTargetSize(scaledWidth, scaledHeight); + return dstDensity; + } + + @NonNull + private String getMimeType() { + return nGetMimeType(mNativePtr); + } + + @Nullable + private ColorSpace getColorSpace() { + return nGetColorSpace(mNativePtr); } /** - * See {@link #decodeBitmap(Source, OnHeaderDecodedListener)}. + * Create a {@link Bitmap} from a {@code Source}. + * + * <p>Since there is no {@link OnHeaderDecodedListener OnHeaderDecodedListener}, + * the default settings will be used. In order to change any settings, call + * {@link #decodeBitmap(Source, OnHeaderDecodedListener)} instead.</p> + * + * @param src representing the encoded image. + * @return Bitmap containing the image. + * @throws IOException if {@code src} is not found, is an unsupported + * format, or cannot be decoded for any reason. */ + @WorkerThread @NonNull public static Bitmap decodeBitmap(@NonNull Source src) throws IOException { - return decodeBitmap(src, null); + return decodeBitmapImpl(src, null); } + + /** + * Private method called by JNI. + */ + @SuppressWarnings("unused") + private int postProcessAndRelease(@NonNull Canvas canvas) { + try { + return mPostProcessor.onPostProcess(canvas); + } finally { + canvas.release(); + } + } + + /** + * Private method called by JNI. + */ + @SuppressWarnings("unused") + private void onPartialImage(@DecodeException.Error int error, @Nullable Throwable cause) + throws DecodeException { + DecodeException exception = new DecodeException(error, cause, mSource); + if (mOnPartialImageListener == null + || !mOnPartialImageListener.onPartialImage(exception)) { + throw exception; + } + } + + private static native ImageDecoder nCreate(long asset, Source src) throws IOException; + private static native ImageDecoder nCreate(ByteBuffer buffer, int position, + int limit, Source src) throws IOException; + private static native ImageDecoder nCreate(byte[] data, int offset, int length, + Source src) throws IOException; + private static native ImageDecoder nCreate(InputStream is, byte[] storage, + Source src) throws IOException; + // The fd must be seekable. + private static native ImageDecoder nCreate(FileDescriptor fd, Source src) throws IOException; + @NonNull + private static native Bitmap nDecodeBitmap(long nativePtr, + @NonNull ImageDecoder decoder, + boolean doPostProcess, + int width, int height, + @Nullable Rect cropRect, boolean mutable, + int allocator, boolean unpremulRequired, + boolean conserveMemory, boolean decodeAsAlphaMask, + @Nullable ColorSpace desiredColorSpace) + throws IOException; + private static native Size nGetSampledSize(long nativePtr, + int sampleSize); + private static native void nGetPadding(long nativePtr, @NonNull Rect outRect); + private static native void nClose(long nativePtr); + private static native String nGetMimeType(long nativePtr); + private static native ColorSpace nGetColorSpace(long nativePtr); } diff --git a/android/graphics/PostProcessor.java b/android/graphics/PostProcessor.java index b1712e92..6fed39b9 100644 --- a/android/graphics/PostProcessor.java +++ b/android/graphics/PostProcessor.java @@ -16,25 +16,26 @@ package android.graphics; -import android.annotation.IntDef; import android.annotation.NonNull; +import android.graphics.drawable.AnimatedImageDrawable; import android.graphics.drawable.Drawable; /** * Helper interface for adding custom processing to an image. * - * <p>The image being processed may be a {@link Drawable}, {@link Bitmap} or frame - * of an animated image produced by {@link ImageDecoder}. This is called before - * the requested object is returned.</p> + * <p>The image being processed may be a {@link Drawable}, a {@link Bitmap}, or + * a frame of an {@link AnimatedImageDrawable} produced by {@link ImageDecoder}. + * This is called before the requested object is returned.</p> * - * <p>This custom processing also applies to image types that are otherwise - * immutable, such as {@link Bitmap.Config#HARDWARE}.</p> + * <p>This custom processing can even be applied to images that will be returned + * as immutable objects, such as a {@link Bitmap} with {@code Config} + * {@link Bitmap.Config#HARDWARE} returned by {@link ImageDecoder}.</p> * - * <p>On an animated image, the callback will only be called once, but the drawing - * commands will be applied to each frame, as if the {@code Canvas} had been - * returned by {@link Picture#beginRecording}.<p> + * <p>On an {@link AnimatedImageDrawable}, the callback will only be called once, + * but the drawing commands will be applied to each frame, as if the {@link Canvas} + * had been returned by {@link Picture#beginRecording Picture.beginRecording}.<p> * - * <p>Supplied to ImageDecoder via {@link ImageDecoder#setPostProcessor}.</p> + * <p>Supplied to ImageDecoder via {@link ImageDecoder#setPostProcessor setPostProcessor}.</p> */ public interface PostProcessor { /** @@ -43,43 +44,44 @@ public interface PostProcessor { * <p>Drawing to the {@link Canvas} will behave as if the initial processing * (e.g. decoding) already exists in the Canvas. An implementation can draw * effects on top of this, or it can even draw behind it using - * {@link PorterDuff.Mode#DST_OVER}. A common effect is to add transparency - * to the corners to achieve rounded corners. That can be done with the - * following code:</p> + * {@link PorterDuff.Mode#DST_OVER PorterDuff.Mode.DST_OVER}. A common + * effect is to add transparency to the corners to achieve rounded corners. + * That can be done with the following code:</p> * - * <code> - * Path path = new Path(); - * path.setFillType(Path.FillType.INVERSE_EVEN_ODD); - * int width = canvas.getWidth(); - * int height = canvas.getHeight(); - * path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW); - * Paint paint = new Paint(); - * paint.setAntiAlias(true); - * paint.setColor(Color.TRANSPARENT); - * paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); - * canvas.drawPath(path, paint); - * return PixelFormat.TRANSLUCENT; - * </code> + * <pre class="prettyprint"> + * Path path = new Path(); + * path.setFillType(Path.FillType.INVERSE_EVEN_ODD); + * int width = canvas.getWidth(); + * int height = canvas.getHeight(); + * path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW); + * Paint paint = new Paint(); + * paint.setAntiAlias(true); + * paint.setColor(Color.TRANSPARENT); + * paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + * canvas.drawPath(path, paint); + * return PixelFormat.TRANSLUCENT; + * </pre> * * * @param canvas The {@link Canvas} to draw to. * @return Opacity of the result after drawing. - * {@link PixelFormat#UNKNOWN} means that the implementation did not - * change whether the image has alpha. Return this unless you added - * transparency (e.g. with the code above, in which case you should - * return {@code PixelFormat.TRANSLUCENT}) or you forced the image to - * be opaque (e.g. by drawing everywhere with an opaque color and - * {@code PorterDuff.Mode.DST_OVER}, in which case you should return - * {@code PixelFormat.OPAQUE}). - * {@link PixelFormat#TRANSLUCENT} means that the implementation added - * transparency. This is safe to return even if the image already had - * transparency. This is also safe to return if the result is opaque, - * though it may draw more slowly. - * {@link PixelFormat#OPAQUE} means that the implementation forced the - * image to be opaque. This is safe to return even if the image was - * already opaque. - * {@link PixelFormat#TRANSPARENT} (or any other integer) is not - * allowed, and will result in throwing an + * {@link PixelFormat#UNKNOWN PixelFormat.UNKNOWN} means that the + * implementation did not change whether the image has alpha. Return + * this unless you added transparency (e.g. with the code above, in + * which case you should return + * {@link PixelFormat#TRANSLUCENT PixelFormat.TRANSLUCENT}) or you + * forced the image to be opaque (e.g. by drawing everywhere with an + * opaque color and {@link PorterDuff.Mode#DST_OVER PorterDuff.Mode.DST_OVER}, + * in which case you should return {@link PixelFormat#OPAQUE PixelFormat.OPAQUE}). + * {@link PixelFormat#TRANSLUCENT PixelFormat.TRANSLUCENT} means that + * the implementation added transparency. This is safe to return even + * if the image already had transparency. This is also safe to return + * if the result is opaque, though it may draw more slowly. + * {@link PixelFormat#OPAQUE PixelFormat.OPAQUE} means that the + * implementation forced the image to be opaque. This is safe to return + * even if the image was already opaque. + * {@link PixelFormat#TRANSPARENT PixelFormat.TRANSPARENT} (or any other + * integer) is not allowed, and will result in throwing an * {@link java.lang.IllegalArgumentException}. */ @PixelFormat.Opacity diff --git a/android/graphics/Typeface.java b/android/graphics/Typeface.java index 20c22b72..18dd97f8 100644 --- a/android/graphics/Typeface.java +++ b/android/graphics/Typeface.java @@ -738,6 +738,23 @@ public class Typeface { /** * Creates a typeface object that best matches the specified existing typeface and the specified * weight and italic style + * <p>Below are numerical values and corresponding common weight names.</p> + * <table> + * <thead> + * <tr><th>Value</th><th>Common weight name</th></tr> + * </thead> + * <tbody> + * <tr><td>100</td><td>Thin</td></tr> + * <tr><td>200</td><td>Extra Light</td></tr> + * <tr><td>300</td><td>Light</td></tr> + * <tr><td>400</td><td>Normal</td></tr> + * <tr><td>500</td><td>Medium</td></tr> + * <tr><td>600</td><td>Semi Bold</td></tr> + * <tr><td>700</td><td>Bold</td></tr> + * <tr><td>800</td><td>Extra Bold</td></tr> + * <tr><td>900</td><td>Black</td></tr> + * </tbody> + * </table> * * <p> * This method is thread safe. @@ -749,6 +766,9 @@ public class Typeface { * @param italic {@code true} if italic style is desired to be drawn. Otherwise, {@code false} * @return A {@link Typeface} object for drawing specified weight and italic style. Never * returns {@code null} + * + * @see #getWeight() + * @see #isItalic() */ public static @NonNull Typeface create(@Nullable Typeface family, @IntRange(from = 1, to = 1000) int weight, boolean italic) { diff --git a/android/hardware/biometrics/BiometricDialog.java b/android/hardware/biometrics/BiometricPrompt.java index dd848a34..1c9de457 100644 --- a/android/hardware/biometrics/BiometricDialog.java +++ b/android/hardware/biometrics/BiometricPrompt.java @@ -38,7 +38,7 @@ import javax.crypto.Mac; /** * A class that manages a system-provided biometric dialog. */ -public class BiometricDialog implements BiometricAuthenticator, BiometricConstants { +public class BiometricPrompt implements BiometricAuthenticator, BiometricConstants { /** * @hide @@ -190,11 +190,11 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan } /** - * Creates a {@link BiometricDialog}. - * @return a {@link BiometricDialog} + * Creates a {@link BiometricPrompt}. + * @return a {@link BiometricPrompt} * @throws IllegalArgumentException if any of the required fields are not set. */ - public BiometricDialog build() { + public BiometricPrompt build() { final CharSequence title = mBundle.getCharSequence(KEY_TITLE); final CharSequence negative = mBundle.getCharSequence(KEY_NEGATIVE_TEXT); @@ -203,7 +203,7 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan } else if (TextUtils.isEmpty(negative)) { throw new IllegalArgumentException("Negative text must be set and non-empty"); } - return new BiometricDialog(mContext, mBundle, mPositiveButtonInfo, mNegativeButtonInfo); + return new BiometricPrompt(mContext, mBundle, mPositiveButtonInfo, mNegativeButtonInfo); } } @@ -213,7 +213,7 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan private ButtonInfo mPositiveButtonInfo; private ButtonInfo mNegativeButtonInfo; - IBiometricDialogReceiver mDialogReceiver = new IBiometricDialogReceiver.Stub() { + IBiometricPromptReceiver mDialogReceiver = new IBiometricPromptReceiver.Stub() { @Override public void onDialogDismissed(int reason) { // Check the reason and invoke OnClickListener(s) if necessary @@ -229,7 +229,7 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan } }; - private BiometricDialog(Context context, Bundle bundle, + private BiometricPrompt(Context context, Bundle bundle, ButtonInfo positiveButtonInfo, ButtonInfo negativeButtonInfo) { mBundle = bundle; mPositiveButtonInfo = positiveButtonInfo; @@ -239,7 +239,7 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan } /** - * A wrapper class for the crypto objects supported by BiometricDialog. Currently the framework + * A wrapper class for the crypto objects supported by BiometricPrompt. Currently the framework * supports {@link Signature}, {@link Cipher} and {@link Mac} objects. */ public static final class CryptoObject extends android.hardware.biometrics.CryptoObject { @@ -308,8 +308,8 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan } /** - * Callback structure provided to {@link BiometricDialog#authenticate(CancellationSignal, - * Executor, AuthenticationCallback)} or {@link BiometricDialog#authenticate(CryptoObject, + * Callback structure provided to {@link BiometricPrompt#authenticate(CancellationSignal, + * Executor, AuthenticationCallback)} or {@link BiometricPrompt#authenticate(CryptoObject, * CancellationSignal, Executor, AuthenticationCallback)}. Users must provide an implementation * of this for listening to authentication events. */ @@ -378,7 +378,7 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan @NonNull CancellationSignal cancel, @NonNull @CallbackExecutor Executor executor, @NonNull BiometricAuthenticator.AuthenticationCallback callback) { - if (!(callback instanceof BiometricDialog.AuthenticationCallback)) { + if (!(callback instanceof BiometricPrompt.AuthenticationCallback)) { throw new IllegalArgumentException("Callback cannot be casted"); } authenticate(crypto, cancel, executor, (AuthenticationCallback) callback); @@ -395,7 +395,7 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan public void authenticate(@NonNull CancellationSignal cancel, @NonNull @CallbackExecutor Executor executor, @NonNull BiometricAuthenticator.AuthenticationCallback callback) { - if (!(callback instanceof BiometricDialog.AuthenticationCallback)) { + if (!(callback instanceof BiometricPrompt.AuthenticationCallback)) { throw new IllegalArgumentException("Callback cannot be casted"); } authenticate(cancel, executor, (AuthenticationCallback) callback); @@ -410,8 +410,8 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan * operation can be canceled by using the provided cancel object. The application will receive * authentication errors through {@link AuthenticationCallback}, and button events through the * corresponding callback set in {@link Builder#setNegativeButton(CharSequence, Executor, - * DialogInterface.OnClickListener)}. It is safe to reuse the {@link BiometricDialog} object, - * and calling {@link BiometricDialog#authenticate( CancellationSignal, Executor, + * DialogInterface.OnClickListener)}. It is safe to reuse the {@link BiometricPrompt} object, + * and calling {@link BiometricPrompt#authenticate( CancellationSignal, Executor, * AuthenticationCallback)} while an existing authentication attempt is occurring will stop the * previous client and start a new authentication. The interrupted client will receive a * cancelled notification through {@link AuthenticationCallback#onAuthenticationError(int, @@ -445,8 +445,8 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan * provided cancel object. The application will receive authentication errors through {@link * AuthenticationCallback}, and button events through the corresponding callback set in {@link * Builder#setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)}. It is - * safe to reuse the {@link BiometricDialog} object, and calling {@link - * BiometricDialog#authenticate(CancellationSignal, Executor, AuthenticationCallback)} while + * safe to reuse the {@link BiometricPrompt} object, and calling {@link + * BiometricPrompt#authenticate(CancellationSignal, Executor, AuthenticationCallback)} while * an existing authentication attempt is occurring will stop the previous client and start a new * authentication. The interrupted client will receive a cancelled notification through {@link * AuthenticationCallback#onAuthenticationError(int, CharSequence)}. @@ -470,15 +470,15 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan private boolean handlePreAuthenticationErrors(AuthenticationCallback callback, Executor executor) { if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { - sendError(BiometricDialog.BIOMETRIC_ERROR_HW_NOT_PRESENT, callback, + sendError(BiometricPrompt.BIOMETRIC_ERROR_HW_NOT_PRESENT, callback, executor); return true; } else if (!mFingerprintManager.isHardwareDetected()) { - sendError(BiometricDialog.BIOMETRIC_ERROR_HW_UNAVAILABLE, callback, + sendError(BiometricPrompt.BIOMETRIC_ERROR_HW_UNAVAILABLE, callback, executor); return true; } else if (!mFingerprintManager.hasEnrolledFingerprints()) { - sendError(BiometricDialog.BIOMETRIC_ERROR_NO_BIOMETRICS, callback, + sendError(BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS, callback, executor); return true; } diff --git a/android/hardware/camera2/CameraManager.java b/android/hardware/camera2/CameraManager.java index 4124536d..7ebe0f9a 100644 --- a/android/hardware/camera2/CameraManager.java +++ b/android/hardware/camera2/CameraManager.java @@ -43,6 +43,9 @@ import android.util.ArrayMap; import android.util.Log; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; + import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; @@ -924,6 +927,37 @@ public final class CameraManager { idCount++; } } + + // The sort logic must match the logic in + // libcameraservice/common/CameraProviderManager.cpp::getAPI1CompatibleCameraDeviceIds + Arrays.sort(cameraIds, new Comparator<String>() { + @Override + public int compare(String s1, String s2) { + int s1Int = 0, s2Int = 0; + try { + s1Int = Integer.parseInt(s1); + } catch (NumberFormatException e) { + s1Int = -1; + } + + try { + s2Int = Integer.parseInt(s2); + } catch (NumberFormatException e) { + s2Int = -1; + } + + // Uint device IDs first + if (s1Int >= 0 && s2Int >= 0) { + return s1Int - s2Int; + } else if (s1Int >= 0) { + return -1; + } else if (s2Int >= 0) { + return 1; + } else { + // Simple string compare if both id are not uint + return s1.compareTo(s2); + } + }}); return cameraIds; } diff --git a/android/hardware/camera2/CaptureRequest.java b/android/hardware/camera2/CaptureRequest.java index 22525719..411a97e3 100644 --- a/android/hardware/camera2/CaptureRequest.java +++ b/android/hardware/camera2/CaptureRequest.java @@ -2105,8 +2105,8 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * the thumbnail data will also be rotated.</p> * <p>Note that this orientation is relative to the orientation of the camera sensor, given * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p> - * <p>To translate from the device orientation given by the Android sensor APIs, the following - * sample code may be used:</p> + * <p>To translate from the device orientation given by the Android sensor APIs for camera + * sensors which are not EXTERNAL, the following sample code may be used:</p> * <pre><code>private int getJpegOrientation(CameraCharacteristics c, int deviceOrientation) { * if (deviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) return 0; * int sensorOrientation = c.get(CameraCharacteristics.SENSOR_ORIENTATION); @@ -2125,6 +2125,8 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * return jpegOrientation; * } * </code></pre> + * <p>For EXTERNAL cameras the sensor orientation will always be set to 0 and the facing will + * also be set to EXTERNAL. The above code is not relevant in such case.</p> * <p><b>Units</b>: Degrees in multiples of 90</p> * <p><b>Range of valid values:</b><br> * 0, 90, 180, 270</p> diff --git a/android/hardware/camera2/CaptureResult.java b/android/hardware/camera2/CaptureResult.java index 8df54472..c1566161 100644 --- a/android/hardware/camera2/CaptureResult.java +++ b/android/hardware/camera2/CaptureResult.java @@ -2422,8 +2422,8 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * the thumbnail data will also be rotated.</p> * <p>Note that this orientation is relative to the orientation of the camera sensor, given * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p> - * <p>To translate from the device orientation given by the Android sensor APIs, the following - * sample code may be used:</p> + * <p>To translate from the device orientation given by the Android sensor APIs for camera + * sensors which are not EXTERNAL, the following sample code may be used:</p> * <pre><code>private int getJpegOrientation(CameraCharacteristics c, int deviceOrientation) { * if (deviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) return 0; * int sensorOrientation = c.get(CameraCharacteristics.SENSOR_ORIENTATION); @@ -2442,6 +2442,8 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * return jpegOrientation; * } * </code></pre> + * <p>For EXTERNAL cameras the sensor orientation will always be set to 0 and the facing will + * also be set to EXTERNAL. The above code is not relevant in such case.</p> * <p><b>Units</b>: Degrees in multiples of 90</p> * <p><b>Range of valid values:</b><br> * 0, 90, 180, 270</p> diff --git a/android/hardware/display/BrightnessConfiguration.java b/android/hardware/display/BrightnessConfiguration.java index 67e97bfd..6d9ba778 100644 --- a/android/hardware/display/BrightnessConfiguration.java +++ b/android/hardware/display/BrightnessConfiguration.java @@ -86,7 +86,9 @@ public final class BrightnessConfiguration implements Parcelable { sb.append("(").append(mLux[i]).append(", ").append(mNits[i]).append(")"); } sb.append("], '"); - sb.append(mDescription); + if (mDescription != null) { + sb.append(mDescription); + } sb.append("'}"); return sb.toString(); } @@ -96,7 +98,9 @@ public final class BrightnessConfiguration implements Parcelable { int result = 1; result = result * 31 + Arrays.hashCode(mLux); result = result * 31 + Arrays.hashCode(mNits); - result = result * 31 + mDescription.hashCode(); + if (mDescription != null) { + result = result * 31 + mDescription.hashCode(); + } return result; } diff --git a/android/hardware/display/Curve.java b/android/hardware/display/Curve.java new file mode 100644 index 00000000..ac28fdd6 --- /dev/null +++ b/android/hardware/display/Curve.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 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 android.hardware.display; + +import android.os.Parcel; +import android.os.Parcelable; + +/** @hide */ +public final class Curve implements Parcelable { + private final float[] mX; + private final float[] mY; + + public Curve(float[] x, float[] y) { + mX = x; + mY = y; + } + + public float[] getX() { + return mX; + } + + public float[] getY() { + return mY; + } + + public static final Creator<Curve> CREATOR = new Creator<Curve>() { + public Curve createFromParcel(Parcel in) { + float[] x = in.createFloatArray(); + float[] y = in.createFloatArray(); + return new Curve(x, y); + } + + public Curve[] newArray(int size) { + return new Curve[size]; + } + }; + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeFloatArray(mX); + out.writeFloatArray(mY); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/android/hardware/display/DisplayManager.java b/android/hardware/display/DisplayManager.java index efb9517a..b182fa2e 100644 --- a/android/hardware/display/DisplayManager.java +++ b/android/hardware/display/DisplayManager.java @@ -28,6 +28,7 @@ import android.content.Context; import android.graphics.Point; import android.media.projection.MediaProjection; import android.os.Handler; +import android.util.Pair; import android.util.SparseArray; import android.view.Display; import android.view.Surface; @@ -748,6 +749,22 @@ public final class DisplayManager { } /** + * Returns the minimum brightness curve, which guarantess that any brightness curve that dips + * below it is rejected by the system. + * This prevent auto-brightness from setting the screen so dark as to prevent the user from + * resetting or disabling it, and maps lux to the absolute minimum nits that are still readable + * in that ambient brightness. + * + * @return The minimum brightness curve (as lux values and their corresponding nits values). + * + * @hide + */ + @SystemApi + public Pair<float[], float[]> getMinimumBrightnessCurve() { + return mGlobal.getMinimumBrightnessCurve(); + } + + /** * Listens for changes in available display devices. */ public interface DisplayListener { diff --git a/android/hardware/display/DisplayManagerGlobal.java b/android/hardware/display/DisplayManagerGlobal.java index 2d0ef2f2..d968a3e9 100644 --- a/android/hardware/display/DisplayManagerGlobal.java +++ b/android/hardware/display/DisplayManagerGlobal.java @@ -31,6 +31,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import android.util.SparseArray; import android.view.Display; import android.view.DisplayAdjustments; @@ -563,6 +564,24 @@ public final class DisplayManagerGlobal { } /** + * Returns the minimum brightness curve, which guarantess that any brightness curve that dips + * below it is rejected by the system. + * This prevent auto-brightness from setting the screen so dark as to prevent the user from + * resetting or disabling it, and maps lux to the absolute minimum nits that are still readable + * in that ambient brightness. + * + * @return The minimum brightness curve (as lux values and their corresponding nits values). + */ + public Pair<float[], float[]> getMinimumBrightnessCurve() { + try { + Curve curve = mDm.getMinimumBrightnessCurve(); + return Pair.create(curve.getX(), curve.getY()); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + + /** * Retrieves ambient brightness stats. */ public List<AmbientBrightnessDayStats> getAmbientBrightnessStats() { diff --git a/android/hardware/fingerprint/FingerprintManager.java b/android/hardware/fingerprint/FingerprintManager.java index a6c8c67d..40d31bfe 100644 --- a/android/hardware/fingerprint/FingerprintManager.java +++ b/android/hardware/fingerprint/FingerprintManager.java @@ -31,9 +31,9 @@ import android.app.ActivityManager; import android.content.Context; import android.content.pm.PackageManager; import android.hardware.biometrics.BiometricAuthenticator; -import android.hardware.biometrics.BiometricDialog; import android.hardware.biometrics.BiometricFingerprintConstants; -import android.hardware.biometrics.IBiometricDialogReceiver; +import android.hardware.biometrics.BiometricPrompt; +import android.hardware.biometrics.IBiometricPromptReceiver; import android.os.Binder; import android.os.Bundle; import android.os.CancellationSignal; @@ -57,7 +57,7 @@ import javax.crypto.Mac; /** * A class that coordinates access to the fingerprint hardware. - * @deprecated See {@link BiometricDialog} which shows a system-provided dialog upon starting + * @deprecated See {@link BiometricPrompt} which shows a system-provided dialog upon starting * authentication. In a world where devices may have different types of biometric authentication, * it's much more realistic to have a system-provided authentication dialog since the method may * vary by vendor/device. @@ -111,7 +111,7 @@ public class FingerprintManager implements BiometricFingerprintConstants { /** * A wrapper class for the crypto objects supported by FingerprintManager. Currently the * framework supports {@link Signature}, {@link Cipher} and {@link Mac} objects. - * @deprecated See {@link android.hardware.biometrics.BiometricDialog.CryptoObject} + * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} */ @Deprecated public static final class CryptoObject extends android.hardware.biometrics.CryptoObject { @@ -155,7 +155,7 @@ public class FingerprintManager implements BiometricFingerprintConstants { /** * Container for callback data from {@link FingerprintManager#authenticate(CryptoObject, * CancellationSignal, int, AuthenticationCallback, Handler)}. - * @deprecated See {@link android.hardware.biometrics.BiometricDialog.AuthenticationResult} + * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationResult} */ @Deprecated public static class AuthenticationResult { @@ -204,7 +204,7 @@ public class FingerprintManager implements BiometricFingerprintConstants { * FingerprintManager#authenticate(CryptoObject, CancellationSignal, * int, AuthenticationCallback, Handler) } must provide an implementation of this for listening to * fingerprint events. - * @deprecated See {@link android.hardware.biometrics.BiometricDialog.AuthenticationCallback} + * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationCallback} */ @Deprecated public static abstract class AuthenticationCallback @@ -378,10 +378,10 @@ public class FingerprintManager implements BiometricFingerprintConstants { * by <a href="{@docRoot}training/articles/keystore.html">Android Keystore * facility</a>. * @throws IllegalStateException if the crypto primitive is not initialized. - * @deprecated See {@link BiometricDialog#authenticate(CancellationSignal, Executor, - * BiometricDialog.AuthenticationCallback)} and {@link BiometricDialog#authenticate( - * BiometricDialog.CryptoObject, CancellationSignal, Executor, - * BiometricDialog.AuthenticationCallback)} + * @deprecated See {@link BiometricPrompt#authenticate(CancellationSignal, Executor, + * BiometricPrompt.AuthenticationCallback)} and {@link BiometricPrompt#authenticate( + * BiometricPrompt.CryptoObject, CancellationSignal, Executor, + * BiometricPrompt.AuthenticationCallback)} */ @Deprecated @RequiresPermission(anyOf = {USE_BIOMETRIC, USE_FINGERPRINT}) @@ -444,7 +444,7 @@ public class FingerprintManager implements BiometricFingerprintConstants { /** * Per-user version, see {@link FingerprintManager#authenticate(CryptoObject, - * CancellationSignal, Bundle, Executor, IBiometricDialogReceiver, AuthenticationCallback)} + * CancellationSignal, Bundle, Executor, IBiometricPromptReceiver, AuthenticationCallback)} * @param userId the user ID that the fingerprint hardware will authenticate for. */ private void authenticate(int userId, @@ -452,7 +452,7 @@ public class FingerprintManager implements BiometricFingerprintConstants { @NonNull CancellationSignal cancel, @NonNull Bundle bundle, @NonNull @CallbackExecutor Executor executor, - @NonNull IBiometricDialogReceiver receiver, + @NonNull IBiometricPromptReceiver receiver, @NonNull BiometricAuthenticator.AuthenticationCallback callback) { mCryptoObject = crypto; if (cancel.isCanceled()) { @@ -480,8 +480,8 @@ public class FingerprintManager implements BiometricFingerprintConstants { } /** - * Private method, see {@link BiometricDialog#authenticate(CancellationSignal, Executor, - * BiometricDialog.AuthenticationCallback)} + * Private method, see {@link BiometricPrompt#authenticate(CancellationSignal, Executor, + * BiometricPrompt.AuthenticationCallback)} * @param cancel * @param executor * @param callback @@ -491,7 +491,7 @@ public class FingerprintManager implements BiometricFingerprintConstants { @NonNull CancellationSignal cancel, @NonNull Bundle bundle, @NonNull @CallbackExecutor Executor executor, - @NonNull IBiometricDialogReceiver receiver, + @NonNull IBiometricPromptReceiver receiver, @NonNull BiometricAuthenticator.AuthenticationCallback callback) { if (cancel == null) { throw new IllegalArgumentException("Must supply a cancellation signal"); @@ -512,8 +512,8 @@ public class FingerprintManager implements BiometricFingerprintConstants { } /** - * Private method, see {@link BiometricDialog#authenticate(BiometricDialog.CryptoObject, - * CancellationSignal, Executor, BiometricDialog.AuthenticationCallback)} + * Private method, see {@link BiometricPrompt#authenticate(BiometricPrompt.CryptoObject, + * CancellationSignal, Executor, BiometricPrompt.AuthenticationCallback)} * @param crypto * @param cancel * @param executor @@ -524,7 +524,7 @@ public class FingerprintManager implements BiometricFingerprintConstants { @NonNull CancellationSignal cancel, @NonNull Bundle bundle, @NonNull @CallbackExecutor Executor executor, - @NonNull IBiometricDialogReceiver receiver, + @NonNull IBiometricPromptReceiver receiver, @NonNull BiometricAuthenticator.AuthenticationCallback callback) { if (crypto == null) { throw new IllegalArgumentException("Must supply a crypto object"); @@ -743,7 +743,7 @@ public class FingerprintManager implements BiometricFingerprintConstants { * Determine if there is at least one fingerprint enrolled. * * @return true if at least one fingerprint is enrolled, false otherwise - * @deprecated See {@link BiometricDialog} and + * @deprecated See {@link BiometricPrompt} and * {@link FingerprintManager#FINGERPRINT_ERROR_NO_FINGERPRINTS} */ @Deprecated @@ -777,7 +777,7 @@ public class FingerprintManager implements BiometricFingerprintConstants { * Determine if fingerprint hardware is present and functional. * * @return true if hardware is present and functional, false otherwise. - * @deprecated See {@link BiometricDialog} and + * @deprecated See {@link BiometricPrompt} and * {@link FingerprintManager#FINGERPRINT_ERROR_HW_UNAVAILABLE} */ @Deprecated @@ -1158,7 +1158,7 @@ public class FingerprintManager implements BiometricFingerprintConstants { @Override // binder call public void onError(long deviceId, int error, int vendorCode) { if (mExecutor != null) { - // BiometricDialog case + // BiometricPrompt case if (error == FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED) { // User tapped somewhere to cancel, the biometric dialog is already dismissed. mExecutor.execute(() -> { @@ -1172,7 +1172,7 @@ public class FingerprintManager implements BiometricFingerprintConstants { mExecutor.execute(() -> { sendErrorResult(deviceId, error, vendorCode); }); - }, BiometricDialog.HIDE_DIALOG_DELAY); + }, BiometricPrompt.HIDE_DIALOG_DELAY); } } else { mHandler.obtainMessage(MSG_ERROR, error, vendorCode, deviceId).sendToTarget(); diff --git a/android/location/LocationManager.java b/android/location/LocationManager.java index a5239580..6eb3d8d1 100644 --- a/android/location/LocationManager.java +++ b/android/location/LocationManager.java @@ -29,6 +29,7 @@ import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.annotation.TestApi; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -392,6 +393,18 @@ public class LocationManager { } /** + * @hide + */ + @TestApi + public String[] getBackgroundThrottlingWhitelist() { + try { + return mService.getBackgroundThrottlingWhitelist(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * @hide - hide this constructor because it has a parameter * of type ILocationManager, which is a system private class. The * right way to create an instance of this class is using the @@ -1249,40 +1262,11 @@ public class LocationManager { @SystemApi @RequiresPermission(WRITE_SECURE_SETTINGS) public void setLocationEnabledForUser(boolean enabled, UserHandle userHandle) { - final List<String> allProvidersList = getAllProviders(); - // Update all providers on device plus gps and network provider when disabling location. - Set<String> allProvidersSet = new ArraySet<>(allProvidersList.size() + 2); - allProvidersSet.addAll(allProvidersList); - // When disabling location, disable gps and network provider that could have been enabled by - // location mode api. - if (enabled == false) { - allProvidersSet.add(GPS_PROVIDER); - allProvidersSet.add(NETWORK_PROVIDER); - } - if (allProvidersSet.isEmpty()) { - return; - } - // to ensure thread safety, we write the provider name with a '+' or '-' - // and let the SettingsProvider handle it rather than reading and modifying - // the list of enabled providers. - final String prefix = enabled ? "+" : "-"; - StringBuilder locationProvidersAllowed = new StringBuilder(); - for (String provider : allProvidersSet) { - checkProvider(provider); - if (provider.equals(PASSIVE_PROVIDER)) { - continue; - } - locationProvidersAllowed.append(prefix); - locationProvidersAllowed.append(provider); - locationProvidersAllowed.append(","); + try { + mService.setLocationEnabledForUser(enabled, userHandle.getIdentifier()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } - // Remove the trailing comma - locationProvidersAllowed.setLength(locationProvidersAllowed.length() - 1); - Settings.Secure.putStringForUser( - mContext.getContentResolver(), - Settings.Secure.LOCATION_PROVIDERS_ALLOWED, - locationProvidersAllowed.toString(), - userHandle.getIdentifier()); } /** @@ -1295,22 +1279,11 @@ public class LocationManager { */ @SystemApi public boolean isLocationEnabledForUser(UserHandle userHandle) { - final String allowedProviders = Settings.Secure.getStringForUser( - mContext.getContentResolver(), Settings.Secure.LOCATION_PROVIDERS_ALLOWED, - userHandle.getIdentifier()); - if (allowedProviders == null) { - return false; - } - final List<String> providerList = Arrays.asList(allowedProviders.split(",")); - for(String provider : getAllProviders()) { - if (provider.equals(PASSIVE_PROVIDER)) { - continue; - } - if (providerList.contains(provider)) { - return true; - } + try { + return mService.isLocationEnabledForUser(userHandle.getIdentifier()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } - return false; } /** @@ -1362,9 +1335,12 @@ public class LocationManager { @SystemApi public boolean isProviderEnabledForUser(String provider, UserHandle userHandle) { checkProvider(provider); - String allowedProviders = Settings.Secure.getStringForUser(mContext.getContentResolver(), - Settings.Secure.LOCATION_PROVIDERS_ALLOWED, userHandle.getIdentifier()); - return TextUtils.delimitedStringContains(allowedProviders, ',', provider); + + try { + return mService.isProviderEnabledForUser(provider, userHandle.getIdentifier()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** @@ -1383,16 +1359,13 @@ public class LocationManager { public boolean setProviderEnabledForUser( String provider, boolean enabled, UserHandle userHandle) { checkProvider(provider); - // to ensure thread safety, we write the provider name with a '+' or '-' - // and let the SettingsProvider handle it rather than reading and modifying - // the list of enabled providers. - if (enabled) { - provider = "+" + provider; - } else { - provider = "-" + provider; - } - return Settings.Secure.putStringForUser(mContext.getContentResolver(), - Settings.Secure.LOCATION_PROVIDERS_ALLOWED, provider, userHandle.getIdentifier()); + + try { + return mService.setProviderEnabledForUser( + provider, enabled, userHandle.getIdentifier()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** diff --git a/android/media/AudioAttributes.java b/android/media/AudioAttributes.java index d4326583..9152ff2e 100644 --- a/android/media/AudioAttributes.java +++ b/android/media/AudioAttributes.java @@ -180,6 +180,7 @@ public final class AudioAttributes implements Parcelable { * IMPORTANT: when adding new usage types, add them to SDK_USAGES and update SUPPRESSIBLE_USAGES * if applicable, as well as audioattributes.proto. * Also consider adding them to <aaudio/AAudio.h> for the NDK. + * Also consider adding them to UsageTypeConverter for service dump and etc. */ /** @@ -249,9 +250,10 @@ public final class AudioAttributes implements Parcelable { SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, SUPPRESSIBLE_MEDIA); SUPPRESSIBLE_USAGES.put(USAGE_GAME, SUPPRESSIBLE_MEDIA); SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANT, SUPPRESSIBLE_MEDIA); + /** default volume assignment is STREAM_MUSIC, handle unknown usage as media */ + SUPPRESSIBLE_USAGES.put(USAGE_UNKNOWN, SUPPRESSIBLE_MEDIA); SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION_SIGNALLING, SUPPRESSIBLE_SYSTEM); SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_SONIFICATION, SUPPRESSIBLE_SYSTEM); - SUPPRESSIBLE_USAGES.put(USAGE_UNKNOWN, SUPPRESSIBLE_SYSTEM); } /** @@ -1056,8 +1058,7 @@ public final class AudioAttributes implements Parcelable { case USAGE_ASSISTANCE_ACCESSIBILITY: return AudioSystem.STREAM_ACCESSIBILITY; case USAGE_UNKNOWN: - return fromGetVolumeControlStream ? - AudioManager.USE_DEFAULT_STREAM_TYPE : AudioSystem.STREAM_MUSIC; + return AudioSystem.STREAM_MUSIC; default: if (fromGetVolumeControlStream) { throw new IllegalArgumentException("Unknown usage value " + aa.getUsage() + diff --git a/android/media/AudioFocusRequest.java b/android/media/AudioFocusRequest.java index 7104dad4..fe89b89d 100644 --- a/android/media/AudioFocusRequest.java +++ b/android/media/AudioFocusRequest.java @@ -19,6 +19,7 @@ package android.media; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.media.AudioManager.OnAudioFocusChangeListener; import android.os.Bundle; import android.os.Handler; @@ -262,6 +263,7 @@ public final class AudioFocusRequest { * Returns the focus change listener set for this {@code AudioFocusRequest}. * @return null if no {@link AudioManager.OnAudioFocusChangeListener} was set. */ + @TestApi public @Nullable OnAudioFocusChangeListener getOnAudioFocusChangeListener() { return mFocusListener; } diff --git a/android/media/AudioFormat.java b/android/media/AudioFormat.java index f98480b2..d0a2c98f 100644 --- a/android/media/AudioFormat.java +++ b/android/media/AudioFormat.java @@ -18,6 +18,7 @@ package android.media; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; @@ -437,6 +438,7 @@ public final class AudioFormat implements Parcelable { * @param mask a combination of the CHANNEL_IN_* definitions, even CHANNEL_IN_DEFAULT * @return number of channels for the mask */ + @TestApi public static int channelCountFromInChannelMask(int mask) { return Integer.bitCount(mask); } @@ -446,6 +448,7 @@ public final class AudioFormat implements Parcelable { * @param mask a combination of the CHANNEL_OUT_* definitions, but not CHANNEL_OUT_DEFAULT * @return number of channels for the mask */ + @TestApi public static int channelCountFromOutChannelMask(int mask) { return Integer.bitCount(mask); } @@ -492,6 +495,7 @@ public final class AudioFormat implements Parcelable { // CHANNEL_IN_ALL is not yet defined; if added then it should match AUDIO_CHANNEL_IN_ALL /** @hide */ + @TestApi public static int getBytesPerSample(int audioFormat) { switch (audioFormat) { @@ -562,6 +566,7 @@ public final class AudioFormat implements Parcelable { } /** @hide */ + @TestApi public static boolean isEncodingLinearPcm(int audioFormat) { switch (audioFormat) { diff --git a/android/media/AudioManager.java b/android/media/AudioManager.java index aeef2158..fdb7499b 100644 --- a/android/media/AudioManager.java +++ b/android/media/AudioManager.java @@ -63,6 +63,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -4786,6 +4787,21 @@ public class AudioManager { } /** + * Add {@link MicrophoneInfo} by device information while filtering certain types. + */ + private void addMicrophonesFromAudioDeviceInfo(ArrayList<MicrophoneInfo> microphones, + HashSet<Integer> filterTypes) { + AudioDeviceInfo[] devices = getDevicesStatic(GET_DEVICES_INPUTS); + for (AudioDeviceInfo device : devices) { + if (filterTypes.contains(device.getType())) { + continue; + } + MicrophoneInfo microphone = microphoneInfoFromAudioDeviceInfo(device); + microphones.add(microphone); + } + } + + /** * Returns a list of {@link MicrophoneInfo} that corresponds to the characteristics * of all available microphones. The list is empty when no microphones are available * on the device. An error during the query will result in an IOException being thrown. @@ -4796,21 +4812,17 @@ public class AudioManager { public List<MicrophoneInfo> getMicrophones() throws IOException { ArrayList<MicrophoneInfo> microphones = new ArrayList<MicrophoneInfo>(); int status = AudioSystem.getMicrophones(microphones); + HashSet<Integer> filterTypes = new HashSet<>(); + filterTypes.add(AudioDeviceInfo.TYPE_TELEPHONY); if (status != AudioManager.SUCCESS) { - // fail and bail! + // fail and populate microphones with unknown characteristics by device information. Log.e(TAG, "getMicrophones failed:" + status); - return new ArrayList<MicrophoneInfo>(); // Always return a list. + addMicrophonesFromAudioDeviceInfo(microphones, filterTypes); + return microphones; } setPortIdForMicrophones(microphones); - AudioDeviceInfo[] devices = getDevicesStatic(GET_DEVICES_INPUTS); - for (AudioDeviceInfo device : devices) { - if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_MIC || - device.getType() == AudioDeviceInfo.TYPE_TELEPHONY) { - continue; - } - MicrophoneInfo microphone = microphoneInfoFromAudioDeviceInfo(device); - microphones.add(microphone); - } + filterTypes.add(AudioDeviceInfo.TYPE_BUILTIN_MIC); + addMicrophonesFromAudioDeviceInfo(microphones, filterTypes); return microphones; } diff --git a/android/media/AudioPlaybackConfiguration.java b/android/media/AudioPlaybackConfiguration.java index 8a36f91c..7dfdb20a 100644 --- a/android/media/AudioPlaybackConfiguration.java +++ b/android/media/AudioPlaybackConfiguration.java @@ -43,6 +43,8 @@ public final class AudioPlaybackConfiguration implements Parcelable { /** @hide */ public static final int PLAYER_PIID_INVALID = -1; /** @hide */ + public static final int PLAYER_PIID_UNASSIGNED = 0; + /** @hide */ public static final int PLAYER_UPID_INVALID = -1; // information about the implementation diff --git a/android/media/AudioPresentation.java b/android/media/AudioPresentation.java index e39cb7db..ce71436b 100644 --- a/android/media/AudioPresentation.java +++ b/android/media/AudioPresentation.java @@ -18,8 +18,7 @@ package android.media; import android.annotation.IntDef; import android.annotation.NonNull; - -import com.android.internal.annotations.VisibleForTesting; +import android.annotation.TestApi; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -94,7 +93,7 @@ public final class AudioPresentation { /** * @hide */ - @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + @TestApi public AudioPresentation(int presentationId, int programId, @NonNull Map<String, String> labels, @@ -119,7 +118,7 @@ public final class AudioPresentation { * decoder. Presentation id is typically sequential, but does not have to be. * @hide */ - @VisibleForTesting + @TestApi public int getPresentationId() { return mPresentationId; } @@ -129,7 +128,7 @@ public final class AudioPresentation { * Program id can be used to further uniquely identify the presentation to a decoder. * @hide */ - @VisibleForTesting + @TestApi public int getProgramId() { return mProgramId; } diff --git a/android/media/AudioRecord.java b/android/media/AudioRecord.java index 4f0dccb8..6b35dd4c 100644 --- a/android/media/AudioRecord.java +++ b/android/media/AudioRecord.java @@ -1628,7 +1628,6 @@ public class AudioRecord implements AudioRouting int status = native_get_active_microphones(activeMicrophones); if (status != AudioManager.SUCCESS) { Log.e(TAG, "getActiveMicrophones failed:" + status); - return new ArrayList<MicrophoneInfo>(); } AudioManager.setPortIdForMicrophones(activeMicrophones); diff --git a/android/media/BufferingParams.java b/android/media/BufferingParams.java index 521e8975..aaae5e7b 100644 --- a/android/media/BufferingParams.java +++ b/android/media/BufferingParams.java @@ -17,6 +17,7 @@ package android.media; import android.annotation.IntDef; +import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; @@ -63,6 +64,7 @@ import java.lang.annotation.RetentionPolicy; * <p>Users should use {@link Builder} to change {@link BufferingParams}. * @hide */ +@TestApi public final class BufferingParams implements Parcelable { private static final int BUFFERING_NO_MARK = -1; diff --git a/android/media/ExifInterface.java b/android/media/ExifInterface.java index bc0e43b5..78884367 100644 --- a/android/media/ExifInterface.java +++ b/android/media/ExifInterface.java @@ -66,7 +66,7 @@ import libcore.io.Streams; /** * This is a class for reading and writing Exif tags in a JPEG file or a RAW image file. * <p> - * Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW and RAF. + * Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW, RAF and HEIF. * <p> * Attribute mutation is supported for JPEG image files. */ @@ -2524,46 +2524,46 @@ public class ExifInterface { private void getHeifAttributes(ByteOrderedDataInputStream in) throws IOException { MediaMetadataRetriever retriever = new MediaMetadataRetriever(); try { - if (mSeekableFileDescriptor != null) { - retriever.setDataSource(mSeekableFileDescriptor); - } else { - retriever.setDataSource(new MediaDataSource() { - long mPosition; - - @Override - public void close() throws IOException {} + retriever.setDataSource(new MediaDataSource() { + long mPosition; - @Override - public int readAt(long position, byte[] buffer, int offset, int size) - throws IOException { - if (size == 0) { - return 0; - } - if (position < 0) { - return -1; - } - if (mPosition != position) { - in.seek(position); - mPosition = position; - } + @Override + public void close() throws IOException {} - int bytesRead = in.read(buffer, offset, size); - if (bytesRead < 0) { - mPosition = -1; // need to seek on next read - return -1; - } - - mPosition += bytesRead; - return bytesRead; + @Override + public int readAt(long position, byte[] buffer, int offset, int size) + throws IOException { + if (size == 0) { + return 0; + } + if (position < 0) { + return -1; + } + if (mPosition != position) { + in.seek(position); + mPosition = position; } - @Override - public long getSize() throws IOException { + int bytesRead = in.read(buffer, offset, size); + if (bytesRead < 0) { + mPosition = -1; // need to seek on next read return -1; } - }); - } + mPosition += bytesRead; + return bytesRead; + } + + @Override + public long getSize() throws IOException { + return -1; + } + }); + + String exifOffsetStr = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET); + String exifLengthStr = retriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH); String hasImage = retriever.extractMetadata( MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE); String hasVideo = retriever.extractMetadata( @@ -2622,6 +2622,30 @@ public class ExifInterface { ExifAttribute.createUShort(orientation, mExifByteOrder)); } + if (exifOffsetStr != null && exifLengthStr != null) { + int offset = Integer.parseInt(exifOffsetStr); + int length = Integer.parseInt(exifLengthStr); + if (length <= 6) { + throw new IOException("Invalid exif length"); + } + in.seek(offset); + byte[] identifier = new byte[6]; + if (in.read(identifier) != 6) { + throw new IOException("Can't read identifier"); + } + offset += 6; + length -= 6; + if (!Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) { + throw new IOException("Invalid identifier"); + } + + byte[] bytes = new byte[length]; + if (in.read(bytes) != length) { + throw new IOException("Can't read exif"); + } + readExifSegment(bytes, IFD_TYPE_PRIMARY); + } + if (DEBUG) { Log.d(TAG, "Heif meta: " + width + "x" + height + ", rotation " + rotation); } diff --git a/android/media/Image.java b/android/media/Image.java index 37c57854..9828275e 100644 --- a/android/media/Image.java +++ b/android/media/Image.java @@ -193,6 +193,13 @@ public abstract class Image implements AutoCloseable { public abstract int getTransform(); /** + * Get the scaling mode associated with this frame. + * @return The scaling mode that needs to be applied for this frame. + * @hide + */ + public abstract int getScalingMode(); + + /** * Get the {@link android.hardware.HardwareBuffer HardwareBuffer} handle of the input image * intended for GPU and/or hardware access. * <p> diff --git a/android/media/ImageReader.java b/android/media/ImageReader.java index 72d52d3d..8ec0e353 100644 --- a/android/media/ImageReader.java +++ b/android/media/ImageReader.java @@ -871,6 +871,12 @@ public class ImageReader implements AutoCloseable { } @Override + public int getScalingMode() { + throwISEIfImageIsInvalid(); + return mScalingMode; + } + + @Override public HardwareBuffer getHardwareBuffer() { throwISEIfImageIsInvalid(); return nativeGetHardwareBuffer(); @@ -1004,14 +1010,11 @@ public class ImageReader implements AutoCloseable { private long mNativeBuffer; /** - * This field is set by native code during nativeImageSetup(). + * These fields are set by native code during nativeImageSetup(). */ private long mTimestamp; - - /** - * This field is set by native code during nativeImageSetup(). - */ private int mTransform; + private int mScalingMode; private SurfacePlane[] mPlanes; private int mFormat = ImageFormat.UNKNOWN; diff --git a/android/media/ImageWriter.java b/android/media/ImageWriter.java index 8ee27ae5..397768af 100644 --- a/android/media/ImageWriter.java +++ b/android/media/ImageWriter.java @@ -371,7 +371,7 @@ public class ImageWriter implements AutoCloseable { Rect crop = image.getCropRect(); nativeQueueInputImage(mNativeContext, image, image.getTimestamp(), crop.left, crop.top, - crop.right, crop.bottom, image.getTransform()); + crop.right, crop.bottom, image.getTransform(), image.getScalingMode()); /** * Only remove and cleanup the Images that are owned by this @@ -558,7 +558,7 @@ public class ImageWriter implements AutoCloseable { Rect crop = image.getCropRect(); nativeAttachAndQueueImage(mNativeContext, image.getNativeContext(), image.getFormat(), image.getTimestamp(), crop.left, crop.top, crop.right, crop.bottom, - image.getTransform()); + image.getTransform(), image.getScalingMode()); } /** @@ -676,6 +676,7 @@ public class ImageWriter implements AutoCloseable { private long mTimestamp = DEFAULT_TIMESTAMP; private int mTransform = 0; //Default no transform + private int mScalingMode = 0; //Default frozen scaling mode public WriterSurfaceImage(ImageWriter writer) { mOwner = writer; @@ -721,6 +722,13 @@ public class ImageWriter implements AutoCloseable { } @Override + public int getScalingMode() { + throwISEIfImageIsInvalid(); + + return mScalingMode; + } + + @Override public long getTimestamp() { throwISEIfImageIsInvalid(); @@ -866,11 +874,12 @@ public class ImageWriter implements AutoCloseable { private synchronized native void nativeDequeueInputImage(long nativeCtx, Image wi); private synchronized native void nativeQueueInputImage(long nativeCtx, Image image, - long timestampNs, int left, int top, int right, int bottom, int transform); + long timestampNs, int left, int top, int right, int bottom, int transform, + int scalingMode); private synchronized native int nativeAttachAndQueueImage(long nativeCtx, long imageNativeBuffer, int imageFormat, long timestampNs, int left, - int top, int right, int bottom, int transform); + int top, int right, int bottom, int transform, int scalingMode); private synchronized native void cancelImage(long nativeCtx, Image image); diff --git a/android/media/MediaCodec.java b/android/media/MediaCodec.java index e3fba0cd..1f00c782 100644 --- a/android/media/MediaCodec.java +++ b/android/media/MediaCodec.java @@ -3574,6 +3574,7 @@ final public class MediaCodec { private final static int TYPE_YUV = 1; private final int mTransform = 0; //Default no transform + private final int mScalingMode = 0; //Default frozen scaling mode @Override public int getFormat() { @@ -3600,6 +3601,12 @@ final public class MediaCodec { } @Override + public int getScalingMode() { + throwISEIfImageIsInvalid(); + return mScalingMode; + } + + @Override public long getTimestamp() { throwISEIfImageIsInvalid(); return mTimestamp; diff --git a/android/media/MediaCodecInfo.java b/android/media/MediaCodecInfo.java index 2a601f9b..c29300d1 100644 --- a/android/media/MediaCodecInfo.java +++ b/android/media/MediaCodecInfo.java @@ -2971,6 +2971,8 @@ public final class MediaCodecInfo { public static final int AACObjectLD = 23; public static final int AACObjectHE_PS = 29; public static final int AACObjectELD = 39; + /** xHE-AAC (includes USAC) */ + public static final int AACObjectXHE = 42; // from OMX_VIDEO_VP8LEVELTYPE public static final int VP8Level_Version0 = 0x01; diff --git a/android/media/MediaMetadataRetriever.java b/android/media/MediaMetadataRetriever.java index 0955dd63..8ab5ec44 100644 --- a/android/media/MediaMetadataRetriever.java +++ b/android/media/MediaMetadataRetriever.java @@ -890,5 +890,14 @@ public class MediaMetadataRetriever */ public static final int METADATA_KEY_VIDEO_FRAME_COUNT = 32; + /** + * @hide + */ + public static final int METADATA_KEY_EXIF_OFFSET = 33; + + /** + * @hide + */ + public static final int METADATA_KEY_EXIF_LENGTH = 34; // Add more here... } diff --git a/android/media/MediaPlayer.java b/android/media/MediaPlayer.java index aef31b11..392a1eb0 100644 --- a/android/media/MediaPlayer.java +++ b/android/media/MediaPlayer.java @@ -19,6 +19,7 @@ package android.media; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.app.ActivityThread; import android.content.ContentProvider; import android.content.ContentResolver; @@ -1680,6 +1681,7 @@ public class MediaPlayer extends PlayerBase * @hide */ @NonNull + @TestApi public native BufferingParams getBufferingParams(); /** @@ -1696,6 +1698,7 @@ public class MediaPlayer extends PlayerBase * @throws IllegalArgumentException if params is invalid or not supported. * @hide */ + @TestApi public native void setBufferingParams(@NonNull BufferingParams params); /** @@ -2128,7 +2131,13 @@ public class MediaPlayer extends PlayerBase mTimeProvider.close(); mTimeProvider = null; } - mOnSubtitleDataListener = null; + synchronized(this) { + mSubtitleDataListenerDisabled = false; + mExtSubtitleDataListener = null; + mExtSubtitleDataHandler = null; + mOnMediaTimeDiscontinuityListener = null; + mOnMediaTimeDiscontinuityHandler = null; + } // Modular DRM clean up mOnDrmConfigHelper = null; @@ -2699,7 +2708,7 @@ public class MediaPlayer extends PlayerBase private int mSelectedSubtitleTrackIndex = -1; private Vector<InputStream> mOpenSubtitleSources; - private OnSubtitleDataListener mSubtitleDataListener = new OnSubtitleDataListener() { + private final OnSubtitleDataListener mIntSubtitleDataListener = new OnSubtitleDataListener() { @Override public void onSubtitleData(MediaPlayer mp, SubtitleData data) { int index = data.getTrackIndex(); @@ -2725,7 +2734,9 @@ public class MediaPlayer extends PlayerBase } mSelectedSubtitleTrackIndex = -1; } - setOnSubtitleDataListener(null); + synchronized (this) { + mSubtitleDataListenerDisabled = true; + } if (track == null) { return; } @@ -2745,7 +2756,9 @@ public class MediaPlayer extends PlayerBase selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, true); } catch (IllegalStateException e) { } - setOnSubtitleDataListener(mSubtitleDataListener); + synchronized (this) { + mSubtitleDataListenerDisabled = false; + } } // no need to select out-of-band tracks } @@ -3304,6 +3317,7 @@ public class MediaPlayer extends PlayerBase private static final int MEDIA_SUBTITLE_DATA = 201; private static final int MEDIA_META_DATA = 202; private static final int MEDIA_DRM_INFO = 210; + private static final int MEDIA_TIME_DISCONTINUITY = 211; private static final int MEDIA_AUDIO_ROUTING_CHANGED = 10000; private TimeProvider mTimeProvider; @@ -3514,15 +3528,34 @@ public class MediaPlayer extends PlayerBase return; case MEDIA_SUBTITLE_DATA: - OnSubtitleDataListener onSubtitleDataListener = mOnSubtitleDataListener; - if (onSubtitleDataListener == null) { - return; + final OnSubtitleDataListener extSubtitleListener; + final Handler extSubtitleHandler; + synchronized(this) { + if (mSubtitleDataListenerDisabled) { + return; + } + extSubtitleListener = mExtSubtitleDataListener; + extSubtitleHandler = mExtSubtitleDataHandler; } if (msg.obj instanceof Parcel) { Parcel parcel = (Parcel) msg.obj; - SubtitleData data = new SubtitleData(parcel); + final SubtitleData data = new SubtitleData(parcel); parcel.recycle(); - onSubtitleDataListener.onSubtitleData(mMediaPlayer, data); + + mIntSubtitleDataListener.onSubtitleData(mMediaPlayer, data); + + if (extSubtitleListener != null) { + if (extSubtitleHandler == null) { + extSubtitleListener.onSubtitleData(mMediaPlayer, data); + } else { + extSubtitleHandler.post(new Runnable() { + @Override + public void run() { + extSubtitleListener.onSubtitleData(mMediaPlayer, data); + } + }); + } + } } return; @@ -3553,6 +3586,43 @@ public class MediaPlayer extends PlayerBase } return; + case MEDIA_TIME_DISCONTINUITY: + final OnMediaTimeDiscontinuityListener mediaTimeListener; + final Handler mediaTimeHandler; + synchronized(this) { + mediaTimeListener = mOnMediaTimeDiscontinuityListener; + mediaTimeHandler = mOnMediaTimeDiscontinuityHandler; + } + if (mediaTimeListener == null) { + return; + } + if (msg.obj instanceof Parcel) { + Parcel parcel = (Parcel) msg.obj; + parcel.setDataPosition(0); + long anchorMediaUs = parcel.readLong(); + long anchorRealUs = parcel.readLong(); + float playbackRate = parcel.readFloat(); + parcel.recycle(); + final MediaTimestamp timestamp; + if (anchorMediaUs != -1 && anchorRealUs != -1) { + timestamp = new MediaTimestamp( + anchorMediaUs /*Us*/, anchorRealUs * 1000 /*Ns*/, playbackRate); + } else { + timestamp = MediaTimestamp.TIMESTAMP_UNKNOWN; + } + if (mediaTimeHandler == null) { + mediaTimeListener.onMediaTimeDiscontinuity(mMediaPlayer, timestamp); + } else { + mediaTimeHandler.post(new Runnable() { + @Override + public void run() { + mediaTimeListener.onMediaTimeDiscontinuity(mMediaPlayer, timestamp); + } + }); + } + } + return; + default: Log.e(TAG, "Unknown message type " + msg.what); return; @@ -3877,13 +3947,15 @@ public class MediaPlayer extends PlayerBase private void setOnSubtitleDataListenerInt( @Nullable OnSubtitleDataListener listener, @Nullable Handler handler) { synchronized (this) { - mOnSubtitleDataListener = listener; - mOnSubtitleDataHandler = handler; + mExtSubtitleDataListener = listener; + mExtSubtitleDataHandler = handler; } } - private OnSubtitleDataListener mOnSubtitleDataListener; - private Handler mOnSubtitleDataHandler; + private boolean mSubtitleDataListenerDisabled; + /** External OnSubtitleDataListener, the one set by {@link #setOnSubtitleDataListener}. */ + private OnSubtitleDataListener mExtSubtitleDataListener; + private Handler mExtSubtitleDataHandler; /** * Interface definition of a callback to be invoked when discontinuity in the normal progression diff --git a/android/media/MediaRecorder.java b/android/media/MediaRecorder.java index 90b6bff6..82d64f30 100644 --- a/android/media/MediaRecorder.java +++ b/android/media/MediaRecorder.java @@ -1434,7 +1434,6 @@ public class MediaRecorder implements AudioRouting int status = native_getActiveMicrophones(activeMicrophones); if (status != AudioManager.SUCCESS) { Log.e(TAG, "getActiveMicrophones failed:" + status); - return new ArrayList<MicrophoneInfo>(); } AudioManager.setPortIdForMicrophones(activeMicrophones); diff --git a/android/media/MediaTimestamp.java b/android/media/MediaTimestamp.java index 938dd14a..dd43b4e0 100644 --- a/android/media/MediaTimestamp.java +++ b/android/media/MediaTimestamp.java @@ -98,4 +98,13 @@ public final class MediaTimestamp && (this.nanoTime == that.nanoTime) && (this.clockRate == that.clockRate); } + + @Override + public String toString() { + return getClass().getName() + + "{AnchorMediaTimeUs=" + mediaTimeUs + + " AnchorSystemNanoTime=" + nanoTime + + " clockRate=" + clockRate + + "}"; + } } diff --git a/android/media/PlaybackParams.java b/android/media/PlaybackParams.java index 938a953a..b85e4d01 100644 --- a/android/media/PlaybackParams.java +++ b/android/media/PlaybackParams.java @@ -17,6 +17,7 @@ package android.media; import android.annotation.IntDef; +import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; @@ -151,6 +152,7 @@ public final class PlaybackParams implements Parcelable { * @param audioStretchMode * @return this <code>PlaybackParams</code> instance. */ + @TestApi public PlaybackParams setAudioStretchMode(@AudioStretchMode int audioStretchMode) { mAudioStretchMode = audioStretchMode; mSet |= SET_AUDIO_STRETCH_MODE; @@ -163,6 +165,7 @@ public final class PlaybackParams implements Parcelable { * @return audio stretch mode * @throws IllegalStateException if the audio stretch mode is not set. */ + @TestApi public @AudioStretchMode int getAudioStretchMode() { if ((mSet & SET_AUDIO_STRETCH_MODE) == 0) { throw new IllegalStateException("audio stretch mode not set"); diff --git a/android/media/PlayerBase.java b/android/media/PlayerBase.java index 80049ba5..7c6367e8 100644 --- a/android/media/PlayerBase.java +++ b/android/media/PlayerBase.java @@ -31,6 +31,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; +import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsService; @@ -58,20 +59,29 @@ public abstract class PlayerBase { protected float mRightVolume = 1.0f; protected float mAuxEffectSendLevel = 0.0f; + // NEVER call into AudioService (see getService()) with mLock held: PlayerBase can run in + // the same process as AudioService, which can synchronously call back into this class, + // causing deadlocks between the two + private final Object mLock = new Object(); + // for AppOps - private IAppOpsService mAppOps; // may be null + private @Nullable IAppOpsService mAppOps; private IAppOpsCallback mAppOpsCallback; - private boolean mHasAppOpsPlayAudio = true; // sync'd on mLock - private final Object mLock = new Object(); + @GuardedBy("mLock") + private boolean mHasAppOpsPlayAudio = true; private final int mImplType; // uniquely identifies the Player Interface throughout the system (P I Id) - private int mPlayerIId; + private int mPlayerIId = AudioPlaybackConfiguration.PLAYER_PIID_UNASSIGNED; - private int mState; // sync'd on mLock - private int mStartDelayMs = 0; // sync'd on mLock - private float mPanMultiplierL = 1.0f; // sync'd on mLock - private float mPanMultiplierR = 1.0f; // sync'd on mLock + @GuardedBy("mLock") + private int mState; + @GuardedBy("mLock") + private int mStartDelayMs = 0; + @GuardedBy("mLock") + private float mPanMultiplierL = 1.0f; + @GuardedBy("mLock") + private float mPanMultiplierR = 1.0f; /** * Constructor. Must be given audio attributes, as they are required for AppOps. @@ -134,16 +144,24 @@ public abstract class PlayerBase { } } - void baseStart() { - if (DEBUG) { Log.v(TAG, "baseStart() piid=" + mPlayerIId); } + private void updateState(int state) { + final int piid; + synchronized (mLock) { + mState = state; + piid = mPlayerIId; + } try { - synchronized (mLock) { - mState = AudioPlaybackConfiguration.PLAYER_STATE_STARTED; - getService().playerEvent(mPlayerIId, mState); - } + getService().playerEvent(piid, state); } catch (RemoteException e) { - Log.e(TAG, "Error talking to audio service, STARTED state will not be tracked", e); + Log.e(TAG, "Error talking to audio service, " + + AudioPlaybackConfiguration.toLogFriendlyPlayerState(state) + + " state will not be tracked for piid=" + piid, e); } + } + + void baseStart() { + if (DEBUG) { Log.v(TAG, "baseStart() piid=" + mPlayerIId); } + updateState(AudioPlaybackConfiguration.PLAYER_STATE_STARTED); synchronized (mLock) { if (isRestricted_sync()) { playerSetVolume(true/*muting*/,0, 0); @@ -165,26 +183,12 @@ public abstract class PlayerBase { void basePause() { if (DEBUG) { Log.v(TAG, "basePause() piid=" + mPlayerIId); } - try { - synchronized (mLock) { - mState = AudioPlaybackConfiguration.PLAYER_STATE_PAUSED; - getService().playerEvent(mPlayerIId, mState); - } - } catch (RemoteException e) { - Log.e(TAG, "Error talking to audio service, PAUSED state will not be tracked", e); - } + updateState(AudioPlaybackConfiguration.PLAYER_STATE_PAUSED); } void baseStop() { if (DEBUG) { Log.v(TAG, "baseStop() piid=" + mPlayerIId); } - try { - synchronized (mLock) { - mState = AudioPlaybackConfiguration.PLAYER_STATE_STOPPED; - getService().playerEvent(mPlayerIId, mState); - } - } catch (RemoteException e) { - Log.e(TAG, "Error talking to audio service, STOPPED state will not be tracked", e); - } + updateState(AudioPlaybackConfiguration.PLAYER_STATE_STOPPED); } void baseSetPan(float pan) { @@ -228,12 +232,16 @@ public abstract class PlayerBase { */ void baseRelease() { if (DEBUG) { Log.v(TAG, "baseRelease() piid=" + mPlayerIId + " state=" + mState); } + boolean releasePlayer = false; + synchronized (mLock) { + if (mState != AudioPlaybackConfiguration.PLAYER_STATE_RELEASED) { + releasePlayer = true; + mState = AudioPlaybackConfiguration.PLAYER_STATE_RELEASED; + } + } try { - synchronized (mLock) { - if (mState != AudioPlaybackConfiguration.PLAYER_STATE_RELEASED) { - getService().releasePlayer(mPlayerIId); - mState = AudioPlaybackConfiguration.PLAYER_STATE_RELEASED; - } + if (releasePlayer) { + getService().releasePlayer(mPlayerIId); } } catch (RemoteException e) { Log.e(TAG, "Error talking to audio service, the player will still be tracked", e); diff --git a/android/media/VolumeShaper.java b/android/media/VolumeShaper.java index 30687065..b654214c 100644 --- a/android/media/VolumeShaper.java +++ b/android/media/VolumeShaper.java @@ -18,6 +18,7 @@ package android.media; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; @@ -843,6 +844,7 @@ public final class VolumeShaper implements AutoCloseable { * @return the same {@code Builder} instance. * @throws IllegalArgumentException if flag is not recognized. */ + @TestApi public @NonNull Builder setOptionFlags(@OptionFlag int optionFlags) { if ((optionFlags & ~OPTION_FLAG_PUBLIC_ALL) != 0) { throw new IllegalArgumentException("invalid bits in flag: " + optionFlags); diff --git a/android/media/audiofx/AudioEffect.java b/android/media/audiofx/AudioEffect.java index 21d68737..24c595f5 100644 --- a/android/media/audiofx/AudioEffect.java +++ b/android/media/audiofx/AudioEffect.java @@ -18,6 +18,7 @@ package android.media.audiofx; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.TestApi; import android.app.ActivityThread; import android.os.Handler; import android.os.Looper; @@ -133,9 +134,10 @@ public class AudioEffect { .fromString("7261676f-6d75-7369-6364-28e2fd3ac39e"); /** - * Null effect UUID. Used when the UUID for effect type of + * Null effect UUID. See {@link AudioEffect(UUID, UUID, int, int)} for use. * @hide */ + @TestApi public static final UUID EFFECT_TYPE_NULL = UUID .fromString("ec7178ec-e5e1-4432-a3f4-4657e6795210"); @@ -492,6 +494,7 @@ public class AudioEffect { * @return true if the device implements the specified effect type, false otherwise. * @hide */ + @TestApi public static boolean isEffectTypeAvailable(UUID type) { AudioEffect.Descriptor[] desc = AudioEffect.queryEffects(); if (desc == null) { @@ -544,6 +547,7 @@ public class AudioEffect { * @throws IllegalStateException * @hide */ + @TestApi public int setParameter(byte[] param, byte[] value) throws IllegalStateException { checkState("setParameter()"); @@ -556,6 +560,7 @@ public class AudioEffect { * @see #setParameter(byte[], byte[]) * @hide */ + @TestApi public int setParameter(int param, int value) throws IllegalStateException { byte[] p = intToByteArray(param); byte[] v = intToByteArray(value); @@ -569,6 +574,7 @@ public class AudioEffect { * @see #setParameter(byte[], byte[]) * @hide */ + @TestApi public int setParameter(int param, short value) throws IllegalStateException { byte[] p = intToByteArray(param); @@ -583,6 +589,7 @@ public class AudioEffect { * @see #setParameter(byte[], byte[]) * @hide */ + @TestApi public int setParameter(int param, byte[] value) throws IllegalStateException { byte[] p = intToByteArray(param); @@ -596,6 +603,7 @@ public class AudioEffect { * @see #setParameter(byte[], byte[]) * @hide */ + @TestApi public int setParameter(int[] param, int[] value) throws IllegalStateException { if (param.length > 2 || value.length > 2) { @@ -647,6 +655,7 @@ public class AudioEffect { * @see #setParameter(byte[], byte[]) * @hide */ + @TestApi public int setParameter(int[] param, byte[] value) throws IllegalStateException { if (param.length > 2) { @@ -675,6 +684,7 @@ public class AudioEffect { * @throws IllegalStateException * @hide */ + @TestApi public int getParameter(byte[] param, byte[] value) throws IllegalStateException { checkState("getParameter()"); @@ -688,6 +698,7 @@ public class AudioEffect { * @see #getParameter(byte[], byte[]) * @hide */ + @TestApi public int getParameter(int param, byte[] value) throws IllegalStateException { byte[] p = intToByteArray(param); @@ -703,6 +714,7 @@ public class AudioEffect { * In case of success, returns the number of meaningful integers in value array. * @hide */ + @TestApi public int getParameter(int param, int[] value) throws IllegalStateException { if (value.length > 2) { @@ -734,6 +746,7 @@ public class AudioEffect { * In case of success, returns the number of meaningful short integers in value array. * @hide */ + @TestApi public int getParameter(int param, short[] value) throws IllegalStateException { if (value.length > 2) { @@ -799,6 +812,7 @@ public class AudioEffect { * In case of success, returns the number of meaningful short integers in value array. * @hide */ + @TestApi public int getParameter(int[] param, short[] value) throws IllegalStateException { if (param.length > 2 || value.length > 2) { @@ -938,6 +952,7 @@ public class AudioEffect { * @param listener * @hide */ + @TestApi public void setParameterListener(OnParameterChangeListener listener) { synchronized (mListenerLock) { mParameterChangeListener = listener; @@ -999,6 +1014,7 @@ public class AudioEffect { * when a parameter is changed in the effect engine by the controlling application. * @hide */ + @TestApi public interface OnParameterChangeListener { /** * Called on the listener to notify it that a parameter value has changed. @@ -1291,6 +1307,7 @@ public class AudioEffect { /** * @hide */ + @TestApi public static boolean isError(int status) { return (status < 0); } @@ -1298,6 +1315,7 @@ public class AudioEffect { /** * @hide */ + @TestApi public static int byteArrayToInt(byte[] valueBuf) { return byteArrayToInt(valueBuf, 0); @@ -1316,6 +1334,7 @@ public class AudioEffect { /** * @hide */ + @TestApi public static byte[] intToByteArray(int value) { ByteBuffer converter = ByteBuffer.allocate(4); converter.order(ByteOrder.nativeOrder()); @@ -1326,6 +1345,7 @@ public class AudioEffect { /** * @hide */ + @TestApi public static short byteArrayToShort(byte[] valueBuf) { return byteArrayToShort(valueBuf, 0); } @@ -1343,6 +1363,7 @@ public class AudioEffect { /** * @hide */ + @TestApi public static byte[] shortToByteArray(short value) { ByteBuffer converter = ByteBuffer.allocate(2); converter.order(ByteOrder.nativeOrder()); diff --git a/android/media/session/MediaController.java b/android/media/session/MediaController.java index f16804c9..84f85e78 100644 --- a/android/media/session/MediaController.java +++ b/android/media/session/MediaController.java @@ -531,7 +531,7 @@ public final class MediaController { * * @param state The new playback state of the session */ - public void onPlaybackStateChanged(@NonNull PlaybackState state) { + public void onPlaybackStateChanged(@Nullable PlaybackState state) { } /** diff --git a/android/media/session/MediaSessionManager.java b/android/media/session/MediaSessionManager.java index 519af1ba..fbc14384 100644 --- a/android/media/session/MediaSessionManager.java +++ b/android/media/session/MediaSessionManager.java @@ -47,6 +47,7 @@ import android.view.KeyEvent; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.concurrent.Executor; /** @@ -342,12 +343,16 @@ public final class MediaSessionManager { } /** - * Returns whether the app is trusted. + * Checks whether the remote user is a trusted app. * <p> * An app is trusted if the app holds the android.Manifest.permission.MEDIA_CONTENT_CONTROL * permission or has an enabled notification listener. * - * @param userInfo The remote user info + * @param userInfo The remote user info from either + * {@link MediaSession#getCurrentControllerInfo()} or + * {@link MediaBrowserService#getCurrentBrowserInfo()}. + * @return {@code true} if the remote user is trusted and its package name matches with the UID. + * {@code false} otherwise. */ public boolean isTrustedForMediaControl(RemoteUserInfo userInfo) { if (userInfo.getPackageName() == null) { @@ -814,6 +819,11 @@ public final class MediaSessionManager { && mPid == otherUserInfo.mPid && mUid == otherUserInfo.mUid; } + + @Override + public int hashCode() { + return Objects.hash(mPackageName, mPid, mUid); + } } private static final class SessionsChangedWrapper { diff --git a/android/net/IpSecManager.java b/android/net/IpSecManager.java index 15255083..a61ea50d 100644 --- a/android/net/IpSecManager.java +++ b/android/net/IpSecManager.java @@ -20,7 +20,6 @@ import static com.android.internal.util.Preconditions.checkNotNull; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; -import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; import android.content.Context; @@ -140,6 +139,7 @@ public final class IpSecManager { } } + private final Context mContext; private final IIpSecService mService; /** @@ -336,6 +336,9 @@ public final class IpSecManager { */ public void applyTransportModeTransform(@NonNull Socket socket, @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException { + // Ensure creation of FD. See b/77548890 for more details. + socket.getSoLinger(); + applyTransportModeTransform(socket.getFileDescriptor$(), direction, transform); } @@ -440,6 +443,9 @@ public final class IpSecManager { * @throws IOException indicating that the transform could not be removed from the socket */ public void removeTransportModeTransforms(@NonNull Socket socket) throws IOException { + // Ensure creation of FD. See b/77548890 for more details. + socket.getSoLinger(); + removeTransportModeTransforms(socket.getFileDescriptor$()); } @@ -659,8 +665,8 @@ public final class IpSecManager { * to create Network objects which are accessible to the Android system. * @hide */ - @SystemApi public static final class IpSecTunnelInterface implements AutoCloseable { + private final String mOpPackageName; private final IIpSecService mService; private final InetAddress mRemoteAddress; private final InetAddress mLocalAddress; @@ -682,13 +688,14 @@ public final class IpSecManager { * tunneled traffic. * * @param address the local address for traffic inside the tunnel + * @param prefixLen length of the InetAddress prefix * @hide */ - @SystemApi @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) - public void addAddress(@NonNull LinkAddress address) throws IOException { + public void addAddress(@NonNull InetAddress address, int prefixLen) throws IOException { try { - mService.addAddressToTunnelInterface(mResourceId, address); + mService.addAddressToTunnelInterface( + mResourceId, new LinkAddress(address, prefixLen), mOpPackageName); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -700,22 +707,24 @@ public final class IpSecManager { * <p>Remove an address which was previously added to the IpSecTunnelInterface * * @param address to be removed + * @param prefixLen length of the InetAddress prefix * @hide */ - @SystemApi @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) - public void removeAddress(@NonNull LinkAddress address) throws IOException { + public void removeAddress(@NonNull InetAddress address, int prefixLen) throws IOException { try { - mService.removeAddressFromTunnelInterface(mResourceId, address); + mService.removeAddressFromTunnelInterface( + mResourceId, new LinkAddress(address, prefixLen), mOpPackageName); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } - private IpSecTunnelInterface(@NonNull IIpSecService service, + private IpSecTunnelInterface(@NonNull Context ctx, @NonNull IIpSecService service, @NonNull InetAddress localAddress, @NonNull InetAddress remoteAddress, @NonNull Network underlyingNetwork) throws ResourceUnavailableException, IOException { + mOpPackageName = ctx.getOpPackageName(); mService = service; mLocalAddress = localAddress; mRemoteAddress = remoteAddress; @@ -727,7 +736,8 @@ public final class IpSecManager { localAddress.getHostAddress(), remoteAddress.getHostAddress(), underlyingNetwork, - new Binder()); + new Binder(), + mOpPackageName); switch (result.status) { case Status.OK: break; @@ -756,7 +766,7 @@ public final class IpSecManager { @Override public void close() { try { - mService.deleteTunnelInterface(mResourceId); + mService.deleteTunnelInterface(mResourceId, mOpPackageName); mResourceId = INVALID_RESOURCE_ID; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -795,13 +805,13 @@ public final class IpSecManager { * @throws ResourceUnavailableException indicating that too many encapsulation sockets are open * @hide */ - @SystemApi @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public IpSecTunnelInterface createIpSecTunnelInterface(@NonNull InetAddress localAddress, @NonNull InetAddress remoteAddress, @NonNull Network underlyingNetwork) throws ResourceUnavailableException, IOException { - return new IpSecTunnelInterface(mService, localAddress, remoteAddress, underlyingNetwork); + return new IpSecTunnelInterface( + mContext, mService, localAddress, remoteAddress, underlyingNetwork); } /** @@ -821,13 +831,13 @@ public final class IpSecManager { * layer failure. * @hide */ - @SystemApi @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public void applyTunnelModeTransform(@NonNull IpSecTunnelInterface tunnel, @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException { try { mService.applyTunnelModeTransform( - tunnel.getResourceId(), direction, transform.getResourceId()); + tunnel.getResourceId(), direction, + transform.getResourceId(), mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -839,7 +849,8 @@ public final class IpSecManager { * @param context the application context for this manager * @hide */ - public IpSecManager(IIpSecService service) { + public IpSecManager(Context ctx, IIpSecService service) { + mContext = ctx; mService = checkNotNull(service, "missing service"); } } diff --git a/android/net/IpSecTransform.java b/android/net/IpSecTransform.java index 099fe02f..62f79965 100644 --- a/android/net/IpSecTransform.java +++ b/android/net/IpSecTransform.java @@ -22,7 +22,6 @@ import static com.android.internal.util.Preconditions.checkNotNull; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; -import android.annotation.SystemApi; import android.content.Context; import android.os.Binder; import android.os.Handler; @@ -130,7 +129,8 @@ public final class IpSecTransform implements AutoCloseable { synchronized (this) { try { IIpSecService svc = getIpSecService(); - IpSecTransformResponse result = svc.createTransform(mConfig, new Binder()); + IpSecTransformResponse result = svc.createTransform( + mConfig, new Binder(), mContext.getOpPackageName()); int status = result.status; checkResultStatus(status); mResourceId = result.resourceId; @@ -249,7 +249,6 @@ public final class IpSecTransform implements AutoCloseable { * * @hide */ - @SystemApi public static class NattKeepaliveCallback { /** The specified {@code Network} is not connected. */ public static final int ERROR_INVALID_NETWORK = 1; @@ -280,7 +279,6 @@ public final class IpSecTransform implements AutoCloseable { * * @hide */ - @SystemApi @RequiresPermission(anyOf = { android.Manifest.permission.MANAGE_IPSEC_TUNNELS, android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD @@ -323,7 +321,6 @@ public final class IpSecTransform implements AutoCloseable { * * @hide */ - @SystemApi @RequiresPermission(anyOf = { android.Manifest.permission.MANAGE_IPSEC_TUNNELS, android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD @@ -476,7 +473,6 @@ public final class IpSecTransform implements AutoCloseable { * @throws IOException indicating other errors * @hide */ - @SystemApi @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public IpSecTransform buildTunnelModeTransform( diff --git a/android/net/NetworkCapabilities.java b/android/net/NetworkCapabilities.java index 374b3abc..a8e81791 100644 --- a/android/net/NetworkCapabilities.java +++ b/android/net/NetworkCapabilities.java @@ -254,9 +254,8 @@ public final class NetworkCapabilities implements Parcelable { /** * Indicates that this network is not congested. * <p> - * When a network is congested, the device should defer network traffic that - * can be done at a later time without breaking developer contracts. - * @hide + * When a network is congested, applications should defer network traffic + * that can be done at a later time, such as uploading analytics. */ public static final int NET_CAPABILITY_NOT_CONGESTED = 20; @@ -318,7 +317,7 @@ public final class NetworkCapabilities implements Parcelable { /** * Capabilities that suggest that a network is restricted. - * {@see #maybeMarkCapabilitiesRestricted}. + * {@see #maybeMarkCapabilitiesRestricted}, {@see #FORCE_RESTRICTED_CAPABILITIES} */ @VisibleForTesting /* package */ static final long RESTRICTED_CAPABILITIES = @@ -329,7 +328,13 @@ public final class NetworkCapabilities implements Parcelable { (1 << NET_CAPABILITY_IA) | (1 << NET_CAPABILITY_IMS) | (1 << NET_CAPABILITY_RCS) | - (1 << NET_CAPABILITY_XCAP) | + (1 << NET_CAPABILITY_XCAP); + + /** + * Capabilities that force network to be restricted. + * {@see #maybeMarkCapabilitiesRestricted}. + */ + private static final long FORCE_RESTRICTED_CAPABILITIES = (1 << NET_CAPABILITY_OEM_PAID); /** @@ -533,16 +538,21 @@ public final class NetworkCapabilities implements Parcelable { * @hide */ public void maybeMarkCapabilitiesRestricted() { + // Check if we have any capability that forces the network to be restricted. + final boolean forceRestrictedCapability = + (mNetworkCapabilities & FORCE_RESTRICTED_CAPABILITIES) != 0; + // Verify there aren't any unrestricted capabilities. If there are we say - // the whole thing is unrestricted. + // the whole thing is unrestricted unless it is forced to be restricted. final boolean hasUnrestrictedCapabilities = - ((mNetworkCapabilities & UNRESTRICTED_CAPABILITIES) != 0); + (mNetworkCapabilities & UNRESTRICTED_CAPABILITIES) != 0; // Must have at least some restricted capabilities. final boolean hasRestrictedCapabilities = - ((mNetworkCapabilities & RESTRICTED_CAPABILITIES) != 0); + (mNetworkCapabilities & RESTRICTED_CAPABILITIES) != 0; - if (hasRestrictedCapabilities && !hasUnrestrictedCapabilities) { + if (forceRestrictedCapability + || (hasRestrictedCapabilities && !hasUnrestrictedCapabilities)) { removeCapability(NET_CAPABILITY_NOT_RESTRICTED); } } diff --git a/android/net/NetworkPolicy.java b/android/net/NetworkPolicy.java index 1a28732e..e84c85ee 100644 --- a/android/net/NetworkPolicy.java +++ b/android/net/NetworkPolicy.java @@ -19,7 +19,7 @@ package android.net; import android.os.Parcel; import android.os.Parcelable; import android.util.BackupUtils; -import android.util.Pair; +import android.util.Range; import android.util.RecurrenceRule; import com.android.internal.util.Preconditions; @@ -136,7 +136,7 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> { return 0; } - public Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator() { + public Iterator<Range<ZonedDateTime>> cycleIterator() { return cycleRule.cycleIterator(); } diff --git a/android/net/NetworkPolicyManager.java b/android/net/NetworkPolicyManager.java index bf6b7e09..6546c391 100644 --- a/android/net/NetworkPolicyManager.java +++ b/android/net/NetworkPolicyManager.java @@ -31,6 +31,7 @@ import android.os.RemoteException; import android.os.UserHandle; import android.util.DebugUtils; import android.util.Pair; +import android.util.Range; import com.google.android.collect.Sets; @@ -258,8 +259,21 @@ public class NetworkPolicyManager { } /** {@hide} */ + @Deprecated public static Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator(NetworkPolicy policy) { - return policy.cycleIterator(); + final Iterator<Range<ZonedDateTime>> it = policy.cycleIterator(); + return new Iterator<Pair<ZonedDateTime, ZonedDateTime>>() { + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public Pair<ZonedDateTime, ZonedDateTime> next() { + final Range<ZonedDateTime> r = it.next(); + return Pair.create(r.getLower(), r.getUpper()); + } + }; } /** diff --git a/android/net/NetworkRequest.java b/android/net/NetworkRequest.java index 3d9d6e29..bd4a27c2 100644 --- a/android/net/NetworkRequest.java +++ b/android/net/NetworkRequest.java @@ -168,9 +168,6 @@ public class NetworkRequest implements Parcelable { * the requested network's required capabilities. Note that when searching * for a network to satisfy a request, all capabilities requested must be * satisfied. - * <p> - * If the given capability was previously added to the list of unwanted capabilities - * then the capability will also be removed from the list of unwanted capabilities. * * @param capability The capability to add. * @return The builder to facilitate chaining @@ -182,8 +179,7 @@ public class NetworkRequest implements Parcelable { } /** - * Removes (if found) the given capability from this builder instance from both required - * and unwanted capabilities lists. + * Removes (if found) the given capability from this builder instance. * * @param capability The capability to remove. * @return The builder to facilitate chaining. @@ -231,6 +227,8 @@ public class NetworkRequest implements Parcelable { * * @param capability The capability to add to unwanted capability list. * @return The builder to facilitate chaining. + * + * @removed */ public Builder addUnwantedCapability(@NetworkCapabilities.NetCapability int capability) { mNetworkCapabilities.addUnwantedCapability(capability); @@ -436,6 +434,15 @@ public class NetworkRequest implements Parcelable { } /** + * @see Builder#addUnwantedCapability(int) + * + * @removed + */ + public boolean hasUnwantedCapability(@NetCapability int capability) { + return networkCapabilities.hasUnwantedCapability(capability); + } + + /** * @see Builder#addTransportType(int) */ public boolean hasTransport(@Transport int transportType) { diff --git a/android/net/NetworkState.java b/android/net/NetworkState.java index b00cb482..321f9718 100644 --- a/android/net/NetworkState.java +++ b/android/net/NetworkState.java @@ -26,6 +26,8 @@ import android.util.Slog; * @hide */ public class NetworkState implements Parcelable { + private static final boolean SANITY_CHECK_ROAMING = false; + public static final NetworkState EMPTY = new NetworkState(null, null, null, null, null, null); public final NetworkInfo networkInfo; @@ -47,7 +49,7 @@ public class NetworkState implements Parcelable { // This object is an atomic view of a network, so the various components // should always agree on roaming state. - if (networkInfo != null && networkCapabilities != null) { + if (SANITY_CHECK_ROAMING && networkInfo != null && networkCapabilities != null) { if (networkInfo.isRoaming() == networkCapabilities .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)) { Slog.wtf("NetworkState", "Roaming state disagreement between " + networkInfo diff --git a/android/net/apf/ApfFilter.java b/android/net/apf/ApfFilter.java index d1904322..d5ff2dd6 100644 --- a/android/net/apf/ApfFilter.java +++ b/android/net/apf/ApfFilter.java @@ -16,21 +16,21 @@ package android.net.apf; +import static android.net.util.NetworkConstants.*; import static android.system.OsConstants.*; - import static com.android.internal.util.BitUtils.bytesToBEInt; import static com.android.internal.util.BitUtils.getUint16; import static com.android.internal.util.BitUtils.getUint32; import static com.android.internal.util.BitUtils.getUint8; -import static com.android.internal.util.BitUtils.uint16; import static com.android.internal.util.BitUtils.uint32; -import static com.android.internal.util.BitUtils.uint8; -import android.os.SystemClock; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.net.LinkAddress; import android.net.LinkProperties; import android.net.NetworkUtils; -import android.net.apf.ApfGenerator; import android.net.apf.ApfGenerator.IllegalInstructionException; import android.net.apf.ApfGenerator.Register; import android.net.ip.IpClient; @@ -39,31 +39,29 @@ import android.net.metrics.ApfStats; import android.net.metrics.IpConnectivityLog; import android.net.metrics.RaEvent; import android.net.util.InterfaceParams; +import android.os.PowerManager; +import android.os.SystemClock; import android.system.ErrnoException; import android.system.Os; import android.system.PacketSocketAddress; import android.text.format.DateUtils; import android.util.Log; import android.util.Pair; - import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.HexDump; import com.android.internal.util.IndentingPrintWriter; - import java.io.FileDescriptor; import java.io.IOException; -import java.lang.Thread; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.SocketException; import java.net.UnknownHostException; -import java.nio.ByteBuffer; import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; - import libcore.io.IoBridge; /** @@ -215,10 +213,6 @@ public class ApfFilter { { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }; private static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN; - private static final int ICMP6_ROUTER_SOLICITATION = 133; - private static final int ICMP6_ROUTER_ADVERTISEMENT = 134; - private static final int ICMP6_NEIGHBOR_SOLICITATION = 135; - private static final int ICMP6_NEIGHBOR_ANNOUNCEMENT = 136; // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 2; @@ -258,9 +252,26 @@ public class ApfFilter { private long mUniqueCounter; @GuardedBy("this") private boolean mMulticastFilter; + @GuardedBy("this") + private boolean mInDozeMode; private final boolean mDrop802_3Frames; private final int[] mEthTypeBlackList; + // Detects doze mode state transitions. + private final BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)) { + PowerManager powerManager = + (PowerManager) context.getSystemService(Context.POWER_SERVICE); + final boolean deviceIdle = powerManager.isDeviceIdleMode(); + setDozeMode(deviceIdle); + } + } + }; + private final Context mContext; + // Our IPv4 address, if we have just one, otherwise null. @GuardedBy("this") private byte[] mIPv4Address; @@ -269,13 +280,14 @@ public class ApfFilter { private int mIPv4PrefixLength; @VisibleForTesting - ApfFilter(ApfConfiguration config, InterfaceParams ifParams, + ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams, IpClient.Callback ipClientCallback, IpConnectivityLog log) { mApfCapabilities = config.apfCapabilities; mIpClientCallback = ipClientCallback; mInterfaceParams = ifParams; mMulticastFilter = config.multicastFilter; mDrop802_3Frames = config.ieee802_3Filter; + mContext = context; // Now fill the black list from the passed array mEthTypeBlackList = filterEthTypeBlackList(config.ethTypeBlackList); @@ -284,6 +296,10 @@ public class ApfFilter { // TODO: ApfFilter should not generate programs until IpClient sends provisioning success. maybeStartFilter(); + + // Listen for doze-mode transition changes to enable/disable the IPv6 multicast filter. + mContext.registerReceiver(mDeviceIdleReceiver, + new IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)); } private void log(String s) { @@ -522,7 +538,7 @@ public class ApfFilter { // to our packet socket. b/29586253 if (getUint16(mPacket, ETH_ETHERTYPE_OFFSET) != ETH_P_IPV6 || getUint8(mPacket, IPV6_NEXT_HEADER_OFFSET) != IPPROTO_ICMPV6 || - getUint8(mPacket, ICMP6_TYPE_OFFSET) != ICMP6_ROUTER_ADVERTISEMENT) { + getUint8(mPacket, ICMP6_TYPE_OFFSET) != ICMPV6_ROUTER_ADVERTISEMENT) { throw new InvalidRaException("Not an ICMP6 router advertisement"); } @@ -889,10 +905,11 @@ public class ApfFilter { private void generateIPv6FilterLocked(ApfGenerator gen) throws IllegalInstructionException { // Here's a basic summary of what the IPv6 filter program does: // - // if it's not ICMPv6: - // if it's multicast and we're dropping multicast: - // drop - // pass + // if we're dropping multicast + // if it's not IPCMv6 or it's ICMPv6 but we're in doze mode: + // if it's multicast: + // drop + // pass // if it's ICMPv6 RS to any: // drop // if it's ICMPv6 NA to ff02::1: @@ -902,28 +919,44 @@ public class ApfFilter { // Drop multicast if the multicast filter is enabled. if (mMulticastFilter) { - // Don't touch ICMPv6 multicast here, we deal with it in more detail later. - String skipIpv6MulticastFilterLabel = "skipIPv6MulticastFilter"; - gen.addJumpIfR0Equals(IPPROTO_ICMPV6, skipIpv6MulticastFilterLabel); + final String skipIPv6MulticastFilterLabel = "skipIPv6MulticastFilter"; + final String dropAllIPv6MulticastsLabel = "dropAllIPv6Multicast"; + + // While in doze mode, drop ICMPv6 multicast pings, let the others pass. + // While awake, let all ICMPv6 multicasts through. + if (mInDozeMode) { + // Not ICMPv6? -> Proceed to multicast filtering + gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, dropAllIPv6MulticastsLabel); + + // ICMPv6 but not ECHO? -> Skip the multicast filter. + // (ICMPv6 ECHO requests will go through the multicast filter below). + gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET); + gen.addJumpIfR0NotEquals(ICMPV6_ECHO_REQUEST_TYPE, skipIPv6MulticastFilterLabel); + } else { + gen.addJumpIfR0Equals(IPPROTO_ICMPV6, skipIPv6MulticastFilterLabel); + } - // Drop all other packets sent to ff00::/8. + // Drop all other packets sent to ff00::/8 (multicast prefix). + gen.defineLabel(dropAllIPv6MulticastsLabel); gen.addLoad8(Register.R0, IPV6_DEST_ADDR_OFFSET); gen.addJumpIfR0Equals(0xff, gen.DROP_LABEL); - // Not multicast and not ICMPv6. Pass. + // Not multicast. Pass. gen.addJump(gen.PASS_LABEL); - gen.defineLabel(skipIpv6MulticastFilterLabel); + gen.defineLabel(skipIPv6MulticastFilterLabel); } else { // If not ICMPv6, pass. gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, gen.PASS_LABEL); } + // If we got this far, the packet is ICMPv6. Drop some specific types. + // Add unsolicited multicast neighbor announcements filter String skipUnsolicitedMulticastNALabel = "skipUnsolicitedMulticastNA"; gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET); // Drop all router solicitations (b/32833400) - gen.addJumpIfR0Equals(ICMP6_ROUTER_SOLICITATION, gen.DROP_LABEL); + gen.addJumpIfR0Equals(ICMPV6_ROUTER_SOLICITATION, gen.DROP_LABEL); // If not neighbor announcements, skip filter. - gen.addJumpIfR0NotEquals(ICMP6_NEIGHBOR_ANNOUNCEMENT, skipUnsolicitedMulticastNALabel); + gen.addJumpIfR0NotEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, skipUnsolicitedMulticastNALabel); // If to ff02::1, drop. // TODO: Drop only if they don't contain the address of on-link neighbours. gen.addLoadImmediate(Register.R0, IPV6_DEST_ADDR_OFFSET); @@ -1168,9 +1201,9 @@ public class ApfFilter { * Create an {@link ApfFilter} if {@code apfCapabilities} indicates support for packet * filtering using APF programs. */ - public static ApfFilter maybeCreate(ApfConfiguration config, + public static ApfFilter maybeCreate(Context context, ApfConfiguration config, InterfaceParams ifParams, IpClient.Callback ipClientCallback) { - if (config == null || ifParams == null) return null; + if (context == null || config == null || ifParams == null) return null; ApfCapabilities apfCapabilities = config.apfCapabilities; if (apfCapabilities == null) return null; if (apfCapabilities.apfVersionSupported == 0) return null; @@ -1187,7 +1220,8 @@ public class ApfFilter { Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported); return null; } - return new ApfFilter(config, ifParams, ipClientCallback, new IpConnectivityLog()); + + return new ApfFilter(context, config, ifParams, ipClientCallback, new IpConnectivityLog()); } public synchronized void shutdown() { @@ -1197,12 +1231,11 @@ public class ApfFilter { mReceiveThread = null; } mRas.clear(); + mContext.unregisterReceiver(mDeviceIdleReceiver); } public synchronized void setMulticastFilter(boolean isEnabled) { - if (mMulticastFilter == isEnabled) { - return; - } + if (mMulticastFilter == isEnabled) return; mMulticastFilter = isEnabled; if (!isEnabled) { mNumProgramUpdatesAllowingMulticast++; @@ -1210,6 +1243,13 @@ public class ApfFilter { installNewProgramLocked(); } + @VisibleForTesting + public synchronized void setDozeMode(boolean isEnabled) { + if (mInDozeMode == isEnabled) return; + mInDozeMode = isEnabled; + installNewProgramLocked(); + } + /** Find the single IPv4 LinkAddress if there is one, otherwise return null. */ private static LinkAddress findIPv4LinkAddress(LinkProperties lp) { LinkAddress ipv4Address = null; diff --git a/android/net/dns/ResolvUtil.java b/android/net/dns/ResolvUtil.java new file mode 100644 index 00000000..97d20f4b --- /dev/null +++ b/android/net/dns/ResolvUtil.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 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 android.net.dns; + +import android.net.Network; +import android.net.NetworkUtils; +import android.system.GaiException; +import android.system.OsConstants; +import android.system.StructAddrinfo; + +import libcore.io.Libcore; + +import java.net.InetAddress; +import java.net.UnknownHostException; + + +/** + * DNS resolution utility class. + * + * @hide + */ +public class ResolvUtil { + // Non-portable DNS resolution flag. + private static final long NETID_USE_LOCAL_NAMESERVERS = 0x80000000L; + + private ResolvUtil() {} + + public static InetAddress[] blockingResolveAllLocally(Network network, String name) + throws UnknownHostException { + final StructAddrinfo hints = new StructAddrinfo(); + // Unnecessary, but expressly no AI_ADDRCONFIG. + hints.ai_flags = 0; + // Fetch all IP addresses at once to minimize re-resolution. + hints.ai_family = OsConstants.AF_UNSPEC; + hints.ai_socktype = OsConstants.SOCK_DGRAM; + + final Network networkForResolv = getNetworkWithUseLocalNameserversFlag(network); + + try { + return Libcore.os.android_getaddrinfo(name, hints, (int) networkForResolv.netId); + } catch (GaiException gai) { + gai.rethrowAsUnknownHostException(name + ": TLS-bypass resolution failed"); + return null; // keep compiler quiet + } + } + + public static Network getNetworkWithUseLocalNameserversFlag(Network network) { + final long netidForResolv = NETID_USE_LOCAL_NAMESERVERS | (long) network.netId; + return new Network((int) netidForResolv); + } +} diff --git a/android/net/http/X509TrustManagerExtensions.java b/android/net/http/X509TrustManagerExtensions.java index e0fa63a5..f9b6dfce 100644 --- a/android/net/http/X509TrustManagerExtensions.java +++ b/android/net/http/X509TrustManagerExtensions.java @@ -21,7 +21,6 @@ import android.security.net.config.UserCertificateSource; import com.android.org.conscrypt.TrustManagerImpl; -import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.cert.CertificateException; @@ -133,8 +132,6 @@ public class X509TrustManagerExtensions { /** * Returns {@code true} if the TrustManager uses the same trust configuration for the provided * hostnames. - * - * @hide */ @SystemApi public boolean isSameTrustConfiguration(String hostname1, String hostname2) { diff --git a/android/net/ip/IpClient.java b/android/net/ip/IpClient.java index 9863370e..87249dfc 100644 --- a/android/net/ip/IpClient.java +++ b/android/net/ip/IpClient.java @@ -40,6 +40,7 @@ import android.net.util.MultinetworkPolicyTracker; import android.net.util.NetdService; import android.net.util.NetworkConstants; import android.net.util.SharedLog; +import android.os.ConditionVariable; import android.os.INetworkManagementService; import android.os.Message; import android.os.RemoteException; @@ -150,6 +151,28 @@ public class IpClient extends StateMachine { public void setNeighborDiscoveryOffload(boolean enable) {} } + public static class WaitForProvisioningCallback extends Callback { + private final ConditionVariable mCV = new ConditionVariable(); + private LinkProperties mCallbackLinkProperties; + + public LinkProperties waitForProvisioning() { + mCV.block(); + return mCallbackLinkProperties; + } + + @Override + public void onProvisioningSuccess(LinkProperties newLp) { + mCallbackLinkProperties = newLp; + mCV.open(); + } + + @Override + public void onProvisioningFailure(LinkProperties newLp) { + mCallbackLinkProperties = null; + mCV.open(); + } + } + // Use a wrapper class to log in order to ensure complete and detailed // logging. This method is lighter weight than annotations/reflection // and has the following benefits: @@ -281,6 +304,11 @@ public class IpClient extends StateMachine { return this; } + public Builder withoutMultinetworkPolicyTracker() { + mConfig.mUsingMultinetworkPolicyTracker = false; + return this; + } + public Builder withoutIpReachabilityMonitor() { mConfig.mUsingIpReachabilityMonitor = false; return this; @@ -343,6 +371,7 @@ public class IpClient extends StateMachine { /* package */ boolean mEnableIPv4 = true; /* package */ boolean mEnableIPv6 = true; + /* package */ boolean mUsingMultinetworkPolicyTracker = true; /* package */ boolean mUsingIpReachabilityMonitor = true; /* package */ int mRequestedPreDhcpActionMs; /* package */ InitialConfiguration mInitialConfig; @@ -374,6 +403,7 @@ public class IpClient extends StateMachine { return new StringJoiner(", ", getClass().getSimpleName() + "{", "}") .add("mEnableIPv4: " + mEnableIPv4) .add("mEnableIPv6: " + mEnableIPv6) + .add("mUsingMultinetworkPolicyTracker: " + mUsingMultinetworkPolicyTracker) .add("mUsingIpReachabilityMonitor: " + mUsingIpReachabilityMonitor) .add("mRequestedPreDhcpActionMs: " + mRequestedPreDhcpActionMs) .add("mInitialConfig: " + mInitialConfig) @@ -559,7 +589,6 @@ public class IpClient extends StateMachine { private final NetlinkTracker mNetlinkTracker; private final WakeupMessage mProvisioningTimeoutAlarm; private final WakeupMessage mDhcpActionTimeoutAlarm; - private final MultinetworkPolicyTracker mMultinetworkPolicyTracker; private final SharedLog mLog; private final LocalLog mConnectivityPacketLog; private final MessageHandlingLogger mMsgStateLogger; @@ -573,6 +602,7 @@ public class IpClient extends StateMachine { */ private LinkProperties mLinkProperties; private ProvisioningConfiguration mConfiguration; + private MultinetworkPolicyTracker mMultinetworkPolicyTracker; private IpReachabilityMonitor mIpReachabilityMonitor; private DhcpClient mDhcpClient; private DhcpResults mDhcpResults; @@ -685,9 +715,6 @@ public class IpClient extends StateMachine { mLinkProperties = new LinkProperties(); mLinkProperties.setInterfaceName(mInterfaceName); - mMultinetworkPolicyTracker = new MultinetworkPolicyTracker(mContext, getHandler(), - () -> { mLog.log("OBSERVED AvoidBadWifi changed"); }); - mProvisioningTimeoutAlarm = new WakeupMessage(mContext, getHandler(), mTag + ".EVENT_PROVISIONING_TIMEOUT", EVENT_PROVISIONING_TIMEOUT); mDhcpActionTimeoutAlarm = new WakeupMessage(mContext, getHandler(), @@ -719,8 +746,6 @@ public class IpClient extends StateMachine { } catch (RemoteException e) { logError("Couldn't register NetlinkTracker: %s", e); } - - mMultinetworkPolicyTracker.start(); } private void stopStateMachineUpdaters() { @@ -729,8 +754,6 @@ public class IpClient extends StateMachine { } catch (RemoteException e) { logError("Couldn't unregister NetlinkTracker: %s", e); } - - mMultinetworkPolicyTracker.shutdown(); } @Override @@ -1028,7 +1051,8 @@ public class IpClient extends StateMachine { // Note that we can still be disconnected by IpReachabilityMonitor // if the IPv6 default gateway (but not the IPv6 DNS servers; see // accompanying code in IpReachabilityMonitor) is unreachable. - final boolean ignoreIPv6ProvisioningLoss = !mMultinetworkPolicyTracker.getAvoidBadWifi(); + final boolean ignoreIPv6ProvisioningLoss = (mMultinetworkPolicyTracker != null) + && !mMultinetworkPolicyTracker.getAvoidBadWifi(); // Additionally: // @@ -1490,7 +1514,7 @@ public class IpClient extends StateMachine { mContext.getResources().getBoolean(R.bool.config_apfDrop802_3Frames); apfConfig.ethTypeBlackList = mContext.getResources().getIntArray(R.array.config_apfEthTypeBlackList); - mApfFilter = ApfFilter.maybeCreate(apfConfig, mInterfaceParams, mCallback); + mApfFilter = ApfFilter.maybeCreate(mContext, apfConfig, mInterfaceParams, mCallback); // TODO: investigate the effects of any multicast filtering racing/interfering with the // rest of this IP configuration startup. if (mApfFilter == null) { @@ -1520,6 +1544,13 @@ public class IpClient extends StateMachine { return; } + if (mConfiguration.mUsingMultinetworkPolicyTracker) { + mMultinetworkPolicyTracker = new MultinetworkPolicyTracker( + mContext, getHandler(), + () -> { mLog.log("OBSERVED AvoidBadWifi changed"); }); + mMultinetworkPolicyTracker.start(); + } + if (mConfiguration.mUsingIpReachabilityMonitor && !startIpReachabilityMonitor()) { doImmediateProvisioningFailure( IpManagerEvent.ERROR_STARTING_IPREACHABILITYMONITOR); @@ -1537,6 +1568,11 @@ public class IpClient extends StateMachine { mIpReachabilityMonitor = null; } + if (mMultinetworkPolicyTracker != null) { + mMultinetworkPolicyTracker.shutdown(); + mMultinetworkPolicyTracker = null; + } + if (mDhcpClient != null) { mDhcpClient.sendMessage(DhcpClient.CMD_STOP_DHCP); mDhcpClient.doQuit(); diff --git a/android/net/ip/IpManager.java b/android/net/ip/IpManager.java index 508a43d0..2eb36a22 100644 --- a/android/net/ip/IpManager.java +++ b/android/net/ip/IpManager.java @@ -114,35 +114,6 @@ public class IpManager extends IpClient { public static class Callback extends IpClient.Callback { } - public static class WaitForProvisioningCallback extends Callback { - private LinkProperties mCallbackLinkProperties; - - public LinkProperties waitForProvisioning() { - synchronized (this) { - try { - wait(); - } catch (InterruptedException e) {} - return mCallbackLinkProperties; - } - } - - @Override - public void onProvisioningSuccess(LinkProperties newLp) { - synchronized (this) { - mCallbackLinkProperties = newLp; - notify(); - } - } - - @Override - public void onProvisioningFailure(LinkProperties newLp) { - synchronized (this) { - mCallbackLinkProperties = null; - notify(); - } - } - } - public IpManager(Context context, String ifName, Callback callback) { super(context, ifName, callback); } diff --git a/android/net/metrics/ApfStats.java b/android/net/metrics/ApfStats.java index 3b0dc7ef..76a781dd 100644 --- a/android/net/metrics/ApfStats.java +++ b/android/net/metrics/ApfStats.java @@ -20,7 +20,7 @@ import android.os.Parcel; import android.os.Parcelable; /** - * An event logged for an interface with APF capabilities when its IpManager state machine exits. + * An event logged for an interface with APF capabilities when its IpClient state machine exits. * {@hide} */ public final class ApfStats implements Parcelable { diff --git a/android/net/util/NetworkConstants.java b/android/net/util/NetworkConstants.java index 984c9f81..53fd01f2 100644 --- a/android/net/util/NetworkConstants.java +++ b/android/net/util/NetworkConstants.java @@ -136,6 +136,8 @@ public final class NetworkConstants { * - https://tools.ietf.org/html/rfc4861 */ public static final int ICMPV6_HEADER_MIN_LEN = 4; + public static final int ICMPV6_ECHO_REQUEST_TYPE = 128; + public static final int ICMPV6_ECHO_REPLY_TYPE = 129; public static final int ICMPV6_ROUTER_SOLICITATION = 133; public static final int ICMPV6_ROUTER_ADVERTISEMENT = 134; public static final int ICMPV6_NEIGHBOR_SOLICITATION = 135; @@ -147,7 +149,6 @@ public final class NetworkConstants { public static final int ICMPV6_ND_OPTION_TLLA = 2; public static final int ICMPV6_ND_OPTION_MTU = 5; - public static final int ICMPV6_ECHO_REQUEST_TYPE = 128; /** * UDP constants. diff --git a/android/net/wifi/WifiConfiguration.java b/android/net/wifi/WifiConfiguration.java index b77b1ad5..f6c67c93 100644 --- a/android/net/wifi/WifiConfiguration.java +++ b/android/net/wifi/WifiConfiguration.java @@ -28,10 +28,12 @@ import android.net.Uri; import android.net.wifi.WifiInfo; import android.os.Parcel; import android.os.Parcelable; +import android.os.SystemClock; import android.os.UserHandle; import android.text.TextUtils; import android.util.BackupUtils; import android.util.Log; +import android.util.TimeUtils; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; @@ -611,37 +613,6 @@ public class WifiConfiguration implements Parcelable { /** * @hide - * Last time the system tried to connect and failed. - */ - public long lastConnectionFailure; - - /** - * @hide - * Last time the system tried to roam and failed because of authentication failure or DHCP - * RENEW failure. - */ - public long lastRoamingFailure; - - /** @hide */ - public static int ROAMING_FAILURE_IP_CONFIG = 1; - /** @hide */ - public static int ROAMING_FAILURE_AUTH_FAILURE = 2; - - /** - * @hide - * Initial amount of time this Wifi configuration gets blacklisted for network switching - * because of roaming failure - */ - public long roamingFailureBlackListTimeMilli = 1000; - - /** - * @hide - * Last roaming failure reason code - */ - public int lastRoamingFailureReason; - - /** - * @hide * Last time the system was disconnected to this configuration. */ public long lastDisconnected; @@ -1620,8 +1591,9 @@ public class WifiConfiguration implements Parcelable { } if (mNetworkSelectionStatus.getConnectChoice() != null) { sbuf.append(" connect choice: ").append(mNetworkSelectionStatus.getConnectChoice()); - sbuf.append(" connect choice set time: ").append(mNetworkSelectionStatus - .getConnectChoiceTimestamp()); + sbuf.append(" connect choice set time: ") + .append(TimeUtils.logTimeOfDay( + mNetworkSelectionStatus.getConnectChoiceTimestamp())); } sbuf.append(" hasEverConnected: ") .append(mNetworkSelectionStatus.getHasEverConnected()).append("\n"); @@ -1724,7 +1696,7 @@ public class WifiConfiguration implements Parcelable { sbuf.append(" networkSelectionBSSID=" + mNetworkSelectionStatus.getNetworkSelectionBSSID()); } - long now_ms = System.currentTimeMillis(); + long now_ms = SystemClock.elapsedRealtime(); if (mNetworkSelectionStatus.getDisableTime() != NetworkSelectionStatus .INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP) { sbuf.append('\n'); @@ -1746,35 +1718,9 @@ public class WifiConfiguration implements Parcelable { if (this.lastConnected != 0) { sbuf.append('\n'); - long diff = now_ms - this.lastConnected; - if (diff <= 0) { - sbuf.append("lastConnected since <incorrect>"); - } else { - sbuf.append("lastConnected: ").append(Long.toString(diff / 1000)).append("sec "); - } - } - if (this.lastConnectionFailure != 0) { - sbuf.append('\n'); - long diff = now_ms - this.lastConnectionFailure; - if (diff <= 0) { - sbuf.append("lastConnectionFailure since <incorrect> "); - } else { - sbuf.append("lastConnectionFailure: ").append(Long.toString(diff / 1000)); - sbuf.append("sec "); - } - } - if (this.lastRoamingFailure != 0) { - sbuf.append('\n'); - long diff = now_ms - this.lastRoamingFailure; - if (diff <= 0) { - sbuf.append("lastRoamingFailure since <incorrect> "); - } else { - sbuf.append("lastRoamingFailure: ").append(Long.toString(diff / 1000)); - sbuf.append("sec "); - } + sbuf.append("lastConnected: ").append(TimeUtils.logTimeOfDay(this.lastConnected)); + sbuf.append(" "); } - sbuf.append("roamingFailureBlackListTimeMilli: "). - append(Long.toString(this.roamingFailureBlackListTimeMilli)); sbuf.append('\n'); if (this.linkedConfigurations != null) { for (String key : this.linkedConfigurations.keySet()) { @@ -2119,10 +2065,6 @@ public class WifiConfiguration implements Parcelable { lastConnected = source.lastConnected; lastDisconnected = source.lastDisconnected; - lastConnectionFailure = source.lastConnectionFailure; - lastRoamingFailure = source.lastRoamingFailure; - lastRoamingFailureReason = source.lastRoamingFailureReason; - roamingFailureBlackListTimeMilli = source.roamingFailureBlackListTimeMilli; numScorerOverride = source.numScorerOverride; numScorerOverrideAndSwitchedNetwork = source.numScorerOverrideAndSwitchedNetwork; numAssociation = source.numAssociation; @@ -2188,10 +2130,6 @@ public class WifiConfiguration implements Parcelable { dest.writeInt(lastUpdateUid); dest.writeString(creatorName); dest.writeString(lastUpdateName); - dest.writeLong(lastConnectionFailure); - dest.writeLong(lastRoamingFailure); - dest.writeInt(lastRoamingFailureReason); - dest.writeLong(roamingFailureBlackListTimeMilli); dest.writeInt(numScorerOverride); dest.writeInt(numScorerOverrideAndSwitchedNetwork); dest.writeInt(numAssociation); @@ -2257,10 +2195,6 @@ public class WifiConfiguration implements Parcelable { config.lastUpdateUid = in.readInt(); config.creatorName = in.readString(); config.lastUpdateName = in.readString(); - config.lastConnectionFailure = in.readLong(); - config.lastRoamingFailure = in.readLong(); - config.lastRoamingFailureReason = in.readInt(); - config.roamingFailureBlackListTimeMilli = in.readLong(); config.numScorerOverride = in.readInt(); config.numScorerOverrideAndSwitchedNetwork = in.readInt(); config.numAssociation = in.readInt(); diff --git a/android/net/wifi/WifiManager.java b/android/net/wifi/WifiManager.java index 433285bf..9c6c8a90 100644 --- a/android/net/wifi/WifiManager.java +++ b/android/net/wifi/WifiManager.java @@ -2141,7 +2141,8 @@ public class WifiManager { } /** - * Sets the Wi-Fi AP Configuration. + * Sets the Wi-Fi AP Configuration. The AP configuration must either be open or + * WPA2 PSK networks. * @return {@code true} if the operation succeeded, {@code false} otherwise * * @hide @@ -2150,8 +2151,7 @@ public class WifiManager { @RequiresPermission(android.Manifest.permission.CHANGE_WIFI_STATE) public boolean setWifiApConfiguration(WifiConfiguration wifiConfig) { try { - mService.setWifiApConfiguration(wifiConfig, mContext.getOpPackageName()); - return true; + return mService.setWifiApConfiguration(wifiConfig, mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/android/os/BatteryManager.java b/android/os/BatteryManager.java index 63631618..954071a0 100644 --- a/android/os/BatteryManager.java +++ b/android/os/BatteryManager.java @@ -353,4 +353,20 @@ public class BatteryManager { public static boolean isPlugWired(int plugType) { return plugType == BATTERY_PLUGGED_USB || plugType == BATTERY_PLUGGED_AC; } + + /** + * Compute an approximation for how much time (in milliseconds) remains until the battery is + * fully charged. Returns -1 if no time can be computed: either there is not enough current + * data to make a decision or the battery is currently discharging. + * + * @return how much time is left, in milliseconds, until the battery is fully charged or -1 if + * the computation fails + */ + public long computeChargeTimeRemaining() { + try { + return mBatteryStats.computeChargeTimeRemaining(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/android/os/BatteryStats.java b/android/os/BatteryStats.java index 6ebb1026..1d232bfc 100644 --- a/android/os/BatteryStats.java +++ b/android/os/BatteryStats.java @@ -3814,6 +3814,9 @@ public abstract class BatteryStats implements Parcelable { final BatterySipper bs = sippers.get(i); String label; switch (bs.drainType) { + case AMBIENT_DISPLAY: + label = "ambi"; + break; case IDLE: label="idle"; break; @@ -4975,6 +4978,9 @@ public abstract class BatteryStats implements Parcelable { final BatterySipper bs = sippers.get(i); pw.print(prefix); switch (bs.drainType) { + case AMBIENT_DISPLAY: + pw.print(" Ambient display: "); + break; case IDLE: pw.print(" Idle: "); break; @@ -7777,6 +7783,9 @@ public abstract class BatteryStats implements Parcelable { int n = SystemProto.PowerUseItem.UNKNOWN_SIPPER; int uid = 0; switch (bs.drainType) { + case AMBIENT_DISPLAY: + n = SystemProto.PowerUseItem.AMBIENT_DISPLAY; + break; case IDLE: n = SystemProto.PowerUseItem.IDLE; break; diff --git a/android/os/Build.java b/android/os/Build.java index 8378a829..7162b8ad 100644 --- a/android/os/Build.java +++ b/android/os/Build.java @@ -907,6 +907,8 @@ public class Build { * <li>{@link android.app.Service#startForeground Service.startForeground} requires * that apps hold the permission * {@link android.Manifest.permission#FOREGROUND_SERVICE}.</li> + * <li>{@link android.widget.LinearLayout} will always remeasure weighted children, + * even if there is no excess space.</li> * </ul> */ public static final int P = CUR_DEVELOPMENT; // STOPSHIP Replace with the real version. diff --git a/android/os/DeviceIdleManager.java b/android/os/DeviceIdleManager.java new file mode 100644 index 00000000..9039f921 --- /dev/null +++ b/android/os/DeviceIdleManager.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 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 android.os; + +import android.annotation.NonNull; +import android.annotation.SystemService; +import android.annotation.TestApi; +import android.content.Context; + +/** + * Access to the service that keeps track of device idleness and drives low power mode based on + * that. + * + * @hide + */ +@TestApi +@SystemService(Context.DEVICE_IDLE_CONTROLLER) +public class DeviceIdleManager { + private final Context mContext; + private final IDeviceIdleController mService; + + /** + * @hide + */ + public DeviceIdleManager(@NonNull Context context, @NonNull IDeviceIdleController service) { + mContext = context; + mService = service; + } + + /** + * @return package names the system has white-listed to opt out of power save restrictions, + * except for device idle mode. + */ + public @NonNull String[] getSystemPowerWhitelistExceptIdle() { + try { + return mService.getSystemPowerWhitelistExceptIdle(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + return new String[0]; + } + } + + /** + * @return package names the system has white-listed to opt out of power save restrictions for + * all modes. + */ + public @NonNull String[] getSystemPowerWhitelist() { + try { + return mService.getSystemPowerWhitelist(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + return new String[0]; + } + } +} diff --git a/android/os/Environment.java b/android/os/Environment.java index 03203d05..213260fa 100644 --- a/android/os/Environment.java +++ b/android/os/Environment.java @@ -16,6 +16,7 @@ package android.os; +import android.annotation.TestApi; import android.app.admin.DevicePolicyManager; import android.content.Context; import android.os.storage.StorageManager; @@ -1033,6 +1034,7 @@ public class Environment { * * @hide */ + @TestApi public static File buildPath(File base, String... segments) { File cur = base; for (String segment : segments) { diff --git a/android/os/MessageQueue.java b/android/os/MessageQueue.java index 96e7a598..b1c33c2d 100644 --- a/android/os/MessageQueue.java +++ b/android/os/MessageQueue.java @@ -254,6 +254,7 @@ public final class MessageQueue { } else if (record != null) { record.mEvents = 0; mFileDescriptorRecords.removeAt(index); + nativeSetFileDescriptorEvents(mPtr, fdNum, 0); } } diff --git a/android/os/Parcel.java b/android/os/Parcel.java index e3c48700..51429287 100644 --- a/android/os/Parcel.java +++ b/android/os/Parcel.java @@ -1857,26 +1857,7 @@ public final class Parcel { int code = readExceptionCode(); if (code != 0) { String msg = readString(); - String remoteStackTrace = null; - final int remoteStackPayloadSize = readInt(); - if (remoteStackPayloadSize > 0) { - remoteStackTrace = readString(); - } - Exception e = createException(code, msg); - // Attach remote stack trace if availalble - if (remoteStackTrace != null) { - RemoteException cause = new RemoteException( - "Remote stack trace:\n" + remoteStackTrace, null, false, false); - try { - Throwable rootCause = ExceptionUtils.getRootCause(e); - if (rootCause != null) { - rootCause.initCause(cause); - } - } catch (RuntimeException ex) { - Log.e(TAG, "Cannot set cause " + cause + " for " + e, ex); - } - } - SneakyThrow.sneakyThrow(e); + readException(code, msg); } } @@ -1921,7 +1902,26 @@ public final class Parcel { * @param msg The exception message. */ public final void readException(int code, String msg) { - SneakyThrow.sneakyThrow(createException(code, msg)); + String remoteStackTrace = null; + final int remoteStackPayloadSize = readInt(); + if (remoteStackPayloadSize > 0) { + remoteStackTrace = readString(); + } + Exception e = createException(code, msg); + // Attach remote stack trace if availalble + if (remoteStackTrace != null) { + RemoteException cause = new RemoteException( + "Remote stack trace:\n" + remoteStackTrace, null, false, false); + try { + Throwable rootCause = ExceptionUtils.getRootCause(e); + if (rootCause != null) { + rootCause.initCause(cause); + } + } catch (RuntimeException ex) { + Log.e(TAG, "Cannot set cause " + cause + " for " + e, ex); + } + } + SneakyThrow.sneakyThrow(e); } /** diff --git a/android/os/ServiceManager.java b/android/os/ServiceManager.java index 34c78455..165276d5 100644 --- a/android/os/ServiceManager.java +++ b/android/os/ServiceManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 The Android Open Source Project + * Copyright (C) 2007 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. @@ -16,9 +16,98 @@ package android.os; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.os.BinderInternal; +import com.android.internal.util.StatLogger; + +import java.util.HashMap; import java.util.Map; +/** @hide */ public final class ServiceManager { + private static final String TAG = "ServiceManager"; + private static final Object sLock = new Object(); + + private static IServiceManager sServiceManager; + + /** + * Cache for the "well known" services, such as WM and AM. + */ + private static HashMap<String, IBinder> sCache = new HashMap<String, IBinder>(); + + /** + * We do the "slow log" at most once every this interval. + */ + private static final int SLOW_LOG_INTERVAL_MS = 5000; + + /** + * We do the "stats log" at most once every this interval. + */ + private static final int STATS_LOG_INTERVAL_MS = 5000; + + /** + * Threshold in uS for a "slow" call, used on core UIDs. We use a more relax value to + * avoid logspam. + */ + private static final long GET_SERVICE_SLOW_THRESHOLD_US_CORE = + SystemProperties.getInt("debug.servicemanager.slow_call_core_ms", 10) * 1000; + + /** + * Threshold in uS for a "slow" call, used on non-core UIDs. We use a more relax value to + * avoid logspam. + */ + private static final long GET_SERVICE_SLOW_THRESHOLD_US_NON_CORE = + SystemProperties.getInt("debug.servicemanager.slow_call_ms", 50) * 1000; + + /** + * We log stats logging ever this many getService() calls. + */ + private static final int GET_SERVICE_LOG_EVERY_CALLS_CORE = + SystemProperties.getInt("debug.servicemanager.log_calls_core", 100); + + /** + * We log stats logging ever this many getService() calls. + */ + private static final int GET_SERVICE_LOG_EVERY_CALLS_NON_CORE = + SystemProperties.getInt("debug.servicemanager.log_calls", 200); + + @GuardedBy("sLock") + private static int sGetServiceAccumulatedUs; + + @GuardedBy("sLock") + private static int sGetServiceAccumulatedCallCount; + + @GuardedBy("sLock") + private static long sLastStatsLogUptime; + + @GuardedBy("sLock") + private static long sLastSlowLogUptime; + + @GuardedBy("sLock") + private static long sLastSlowLogActualTime; + + interface Stats { + int GET_SERVICE = 0; + + int COUNT = GET_SERVICE + 1; + } + + public static final StatLogger sStatLogger = new StatLogger(new String[] { + "getService()", + }); + + private static IServiceManager getIServiceManager() { + if (sServiceManager != null) { + return sServiceManager; + } + + // Find the service manager + sServiceManager = ServiceManagerNative + .asInterface(Binder.allowBlocking(BinderInternal.getContextObject())); + return sServiceManager; + } /** * Returns a reference to a service with the given name. @@ -27,14 +116,32 @@ public final class ServiceManager { * @return a reference to the service, or <code>null</code> if the service doesn't exist */ public static IBinder getService(String name) { + try { + IBinder service = sCache.get(name); + if (service != null) { + return service; + } else { + return Binder.allowBlocking(rawGetService(name)); + } + } catch (RemoteException e) { + Log.e(TAG, "error in getService", e); + } return null; } /** - * Is not supposed to return null, but that is fine for layoutlib. + * Returns a reference to a service with the given name, or throws + * {@link NullPointerException} if none is found. + * + * @hide */ public static IBinder getServiceOrThrow(String name) throws ServiceNotFoundException { - throw new ServiceNotFoundException(name); + final IBinder binder = getService(name); + if (binder != null) { + return binder; + } else { + throw new ServiceNotFoundException(name); + } } /** @@ -45,7 +152,39 @@ public final class ServiceManager { * @param service the service object */ public static void addService(String name, IBinder service) { - // pass + addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT); + } + + /** + * Place a new @a service called @a name into the service + * manager. + * + * @param name the name of the new service + * @param service the service object + * @param allowIsolated set to true to allow isolated sandboxed processes + * to access this service + */ + public static void addService(String name, IBinder service, boolean allowIsolated) { + addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT); + } + + /** + * Place a new @a service called @a name into the service + * manager. + * + * @param name the name of the new service + * @param service the service object + * @param allowIsolated set to true to allow isolated sandboxed processes + * @param dumpPriority supported dump priority levels as a bitmask + * to access this service + */ + public static void addService(String name, IBinder service, boolean allowIsolated, + int dumpPriority) { + try { + getIServiceManager().addService(name, service, allowIsolated, dumpPriority); + } catch (RemoteException e) { + Log.e(TAG, "error in addService", e); + } } /** @@ -53,7 +192,17 @@ public final class ServiceManager { * service manager. Non-blocking. */ public static IBinder checkService(String name) { - return null; + try { + IBinder service = sCache.get(name); + if (service != null) { + return service; + } else { + return Binder.allowBlocking(getIServiceManager().checkService(name)); + } + } catch (RemoteException e) { + Log.e(TAG, "error in checkService", e); + return null; + } } /** @@ -62,9 +211,12 @@ public final class ServiceManager { * case of an exception */ public static String[] listServices() { - // actual implementation returns null sometimes, so it's ok - // to return null instead of an empty list. - return null; + try { + return getIServiceManager().listServices(IServiceManager.DUMP_FLAG_PRIORITY_ALL); + } catch (RemoteException e) { + Log.e(TAG, "error in listServices", e); + return null; + } } /** @@ -76,7 +228,10 @@ public final class ServiceManager { * @hide */ public static void initServiceCache(Map<String, IBinder> cache) { - // pass + if (sCache.size() != 0) { + throw new IllegalStateException("setServiceCache may only be called once"); + } + sCache.putAll(cache); } /** @@ -87,9 +242,63 @@ public final class ServiceManager { * @hide */ public static class ServiceNotFoundException extends Exception { - // identical to the original implementation public ServiceNotFoundException(String name) { super("No service published for: " + name); } } + + private static IBinder rawGetService(String name) throws RemoteException { + final long start = sStatLogger.getTime(); + + final IBinder binder = getIServiceManager().getService(name); + + final int time = (int) sStatLogger.logDurationStat(Stats.GET_SERVICE, start); + + final int myUid = Process.myUid(); + final boolean isCore = UserHandle.isCore(myUid); + + final long slowThreshold = isCore + ? GET_SERVICE_SLOW_THRESHOLD_US_CORE + : GET_SERVICE_SLOW_THRESHOLD_US_NON_CORE; + + synchronized (sLock) { + sGetServiceAccumulatedUs += time; + sGetServiceAccumulatedCallCount++; + + final long nowUptime = SystemClock.uptimeMillis(); + + // Was a slow call? + if (time >= slowThreshold) { + // We do a slow log: + // - At most once in every SLOW_LOG_INTERVAL_MS + // - OR it was slower than the previously logged slow call. + if ((nowUptime > (sLastSlowLogUptime + SLOW_LOG_INTERVAL_MS)) + || (sLastSlowLogActualTime < time)) { + EventLogTags.writeServiceManagerSlow(time / 1000, name); + + sLastSlowLogUptime = nowUptime; + sLastSlowLogActualTime = time; + } + } + + // Every GET_SERVICE_LOG_EVERY_CALLS calls, log the total time spent in getService(). + + final int logInterval = isCore + ? GET_SERVICE_LOG_EVERY_CALLS_CORE + : GET_SERVICE_LOG_EVERY_CALLS_NON_CORE; + + if ((sGetServiceAccumulatedCallCount >= logInterval) + && (nowUptime >= (sLastStatsLogUptime + STATS_LOG_INTERVAL_MS))) { + + EventLogTags.writeServiceManagerStats( + sGetServiceAccumulatedCallCount, // Total # of getService() calls. + sGetServiceAccumulatedUs / 1000, // Total time spent in getService() calls. + (int) (nowUptime - sLastStatsLogUptime)); // Uptime duration since last log. + sGetServiceAccumulatedCallCount = 0; + sGetServiceAccumulatedUs = 0; + sLastStatsLogUptime = nowUptime; + } + } + return binder; + } } diff --git a/android/os/StrictMode.java b/android/os/StrictMode.java index a93e25aa..59380fd3 100644 --- a/android/os/StrictMode.java +++ b/android/os/StrictMode.java @@ -39,6 +39,7 @@ import android.os.strictmode.InstanceCountViolation; import android.os.strictmode.IntentReceiverLeakedViolation; import android.os.strictmode.LeakedClosableViolation; import android.os.strictmode.NetworkViolation; +import android.os.strictmode.NonSdkApiUsedViolation; import android.os.strictmode.ResourceMismatchViolation; import android.os.strictmode.ServiceConnectionLeakedViolation; import android.os.strictmode.SqliteObjectLeakedViolation; @@ -76,6 +77,7 @@ import java.util.HashMap; import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; /** * StrictMode is a developer tool which detects things you might be doing by accident and brings @@ -262,6 +264,9 @@ public final class StrictMode { /** @hide */ @TestApi public static final int DETECT_VM_UNTAGGED_SOCKET = 0x80 << 24; // for VmPolicy + /** @hide */ + @TestApi public static final int DETECT_VM_NON_SDK_API_USAGE = 0x40 << 24; // for VmPolicy + private static final int ALL_VM_DETECT_BITS = DETECT_VM_CURSOR_LEAKS | DETECT_VM_CLOSABLE_LEAKS @@ -271,7 +276,9 @@ public final class StrictMode { | DETECT_VM_FILE_URI_EXPOSURE | DETECT_VM_CLEARTEXT_NETWORK | DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION - | DETECT_VM_UNTAGGED_SOCKET; + | DETECT_VM_UNTAGGED_SOCKET + | DETECT_VM_NON_SDK_API_USAGE; + // Byte 3: Penalty @@ -413,6 +420,13 @@ public final class StrictMode { */ private static final AtomicInteger sDropboxCallsInFlight = new AtomicInteger(0); + /** + * Callback supplied to dalvik / libcore to get informed of usages of java API that are not + * a part of the public SDK. + */ + private static final Consumer<String> sNonSdkApiUsageConsumer = + message -> onVmPolicyViolation(new NonSdkApiUsedViolation(message)); + private StrictMode() {} /** @@ -796,6 +810,23 @@ public final class StrictMode { } /** + * Detect reflective usage of APIs that are not part of the public Android SDK. + */ + public Builder detectNonSdkApiUsage() { + return enable(DETECT_VM_NON_SDK_API_USAGE); + } + + /** + * Permit reflective usage of APIs that are not part of the public Android SDK. Note + * that this <b>only</b> affects {@code StrictMode}, the underlying runtime may + * continue to restrict or warn on access to methods that are not part of the + * public SDK. + */ + public Builder permitNonSdkApiUsage() { + return disable(DETECT_VM_NON_SDK_API_USAGE); + } + + /** * Detect everything that's potentially suspect. * * <p>In the Honeycomb release this includes leaks of SQLite cursors, Activities, and @@ -826,6 +857,8 @@ public final class StrictMode { detectContentUriWithoutPermission(); detectUntaggedSockets(); } + + // TODO: Decide whether to detect non SDK API usage beyond a certain API level. return this; } @@ -1848,6 +1881,13 @@ public final class StrictMode { } else if (networkPolicy != NETWORK_POLICY_ACCEPT) { Log.w(TAG, "Dropping requested network policy due to missing service!"); } + + + if ((sVmPolicy.mask & DETECT_VM_NON_SDK_API_USAGE) != 0) { + VMRuntime.setNonSdkApiUsageConsumer(sNonSdkApiUsageConsumer); + } else { + VMRuntime.setNonSdkApiUsageConsumer(null); + } } } @@ -2576,6 +2616,8 @@ public final class StrictMode { return DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION; } else if (mViolation instanceof UntaggedSocketViolation) { return DETECT_VM_UNTAGGED_SOCKET; + } else if (mViolation instanceof NonSdkApiUsedViolation) { + return DETECT_VM_NON_SDK_API_USAGE; } throw new IllegalStateException("missing violation bit"); } diff --git a/android/os/SystemProperties.java b/android/os/SystemProperties.java index 8eb39c02..7d3ba6a3 100644 --- a/android/os/SystemProperties.java +++ b/android/os/SystemProperties.java @@ -19,6 +19,7 @@ package android.os; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.util.Log; import android.util.MutableInt; @@ -35,6 +36,7 @@ import java.util.HashMap; * {@hide} */ @SystemApi +@TestApi public class SystemProperties { private static final String TAG = "SystemProperties"; private static final boolean TRACK_KEY_ACCESS = false; @@ -110,6 +112,7 @@ public class SystemProperties { */ @NonNull @SystemApi + @TestApi public static String get(@NonNull String key, @Nullable String def) { if (TRACK_KEY_ACCESS) onKeyAccess(key); return native_get(key, def); diff --git a/android/os/UserHandle.java b/android/os/UserHandle.java index 094f0046..4d4f31de 100644 --- a/android/os/UserHandle.java +++ b/android/os/UserHandle.java @@ -82,6 +82,7 @@ public final class UserHandle implements Parcelable { public static final int USER_SERIAL_SYSTEM = 0; /** @hide A user handle to indicate the "system" user of the device */ + @TestApi public static final UserHandle SYSTEM = new UserHandle(USER_SYSTEM); /** diff --git a/android/os/UserManager.java b/android/os/UserManager.java index a9eb3600..9b20ed2e 100644 --- a/android/os/UserManager.java +++ b/android/os/UserManager.java @@ -1149,6 +1149,7 @@ public class UserManager { * primary user are two separate users. Previously system user and primary user are combined as * a single owner user. see @link {android.os.UserHandle#USER_OWNER} */ + @TestApi public static boolean isSplitSystemUser() { return RoSystemProperties.FW_SYSTEM_USER_SPLIT; } diff --git a/android/os/WorkSource.java b/android/os/WorkSource.java index 17d83db6..32707190 100644 --- a/android/os/WorkSource.java +++ b/android/os/WorkSource.java @@ -924,13 +924,17 @@ public class WorkSource implements Parcelable { /** @hide */ @VisibleForTesting public int[] getUids() { - return mUids; + int[] uids = new int[mSize]; + System.arraycopy(mUids, 0, uids, 0, mSize); + return uids; } /** @hide */ @VisibleForTesting public String[] getTags() { - return mTags; + String[] tags = new String[mSize]; + System.arraycopy(mTags, 0, tags, 0, mSize); + return tags; } /** @hide */ diff --git a/android/os/ZygoteProcess.java b/android/os/ZygoteProcess.java index b9dd376f..673da507 100644 --- a/android/os/ZygoteProcess.java +++ b/android/os/ZygoteProcess.java @@ -166,6 +166,11 @@ public class ZygoteProcess { private List<String> mApiBlacklistExemptions = Collections.emptyList(); /** + * Proportion of hidden API accesses that should be logged to the event log; 0 - 0x10000. + */ + private int mHiddenApiAccessLogSampleRate; + + /** * The state of the connection to the primary zygote. */ private ZygoteState primaryZygoteState; @@ -467,7 +472,8 @@ public class ZygoteProcess { * <p>The list of exemptions will take affect for all new processes forked from the zygote after * this call. * - * @param exemptions List of hidden API exemption prefixes. + * @param exemptions List of hidden API exemption prefixes. Any matching members are treated as + * whitelisted/public APIs (i.e. allowed, no logging of usage). */ public void setApiBlacklistExemptions(List<String> exemptions) { synchronized (mLock) { @@ -477,6 +483,21 @@ public class ZygoteProcess { } } + /** + * Set the precentage of detected hidden API accesses that are logged to the event log. + * + * <p>This rate will take affect for all new processes forked from the zygote after this call. + * + * @param rate An integer between 0 and 0x10000 inclusive. 0 means no event logging. + */ + public void setHiddenApiAccessLogSampleRate(int rate) { + synchronized (mLock) { + mHiddenApiAccessLogSampleRate = rate; + maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState); + maybeSetHiddenApiAccessLogSampleRate(secondaryZygoteState); + } + } + @GuardedBy("mLock") private void maybeSetApiBlacklistExemptions(ZygoteState state, boolean sendIfEmpty) { if (state == null || state.isClosed()) { @@ -504,6 +525,29 @@ public class ZygoteProcess { } } + private void maybeSetHiddenApiAccessLogSampleRate(ZygoteState state) { + if (state == null || state.isClosed()) { + return; + } + if (mHiddenApiAccessLogSampleRate == -1) { + return; + } + try { + state.writer.write(Integer.toString(1)); + state.writer.newLine(); + state.writer.write("--hidden-api-log-sampling-rate=" + + Integer.toString(mHiddenApiAccessLogSampleRate)); + state.writer.newLine(); + state.writer.flush(); + int status = state.inputStream.readInt(); + if (status != 0) { + Slog.e(LOG_TAG, "Failed to set hidden API log sampling rate; status " + status); + } + } catch (IOException ioe) { + Slog.e(LOG_TAG, "Failed to set hidden API log sampling rate", ioe); + } + } + /** * Tries to open socket to Zygote process if not already open. If * already open, does nothing. May block and retry. Requires that mLock be held. @@ -519,6 +563,7 @@ public class ZygoteProcess { throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe); } maybeSetApiBlacklistExemptions(primaryZygoteState, false); + maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState); } if (primaryZygoteState.matches(abi)) { return primaryZygoteState; @@ -532,6 +577,7 @@ public class ZygoteProcess { throw new ZygoteStartFailedEx("Error connecting to secondary zygote", ioe); } maybeSetApiBlacklistExemptions(secondaryZygoteState, false); + maybeSetHiddenApiAccessLogSampleRate(secondaryZygoteState); } if (secondaryZygoteState.matches(abi)) { diff --git a/android/os/storage/DiskInfo.java b/android/os/storage/DiskInfo.java index 91141074..d493cceb 100644 --- a/android/os/storage/DiskInfo.java +++ b/android/os/storage/DiskInfo.java @@ -17,6 +17,7 @@ package android.os.storage; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.res.Resources; import android.os.Parcel; import android.os.Parcelable; @@ -93,7 +94,7 @@ public class DiskInfo implements Parcelable { return true; } - public String getDescription() { + public @Nullable String getDescription() { final Resources res = Resources.getSystem(); if ((flags & FLAG_SD) != 0) { if (isInteresting(label)) { @@ -112,6 +113,17 @@ public class DiskInfo implements Parcelable { } } + public @Nullable String getShortDescription() { + final Resources res = Resources.getSystem(); + if (isSd()) { + return res.getString(com.android.internal.R.string.storage_sd_card); + } else if (isUsb()) { + return res.getString(com.android.internal.R.string.storage_usb_drive); + } else { + return null; + } + } + public boolean isAdoptable() { return (flags & FLAG_ADOPTABLE) != 0; } diff --git a/android/security/keystore/SessionExpiredException.java b/android/os/strictmode/NonSdkApiUsedViolation.java index 7c8d5e4f..2f0cb50c 100644 --- a/android/security/keystore/SessionExpiredException.java +++ b/android/os/strictmode/NonSdkApiUsedViolation.java @@ -14,14 +14,15 @@ * limitations under the License. */ -package android.security.keystore; +package android.os.strictmode; /** - * @deprecated Use {@link android.security.keystore.recovery.SessionExpiredException}. - * @hide + * Subclass of {@code Violation} that is used when a process accesses + * a non SDK API. */ -public class SessionExpiredException extends RecoveryControllerException { - public SessionExpiredException(String msg) { - super(msg); +public final class NonSdkApiUsedViolation extends Violation { + /** @hide */ + public NonSdkApiUsedViolation(String message) { + super(message); } } diff --git a/android/provider/Settings.java b/android/provider/Settings.java index 68fc6c16..5b7adf01 100644 --- a/android/provider/Settings.java +++ b/android/provider/Settings.java @@ -1179,6 +1179,23 @@ public final class Settings { public static final String ACTION_ZEN_MODE_SETTINGS = "android.settings.ZEN_MODE_SETTINGS"; /** + * Activity Action: Show Zen Mode visual effects configuration settings. + * + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ZEN_MODE_BLOCKED_EFFECTS_SETTINGS = + "android.settings.ZEN_MODE_BLOCKED_EFFECTS_SETTINGS"; + + /** + * Activity Action: Show Zen Mode onboarding activity. + * + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ZEN_MODE_ONBOARDING = "android.settings.ZEN_MODE_ONBOARDING"; + + /** * Activity Action: Show Zen Mode (aka Do Not Disturb) priority configuration settings. */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) @@ -3113,6 +3130,9 @@ public final class Settings { */ public static final String DISPLAY_COLOR_MODE = "display_color_mode"; + private static final Validator DISPLAY_COLOR_MODE_VALIDATOR = + new SettingsValidators.InclusiveIntegerRangeValidator(0, 2); + /** * The amount of time in milliseconds before the device goes to sleep or begins * to dream after a period of inactivity. This value is also known as the @@ -3133,9 +3153,6 @@ public final class Settings { */ public static final String SCREEN_BRIGHTNESS = "screen_brightness"; - private static final Validator SCREEN_BRIGHTNESS_VALIDATOR = - new SettingsValidators.InclusiveIntegerRangeValidator(0, 255); - /** * The screen backlight brightness between 0 and 255. * @hide @@ -3753,17 +3770,6 @@ public final class Settings { new SettingsValidators.InclusiveIntegerRangeValidator(0, 3); /** - * User-selected RTT mode. When on, outgoing and incoming calls will be answered as RTT - * calls when supported by the device and carrier. Boolean value. - * 0 = OFF - * 1 = ON - */ - public static final String RTT_CALLING_MODE = "rtt_calling_mode"; - - /** @hide */ - public static final Validator RTT_CALLING_MODE_VALIDATOR = BOOLEAN_VALIDATOR; - - /** * Whether the sounds effects (key clicks, lid open ...) are enabled. The value is * boolean (1 or 0). */ @@ -4071,7 +4077,6 @@ public final class Settings { FONT_SCALE, DIM_SCREEN, SCREEN_OFF_TIMEOUT, - SCREEN_BRIGHTNESS, SCREEN_BRIGHTNESS_MODE, SCREEN_AUTO_BRIGHTNESS_ADJ, SCREEN_BRIGHTNESS_FOR_VR, @@ -4088,7 +4093,6 @@ public final class Settings { DTMF_TONE_WHEN_DIALING, DTMF_TONE_TYPE_WHEN_DIALING, HEARING_AID, - RTT_CALLING_MODE, TTY_MODE, MASTER_MONO, SOUND_EFFECTS_ENABLED, @@ -4108,6 +4112,7 @@ public final class Settings { SHOW_BATTERY_PERCENT, NOTIFICATION_VIBRATION_INTENSITY, HAPTIC_FEEDBACK_INTENSITY, + DISPLAY_COLOR_MODE }; /** @@ -4220,6 +4225,7 @@ public final class Settings { PRIVATE_SETTINGS.add(LOCK_TO_APP_ENABLED); PRIVATE_SETTINGS.add(EGG_MODE); PRIVATE_SETTINGS.add(SHOW_BATTERY_PERCENT); + PRIVATE_SETTINGS.add(DISPLAY_COLOR_MODE); } /** @@ -4241,8 +4247,8 @@ public final class Settings { VALIDATORS.put(NEXT_ALARM_FORMATTED, NEXT_ALARM_FORMATTED_VALIDATOR); VALIDATORS.put(FONT_SCALE, FONT_SCALE_VALIDATOR); VALIDATORS.put(DIM_SCREEN, DIM_SCREEN_VALIDATOR); + VALIDATORS.put(DISPLAY_COLOR_MODE, DISPLAY_COLOR_MODE_VALIDATOR); VALIDATORS.put(SCREEN_OFF_TIMEOUT, SCREEN_OFF_TIMEOUT_VALIDATOR); - VALIDATORS.put(SCREEN_BRIGHTNESS, SCREEN_BRIGHTNESS_VALIDATOR); VALIDATORS.put(SCREEN_BRIGHTNESS_FOR_VR, SCREEN_BRIGHTNESS_FOR_VR_VALIDATOR); VALIDATORS.put(SCREEN_BRIGHTNESS_MODE, SCREEN_BRIGHTNESS_MODE_VALIDATOR); VALIDATORS.put(MODE_RINGER_STREAMS_AFFECTED, MODE_RINGER_STREAMS_AFFECTED_VALIDATOR); @@ -4287,7 +4293,6 @@ public final class Settings { VALIDATORS.put(DTMF_TONE_TYPE_WHEN_DIALING, DTMF_TONE_TYPE_WHEN_DIALING_VALIDATOR); VALIDATORS.put(HEARING_AID, HEARING_AID_VALIDATOR); VALIDATORS.put(TTY_MODE, TTY_MODE_VALIDATOR); - VALIDATORS.put(RTT_CALLING_MODE, RTT_CALLING_MODE_VALIDATOR); VALIDATORS.put(NOTIFICATION_LIGHT_PULSE, NOTIFICATION_LIGHT_PULSE_VALIDATOR); VALIDATORS.put(POINTER_LOCATION, POINTER_LOCATION_VALIDATOR); VALIDATORS.put(SHOW_TOUCHES, SHOW_TOUCHES_VALIDATOR); @@ -6660,6 +6665,17 @@ public final class Settings { private static final Validator TTY_MODE_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR; /** + * User-selected RTT mode. When on, outgoing and incoming calls will be answered as RTT + * calls when supported by the device and carrier. Boolean value. + * 0 = OFF + * 1 = ON + */ + public static final String RTT_CALLING_MODE = "rtt_calling_mode"; + + private static final Validator RTT_CALLING_MODE_VALIDATOR = BOOLEAN_VALIDATOR; + + /** + /** * Controls whether settings backup is enabled. * Type: int ( 0 = disabled, 1 = enabled ) * @hide @@ -7383,6 +7399,17 @@ public final class Settings { BOOLEAN_VALIDATOR; /** + * Whether the swipe up gesture to switch apps should be enabled. + * + * @hide + */ + public static final String SWIPE_UP_TO_SWITCH_APPS_ENABLED = + "swipe_up_to_switch_apps_enabled"; + + private static final Validator SWIPE_UP_TO_SWITCH_APPS_ENABLED_VALIDATOR = + BOOLEAN_VALIDATOR; + + /** * Whether or not the smart camera lift trigger that launches the camera when the user moves * the phone into a position for taking photos should be enabled. * @@ -7885,6 +7912,7 @@ public final class Settings { PREFERRED_TTY_MODE, ENHANCED_VOICE_PRIVACY_ENABLED, TTY_MODE_ENABLED, + RTT_CALLING_MODE, INCALL_POWER_BUTTON_BEHAVIOR, NIGHT_DISPLAY_CUSTOM_START_TIME, NIGHT_DISPLAY_CUSTOM_END_TIME, @@ -7892,6 +7920,7 @@ public final class Settings { NIGHT_DISPLAY_AUTO_MODE, SYNC_PARENT_SOUNDS, CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED, + SWIPE_UP_TO_SWITCH_APPS_ENABLED, CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, SYSTEM_NAVIGATION_KEYS_ENABLED, QS_TILES, @@ -8014,6 +8043,7 @@ public final class Settings { VALIDATORS.put(ENHANCED_VOICE_PRIVACY_ENABLED, ENHANCED_VOICE_PRIVACY_ENABLED_VALIDATOR); VALIDATORS.put(TTY_MODE_ENABLED, TTY_MODE_ENABLED_VALIDATOR); + VALIDATORS.put(RTT_CALLING_MODE, RTT_CALLING_MODE_VALIDATOR); VALIDATORS.put(INCALL_POWER_BUTTON_BEHAVIOR, INCALL_POWER_BUTTON_BEHAVIOR_VALIDATOR); VALIDATORS.put(NIGHT_DISPLAY_CUSTOM_START_TIME, NIGHT_DISPLAY_CUSTOM_START_TIME_VALIDATOR); @@ -8024,6 +8054,8 @@ public final class Settings { VALIDATORS.put(SYNC_PARENT_SOUNDS, SYNC_PARENT_SOUNDS_VALIDATOR); VALIDATORS.put(CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED, CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED_VALIDATOR); + VALIDATORS.put(SWIPE_UP_TO_SWITCH_APPS_ENABLED, + SWIPE_UP_TO_SWITCH_APPS_ENABLED_VALIDATOR); VALIDATORS.put(CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED_VALIDATOR); VALIDATORS.put(SYSTEM_NAVIGATION_KEYS_ENABLED, @@ -8935,6 +8967,20 @@ public final class Settings { /** {@hide} */ public static final String NETSTATS_UID_TAG_DELETE_AGE = "netstats_uid_tag_delete_age"; + /** {@hide} */ + public static final String NETPOLICY_QUOTA_ENABLED = "netpolicy_quota_enabled"; + /** {@hide} */ + public static final String NETPOLICY_QUOTA_UNLIMITED = "netpolicy_quota_unlimited"; + /** {@hide} */ + public static final String NETPOLICY_QUOTA_LIMITED = "netpolicy_quota_limited"; + /** {@hide} */ + public static final String NETPOLICY_QUOTA_FRAC_JOBS = "netpolicy_quota_frac_jobs"; + /** {@hide} */ + public static final String NETPOLICY_QUOTA_FRAC_MULTIPATH = "netpolicy_quota_frac_multipath"; + + /** {@hide} */ + public static final String NETPOLICY_OVERRIDE_ENABLED = "netpolicy_override_enabled"; + /** * User preference for which network(s) should be used. Only the * connectivity service should touch this. @@ -9309,6 +9355,15 @@ public final class Settings { "network_metered_multipath_preference"; /** + * Default daily multipath budget used by ConnectivityManager.getMultipathPreference() + * on metered networks. This default quota is only used if quota could not be determined + * from data plan or data limit/warning set by the user. + * @hide + */ + public static final String NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES = + "network_default_daily_multipath_quota_bytes"; + + /** * Network watchlist last report time. * @hide */ @@ -10797,6 +10852,15 @@ public final class Settings { = "time_only_mode_constants"; /** + * Whether of not to send keycode sleep for ungaze when Home is the foreground activity on + * watch type devices. + * Type: int (0 for false, 1 for true) + * Default: 0 + * @hide + */ + public static final String UNGAZE_SLEEP_ENABLED = "ungaze_sleep_enabled"; + + /** * Whether or not Network Watchlist feature is enabled. * Type: int (0 for false, 1 for true) * Default: 0 @@ -11076,6 +11140,14 @@ public final class Settings { public static final String ALWAYS_FINISH_ACTIVITIES = "always_finish_activities"; /** + * If nonzero, all system error dialogs will be hidden. For example, the + * crash and ANR dialogs will not be shown, and the system will just proceed + * as if they had been accepted by the user. + * @hide + */ + public static final String HIDE_ERROR_DIALOGS = "hide_error_dialogs"; + + /** * Use Dock audio output for media: * 0 = disabled * 1 = enabled @@ -11694,6 +11766,38 @@ public final class Settings { "hidden_api_blacklist_exemptions"; /** + * Sampling rate for hidden API access event logs, as an integer in the range 0 to 0x10000 + * inclusive. + * + * @hide + */ + public static final String HIDDEN_API_ACCESS_LOG_SAMPLING_RATE = + "hidden_api_access_log_sampling_rate"; + + /** + * Hidden API enforcement policy for apps targeting SDK versions prior to the latest + * version. + * + * Values correspond to @{@link + * android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy} + * + * @hide + */ + public static final String HIDDEN_API_POLICY_PRE_P_APPS = + "hidden_api_policy_pre_p_apps"; + + /** + * Hidden API enforcement policy for apps targeting the current SDK version. + * + * Values correspond to @{@link + * android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy} + * + * @hide + */ + public static final String HIDDEN_API_POLICY_P_APPS = + "hidden_api_policy_p_apps"; + + /** * Timeout for a single {@link android.media.soundtrigger.SoundTriggerDetectionService} * operation (in ms). * @@ -12532,6 +12636,19 @@ public final class Settings { */ public static final String SWAP_ENABLED = "swap_enabled"; + /** + * Blacklist of GNSS satellites. + * + * This is a list of integers separated by commas to represent pairs of (constellation, + * svid). Thus, the number of integers should be even. + * + * E.g.: "3,0,5,24" denotes (constellation=3, svid=0) and (constellation=5, svid=24) are + * blacklisted. Note that svid=0 denotes all svids in the + * constellation are blacklisted. + * + * @hide + */ + public static final String GNSS_SATELLITE_BLACKLIST = "gnss_satellite_blacklist"; } /** diff --git a/android/se/omapi/Channel.java b/android/se/omapi/Channel.java index c8efede3..5db3c1a9 100644 --- a/android/se/omapi/Channel.java +++ b/android/se/omapi/Channel.java @@ -39,7 +39,7 @@ import java.io.IOException; * * @see <a href="http://globalplatform.org">GlobalPlatform Open Mobile API</a> */ -public class Channel { +public final class Channel implements java.nio.channels.Channel { private static final String TAG = "OMAPI.Channel"; private Session mSession; @@ -64,7 +64,7 @@ public class Channel { * before closing the channel. */ public void close() { - if (!isClosed()) { + if (isOpen()) { synchronized (mLock) { try { mChannel.close(); @@ -76,21 +76,21 @@ public class Channel { } /** - * Tells if this channel is closed. + * Tells if this channel is open. * - * @return <code>true</code> if the channel is closed or in case of an error. - * <code>false</code> otherwise. + * @return <code>false</code> if the channel is closed or in case of an error. + * <code>true</code> otherwise. */ - public boolean isClosed() { + public boolean isOpen() { if (!mService.isConnected()) { Log.e(TAG, "service not connected to system"); - return true; + return false; } try { - return mChannel.isClosed(); + return !mChannel.isClosed(); } catch (RemoteException e) { Log.e(TAG, "Exception in isClosed()"); - return true; + return false; } } diff --git a/android/se/omapi/Reader.java b/android/se/omapi/Reader.java index 9be3da6c..80262f75 100644 --- a/android/se/omapi/Reader.java +++ b/android/se/omapi/Reader.java @@ -37,7 +37,7 @@ import java.io.IOException; * * @see <a href="http://globalplatform.org">GlobalPlatform Open Mobile API</a> */ -public class Reader { +public final class Reader { private static final String TAG = "OMAPI.Reader"; private final String mName; diff --git a/android/se/omapi/SEService.java b/android/se/omapi/SEService.java index 311dc4c7..14727f02 100644 --- a/android/se/omapi/SEService.java +++ b/android/se/omapi/SEService.java @@ -32,6 +32,7 @@ import android.os.RemoteException; import android.util.Log; import java.util.HashMap; +import java.util.concurrent.Executor; /** * The SEService realises the communication to available Secure Elements on the @@ -40,7 +41,7 @@ import java.util.HashMap; * * @see <a href="http://simalliance.org">SIMalliance Open Mobile API v3.0</a> */ -public class SEService { +public final class SEService { /** * Error code used with ServiceSpecificException. @@ -62,11 +63,11 @@ public class SEService { /** * Interface to send call-backs to the application when the service is connected. */ - public interface SecureElementListener { + public interface OnConnectedListener { /** * Called by the framework when the service is connected. */ - void onServiceConnected(); + void onConnected(); } /** @@ -74,16 +75,22 @@ public class SEService { * SEService could be bound to the backend. */ private class SEListener extends ISecureElementListener.Stub { - public SecureElementListener mListener = null; + public OnConnectedListener mListener = null; + public Executor mExecutor = null; @Override public IBinder asBinder() { return this; } - public void onServiceConnected() { - if (mListener != null) { - mListener.onServiceConnected(); + public void onConnected() { + if (mListener != null && mExecutor != null) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mListener.onConnected(); + } + }); } } } @@ -116,22 +123,26 @@ public class SEService { * the specified listener is called or if isConnected() returns * <code>true</code>. <br> * The call-back object passed as a parameter will have its - * onServiceConnected() method called when the connection actually happen. + * onConnected() method called when the connection actually happen. * * @param context * the context of the calling application. Cannot be * <code>null</code>. * @param listener - * a SecureElementListener object. + * a OnConnectedListener object. + * @param executor + * an Executor which will be used when invoking the callback. */ - public SEService(@NonNull Context context, @NonNull SecureElementListener listener) { + public SEService(@NonNull Context context, @NonNull Executor executor, + @NonNull OnConnectedListener listener) { - if (context == null) { - throw new NullPointerException("context must not be null"); + if (context == null || listener == null || executor == null) { + throw new NullPointerException("Arguments must not be null"); } mContext = context; mSEListener.mListener = listener; + mSEListener.mExecutor = executor; mConnection = new ServiceConnection() { @@ -140,7 +151,7 @@ public class SEService { mSecureElementService = ISecureElementService.Stub.asInterface(service); if (mSEListener != null) { - mSEListener.onServiceConnected(); + mSEListener.onConnected(); } Log.i(TAG, "Service onServiceConnected"); } @@ -171,12 +182,12 @@ public class SEService { } /** - * Returns the list of available Secure Element readers. + * Returns an array of available Secure Element readers. * There must be no duplicated objects in the returned list. * All available readers shall be listed even if no card is inserted. * - * @return The readers list, as an array of Readers. If there are no - * readers the returned array is of length 0. + * @return An array of Readers. If there are no readers the returned array + * is of length 0. */ public @NonNull Reader[] getReaders() { if (mSecureElementService == null) { @@ -212,7 +223,8 @@ public class SEService { * (including any binding to an underlying service). * As a result isConnected() will return false after shutdown() was called. * After this method call, the SEService object is not connected. - * It is recommended to call this method in the termination method of the calling application + * This method should be called when connection to the Secure Element is not needed + * or in the termination method of the calling application * (or part of this application) which is bound to this SEService. */ public void shutdown() { diff --git a/android/se/omapi/Session.java b/android/se/omapi/Session.java index adfeddd5..d5f8c82b 100644 --- a/android/se/omapi/Session.java +++ b/android/se/omapi/Session.java @@ -39,7 +39,7 @@ import java.util.NoSuchElementException; * * @see <a href="http://simalliance.org">SIMalliance Open Mobile API v3.0</a> */ -public class Session { +public final class Session { private final Object mLock = new Object(); private final SEService mService; @@ -225,6 +225,32 @@ public class Session { } /** + * This method is provided to ease the development of mobile application and for compliancy + * with existing applications. + * This method is equivalent to openBasicChannel(aid, P2=0x00) + * + * @param aid the AID of the Applet to be selected on this channel, as a + * byte array, or null if no Applet is to be selected. + * @throws IOException if there is a communication problem to the reader or + * the Secure Element. + * @throws IllegalStateException if the Secure Element session is used after + * being closed. + * @throws IllegalArgumentException if the aid's length is not within 5 to + * 16 (inclusive). + * @throws SecurityException if the calling application cannot be granted + * access to this AID or the default Applet on this + * session. + * @throws NoSuchElementException if the AID on the Secure Element is not available or cannot be + * selected. + * @throws UnsupportedOperationException if the given P2 parameter is not + * supported by the device + * @return an instance of Channel if available or null. + */ + public @Nullable Channel openBasicChannel(@Nullable byte[] aid) throws IOException { + return openBasicChannel(aid, (byte) 0x00); + } + + /** * Open a logical channel with the Secure Element, selecting the Applet represented by * the given AID. If the AID is null, which means no Applet is to be selected on this * channel, the default Applet is used. It's up to the Secure Element to choose which @@ -304,4 +330,32 @@ public class Session { } } } + + /** + * This method is provided to ease the development of mobile application and for compliancy + * with existing applications. + * This method is equivalent to openLogicalChannel(aid, P2=0x00) + * + * @param aid the AID of the Applet to be selected on this channel, as a + * byte array. + * @throws IOException if there is a communication problem to the reader or + * the Secure Element. + * @throws IllegalStateException if the Secure Element is used after being + * closed. + * @throws IllegalArgumentException if the aid's length is not within 5 to + * 16 (inclusive). + * @throws SecurityException if the calling application cannot be granted + * access to this AID or the default Applet on this + * session. + * @throws NoSuchElementException if the AID on the Secure Element is not + * available or cannot be selected or a logical channel is already + * open to a non-multiselectable Applet. + * @throws UnsupportedOperationException if the given P2 parameter is not + * supported by the device. + * @return an instance of Channel. Null if the Secure Element is unable to + * provide a new logical channel. + */ + public @Nullable Channel openLogicalChannel(@Nullable byte[] aid) throws IOException { + return openLogicalChannel(aid, (byte) 0x00); + } } diff --git a/android/security/ConfirmationCallback.java b/android/security/ConfirmationCallback.java index 4670bce3..fd027f0f 100644 --- a/android/security/ConfirmationCallback.java +++ b/android/security/ConfirmationCallback.java @@ -33,22 +33,22 @@ public abstract class ConfirmationCallback { * * @param dataThatWasConfirmed the data that was confirmed, see above for the format. */ - public void onConfirmedByUser(@NonNull byte[] dataThatWasConfirmed) {} + public void onConfirmed(@NonNull byte[] dataThatWasConfirmed) {} /** * Called when the requested prompt was dismissed (not accepted) by the user. */ - public void onDismissedByUser() {} + public void onDismissed() {} /** * Called when the requested prompt was dismissed by the application. */ - public void onDismissedByApplication() {} + public void onCanceled() {} /** * Called when the requested prompt was dismissed because of a low-level error. * - * @param e an exception representing the error. + * @param e a throwable representing the error. */ - public void onError(Exception e) {} + public void onError(Throwable e) {} } diff --git a/android/security/ConfirmationDialog.java b/android/security/ConfirmationPrompt.java index 1697106c..5330cffe 100644 --- a/android/security/ConfirmationDialog.java +++ b/android/security/ConfirmationPrompt.java @@ -68,7 +68,7 @@ import java.util.concurrent.Executor; * {@link #presentPrompt presentPrompt()} method. The <i>Relying Party</i> stores the nonce locally * since it'll use it in a later step. * <li> If the user approves the prompt a <i>Confirmation Response</i> is returned in the - * {@link ConfirmationCallback#onConfirmedByUser onConfirmedByUser(byte[])} callback as the + * {@link ConfirmationCallback#onConfirmed onConfirmed(byte[])} callback as the * <code>dataThatWasConfirmed</code> parameter. This blob contains the text that was shown to the * user, the <code>extraData</code> parameter, and possibly other data. * <li> The application signs the <i>Confirmation Response</i> with the previously created key and @@ -82,8 +82,8 @@ import java.util.concurrent.Executor; * last bullet, is to have the <i>Relying Party</i> generate <code>promptText</code> and store it * along the nonce in the <code>extraData</code> blob. */ -public class ConfirmationDialog { - private static final String TAG = "ConfirmationDialog"; +public class ConfirmationPrompt { + private static final String TAG = "ConfirmationPrompt"; private CharSequence mPromptText; private byte[] mExtraData; @@ -97,15 +97,15 @@ public class ConfirmationDialog { ConfirmationCallback callback) { switch (responseCode) { case KeyStore.CONFIRMATIONUI_OK: - callback.onConfirmedByUser(dataThatWasConfirmed); + callback.onConfirmed(dataThatWasConfirmed); break; case KeyStore.CONFIRMATIONUI_CANCELED: - callback.onDismissedByUser(); + callback.onDismissed(); break; case KeyStore.CONFIRMATIONUI_ABORTED: - callback.onDismissedByApplication(); + callback.onCanceled(); break; case KeyStore.CONFIRMATIONUI_SYSTEM_ERROR: @@ -145,21 +145,25 @@ public class ConfirmationDialog { }; /** - * A builder that collects arguments, to be shown on the system-provided confirmation dialog. + * A builder that collects arguments, to be shown on the system-provided confirmation prompt. */ - public static class Builder { + public static final class Builder { + private Context mContext; private CharSequence mPromptText; private byte[] mExtraData; /** - * Creates a builder for the confirmation dialog. + * Creates a builder for the confirmation prompt. + * + * @param context the application context */ - public Builder() { + public Builder(Context context) { + mContext = context; } /** - * Sets the prompt text for the dialog. + * Sets the prompt text for the prompt. * * @param promptText the text to present in the prompt. * @return the builder. @@ -170,7 +174,7 @@ public class ConfirmationDialog { } /** - * Sets the extra data for the dialog. + * Sets the extra data for the prompt. * * @param extraData data to include in the response data. * @return the builder. @@ -181,24 +185,23 @@ public class ConfirmationDialog { } /** - * Creates a {@link ConfirmationDialog} with the arguments supplied to this builder. + * Creates a {@link ConfirmationPrompt} with the arguments supplied to this builder. * - * @param context the application context - * @return a {@link ConfirmationDialog} + * @return a {@link ConfirmationPrompt} * @throws IllegalArgumentException if any of the required fields are not set. */ - public ConfirmationDialog build(Context context) { + public ConfirmationPrompt build() { if (TextUtils.isEmpty(mPromptText)) { throw new IllegalArgumentException("prompt text must be set and non-empty"); } if (mExtraData == null) { throw new IllegalArgumentException("extraData must be set"); } - return new ConfirmationDialog(context, mPromptText, mExtraData); + return new ConfirmationPrompt(mContext, mPromptText, mExtraData); } } - private ConfirmationDialog(Context context, CharSequence promptText, byte[] extraData) { + private ConfirmationPrompt(Context context, CharSequence promptText, byte[] extraData) { mContext = context; mPromptText = promptText; mExtraData = extraData; @@ -227,10 +230,10 @@ public class ConfirmationDialog { return uiOptionsAsFlags; } - private boolean isAccessibilityServiceRunning() { + private static boolean isAccessibilityServiceRunning(Context context) { boolean serviceRunning = false; try { - ContentResolver contentResolver = mContext.getContentResolver(); + ContentResolver contentResolver = context.getContentResolver(); int a11yEnabled = Settings.Secure.getInt(contentResolver, Settings.Secure.ACCESSIBILITY_ENABLED); if (a11yEnabled == 1) { @@ -249,12 +252,12 @@ public class ConfirmationDialog { * When the prompt is no longer being presented, one of the methods in * {@link ConfirmationCallback} is called on the supplied callback object. * - * Confirmation dialogs may not be available when accessibility services are running so this + * Confirmation prompts may not be available when accessibility services are running so this * may fail with a {@link ConfirmationNotAvailableException} exception even if * {@link #isSupported} returns {@code true}. * * @param executor the executor identifying the thread that will receive the callback. - * @param callback the callback to use when the dialog is done showing. + * @param callback the callback to use when the prompt is done showing. * @throws IllegalArgumentException if the prompt text is too long or malfomed. * @throws ConfirmationAlreadyPresentingException if another prompt is being presented. * @throws ConfirmationNotAvailableException if confirmation prompts are not supported. @@ -265,7 +268,7 @@ public class ConfirmationDialog { if (mCallback != null) { throw new ConfirmationAlreadyPresentingException(); } - if (isAccessibilityServiceRunning()) { + if (isAccessibilityServiceRunning(mContext)) { throw new ConfirmationNotAvailableException(); } mCallback = callback; @@ -301,7 +304,7 @@ public class ConfirmationDialog { * Cancels a prompt currently being displayed. * * On success, the - * {@link ConfirmationCallback#onDismissedByApplication onDismissedByApplication()} method on + * {@link ConfirmationCallback#onCanceled onCanceled()} method on * the supplied callback object will be called asynchronously. * * @throws IllegalStateException if no prompt is currently being presented. @@ -324,9 +327,13 @@ public class ConfirmationDialog { /** * Checks if the device supports confirmation prompts. * + * @param context the application context. * @return true if confirmation prompts are supported by the device. */ - public static boolean isSupported() { + public static boolean isSupported(Context context) { + if (isAccessibilityServiceRunning(context)) { + return false; + } return KeyStore.getInstance().isConfirmationPromptSupported(); } } diff --git a/android/security/KeyStoreException.java b/android/security/KeyStoreException.java index 88e768ce..30389a29 100644 --- a/android/security/KeyStoreException.java +++ b/android/security/KeyStoreException.java @@ -16,12 +16,15 @@ package android.security; +import android.annotation.TestApi; + /** * KeyStore/keymaster exception with positive error codes coming from the KeyStore and negative * ones from keymaster. * * @hide */ +@TestApi public class KeyStoreException extends Exception { private final int mErrorCode; diff --git a/android/security/keystore/BackwardsCompat.java b/android/security/keystore/BackwardsCompat.java deleted file mode 100644 index cf5fe1f0..00000000 --- a/android/security/keystore/BackwardsCompat.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2018 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 android.security.keystore; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Function; - -/** - * Helpers for converting classes between old and new API, so we can preserve backwards - * compatibility while teamfooding. This will be removed soon. - * - * @hide - */ -class BackwardsCompat { - - - static KeychainProtectionParams toLegacyKeychainProtectionParams( - android.security.keystore.recovery.KeyChainProtectionParams keychainProtectionParams - ) { - return new KeychainProtectionParams.Builder() - .setUserSecretType(keychainProtectionParams.getUserSecretType()) - .setSecret(keychainProtectionParams.getSecret()) - .setLockScreenUiFormat(keychainProtectionParams.getLockScreenUiFormat()) - .setKeyDerivationParams( - toLegacyKeyDerivationParams( - keychainProtectionParams.getKeyDerivationParams())) - .build(); - } - - static KeyDerivationParams toLegacyKeyDerivationParams( - android.security.keystore.recovery.KeyDerivationParams keyDerivationParams - ) { - return new KeyDerivationParams( - keyDerivationParams.getAlgorithm(), keyDerivationParams.getSalt()); - } - - static WrappedApplicationKey toLegacyWrappedApplicationKey( - android.security.keystore.recovery.WrappedApplicationKey wrappedApplicationKey - ) { - return new WrappedApplicationKey.Builder() - .setAlias(wrappedApplicationKey.getAlias()) - .setEncryptedKeyMaterial(wrappedApplicationKey.getEncryptedKeyMaterial()) - .build(); - } - - static android.security.keystore.recovery.KeyDerivationParams fromLegacyKeyDerivationParams( - KeyDerivationParams keyDerivationParams - ) { - return android.security.keystore.recovery.KeyDerivationParams.createSha256Params( - keyDerivationParams.getSalt()); - } - - static android.security.keystore.recovery.WrappedApplicationKey fromLegacyWrappedApplicationKey( - WrappedApplicationKey wrappedApplicationKey - ) { - return new android.security.keystore.recovery.WrappedApplicationKey.Builder() - .setAlias(wrappedApplicationKey.getAlias()) - .setEncryptedKeyMaterial(wrappedApplicationKey.getEncryptedKeyMaterial()) - .build(); - } - - static List<android.security.keystore.recovery.WrappedApplicationKey> - fromLegacyWrappedApplicationKeys(List<WrappedApplicationKey> wrappedApplicationKeys - ) { - return map(wrappedApplicationKeys, BackwardsCompat::fromLegacyWrappedApplicationKey); - } - - static List<android.security.keystore.recovery.KeyChainProtectionParams> - fromLegacyKeychainProtectionParams( - List<KeychainProtectionParams> keychainProtectionParams) { - return map(keychainProtectionParams, BackwardsCompat::fromLegacyKeychainProtectionParam); - } - - static android.security.keystore.recovery.KeyChainProtectionParams - fromLegacyKeychainProtectionParam(KeychainProtectionParams keychainProtectionParams) { - return new android.security.keystore.recovery.KeyChainProtectionParams.Builder() - .setUserSecretType(keychainProtectionParams.getUserSecretType()) - .setSecret(keychainProtectionParams.getSecret()) - .setLockScreenUiFormat(keychainProtectionParams.getLockScreenUiFormat()) - .setKeyDerivationParams( - fromLegacyKeyDerivationParams( - keychainProtectionParams.getKeyDerivationParams())) - .build(); - } - - static KeychainSnapshot toLegacyKeychainSnapshot( - android.security.keystore.recovery.KeyChainSnapshot keychainSnapshot - ) { - return new KeychainSnapshot.Builder() - .setCounterId(keychainSnapshot.getCounterId()) - .setEncryptedRecoveryKeyBlob(keychainSnapshot.getEncryptedRecoveryKeyBlob()) - .setTrustedHardwarePublicKey(keychainSnapshot.getTrustedHardwarePublicKey()) - .setSnapshotVersion(keychainSnapshot.getSnapshotVersion()) - .setMaxAttempts(keychainSnapshot.getMaxAttempts()) - .setServerParams(keychainSnapshot.getServerParams()) - .setKeychainProtectionParams( - map(keychainSnapshot.getKeyChainProtectionParams(), - BackwardsCompat::toLegacyKeychainProtectionParams)) - .setWrappedApplicationKeys( - map(keychainSnapshot.getWrappedApplicationKeys(), - BackwardsCompat::toLegacyWrappedApplicationKey)) - .build(); - } - - static <A, B> List<B> map(List<A> as, Function<A, B> f) { - ArrayList<B> bs = new ArrayList<>(as.size()); - for (A a : as) { - bs.add(f.apply(a)); - } - return bs; - } -} diff --git a/android/security/keystore/BadCertificateFormatException.java b/android/security/keystore/BadCertificateFormatException.java deleted file mode 100644 index c51b7737..00000000 --- a/android/security/keystore/BadCertificateFormatException.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2018 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 android.security.keystore; - -/** - * @deprecated Use {@link android.security.keystore.recovery.BadCertificateFormatException}. - * @hide - */ -public class BadCertificateFormatException extends RecoveryControllerException { - public BadCertificateFormatException(String msg) { - super(msg); - } -} diff --git a/android/security/keystore/DecryptionFailedException.java b/android/security/keystore/DecryptionFailedException.java deleted file mode 100644 index c0b52f71..00000000 --- a/android/security/keystore/DecryptionFailedException.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2018 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 android.security.keystore; - -/** - * @deprecated Use {@link android.security.keystore.recovery.DecryptionFailedException}. - * @hide - */ -public class DecryptionFailedException extends RecoveryControllerException { - - public DecryptionFailedException(String msg) { - super(msg); - } -} diff --git a/android/security/keystore/InternalRecoveryServiceException.java b/android/security/keystore/InternalRecoveryServiceException.java deleted file mode 100644 index 40076f73..00000000 --- a/android/security/keystore/InternalRecoveryServiceException.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2018 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 android.security.keystore; - -/** - * @deprecated Use {@link android.security.keystore.recovery.InternalRecoveryServiceException}. - * @hide - */ -public class InternalRecoveryServiceException extends RecoveryControllerException { - public InternalRecoveryServiceException(String msg) { - super(msg); - } - - public InternalRecoveryServiceException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/android/security/keystore/KeyDerivationParams.java b/android/security/keystore/KeyDerivationParams.java deleted file mode 100644 index e475dc36..00000000 --- a/android/security/keystore/KeyDerivationParams.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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 android.security.keystore; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.os.Parcel; -import android.os.Parcelable; - -import com.android.internal.util.Preconditions; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * @deprecated Use {@link android.security.keystore.recovery.KeyDerivationParams}. - * @hide - */ -public final class KeyDerivationParams implements Parcelable { - private final int mAlgorithm; - private byte[] mSalt; - - /** @hide */ - @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = {"ALGORITHM_"}, value = {ALGORITHM_SHA256, ALGORITHM_ARGON2ID}) - public @interface KeyDerivationAlgorithm { - } - - /** - * Salted SHA256 - */ - public static final int ALGORITHM_SHA256 = 1; - - /** - * Argon2ID - * @hide - */ - // TODO: add Argon2ID support. - public static final int ALGORITHM_ARGON2ID = 2; - - /** - * Creates instance of the class to to derive key using salted SHA256 hash. - */ - public static KeyDerivationParams createSha256Params(@NonNull byte[] salt) { - return new KeyDerivationParams(ALGORITHM_SHA256, salt); - } - - KeyDerivationParams(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) { - mAlgorithm = algorithm; - mSalt = Preconditions.checkNotNull(salt); - } - - /** - * Gets algorithm. - */ - public @KeyDerivationAlgorithm int getAlgorithm() { - return mAlgorithm; - } - - /** - * Gets salt. - */ - public @NonNull byte[] getSalt() { - return mSalt; - } - - public static final Parcelable.Creator<KeyDerivationParams> CREATOR = - new Parcelable.Creator<KeyDerivationParams>() { - public KeyDerivationParams createFromParcel(Parcel in) { - return new KeyDerivationParams(in); - } - - public KeyDerivationParams[] newArray(int length) { - return new KeyDerivationParams[length]; - } - }; - - /** - * @hide - */ - @Override - public void writeToParcel(Parcel out, int flags) { - out.writeInt(mAlgorithm); - out.writeByteArray(mSalt); - } - - /** - * @hide - */ - protected KeyDerivationParams(Parcel in) { - mAlgorithm = in.readInt(); - mSalt = in.createByteArray(); - } - - @Override - public int describeContents() { - return 0; - } -} diff --git a/android/security/keystore/KeyGenParameterSpec.java b/android/security/keystore/KeyGenParameterSpec.java index c0d0fb00..b2e0f675 100644 --- a/android/security/keystore/KeyGenParameterSpec.java +++ b/android/security/keystore/KeyGenParameterSpec.java @@ -19,6 +19,7 @@ package android.security.keystore; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.app.KeyguardManager; import android.hardware.fingerprint.FingerprintManager; import android.security.GateKeeper; @@ -594,6 +595,14 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu /** * Returns {@code true} if the key is authorized to be used only if a test of user presence has * been performed between the {@code Signature.initSign()} and {@code Signature.sign()} calls. + * It requires that the KeyStore implementation have a direct way to validate the user presence + * for example a KeyStore hardware backed strongbox can use a button press that is observable + * in hardware. A test for user presence is tangential to authentication. The test can be part + * of an authentication step as long as this step can be validated by the hardware protecting + * the key and cannot be spoofed. For example, a physical button press can be used as a test of + * user presence if the other pins connected to the button are not able to simulate a button + * press. There must be no way for the primary processor to fake a button press, or that + * button must not be used as a test of user presence. */ public boolean isUserPresenceRequired() { return mUserPresenceRequired; @@ -673,8 +682,8 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu } /** - * Returns {@code true} if the screen must be unlocked for this key to be used for encryption or - * signing. Decryption and signature verification will still be available when the screen is + * Returns {@code true} if the screen must be unlocked for this key to be used for decryption or + * signing. Encryption and signature verification will still be available when the screen is * locked. * * @see Builder#setUnlockedDeviceRequired(boolean) @@ -1180,6 +1189,14 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu /** * Sets whether a test of user presence is required to be performed between the * {@code Signature.initSign()} and {@code Signature.sign()} method calls. + * It requires that the KeyStore implementation have a direct way to validate the user + * presence for example a KeyStore hardware backed strongbox can use a button press that + * is observable in hardware. A test for user presence is tangential to authentication. The + * test can be part of an authentication step as long as this step can be validated by the + * hardware protecting the key and cannot be spoofed. For example, a physical button press + * can be used as a test of user presence if the other pins connected to the button are not + * able to simulate a button press.There must be no way for the primary processor to fake a + * button press, or that button must not be used as a test of user presence. */ @NonNull public Builder setUserPresenceRequired(boolean required) { @@ -1227,6 +1244,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu * * Sets whether to include a temporary unique ID field in the attestation certificate. */ + @TestApi @NonNull public Builder setUniqueIdIncluded(boolean uniqueIdIncluded) { mUniqueIdIncluded = uniqueIdIncluded; diff --git a/android/security/keystore/KeyProtection.java b/android/security/keystore/KeyProtection.java index 4daf30ce..fdcad85b 100644 --- a/android/security/keystore/KeyProtection.java +++ b/android/security/keystore/KeyProtection.java @@ -19,6 +19,7 @@ package android.security.keystore; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.app.KeyguardManager; import android.hardware.fingerprint.FingerprintManager; import android.security.GateKeeper; @@ -445,6 +446,14 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs { /** * Returns {@code true} if the key is authorized to be used only if a test of user presence has * been performed between the {@code Signature.initSign()} and {@code Signature.sign()} calls. + * It requires that the KeyStore implementation have a direct way to validate the user presence + * for example a KeyStore hardware backed strongbox can use a button press that is observable + * in hardware. A test for user presence is tangential to authentication. The test can be part + * of an authentication step as long as this step can be validated by the hardware protecting + * the key and cannot be spoofed. For example, a physical button press can be used as a test of + * user presence if the other pins connected to the button are not able to simulate a button + * press. There must be no way for the primary processor to fake a button press, or that + * button must not be used as a test of user presence. */ public boolean isUserPresenceRequired() { return mUserPresenceRequred; @@ -493,6 +502,7 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs { * @see KeymasterUtils#addUserAuthArgs * @hide */ + @TestApi public long getBoundToSpecificSecureUserId() { return mBoundToSecureUserId; } @@ -508,8 +518,8 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs { } /** - * Returns {@code true} if the screen must be unlocked for this key to be used for encryption or - * signing. Decryption and signature verification will still be available when the screen is + * Returns {@code true} if the screen must be unlocked for this key to be used for decryption or + * signing. Encryption and signature verification will still be available when the screen is * locked. * * @see Builder#setUnlockedDeviceRequired(boolean) @@ -840,7 +850,15 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs { /** * Sets whether a test of user presence is required to be performed between the - * {@code Signature.initSign()} and {@code Signature.sign()} method calls. + * {@code Signature.initSign()} and {@code Signature.sign()} method calls. It requires that + * the KeyStore implementation have a direct way to validate the user presence for example + * a KeyStore hardware backed strongbox can use a button press that is observable in + * hardware. A test for user presence is tangential to authentication. The test can be part + * of an authentication step as long as this step can be validated by the hardware + * protecting the key and cannot be spoofed. For example, a physical button press can be + * used as a test of user presence if the other pins connected to the button are not able + * to simulate a button press. There must be no way for the primary processor to fake a + * button press, or that button must not be used as a test of user presence. */ @NonNull public Builder setUserPresenceRequired(boolean required) { @@ -910,6 +928,7 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs { * @see KeyProtection#getBoundToSpecificSecureUserId() * @hide */ + @TestApi public Builder setBoundToSpecificSecureUserId(long secureUserId) { mBoundToSecureUserId = secureUserId; return this; diff --git a/android/security/keystore/KeychainProtectionParams.java b/android/security/keystore/KeychainProtectionParams.java deleted file mode 100644 index 19a087d5..00000000 --- a/android/security/keystore/KeychainProtectionParams.java +++ /dev/null @@ -1,269 +0,0 @@ -/* - * 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 android.security.keystore; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.os.Parcel; -import android.os.Parcelable; - -import com.android.internal.util.Preconditions; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.Arrays; - -/** - * @deprecated Use {@link android.security.keystore.recovery.KeyChainProtectionParams}. - * @hide - */ -public final class KeychainProtectionParams implements Parcelable { - /** @hide */ - @Retention(RetentionPolicy.SOURCE) - @IntDef({TYPE_LOCKSCREEN, TYPE_CUSTOM_PASSWORD}) - public @interface UserSecretType { - } - - /** - * Lockscreen secret is required to recover KeyStore. - */ - public static final int TYPE_LOCKSCREEN = 100; - - /** - * Custom passphrase, unrelated to lock screen, is required to recover KeyStore. - */ - public static final int TYPE_CUSTOM_PASSWORD = 101; - - /** @hide */ - @Retention(RetentionPolicy.SOURCE) - @IntDef({TYPE_PIN, TYPE_PASSWORD, TYPE_PATTERN}) - public @interface LockScreenUiFormat { - } - - /** - * Pin with digits only. - */ - public static final int TYPE_PIN = 1; - - /** - * Password. String with latin-1 characters only. - */ - public static final int TYPE_PASSWORD = 2; - - /** - * Pattern with 3 by 3 grid. - */ - public static final int TYPE_PATTERN = 3; - - @UserSecretType - private Integer mUserSecretType; - - @LockScreenUiFormat - private Integer mLockScreenUiFormat; - - /** - * Parameters of the key derivation function, including algorithm, difficulty, salt. - */ - private KeyDerivationParams mKeyDerivationParams; - private byte[] mSecret; // Derived from user secret. The field must have limited visibility. - - /** - * @param secret Constructor creates a reference to the secret. Caller must use - * @link {#clearSecret} to overwrite its value in memory. - * @hide - */ - public KeychainProtectionParams(@UserSecretType int userSecretType, - @LockScreenUiFormat int lockScreenUiFormat, - @NonNull KeyDerivationParams keyDerivationParams, - @NonNull byte[] secret) { - mUserSecretType = userSecretType; - mLockScreenUiFormat = lockScreenUiFormat; - mKeyDerivationParams = Preconditions.checkNotNull(keyDerivationParams); - mSecret = Preconditions.checkNotNull(secret); - } - - private KeychainProtectionParams() { - - } - - /** - * @see TYPE_LOCKSCREEN - * @see TYPE_CUSTOM_PASSWORD - */ - public @UserSecretType int getUserSecretType() { - return mUserSecretType; - } - - /** - * Specifies UX shown to user during recovery. - * Default value is {@code TYPE_LOCKSCREEN} - * - * @see TYPE_PIN - * @see TYPE_PASSWORD - * @see TYPE_PATTERN - */ - public @LockScreenUiFormat int getLockScreenUiFormat() { - return mLockScreenUiFormat; - } - - /** - * Specifies function used to derive symmetric key from user input - * Format is defined in separate util class. - */ - public @NonNull KeyDerivationParams getKeyDerivationParams() { - return mKeyDerivationParams; - } - - /** - * Secret derived from user input. - * Default value is empty array - * - * @return secret or empty array - */ - public @NonNull byte[] getSecret() { - return mSecret; - } - - /** - * Builder for creating {@link KeychainProtectionParams}. - */ - public static class Builder { - private KeychainProtectionParams mInstance = new KeychainProtectionParams(); - - /** - * Sets user secret type. - * - * @see TYPE_LOCKSCREEN - * @see TYPE_CUSTOM_PASSWORD - * @param userSecretType The secret type - * @return This builder. - */ - public Builder setUserSecretType(@UserSecretType int userSecretType) { - mInstance.mUserSecretType = userSecretType; - return this; - } - - /** - * Sets UI format. - * - * @see TYPE_PIN - * @see TYPE_PASSWORD - * @see TYPE_PATTERN - * @param lockScreenUiFormat The UI format - * @return This builder. - */ - public Builder setLockScreenUiFormat(@LockScreenUiFormat int lockScreenUiFormat) { - mInstance.mLockScreenUiFormat = lockScreenUiFormat; - return this; - } - - /** - * Sets parameters of the key derivation function. - * - * @param keyDerivationParams Key derivation Params - * @return This builder. - */ - public Builder setKeyDerivationParams(@NonNull KeyDerivationParams - keyDerivationParams) { - mInstance.mKeyDerivationParams = keyDerivationParams; - return this; - } - - /** - * Secret derived from user input, or empty array. - * - * @param secret The secret. - * @return This builder. - */ - public Builder setSecret(@NonNull byte[] secret) { - mInstance.mSecret = secret; - return this; - } - - - /** - * Creates a new {@link KeychainProtectionParams} instance. - * The instance will include default values, if {@link setSecret} - * or {@link setUserSecretType} were not called. - * - * @return new instance - * @throws NullPointerException if some required fields were not set. - */ - @NonNull public KeychainProtectionParams build() { - if (mInstance.mUserSecretType == null) { - mInstance.mUserSecretType = TYPE_LOCKSCREEN; - } - Preconditions.checkNotNull(mInstance.mLockScreenUiFormat); - Preconditions.checkNotNull(mInstance.mKeyDerivationParams); - if (mInstance.mSecret == null) { - mInstance.mSecret = new byte[]{}; - } - return mInstance; - } - } - - /** - * Removes secret from memory than object is no longer used. - * Since finalizer call is not reliable, please use @link {#clearSecret} directly. - */ - @Override - protected void finalize() throws Throwable { - clearSecret(); - super.finalize(); - } - - /** - * Fills mSecret with zeroes. - */ - public void clearSecret() { - Arrays.fill(mSecret, (byte) 0); - } - - public static final Parcelable.Creator<KeychainProtectionParams> CREATOR = - new Parcelable.Creator<KeychainProtectionParams>() { - public KeychainProtectionParams createFromParcel(Parcel in) { - return new KeychainProtectionParams(in); - } - - public KeychainProtectionParams[] newArray(int length) { - return new KeychainProtectionParams[length]; - } - }; - - @Override - public void writeToParcel(Parcel out, int flags) { - out.writeInt(mUserSecretType); - out.writeInt(mLockScreenUiFormat); - out.writeTypedObject(mKeyDerivationParams, flags); - out.writeByteArray(mSecret); - } - - /** - * @hide - */ - protected KeychainProtectionParams(Parcel in) { - mUserSecretType = in.readInt(); - mLockScreenUiFormat = in.readInt(); - mKeyDerivationParams = in.readTypedObject(KeyDerivationParams.CREATOR); - mSecret = in.createByteArray(); - } - - @Override - public int describeContents() { - return 0; - } -} diff --git a/android/security/keystore/KeychainSnapshot.java b/android/security/keystore/KeychainSnapshot.java deleted file mode 100644 index cf18fd1c..00000000 --- a/android/security/keystore/KeychainSnapshot.java +++ /dev/null @@ -1,276 +0,0 @@ -/* - * 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 android.security.keystore; - -import android.annotation.NonNull; -import android.os.Parcel; -import android.os.Parcelable; - -import com.android.internal.util.Preconditions; - -import java.util.List; - -/** - * @deprecated Use {@link android.security.keystore.recovery.KeyChainSnapshot}. - * @hide - */ -public final class KeychainSnapshot implements Parcelable { - private static final int DEFAULT_MAX_ATTEMPTS = 10; - private static final long DEFAULT_COUNTER_ID = 1L; - - private int mSnapshotVersion; - private int mMaxAttempts = DEFAULT_MAX_ATTEMPTS; - private long mCounterId = DEFAULT_COUNTER_ID; - private byte[] mServerParams; - private byte[] mPublicKey; - private List<KeychainProtectionParams> mKeychainProtectionParams; - private List<WrappedApplicationKey> mEntryRecoveryData; - private byte[] mEncryptedRecoveryKeyBlob; - - /** - * @hide - * Deprecated, consider using builder. - */ - public KeychainSnapshot( - int snapshotVersion, - @NonNull List<KeychainProtectionParams> keychainProtectionParams, - @NonNull List<WrappedApplicationKey> wrappedApplicationKeys, - @NonNull byte[] encryptedRecoveryKeyBlob) { - mSnapshotVersion = snapshotVersion; - mKeychainProtectionParams = - Preconditions.checkCollectionElementsNotNull(keychainProtectionParams, - "keychainProtectionParams"); - mEntryRecoveryData = Preconditions.checkCollectionElementsNotNull(wrappedApplicationKeys, - "wrappedApplicationKeys"); - mEncryptedRecoveryKeyBlob = Preconditions.checkNotNull(encryptedRecoveryKeyBlob); - } - - private KeychainSnapshot() { - - } - - /** - * Snapshot version for given account. It is incremented when user secret or list of application - * keys changes. - */ - public int getSnapshotVersion() { - return mSnapshotVersion; - } - - /** - * Number of user secret guesses allowed during Keychain recovery. - */ - public int getMaxAttempts() { - return mMaxAttempts; - } - - /** - * CounterId which is rotated together with user secret. - */ - public long getCounterId() { - return mCounterId; - } - - /** - * Server parameters. - */ - public @NonNull byte[] getServerParams() { - return mServerParams; - } - - /** - * Public key used to encrypt {@code encryptedRecoveryKeyBlob}. - * - * See implementation for binary key format - */ - // TODO: document key format. - public @NonNull byte[] getTrustedHardwarePublicKey() { - return mPublicKey; - } - - /** - * UI and key derivation parameters. Note that combination of secrets may be used. - */ - public @NonNull List<KeychainProtectionParams> getKeychainProtectionParams() { - return mKeychainProtectionParams; - } - - /** - * List of application keys, with key material encrypted by - * the recovery key ({@link #getEncryptedRecoveryKeyBlob}). - */ - public @NonNull List<WrappedApplicationKey> getWrappedApplicationKeys() { - return mEntryRecoveryData; - } - - /** - * Recovery key blob, encrypted by user secret and recovery service public key. - */ - public @NonNull byte[] getEncryptedRecoveryKeyBlob() { - return mEncryptedRecoveryKeyBlob; - } - - public static final Parcelable.Creator<KeychainSnapshot> CREATOR = - new Parcelable.Creator<KeychainSnapshot>() { - public KeychainSnapshot createFromParcel(Parcel in) { - return new KeychainSnapshot(in); - } - - public KeychainSnapshot[] newArray(int length) { - return new KeychainSnapshot[length]; - } - }; - - /** - * Builder for creating {@link KeychainSnapshot}. - * - * @hide - */ - public static class Builder { - private KeychainSnapshot mInstance = new KeychainSnapshot(); - - /** - * Snapshot version for given account. - * - * @param snapshotVersion The snapshot version - * @return This builder. - */ - public Builder setSnapshotVersion(int snapshotVersion) { - mInstance.mSnapshotVersion = snapshotVersion; - return this; - } - - /** - * Sets the number of user secret guesses allowed during Keychain recovery. - * - * @param maxAttempts The maximum number of guesses. - * @return This builder. - */ - public Builder setMaxAttempts(int maxAttempts) { - mInstance.mMaxAttempts = maxAttempts; - return this; - } - - /** - * Sets counter id. - * - * @param counterId The counter id. - * @return This builder. - */ - public Builder setCounterId(long counterId) { - mInstance.mCounterId = counterId; - return this; - } - - /** - * Sets server parameters. - * - * @param serverParams The server parameters - * @return This builder. - */ - public Builder setServerParams(byte[] serverParams) { - mInstance.mServerParams = serverParams; - return this; - } - - /** - * Sets public key used to encrypt recovery blob. - * - * @param publicKey The public key - * @return This builder. - */ - public Builder setTrustedHardwarePublicKey(byte[] publicKey) { - mInstance.mPublicKey = publicKey; - return this; - } - - /** - * Sets UI and key derivation parameters - * - * @param recoveryMetadata The UI and key derivation parameters - * @return This builder. - */ - public Builder setKeychainProtectionParams( - @NonNull List<KeychainProtectionParams> recoveryMetadata) { - mInstance.mKeychainProtectionParams = recoveryMetadata; - return this; - } - - /** - * List of application keys. - * - * @param entryRecoveryData List of application keys - * @return This builder. - */ - public Builder setWrappedApplicationKeys(List<WrappedApplicationKey> entryRecoveryData) { - mInstance.mEntryRecoveryData = entryRecoveryData; - return this; - } - - /** - * Sets recovery key blob - * - * @param encryptedRecoveryKeyBlob The recovery key blob. - * @return This builder. - */ - public Builder setEncryptedRecoveryKeyBlob(@NonNull byte[] encryptedRecoveryKeyBlob) { - mInstance.mEncryptedRecoveryKeyBlob = encryptedRecoveryKeyBlob; - return this; - } - - - /** - * Creates a new {@link KeychainSnapshot} instance. - * - * @return new instance - * @throws NullPointerException if some required fields were not set. - */ - @NonNull public KeychainSnapshot build() { - Preconditions.checkCollectionElementsNotNull(mInstance.mKeychainProtectionParams, - "recoveryMetadata"); - Preconditions.checkCollectionElementsNotNull(mInstance.mEntryRecoveryData, - "entryRecoveryData"); - Preconditions.checkNotNull(mInstance.mEncryptedRecoveryKeyBlob); - Preconditions.checkNotNull(mInstance.mServerParams); - Preconditions.checkNotNull(mInstance.mPublicKey); - return mInstance; - } - } - - @Override - public void writeToParcel(Parcel out, int flags) { - out.writeInt(mSnapshotVersion); - out.writeTypedList(mKeychainProtectionParams); - out.writeByteArray(mEncryptedRecoveryKeyBlob); - out.writeTypedList(mEntryRecoveryData); - } - - /** - * @hide - */ - protected KeychainSnapshot(Parcel in) { - mSnapshotVersion = in.readInt(); - mKeychainProtectionParams = in.createTypedArrayList(KeychainProtectionParams.CREATOR); - mEncryptedRecoveryKeyBlob = in.createByteArray(); - mEntryRecoveryData = in.createTypedArrayList(WrappedApplicationKey.CREATOR); - } - - @Override - public int describeContents() { - return 0; - } -} diff --git a/android/security/keystore/LockScreenRequiredException.java b/android/security/keystore/LockScreenRequiredException.java deleted file mode 100644 index 09702845..00000000 --- a/android/security/keystore/LockScreenRequiredException.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2018 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 android.security.keystore; - -/** - * @deprecated Use {@link android.security.keystore.recovery.LockScreenRequiredException}. - * @hide - */ -public class LockScreenRequiredException extends RecoveryControllerException { - public LockScreenRequiredException(String msg) { - super(msg); - } -} diff --git a/android/security/keystore/RecoveryClaim.java b/android/security/keystore/RecoveryClaim.java deleted file mode 100644 index 12be607a..00000000 --- a/android/security/keystore/RecoveryClaim.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2018 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 android.security.keystore; - -/** - * @deprecated Use {@link android.security.keystore.recovery.RecoverySession}. - * @hide - */ -public class RecoveryClaim { - - private final RecoverySession mRecoverySession; - private final byte[] mClaimBytes; - - RecoveryClaim(RecoverySession recoverySession, byte[] claimBytes) { - mRecoverySession = recoverySession; - mClaimBytes = claimBytes; - } - - /** - * Returns the session associated with the recovery attempt. This is used to match the symmetric - * key, which remains internal to the framework, for decrypting the claim response. - * - * @return The session data. - */ - public RecoverySession getRecoverySession() { - return mRecoverySession; - } - - /** - * Returns the encrypted claim's bytes. - * - * <p>This should be sent by the recovery agent to the remote secure hardware, which will use - * it to decrypt the keychain, before sending it re-encrypted with the session's symmetric key - * to the device. - */ - public byte[] getClaimBytes() { - return mClaimBytes; - } -} diff --git a/android/security/keystore/RecoveryController.java b/android/security/keystore/RecoveryController.java deleted file mode 100644 index ca67e35b..00000000 --- a/android/security/keystore/RecoveryController.java +++ /dev/null @@ -1,467 +0,0 @@ -/* - * 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 android.security.keystore; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.PendingIntent; -import android.content.pm.PackageManager.NameNotFoundException; -import android.os.RemoteException; -import android.os.ServiceSpecificException; -import android.util.Log; - -import com.android.internal.widget.ILockSettings; - -import java.util.List; -import java.util.Map; - -/** - * @deprecated Use {@link android.security.keystore.recovery.RecoveryController}. - * @hide - */ -public class RecoveryController { - private static final String TAG = "RecoveryController"; - - /** Key has been successfully synced. */ - public static final int RECOVERY_STATUS_SYNCED = 0; - /** Waiting for recovery agent to sync the key. */ - public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1; - /** Recovery account is not available. */ - public static final int RECOVERY_STATUS_MISSING_ACCOUNT = 2; - /** Key cannot be synced. */ - public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3; - - /** - * Failed because no snapshot is yet pending to be synced for the user. - * - * @hide - */ - public static final int ERROR_NO_SNAPSHOT_PENDING = 21; - - /** - * Failed due to an error internal to the recovery service. This is unexpected and indicates - * either a problem with the logic in the service, or a problem with a dependency of the - * service (such as AndroidKeyStore). - * - * @hide - */ - public static final int ERROR_SERVICE_INTERNAL_ERROR = 22; - - /** - * Failed because the user does not have a lock screen set. - * - * @hide - */ - public static final int ERROR_INSECURE_USER = 23; - - /** - * Error thrown when attempting to use a recovery session that has since been closed. - * - * @hide - */ - public static final int ERROR_SESSION_EXPIRED = 24; - - /** - * Failed because the provided certificate was not a valid X509 certificate. - * - * @hide - */ - public static final int ERROR_BAD_CERTIFICATE_FORMAT = 25; - - /** - * Error thrown if decryption failed. This might be because the tag is wrong, the key is wrong, - * the data has become corrupted, the data has been tampered with, etc. - * - * @hide - */ - public static final int ERROR_DECRYPTION_FAILED = 26; - - - private final ILockSettings mBinder; - - private RecoveryController(ILockSettings binder) { - mBinder = binder; - } - - /** - * Deprecated. - * Gets a new instance of the class. - */ - public static RecoveryController getInstance() { - throw new UnsupportedOperationException("using Deprecated RecoveryController version"); - } - - /** - * Initializes key recovery service for the calling application. RecoveryController - * randomly chooses one of the keys from the list and keeps it to use for future key export - * operations. Collection of all keys in the list must be signed by the provided {@code - * rootCertificateAlias}, which must also be present in the list of root certificates - * preinstalled on the device. The random selection allows RecoveryController to select - * which of a set of remote recovery service devices will be used. - * - * <p>In addition, RecoveryController enforces a delay of three months between - * consecutive initialization attempts, to limit the ability of an attacker to often switch - * remote recovery devices and significantly increase number of recovery attempts. - * - * @param rootCertificateAlias alias of a root certificate preinstalled on the device - * @param signedPublicKeyList binary blob a list of X509 certificates and signature - * @throws BadCertificateFormatException if the {@code signedPublicKeyList} is in a bad format. - * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery - * service. - */ - public void initRecoveryService( - @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList) - throws BadCertificateFormatException, InternalRecoveryServiceException { - throw new UnsupportedOperationException("Deprecated initRecoveryService method called"); - - } - - /** - * Returns data necessary to store all recoverable keys for given account. Key material is - * encrypted with user secret and recovery public key. - * - * @param account specific to Recovery agent. - * @return Data necessary to recover keystore. - * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery - * service. - */ - public @NonNull KeychainSnapshot getRecoveryData(@NonNull byte[] account) - throws InternalRecoveryServiceException { - try { - return BackwardsCompat.toLegacyKeychainSnapshot(mBinder.getKeyChainSnapshot()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - if (e.errorCode == ERROR_NO_SNAPSHOT_PENDING) { - return null; - } - throw wrapUnexpectedServiceSpecificException(e); - } - } - - /** - * Sets a listener which notifies recovery agent that new recovery snapshot is available. {@link - * #getRecoveryData} can be used to get the snapshot. Note that every recovery agent can have at - * most one registered listener at any time. - * - * @param intent triggered when new snapshot is available. Unregisters listener if the value is - * {@code null}. - * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery - * service. - */ - public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent) - throws InternalRecoveryServiceException { - try { - mBinder.setSnapshotCreatedPendingIntent(intent); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw wrapUnexpectedServiceSpecificException(e); - } - } - - /** - * Returns a map from recovery agent accounts to corresponding KeyStore recovery snapshot - * version. Version zero is used, if no snapshots were created for the account. - * - * @return Map from recovery agent accounts to snapshot versions. - * @see KeychainSnapshot#getSnapshotVersion - * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery - * service. - */ - public @NonNull Map<byte[], Integer> getRecoverySnapshotVersions() - throws InternalRecoveryServiceException { - throw new UnsupportedOperationException(); - } - - /** - * Server parameters used to generate new recovery key blobs. This value will be included in - * {@code KeychainSnapshot.getEncryptedRecoveryKeyBlob()}. The same value must be included - * in vaultParams {@link #startRecoverySession} - * - * @param serverParams included in recovery key blob. - * @see #getRecoveryData - * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery - * service. - */ - public void setServerParams(byte[] serverParams) throws InternalRecoveryServiceException { - try { - mBinder.setServerParams(serverParams); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw wrapUnexpectedServiceSpecificException(e); - } - } - - /** - * Updates recovery status for given keys. It is used to notify keystore that key was - * successfully stored on the server or there were an error. Application can check this value - * using {@code getRecoveyStatus}. - * - * @param packageName Application whose recoverable keys' statuses are to be updated. - * @param aliases List of application-specific key aliases. If the array is empty, updates the - * status for all existing recoverable keys. - * @param status Status specific to recovery agent. - * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery - * service. - */ - public void setRecoveryStatus( - @NonNull String packageName, @Nullable String[] aliases, int status) - throws NameNotFoundException, InternalRecoveryServiceException { - try { - for (String alias : aliases) { - mBinder.setRecoveryStatus(alias, status); - } - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw wrapUnexpectedServiceSpecificException(e); - } - } - - /** - * Returns a {@code Map} from Application's KeyStore key aliases to their recovery status. - * Negative status values are reserved for recovery agent specific codes. List of common codes: - * - * <ul> - * <li>{@link #RECOVERY_STATUS_SYNCED} - * <li>{@link #RECOVERY_STATUS_SYNC_IN_PROGRESS} - * <li>{@link #RECOVERY_STATUS_MISSING_ACCOUNT} - * <li>{@link #RECOVERY_STATUS_PERMANENT_FAILURE} - * </ul> - * - * @return {@code Map} from KeyStore alias to recovery status. - * @see #setRecoveryStatus - * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery - * service. - */ - public Map<String, Integer> getRecoveryStatus() throws InternalRecoveryServiceException { - try { - // IPC doesn't support generic Maps. - @SuppressWarnings("unchecked") - Map<String, Integer> result = - (Map<String, Integer>) mBinder.getRecoveryStatus(); - return result; - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw wrapUnexpectedServiceSpecificException(e); - } - } - - /** - * Specifies a set of secret types used for end-to-end keystore encryption. Knowing all of them - * is necessary to recover data. - * - * @param secretTypes {@link KeychainProtectionParams#TYPE_LOCKSCREEN} or {@link - * KeychainProtectionParams#TYPE_CUSTOM_PASSWORD} - * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery - * service. - */ - public void setRecoverySecretTypes( - @NonNull @KeychainProtectionParams.UserSecretType int[] secretTypes) - throws InternalRecoveryServiceException { - try { - mBinder.setRecoverySecretTypes(secretTypes); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw wrapUnexpectedServiceSpecificException(e); - } - } - - /** - * Defines a set of secret types used for end-to-end keystore encryption. Knowing all of them is - * necessary to generate KeychainSnapshot. - * - * @return list of recovery secret types - * @see KeychainSnapshot - * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery - * service. - */ - public @NonNull @KeychainProtectionParams.UserSecretType int[] getRecoverySecretTypes() - throws InternalRecoveryServiceException { - try { - return mBinder.getRecoverySecretTypes(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw wrapUnexpectedServiceSpecificException(e); - } - } - - /** - * Returns a list of recovery secret types, necessary to create a pending recovery snapshot. - * When user enters a secret of a pending type {@link #recoverySecretAvailable} should be - * called. - * - * @return list of recovery secret types - * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery - * service. - */ - @NonNull - public @KeychainProtectionParams.UserSecretType int[] getPendingRecoverySecretTypes() - throws InternalRecoveryServiceException { - throw new UnsupportedOperationException(); - } - - /** - * Initializes recovery session and returns a blob with proof of recovery secrets possession. - * The method generates symmetric key for a session, which trusted remote device can use to - * return recovery key. - * - * @param verifierPublicKey Encoded {@code java.security.cert.X509Certificate} with Public key - * used to create the recovery blob on the source device. - * Keystore will verify the certificate using root of trust. - * @param vaultParams Must match the parameters in the corresponding field in the recovery blob. - * Used to limit number of guesses. - * @param vaultChallenge Data passed from server for this recovery session and used to prevent - * replay attacks - * @param secrets Secrets provided by user, the method only uses type and secret fields. - * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is - * encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric - * key and parameters necessary to identify the counter with the number of failed recovery - * attempts. - * @throws BadCertificateFormatException if the {@code verifierPublicKey} is in an incorrect - * format. - * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery - * service. - */ - @NonNull public RecoveryClaim startRecoverySession( - @NonNull byte[] verifierPublicKey, - @NonNull byte[] vaultParams, - @NonNull byte[] vaultChallenge, - @NonNull List<KeychainProtectionParams> secrets) - throws BadCertificateFormatException, InternalRecoveryServiceException { - try { - RecoverySession recoverySession = RecoverySession.newInstance(this); - byte[] recoveryClaim = - mBinder.startRecoverySession( - recoverySession.getSessionId(), - verifierPublicKey, - vaultParams, - vaultChallenge, - BackwardsCompat.fromLegacyKeychainProtectionParams(secrets)); - return new RecoveryClaim(recoverySession, recoveryClaim); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT) { - throw new BadCertificateFormatException(e.getMessage()); - } - throw wrapUnexpectedServiceSpecificException(e); - } - } - - /** - * Imports keys. - * - * @param session Related recovery session, as originally created by invoking - * {@link #startRecoverySession(byte[], byte[], byte[], List)}. - * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session. - * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob - * and session. KeyStore only uses package names from the application info in {@link - * WrappedApplicationKey}. Caller is responsibility to perform certificates check. - * @return Map from alias to raw key material. - * @throws SessionExpiredException if {@code session} has since been closed. - * @throws DecryptionFailedException if unable to decrypt the snapshot. - * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service. - */ - public Map<String, byte[]> recoverKeys( - @NonNull RecoverySession session, - @NonNull byte[] recoveryKeyBlob, - @NonNull List<WrappedApplicationKey> applicationKeys) - throws SessionExpiredException, DecryptionFailedException, - InternalRecoveryServiceException { - try { - return (Map<String, byte[]>) mBinder.recoverKeys( - session.getSessionId(), - recoveryKeyBlob, - BackwardsCompat.fromLegacyWrappedApplicationKeys(applicationKeys)); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - if (e.errorCode == ERROR_DECRYPTION_FAILED) { - throw new DecryptionFailedException(e.getMessage()); - } - if (e.errorCode == ERROR_SESSION_EXPIRED) { - throw new SessionExpiredException(e.getMessage()); - } - throw wrapUnexpectedServiceSpecificException(e); - } - } - - /** - * Deletes all data associated with {@code session}. Should not be invoked directly but via - * {@link RecoverySession#close()}. - * - * @hide - */ - void closeSession(RecoverySession session) { - try { - mBinder.closeSession(session.getSessionId()); - } catch (RemoteException | ServiceSpecificException e) { - Log.e(TAG, "Unexpected error trying to close session", e); - } - } - - /** - * Generates a key called {@code alias} and loads it into the recoverable key store. Returns the - * raw material of the key. - * - * @param alias The key alias. - * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery - * service. - * @throws LockScreenRequiredException if the user has not set a lock screen. This is required - * to generate recoverable keys, as the snapshots are encrypted using a key derived from the - * lock screen. - */ - public byte[] generateAndStoreKey(@NonNull String alias) - throws InternalRecoveryServiceException, LockScreenRequiredException { - throw new UnsupportedOperationException(); - } - - /** - * Removes a key called {@code alias} from the recoverable key store. - * - * @param alias The key alias. - * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery - * service. - */ - public void removeKey(@NonNull String alias) throws InternalRecoveryServiceException { - try { - mBinder.removeKey(alias); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } catch (ServiceSpecificException e) { - throw wrapUnexpectedServiceSpecificException(e); - } - } - - private InternalRecoveryServiceException wrapUnexpectedServiceSpecificException( - ServiceSpecificException e) { - if (e.errorCode == ERROR_SERVICE_INTERNAL_ERROR) { - return new InternalRecoveryServiceException(e.getMessage()); - } - - // Should never happen. If it does, it's a bug, and we need to update how the method that - // called this throws its exceptions. - return new InternalRecoveryServiceException("Unexpected error code for method: " - + e.errorCode, e); - } -} diff --git a/android/security/keystore/RecoveryControllerException.java b/android/security/keystore/RecoveryControllerException.java deleted file mode 100644 index f990c236..00000000 --- a/android/security/keystore/RecoveryControllerException.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2018 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 android.security.keystore; - -import java.security.GeneralSecurityException; - -/** - * @deprecated Use {@link android.security.keystore.recovery.RecoveryController}. - * @hide - */ -public abstract class RecoveryControllerException extends GeneralSecurityException { - RecoveryControllerException() { } - - RecoveryControllerException(String msg) { - super(msg); - } - - public RecoveryControllerException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/android/security/keystore/RecoverySession.java b/android/security/keystore/RecoverySession.java deleted file mode 100644 index 8a3e06b7..00000000 --- a/android/security/keystore/RecoverySession.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2018 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 android.security.keystore; - -import java.security.SecureRandom; - -/** - * @deprecated Use {@link android.security.keystore.recovery.RecoverySession}. - * @hide - */ -public class RecoverySession implements AutoCloseable { - - private static final int SESSION_ID_LENGTH_BYTES = 16; - - private final String mSessionId; - private final RecoveryController mRecoveryController; - - private RecoverySession(RecoveryController recoveryController, String sessionId) { - mRecoveryController = recoveryController; - mSessionId = sessionId; - } - - /** - * A new session, started by {@code recoveryManager}. - */ - static RecoverySession newInstance(RecoveryController recoveryController) { - return new RecoverySession(recoveryController, newSessionId()); - } - - /** - * Returns a new random session ID. - */ - private static String newSessionId() { - SecureRandom secureRandom = new SecureRandom(); - byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES]; - secureRandom.nextBytes(sessionId); - StringBuilder sb = new StringBuilder(); - for (byte b : sessionId) { - sb.append(Byte.toHexString(b, /*upperCase=*/ false)); - } - return sb.toString(); - } - - /** - * An internal session ID, used by the framework to match recovery claims to snapshot responses. - */ - String getSessionId() { - return mSessionId; - } - - @Override - public void close() { - mRecoveryController.closeSession(this); - } -} diff --git a/android/security/keystore/UserPresenceUnavailableException.java b/android/security/keystore/UserPresenceUnavailableException.java index cf4099ef..1b053a5c 100644 --- a/android/security/keystore/UserPresenceUnavailableException.java +++ b/android/security/keystore/UserPresenceUnavailableException.java @@ -16,13 +16,13 @@ package android.security.keystore; -import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; /** * Indicates the condition that a proof of user-presence was * requested but this proof was not presented. */ -public class UserPresenceUnavailableException extends InvalidAlgorithmParameterException { +public class UserPresenceUnavailableException extends InvalidKeyException { /** * Constructs a {@code UserPresenceUnavailableException} without a detail message or cause. */ diff --git a/android/security/keystore/WrappedApplicationKey.java b/android/security/keystore/WrappedApplicationKey.java deleted file mode 100644 index 2ce8c7d3..00000000 --- a/android/security/keystore/WrappedApplicationKey.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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 android.security.keystore; - -import android.annotation.NonNull; -import android.os.Parcel; -import android.os.Parcelable; - -import com.android.internal.util.Preconditions; - -/** - * @deprecated Use {@link android.security.keystore.recovery.WrappedApplicationKey}. - * @hide - */ -public final class WrappedApplicationKey implements Parcelable { - private String mAlias; - // The only supported format is AES-256 symmetric key. - private byte[] mEncryptedKeyMaterial; - - /** - * Builder for creating {@link WrappedApplicationKey}. - */ - public static class Builder { - private WrappedApplicationKey mInstance = new WrappedApplicationKey(); - - /** - * Sets Application-specific alias of the key. - * - * @param alias The alias. - * @return This builder. - */ - public Builder setAlias(@NonNull String alias) { - mInstance.mAlias = alias; - return this; - } - - /** - * Sets key material encrypted by recovery key. - * - * @param encryptedKeyMaterial The key material - * @return This builder - */ - - public Builder setEncryptedKeyMaterial(@NonNull byte[] encryptedKeyMaterial) { - mInstance.mEncryptedKeyMaterial = encryptedKeyMaterial; - return this; - } - - /** - * Creates a new {@link WrappedApplicationKey} instance. - * - * @return new instance - * @throws NullPointerException if some required fields were not set. - */ - @NonNull public WrappedApplicationKey build() { - Preconditions.checkNotNull(mInstance.mAlias); - Preconditions.checkNotNull(mInstance.mEncryptedKeyMaterial); - return mInstance; - } - } - - private WrappedApplicationKey() { - - } - - /** - * Deprecated - consider using Builder. - * @hide - */ - public WrappedApplicationKey(@NonNull String alias, @NonNull byte[] encryptedKeyMaterial) { - mAlias = Preconditions.checkNotNull(alias); - mEncryptedKeyMaterial = Preconditions.checkNotNull(encryptedKeyMaterial); - } - - /** - * Application-specific alias of the key. - * - * @see java.security.KeyStore.aliases - */ - public @NonNull String getAlias() { - return mAlias; - } - - /** Key material encrypted by recovery key. */ - public @NonNull byte[] getEncryptedKeyMaterial() { - return mEncryptedKeyMaterial; - } - - public static final Parcelable.Creator<WrappedApplicationKey> CREATOR = - new Parcelable.Creator<WrappedApplicationKey>() { - public WrappedApplicationKey createFromParcel(Parcel in) { - return new WrappedApplicationKey(in); - } - - public WrappedApplicationKey[] newArray(int length) { - return new WrappedApplicationKey[length]; - } - }; - - /** - * @hide - */ - @Override - public void writeToParcel(Parcel out, int flags) { - out.writeString(mAlias); - out.writeByteArray(mEncryptedKeyMaterial); - } - - /** - * @hide - */ - protected WrappedApplicationKey(Parcel in) { - mAlias = in.readString(); - mEncryptedKeyMaterial = in.createByteArray(); - } - - @Override - public int describeContents() { - return 0; - } -} diff --git a/android/security/keystore/recovery/RecoveryController.java b/android/security/keystore/recovery/RecoveryController.java index 281822a3..b84843bf 100644 --- a/android/security/keystore/recovery/RecoveryController.java +++ b/android/security/keystore/recovery/RecoveryController.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; +import android.app.KeyguardManager; import android.app.PendingIntent; import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; @@ -250,6 +251,16 @@ public class RecoveryController { */ public static final int ERROR_INVALID_CERTIFICATE = 28; + + /** + * Failed because the provided certificate contained serial version which is lower that the + * version device is already initialized with. It is not possible to downgrade serial version of + * the provided certificate. + * + * @hide + */ + public static final int ERROR_DOWNGRADE_CERTIFICATE = 29; + private final ILockSettings mBinder; private final KeyStore mKeyStore; @@ -278,6 +289,18 @@ public class RecoveryController { } /** + * Checks whether the recoverable key store is currently available. + * + * <p>If it returns true, the device must currently be using a screen lock that is supported for + * use with the recoverable key store, i.e. AOSP PIN, pattern or password. + */ + @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) + public static boolean isRecoverableKeyStoreEnabled(@NonNull Context context) { + KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class); + return keyguardManager != null && keyguardManager.isDeviceSecure(); + } + + /** * @deprecated Use {@link #initRecoveryService(String, byte[], byte[])} instead. */ @Deprecated @@ -340,6 +363,10 @@ public class RecoveryController { || e.errorCode == ERROR_INVALID_CERTIFICATE) { throw new CertificateException("Invalid certificate for recovery service", e); } + if (e.errorCode == ERROR_DOWNGRADE_CERTIFICATE) { + throw new CertificateException( + "Downgrading certificate serial version isn't supported.", e); + } throw wrapUnexpectedServiceSpecificException(e); } } diff --git a/android/service/notification/NotificationListenerService.java b/android/service/notification/NotificationListenerService.java index 3726e66d..32737c54 100644 --- a/android/service/notification/NotificationListenerService.java +++ b/android/service/notification/NotificationListenerService.java @@ -28,6 +28,7 @@ import android.app.Notification.Builder; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationManager; +import android.app.Person; import android.app.Service; import android.companion.CompanionDeviceManager; import android.content.ComponentName; @@ -1195,13 +1196,13 @@ public abstract class NotificationListenerService extends Service { */ private void maybePopulatePeople(Notification notification) { if (getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P) { - ArrayList<Notification.Person> people = notification.extras.getParcelableArrayList( + ArrayList<Person> people = notification.extras.getParcelableArrayList( Notification.EXTRA_PEOPLE_LIST); if (people != null && people.isEmpty()) { int size = people.size(); String[] peopleArray = new String[size]; for (int i = 0; i < size; i++) { - Notification.Person person = people.get(i); + Person person = people.get(i); peopleArray[i] = person.resolveToLegacyUri(); } notification.extras.putStringArray(Notification.EXTRA_PEOPLE, peopleArray); diff --git a/android/service/notification/ZenModeConfig.java b/android/service/notification/ZenModeConfig.java index 3830b7ac..7b01f7a4 100644 --- a/android/service/notification/ZenModeConfig.java +++ b/android/service/notification/ZenModeConfig.java @@ -83,7 +83,8 @@ public class ZenModeConfig implements Parcelable { private static final int DAY_MINUTES = 24 * 60; private static final int ZERO_VALUE_MS = 10 * SECONDS_MS; - // Default allow categories set in readXml() from default_zen_mode_config.xml, fallback values: + // Default allow categories set in readXml() from default_zen_mode_config.xml, + // fallback/upgrade values: private static final boolean DEFAULT_ALLOW_ALARMS = true; private static final boolean DEFAULT_ALLOW_MEDIA = true; private static final boolean DEFAULT_ALLOW_SYSTEM = false; @@ -97,7 +98,7 @@ public class ZenModeConfig implements Parcelable { private static final int DEFAULT_SUPPRESSED_VISUAL_EFFECTS = Policy.getAllSuppressedVisualEffects(); - public static final int XML_VERSION = 6; + public static final int XML_VERSION = 7; public static final String ZEN_TAG = "zen"; private static final String ZEN_ATT_VERSION = "version"; private static final String ZEN_ATT_USER = "user"; @@ -1487,14 +1488,18 @@ public class ZenModeConfig implements Parcelable { /** * Returns a description of the current do not disturb settings from config. * - If turned on manually and end time is known, returns end time. + * - If turned on manually and end time is on forever until turned off, return null if + * describeForeverCondition is false, else return String describing indefinite behavior * - If turned on by an automatic rule, returns the automatic rule name. * - If on due to an app, returns the app name. * - If there's a combination of rules/apps that trigger, then shows the one that will * last the longest if applicable. - * @return null if do not disturb is off. + * @return null if DND is off or describeForeverCondition is false and + * DND is on forever (until turned off) */ - public static String getDescription(Context context, boolean zenOn, ZenModeConfig config) { - if (!zenOn) { + public static String getDescription(Context context, boolean zenOn, ZenModeConfig config, + boolean describeForeverCondition) { + if (!zenOn || config == null) { return null; } @@ -1513,8 +1518,11 @@ public class ZenModeConfig implements Parcelable { } else { if (id == null) { // Do not disturb manually triggered to remain on forever until turned off - // No subtext - return null; + if (describeForeverCondition) { + return context.getString(R.string.zen_mode_forever); + } else { + return null; + } } else { latestEndTime = tryParseCountdownConditionId(id); if (latestEndTime > 0) { diff --git a/android/service/textclassifier/TextClassifierService.java b/android/service/textclassifier/TextClassifierService.java index f1bb72cf..b461c0da 100644 --- a/android/service/textclassifier/TextClassifierService.java +++ b/android/service/textclassifier/TextClassifierService.java @@ -17,6 +17,7 @@ package android.service.textclassifier; import android.Manifest; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; @@ -97,7 +98,8 @@ public abstract class TextClassifierService extends Service { Preconditions.checkNotNull(request); Preconditions.checkNotNull(callback); TextClassifierService.this.onSuggestSelection( - sessionId, request, mCancellationSignal, + request.getText(), request.getStartIndex(), request.getEndIndex(), + TextSelection.Options.from(sessionId, request), mCancellationSignal, new Callback<TextSelection>() { @Override public void onSuccess(TextSelection result) { @@ -130,7 +132,8 @@ public abstract class TextClassifierService extends Service { Preconditions.checkNotNull(request); Preconditions.checkNotNull(callback); TextClassifierService.this.onClassifyText( - sessionId, request, mCancellationSignal, + request.getText(), request.getStartIndex(), request.getEndIndex(), + TextClassification.Options.from(sessionId, request), mCancellationSignal, new Callback<TextClassification>() { @Override public void onSuccess(TextClassification result) { @@ -161,7 +164,8 @@ public abstract class TextClassifierService extends Service { Preconditions.checkNotNull(request); Preconditions.checkNotNull(callback); TextClassifierService.this.onGenerateLinks( - sessionId, request, mCancellationSignal, + request.getText(), TextLinks.Options.from(sessionId, request), + mCancellationSignal, new Callback<TextLinks>() { @Override public void onSuccess(TextLinks result) { @@ -234,6 +238,25 @@ public abstract class TextClassifierService extends Service { @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextSelection> callback); + // TODO: Remove once apps can build against the latest sdk. + /** @hide */ + public void onSuggestSelection( + @NonNull CharSequence text, + @IntRange(from = 0) int selectionStartIndex, + @IntRange(from = 0) int selectionEndIndex, + @Nullable TextSelection.Options options, + @NonNull CancellationSignal cancellationSignal, + @NonNull Callback<TextSelection> callback) { + final TextClassificationSessionId sessionId = options.getSessionId(); + final TextSelection.Request request = options.getRequest() != null + ? options.getRequest() + : new TextSelection.Request.Builder( + text, selectionStartIndex, selectionEndIndex) + .setDefaultLocales(options.getDefaultLocales()) + .build(); + onSuggestSelection(sessionId, request, cancellationSignal, callback); + } + /** * Classifies the specified text and returns a {@link TextClassification} object that can be * used to generate a widget for handling the classified text. @@ -249,6 +272,26 @@ public abstract class TextClassifierService extends Service { @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextClassification> callback); + // TODO: Remove once apps can build against the latest sdk. + /** @hide */ + public void onClassifyText( + @NonNull CharSequence text, + @IntRange(from = 0) int startIndex, + @IntRange(from = 0) int endIndex, + @Nullable TextClassification.Options options, + @NonNull CancellationSignal cancellationSignal, + @NonNull Callback<TextClassification> callback) { + final TextClassificationSessionId sessionId = options.getSessionId(); + final TextClassification.Request request = options.getRequest() != null + ? options.getRequest() + : new TextClassification.Request.Builder( + text, startIndex, endIndex) + .setDefaultLocales(options.getDefaultLocales()) + .setReferenceTime(options.getReferenceTime()) + .build(); + onClassifyText(sessionId, request, cancellationSignal, callback); + } + /** * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with * links information. @@ -264,6 +307,23 @@ public abstract class TextClassifierService extends Service { @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextLinks> callback); + // TODO: Remove once apps can build against the latest sdk. + /** @hide */ + public void onGenerateLinks( + @NonNull CharSequence text, + @Nullable TextLinks.Options options, + @NonNull CancellationSignal cancellationSignal, + @NonNull Callback<TextLinks> callback) { + final TextClassificationSessionId sessionId = options.getSessionId(); + final TextLinks.Request request = options.getRequest() != null + ? options.getRequest() + : new TextLinks.Request.Builder(text) + .setDefaultLocales(options.getDefaultLocales()) + .setEntityConfig(options.getEntityConfig()) + .build(); + onGenerateLinks(sessionId, request, cancellationSignal, callback); + } + /** * Writes the selection event. * This is called when a selection event occurs. e.g. user changed selection; or smart selection @@ -283,17 +343,17 @@ public abstract class TextClassifierService extends Service { * @param context the text classification context * @param sessionId the session's Id */ - public abstract void onCreateTextClassificationSession( + public void onCreateTextClassificationSession( @NonNull TextClassificationContext context, - @NonNull TextClassificationSessionId sessionId); + @NonNull TextClassificationSessionId sessionId) {} /** * Destroys the text classification session identified by the specified sessionId. * * @param sessionId the id of the session to destroy */ - public abstract void onDestroyTextClassificationSession( - @NonNull TextClassificationSessionId sessionId); + public void onDestroyTextClassificationSession( + @NonNull TextClassificationSessionId sessionId) {} /** * Returns a TextClassifier that runs in this service's process. diff --git a/android/service/wallpaper/WallpaperService.java b/android/service/wallpaper/WallpaperService.java index a1327301..7f75f0a6 100644 --- a/android/service/wallpaper/WallpaperService.java +++ b/android/service/wallpaper/WallpaperService.java @@ -813,7 +813,7 @@ public abstract class WallpaperService extends Service { } final int relayoutResult = mSession.relayout( mWindow, mWindow.mSeq, mLayout, mWidth, mHeight, - View.VISIBLE, 0, mWinFrame, mOverscanInsets, mContentInsets, + View.VISIBLE, 0, -1, mWinFrame, mOverscanInsets, mContentInsets, mVisibleInsets, mStableInsets, mOutsets, mBackdropFrame, mDisplayCutout, mMergedConfiguration, mSurfaceHolder.mSurface); diff --git a/android/support/v4/media/session/MediaControllerCompat.java b/android/support/v4/media/session/MediaControllerCompat.java index 5e6f4eac..4a4ad322 100644 --- a/android/support/v4/media/session/MediaControllerCompat.java +++ b/android/support/v4/media/session/MediaControllerCompat.java @@ -667,13 +667,13 @@ public final class MediaControllerCompat { public static abstract class Callback implements IBinder.DeathRecipient { private final Object mCallbackObj; MessageHandler mHandler; - boolean mHasExtraCallback; + IMediaControllerCallback mIControllerCallback; public Callback() { if (android.os.Build.VERSION.SDK_INT >= 21) { mCallbackObj = MediaControllerCompatApi21.createCallback(new StubApi21(this)); } else { - mCallbackObj = new StubCompat(this); + mCallbackObj = mIControllerCallback = new StubCompat(this); } } @@ -789,6 +789,14 @@ public final class MediaControllerCompat { public void onShuffleModeChanged(@PlaybackStateCompat.ShuffleMode int shuffleMode) { } + /** + * @hide + */ + @RestrictTo(LIBRARY) + public IMediaControllerCallback getIControllerCallback() { + return mIControllerCallback; + } + @Override public void binderDied() { onSessionDestroyed(); @@ -837,7 +845,8 @@ public final class MediaControllerCompat { public void onSessionEvent(String event, Bundle extras) { MediaControllerCompat.Callback callback = mCallback.get(); if (callback != null) { - if (callback.mHasExtraCallback && android.os.Build.VERSION.SDK_INT < 23) { + if (callback.mIControllerCallback != null + && android.os.Build.VERSION.SDK_INT < 23) { // Ignore. ExtraCallback will handle this. } else { callback.onSessionEvent(event, extras); @@ -849,7 +858,7 @@ public final class MediaControllerCompat { public void onPlaybackStateChanged(Object stateObj) { MediaControllerCompat.Callback callback = mCallback.get(); if (callback != null) { - if (callback.mHasExtraCallback) { + if (callback.mIControllerCallback != null) { // Ignore. ExtraCallback will handle this. } else { callback.onPlaybackStateChanged( @@ -1944,7 +1953,7 @@ public final class MediaControllerCompat { if (mExtraBinder != null) { ExtraCallback extraCallback = new ExtraCallback(callback); mCallbackMap.put(callback, extraCallback); - callback.mHasExtraCallback = true; + callback.mIControllerCallback = extraCallback; try { mExtraBinder.registerCallbackListener(extraCallback); } catch (RemoteException e) { @@ -1952,7 +1961,7 @@ public final class MediaControllerCompat { } } else { synchronized (mPendingCallbacks) { - callback.mHasExtraCallback = false; + callback.mIControllerCallback = null; mPendingCallbacks.add(callback); } } @@ -1965,7 +1974,7 @@ public final class MediaControllerCompat { try { ExtraCallback extraCallback = mCallbackMap.remove(callback); if (extraCallback != null) { - callback.mHasExtraCallback = false; + callback.mIControllerCallback = null; mExtraBinder.unregisterCallbackListener(extraCallback); } } catch (RemoteException e) { @@ -2173,7 +2182,7 @@ public final class MediaControllerCompat { for (Callback callback : mPendingCallbacks) { ExtraCallback extraCallback = new ExtraCallback(callback); mCallbackMap.put(callback, extraCallback); - callback.mHasExtraCallback = true; + callback.mIControllerCallback = extraCallback; try { mExtraBinder.registerCallbackListener(extraCallback); } catch (RemoteException e) { diff --git a/android/support/v4/media/session/PlaybackStateCompat.java b/android/support/v4/media/session/PlaybackStateCompat.java index e6420ea0..b9c51caa 100644 --- a/android/support/v4/media/session/PlaybackStateCompat.java +++ b/android/support/v4/media/session/PlaybackStateCompat.java @@ -15,7 +15,6 @@ */ package android.support.v4.media.session; - import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.os.Build; diff --git a/android/system/Os.java b/android/system/Os.java index 404b8221..6301ff9f 100644 --- a/android/system/Os.java +++ b/android/system/Os.java @@ -186,17 +186,6 @@ public final class Os { public static int getgid() { return Libcore.os.getgid(); } /** - * See <a href="http://man7.org/linux/man-pages/man2/getgroups.2.html">getgroups(2)</a>. - * - * <p>Should the number of groups change during the execution of this call, the call may - * return an arbitrary subset. This may be worth reconsidering should this be exposed - * as public API. - * - * @hide - */ - public static int[] getgroups() throws ErrnoException { return Libcore.os.getgroups(); } - - /** * See <a href="http://man7.org/linux/man-pages/man3/getenv.3.html">getenv(3)</a>. */ public static String getenv(String name) { return Libcore.os.getenv(name); } @@ -507,13 +496,6 @@ public final class Os { public static void setgid(int gid) throws ErrnoException { Libcore.os.setgid(gid); } /** - * See <a href="http://man7.org/linux/man-pages/man2/setgroups.2.html">setgroups(2)</a>. - * - * @hide - */ - public static void setgroups(int[] gids) throws ErrnoException { Libcore.os.setgroups(gids); } - - /** * See <a href="http://man7.org/linux/man-pages/man2/setpgid.2.html">setpgid(2)</a>. */ /** @hide */ public static void setpgid(int pid, int pgid) throws ErrnoException { Libcore.os.setpgid(pid, pgid); } diff --git a/android/telecom/Connection.java b/android/telecom/Connection.java index 36333e44..3bf951d4 100644 --- a/android/telecom/Connection.java +++ b/android/telecom/Connection.java @@ -2600,7 +2600,6 @@ public abstract class Connection extends Conferenceable { } /** - * * Request audio routing to a specific bluetooth device. Calling this method may result in * the device routing audio to a different bluetooth device than the one specified if the * bluetooth stack is unable to route audio to the requested device. @@ -2611,13 +2610,13 @@ public abstract class Connection extends Conferenceable { * Used by self-managed {@link ConnectionService}s which wish to use bluetooth audio for a * self-managed {@link Connection} (see {@link PhoneAccount#CAPABILITY_SELF_MANAGED}.) * <p> - * See also {@link InCallService#requestBluetoothAudio(String)} - * @param bluetoothAddress The address of the bluetooth device to connect to, as returned by - * {@link BluetoothDevice#getAddress()}. + * See also {@link InCallService#requestBluetoothAudio(BluetoothDevice)} + * @param bluetoothDevice The bluetooth device to connect to. */ - public void requestBluetoothAudio(@NonNull String bluetoothAddress) { + public void requestBluetoothAudio(@NonNull BluetoothDevice bluetoothDevice) { for (Listener l : mListeners) { - l.onAudioRouteChanged(this, CallAudioState.ROUTE_BLUETOOTH, bluetoothAddress); + l.onAudioRouteChanged(this, CallAudioState.ROUTE_BLUETOOTH, + bluetoothDevice.getAddress()); } } diff --git a/android/telecom/InCallService.java b/android/telecom/InCallService.java index af65c65a..bd25ab2b 100644 --- a/android/telecom/InCallService.java +++ b/android/telecom/InCallService.java @@ -428,12 +428,11 @@ public abstract class InCallService extends Service { * A list of available devices can be obtained via * {@link CallAudioState#getSupportedBluetoothDevices()} * - * @param bluetoothAddress The address of the bluetooth device to connect to, as returned by - * {@link BluetoothDevice#getAddress()}. + * @param bluetoothDevice The bluetooth device to connect to. */ - public final void requestBluetoothAudio(@NonNull String bluetoothAddress) { + public final void requestBluetoothAudio(@NonNull BluetoothDevice bluetoothDevice) { if (mPhone != null) { - mPhone.requestBluetoothAudio(bluetoothAddress); + mPhone.requestBluetoothAudio(bluetoothDevice.getAddress()); } } diff --git a/android/telecom/PhoneAccount.java b/android/telecom/PhoneAccount.java index 95eb14ad..b3a3bf21 100644 --- a/android/telecom/PhoneAccount.java +++ b/android/telecom/PhoneAccount.java @@ -129,6 +129,9 @@ public final class PhoneAccount implements Parcelable { * <p> * By default, Self-Managed {@link PhoneAccount}s do not log their calls to the call log. * Setting this extra to {@code true} provides a means for them to log their calls. + * <p> + * Note: Only calls where the {@link Call.Details#getHandle()} {@link Uri#getScheme()} is + * {@link #SCHEME_SIP} or {@link #SCHEME_TEL} will be logged at the current time. */ public static final String EXTRA_LOG_SELF_MANAGED_CALLS = "android.telecom.extra.LOG_SELF_MANAGED_CALLS"; diff --git a/android/telephony/AccessNetworkConstants.java b/android/telephony/AccessNetworkConstants.java index cac9f2b5..3b773b3a 100644 --- a/android/telephony/AccessNetworkConstants.java +++ b/android/telephony/AccessNetworkConstants.java @@ -16,8 +16,6 @@ package android.telephony; -import android.annotation.SystemApi; - /** * Contains access network related constants. */ @@ -39,7 +37,6 @@ public final class AccessNetworkConstants { * Wireless transportation type * @hide */ - @SystemApi public static final class TransportType { /** Wireless Wide Area Networks (i.e. Cellular) */ public static final int WWAN = 1; diff --git a/android/telephony/CarrierConfigManager.java b/android/telephony/CarrierConfigManager.java index 4683161d..e2441316 100644 --- a/android/telephony/CarrierConfigManager.java +++ b/android/telephony/CarrierConfigManager.java @@ -1684,6 +1684,14 @@ public class CarrierConfigManager { "data_warning_threshold_bytes_long"; /** + * Controls if the device should automatically notify the user as they reach + * their cellular data warning. When set to {@code false} the carrier is + * expected to have implemented their own notification mechanism. + */ + public static final String KEY_DATA_WARNING_NOTIFICATION_BOOL = + "data_warning_notification_bool"; + + /** * Controls the cellular data limit. * <p> * If the user uses more than this amount of data in their billing cycle, as defined by @@ -1698,6 +1706,22 @@ public class CarrierConfigManager { "data_limit_threshold_bytes_long"; /** + * Controls if the device should automatically notify the user as they reach + * their cellular data limit. When set to {@code false} the carrier is + * expected to have implemented their own notification mechanism. + */ + public static final String KEY_DATA_LIMIT_NOTIFICATION_BOOL = + "data_limit_notification_bool"; + + /** + * Controls if the device should automatically notify the user when rapid + * cellular data usage is observed. When set to {@code false} the carrier is + * expected to have implemented their own notification mechanism. + */ + public static final String KEY_DATA_RAPID_NOTIFICATION_BOOL = + "data_rapid_notification_bool"; + + /** * Offset to be reduced from rsrp threshold while calculating signal strength level. * @hide */ @@ -1954,7 +1978,7 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_CARRIER_FORCE_DISABLE_ETWS_CMAS_TEST_BOOL, false); sDefaults.putBoolean(KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL, false); sDefaults.putBoolean(KEY_CARRIER_VOLTE_OVERRIDE_WFC_PROVISIONING_BOOL, false); - sDefaults.putBoolean(KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL, false); + sDefaults.putBoolean(KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL, true); sDefaults.putBoolean(KEY_CARRIER_ALLOW_TURNOFF_IMS_BOOL, true); sDefaults.putBoolean(KEY_CARRIER_IMS_GBA_REQUIRED_BOOL, false); sDefaults.putBoolean(KEY_CARRIER_INSTANT_LETTERING_AVAILABLE_BOOL, false); @@ -2165,7 +2189,10 @@ public class CarrierConfigManager { sDefaults.putInt(KEY_MONTHLY_DATA_CYCLE_DAY_INT, DATA_CYCLE_USE_PLATFORM_DEFAULT); sDefaults.putLong(KEY_DATA_WARNING_THRESHOLD_BYTES_LONG, DATA_CYCLE_USE_PLATFORM_DEFAULT); + sDefaults.putBoolean(KEY_DATA_WARNING_NOTIFICATION_BOOL, true); sDefaults.putLong(KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG, DATA_CYCLE_USE_PLATFORM_DEFAULT); + sDefaults.putBoolean(KEY_DATA_LIMIT_NOTIFICATION_BOOL, true); + sDefaults.putBoolean(KEY_DATA_RAPID_NOTIFICATION_BOOL, true); // Rat families: {GPRS, EDGE}, {EVDO, EVDO_A, EVDO_B}, {UMTS, HSPA, HSDPA, HSUPA, HSPAP}, // {LTE, LTE_CA} @@ -2187,7 +2214,7 @@ public class CarrierConfigManager { sDefaults.putStringArray(KEY_FILTERED_CNAP_NAMES_STRING_ARRAY, null); sDefaults.putBoolean(KEY_EDITABLE_WFC_ROAMING_MODE_BOOL, false); sDefaults.putBoolean(KEY_STK_DISABLE_LAUNCH_BROWSER_BOOL, false); - sDefaults.putBoolean(KEY_PERSIST_LPP_MODE_BOOL, false); + sDefaults.putBoolean(KEY_PERSIST_LPP_MODE_BOOL, true); sDefaults.putStringArray(KEY_CARRIER_WIFI_STRING_ARRAY, null); sDefaults.putInt(KEY_PREF_NETWORK_NOTIFICATION_DELAY_INT, -1); sDefaults.putInt(KEY_EMERGENCY_NOTIFICATION_DELAY_INT, -1); diff --git a/android/telephony/NetworkRegistrationState.java b/android/telephony/NetworkRegistrationState.java index bba779d0..0e2e0cea 100644 --- a/android/telephony/NetworkRegistrationState.java +++ b/android/telephony/NetworkRegistrationState.java @@ -18,7 +18,6 @@ package android.telephony; import android.annotation.IntDef; import android.annotation.Nullable; -import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; @@ -31,7 +30,6 @@ import java.util.Objects; * Description of a mobile network registration state * @hide */ -@SystemApi public class NetworkRegistrationState implements Parcelable { /** * Network domain diff --git a/android/telephony/NetworkService.java b/android/telephony/NetworkService.java index 35682a74..b431590d 100644 --- a/android/telephony/NetworkService.java +++ b/android/telephony/NetworkService.java @@ -17,7 +17,6 @@ package android.telephony; import android.annotation.CallSuper; -import android.annotation.SystemApi; import android.app.Service; import android.content.Intent; import android.os.Handler; @@ -47,7 +46,6 @@ import java.util.List; * </service> * @hide */ -@SystemApi public abstract class NetworkService extends Service { private final String TAG = NetworkService.class.getSimpleName(); @@ -206,8 +204,10 @@ public abstract class NetworkService extends Service { } } - /** @hide */ - protected NetworkService() { + /** + * Default constructor. + */ + public NetworkService() { mHandlerThread = new HandlerThread(TAG); mHandlerThread.start(); diff --git a/android/telephony/NetworkServiceCallback.java b/android/telephony/NetworkServiceCallback.java index dbad02fd..ad3b00fd 100644 --- a/android/telephony/NetworkServiceCallback.java +++ b/android/telephony/NetworkServiceCallback.java @@ -17,7 +17,6 @@ package android.telephony; import android.annotation.IntDef; -import android.annotation.SystemApi; import android.os.RemoteException; import android.telephony.NetworkService.NetworkServiceProvider; @@ -33,7 +32,6 @@ import java.lang.ref.WeakReference; * * @hide */ -@SystemApi public class NetworkServiceCallback { private static final String mTag = NetworkServiceCallback.class.getSimpleName(); diff --git a/android/telephony/ServiceState.java b/android/telephony/ServiceState.java index e971d08a..8ffdb21c 100644 --- a/android/telephony/ServiceState.java +++ b/android/telephony/ServiceState.java @@ -17,7 +17,6 @@ package android.telephony; import android.annotation.IntDef; -import android.annotation.SystemApi; import android.annotation.TestApi; import android.os.Bundle; import android.os.Parcel; @@ -977,11 +976,13 @@ public class ServiceState implements Parcelable { } /** @hide */ + @TestApi public void setCellBandwidths(int[] bandwidths) { mCellBandwidths = bandwidths; } /** @hide */ + @TestApi public void setChannelNumber(int channelNumber) { mChannelNumber = channelNumber; } @@ -1172,6 +1173,7 @@ public class ServiceState implements Parcelable { } /** @hide */ + @TestApi public void setRilVoiceRadioTechnology(int rt) { if (rt == RIL_RADIO_TECHNOLOGY_LTE_CA) { rt = RIL_RADIO_TECHNOLOGY_LTE; @@ -1181,6 +1183,7 @@ public class ServiceState implements Parcelable { } /** @hide */ + @TestApi public void setRilDataRadioTechnology(int rt) { if (rt == RIL_RADIO_TECHNOLOGY_LTE_CA) { rt = RIL_RADIO_TECHNOLOGY_LTE; @@ -1530,7 +1533,6 @@ public class ServiceState implements Parcelable { * @return List of registration states * @hide */ - @SystemApi public List<NetworkRegistrationState> getNetworkRegistrationStates() { synchronized (mNetworkRegistrationStates) { return new ArrayList<>(mNetworkRegistrationStates); @@ -1544,7 +1546,6 @@ public class ServiceState implements Parcelable { * @return List of registration states. * @hide */ - @SystemApi public List<NetworkRegistrationState> getNetworkRegistrationStates(int transportType) { List<NetworkRegistrationState> list = new ArrayList<>(); @@ -1567,7 +1568,6 @@ public class ServiceState implements Parcelable { * @return The matching NetworkRegistrationState. * @hide */ - @SystemApi public NetworkRegistrationState getNetworkRegistrationStates(int transportType, int domain) { synchronized (mNetworkRegistrationStates) { for (NetworkRegistrationState networkRegistrationState : mNetworkRegistrationStates) { diff --git a/android/telephony/SubscriptionManager.java b/android/telephony/SubscriptionManager.java index 754fe687..a9389bea 100644 --- a/android/telephony/SubscriptionManager.java +++ b/android/telephony/SubscriptionManager.java @@ -477,6 +477,9 @@ public class SubscriptionManager { * <p> * Contains {@link #EXTRA_SUBSCRIPTION_INDEX} to indicate which subscription * the user is interested in. + * <p> + * Receivers should protect themselves by checking that the sender holds the + * {@code android.permission.MANAGE_SUBSCRIPTION_PLANS} permission. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) @SystemApi @@ -1719,6 +1722,8 @@ public class SubscriptionManager { * </ul> * * @param subId the subscriber this relationship applies to + * @throws SecurityException if the caller doesn't meet the requirements + * outlined above. */ @SystemApi public @NonNull List<SubscriptionPlan> getSubscriptionPlans(int subId) { @@ -1744,10 +1749,13 @@ public class SubscriptionManager { * {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}. * </ul> * - * @param subId the subscriber this relationship applies to + * @param subId the subscriber this relationship applies to. An empty list + * may be sent to clear any existing plans. * @param plans the list of plans. The first plan is always the primary and * most important plan. Any additional plans are secondary and * may not be displayed or used by decision making logic. + * @throws SecurityException if the caller doesn't meet the requirements + * outlined above. */ @SystemApi public void setSubscriptionPlans(int subId, @NonNull List<SubscriptionPlan> plans) { @@ -1788,6 +1796,8 @@ public class SubscriptionManager { * be automatically cleared, or {@code 0} to leave in the * requested state until explicitly cleared, or the next reboot, * whichever happens first. + * @throws SecurityException if the caller doesn't meet the requirements + * outlined above. */ @SystemApi public void setSubscriptionOverrideUnmetered(int subId, boolean overrideUnmetered, @@ -1822,6 +1832,8 @@ public class SubscriptionManager { * be automatically cleared, or {@code 0} to leave in the * requested state until explicitly cleared, or the next reboot, * whichever happens first. + * @throws SecurityException if the caller doesn't meet the requirements + * outlined above. */ @SystemApi public void setSubscriptionOverrideCongested(int subId, boolean overrideCongested, diff --git a/android/telephony/SubscriptionPlan.java b/android/telephony/SubscriptionPlan.java index 4ffb70ba..e8bbe42e 100644 --- a/android/telephony/SubscriptionPlan.java +++ b/android/telephony/SubscriptionPlan.java @@ -24,7 +24,7 @@ import android.annotation.Nullable; import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; -import android.util.Pair; +import android.util.Range; import android.util.RecurrenceRule; import com.android.internal.util.Preconditions; @@ -209,7 +209,7 @@ public final class SubscriptionPlan implements Parcelable { * any recurrence rules. The iterator starts from the currently active cycle * and walks backwards through time. */ - public Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator() { + public Iterator<Range<ZonedDateTime>> cycleIterator() { return cycleRule.cycleIterator(); } @@ -227,6 +227,9 @@ public final class SubscriptionPlan implements Parcelable { /** * Start defining a {@link SubscriptionPlan} that covers a very specific * window of time, and never automatically recurs. + * + * @param start The exact time at which the plan starts. + * @param end The exact time at which the plan ends. */ public static Builder createNonrecurring(ZonedDateTime start, ZonedDateTime end) { if (!end.isAfter(start)) { @@ -237,28 +240,43 @@ public final class SubscriptionPlan implements Parcelable { } /** - * Start defining a {@link SubscriptionPlan} that will recur - * automatically every month. It will always recur on the same day of a - * particular month. When a particular month ends before the defined - * recurrence day, the plan will recur on the last instant of that - * month. + * Start defining a {@link SubscriptionPlan} that starts at a specific + * time, and automatically recurs after each specific period of time, + * repeating indefinitely. + * <p> + * When the given period is set to exactly one month, the plan will + * always recur on the day of the month defined by + * {@link ZonedDateTime#getDayOfMonth()}. When a particular month ends + * before this day, the plan will recur on the last possible instant of + * that month. + * + * @param start The exact time at which the plan starts. + * @param period The period after which the plan automatically recurs. */ + public static Builder createRecurring(ZonedDateTime start, Period period) { + if (period.isZero() || period.isNegative()) { + throw new IllegalArgumentException("Period " + period + " must be positive"); + } + return new Builder(start, null, period); + } + + /** {@hide} */ + @SystemApi + @Deprecated public static Builder createRecurringMonthly(ZonedDateTime start) { return new Builder(start, null, Period.ofMonths(1)); } - /** - * Start defining a {@link SubscriptionPlan} that will recur - * automatically every week. - */ + /** {@hide} */ + @SystemApi + @Deprecated public static Builder createRecurringWeekly(ZonedDateTime start) { return new Builder(start, null, Period.ofDays(7)); } - /** - * Start defining a {@link SubscriptionPlan} that will recur - * automatically every day. - */ + /** {@hide} */ + @SystemApi + @Deprecated public static Builder createRecurringDaily(ZonedDateTime start) { return new Builder(start, null, Period.ofDays(1)); } diff --git a/android/telephony/TelephonyManager.java b/android/telephony/TelephonyManager.java index e15d35b5..956a5b14 100644 --- a/android/telephony/TelephonyManager.java +++ b/android/telephony/TelephonyManager.java @@ -2968,7 +2968,7 @@ public class TelephonyManager { IPhoneSubInfo info = getSubscriberInfo(); if (info == null) return null; - return info.getGroupIdLevel1(mContext.getOpPackageName()); + return info.getGroupIdLevel1ForSubscriber(getSubId(), mContext.getOpPackageName()); } catch (RemoteException ex) { return null; } catch (NullPointerException ex) { @@ -5156,7 +5156,12 @@ public class TelephonyManager { * {@link #AUTHTYPE_EAP_SIM} * @param data authentication challenge data, base64 encoded. * See 3GPP TS 31.102 7.1.2 for more details. - * @return the response of authentication, or null if not available + * @return the response of authentication. This value will be null in the following cases: + * Authentication error, incorrect MAC + * Authentication error, security context not supported + * Key freshness failure + * Authentication error, no memory space available + * Authentication error, no memory space available in EFMUK */ // TODO(b/73660190): This should probably require MODIFY_PHONE_STATE, not // READ_PRIVILEGED_PHONE_STATE. It certainly shouldn't reference the permission in Javadoc since @@ -5177,7 +5182,13 @@ public class TelephonyManager { * {@link #AUTHTYPE_EAP_SIM} * @param data authentication challenge data, base64 encoded. * See 3GPP TS 31.102 7.1.2 for more details. - * @return the response of authentication, or null if not available + * @return the response of authentication. This value will be null in the following cases only + * (see 3GPP TS 31.102 7.3.1): + * Authentication error, incorrect MAC + * Authentication error, security context not supported + * Key freshness failure + * Authentication error, no memory space available + * Authentication error, no memory space available in EFMUK * @hide */ public String getIccAuthentication(int subId, int appType, int authType, String data) { diff --git a/android/telephony/data/DataCallResponse.java b/android/telephony/data/DataCallResponse.java index 25f51333..acc24704 100644 --- a/android/telephony/data/DataCallResponse.java +++ b/android/telephony/data/DataCallResponse.java @@ -19,7 +19,6 @@ package android.telephony.data; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.SystemApi; import android.net.LinkAddress; import android.os.Parcel; import android.os.Parcelable; @@ -34,7 +33,6 @@ import java.util.Objects; * * @hide */ -@SystemApi public final class DataCallResponse implements Parcelable { private final int mStatus; private final int mSuggestedRetryTime; diff --git a/android/telephony/data/DataProfile.java b/android/telephony/data/DataProfile.java index e8597b22..dd274c56 100644 --- a/android/telephony/data/DataProfile.java +++ b/android/telephony/data/DataProfile.java @@ -16,7 +16,6 @@ package android.telephony.data; -import android.annotation.SystemApi; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; @@ -30,7 +29,6 @@ import com.android.internal.telephony.RILConstants; * * @hide */ -@SystemApi public final class DataProfile implements Parcelable { // The types indicating the data profile is used on GSM (3GPP) or CDMA (3GPP2) network. diff --git a/android/telephony/data/DataService.java b/android/telephony/data/DataService.java index e8c1cb11..0835f7d9 100644 --- a/android/telephony/data/DataService.java +++ b/android/telephony/data/DataService.java @@ -20,7 +20,6 @@ import android.annotation.CallSuper; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.SystemApi; import android.app.Service; import android.content.Intent; import android.net.LinkProperties; @@ -55,7 +54,6 @@ import java.util.List; * </service> * @hide */ -@SystemApi public abstract class DataService extends Service { private static final String TAG = DataService.class.getSimpleName(); @@ -429,8 +427,10 @@ public abstract class DataService extends Service { } } - /** @hide */ - protected DataService() { + /** + * Default constructor. + */ + public DataService() { mHandlerThread = new HandlerThread(TAG); mHandlerThread.start(); diff --git a/android/telephony/data/DataServiceCallback.java b/android/telephony/data/DataServiceCallback.java index 4af31b5e..bff82608 100644 --- a/android/telephony/data/DataServiceCallback.java +++ b/android/telephony/data/DataServiceCallback.java @@ -17,7 +17,6 @@ package android.telephony.data; import android.annotation.IntDef; -import android.annotation.SystemApi; import android.net.LinkProperties; import android.os.RemoteException; import android.telephony.Rlog; @@ -35,7 +34,6 @@ import java.util.List; * * @hide */ -@SystemApi public class DataServiceCallback { private static final String TAG = DataServiceCallback.class.getSimpleName(); @@ -125,7 +123,6 @@ public class DataServiceCallback { * * @param result The result code. Must be one of the {@link ResultCode}. */ - @SystemApi public void onSetDataProfileComplete(@ResultCode int result) { IDataServiceCallback callback = mCallback.get(); if (callback != null) { diff --git a/android/telephony/ims/stub/ImsFeatureConfiguration.java b/android/telephony/ims/stub/ImsFeatureConfiguration.java index 2f52c0ac..dfb6e2ce 100644 --- a/android/telephony/ims/stub/ImsFeatureConfiguration.java +++ b/android/telephony/ims/stub/ImsFeatureConfiguration.java @@ -77,6 +77,11 @@ public final class ImsFeatureConfiguration implements Parcelable { result = 31 * result + featureType; return result; } + + @Override + public String toString() { + return "{s=" + slotId + ", f=" + featureType + "}"; + } } /** diff --git a/android/telephony/mbms/DownloadRequest.java b/android/telephony/mbms/DownloadRequest.java index 602c796a..9e3302bd 100644 --- a/android/telephony/mbms/DownloadRequest.java +++ b/android/telephony/mbms/DownloadRequest.java @@ -18,6 +18,7 @@ package android.telephony.mbms; import android.annotation.NonNull; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.content.Intent; import android.net.Uri; import android.os.Parcel; @@ -184,6 +185,7 @@ public final class DownloadRequest implements Parcelable { * @hide */ @SystemApi + @TestApi public Builder setServiceId(String serviceId) { fileServiceId = serviceId; return this; diff --git a/android/text/MeasuredParagraph.java b/android/text/MeasuredParagraph.java index 96edfa31..c2c3182c 100644 --- a/android/text/MeasuredParagraph.java +++ b/android/text/MeasuredParagraph.java @@ -303,10 +303,9 @@ public class MeasuredParagraph { * * This is available only if the MeasuredParagraph is computed with buildForStaticLayout. */ - public void getBounds(@NonNull Paint paint, @IntRange(from = 0) int start, - @IntRange(from = 0) int end, @NonNull Rect bounds) { - nGetBounds(mNativePtr, mCopiedBuffer, paint.getNativeInstance(), start, end, - paint.getBidiFlags(), bounds); + public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end, + @NonNull Rect bounds) { + nGetBounds(mNativePtr, mCopiedBuffer, start, end, bounds); } /** @@ -743,6 +742,6 @@ public class MeasuredParagraph { @CriticalNative private static native int nGetMemoryUsage(/* Non Zero */ long nativePtr); - private static native void nGetBounds(long nativePtr, char[] buf, long paintPtr, int start, - int end, int bidiFlag, Rect rect); + private static native void nGetBounds(long nativePtr, char[] buf, int start, int end, + Rect rect); } diff --git a/android/text/PrecomputedText.java b/android/text/PrecomputedText.java index 413df05d..369f357d 100644 --- a/android/text/PrecomputedText.java +++ b/android/text/PrecomputedText.java @@ -16,6 +16,7 @@ package android.text; +import android.annotation.FloatRange; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; @@ -44,13 +45,17 @@ import java.util.Objects; * <pre> * An example usage is: * <code> - * void asyncSetText(final TextView textView, final String longString, Handler bgThreadHandler) { + * static void asyncSetText(TextView textView, final String longString, Executor bgExecutor) { * // construct precompute related parameters using the TextView that we will set the text on. - * final PrecomputedText.Params params = textView.getTextParams(); - * bgThreadHandler.post(() -> { - * final PrecomputedText precomputedText = - * PrecomputedText.create(expensiveLongString, params); + * final PrecomputedText.Params params = textView.getTextMetricsParams(); + * final Reference textViewRef = new WeakReference<>(textView); + * bgExecutor.submit(() -> { + * TextView textView = textViewRef.get(); + * if (textView == null) return; + * final PrecomputedText precomputedText = PrecomputedText.create(longString, params); * textView.post(() -> { + * TextView textView = textViewRef.get(); + * if (textView == null) return; * textView.setText(precomputedText); * }); * }); @@ -363,6 +368,7 @@ public class PrecomputedText implements Spannable { /** * Return the underlying text. + * @hide */ public @NonNull CharSequence getText() { return mText; @@ -451,32 +457,65 @@ public class PrecomputedText implements Spannable { + ", gave " + pos); } - /** @hide */ - public float getWidth(@IntRange(from = 0) int start, @IntRange(from = 0) int end) { + /** + * Returns text width for the given range. + * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise + * IllegalArgumentException will be thrown. + * + * @param start the inclusive start offset in the text + * @param end the exclusive end offset in the text + * @return the text width + * @throws IllegalArgumentException if start and end offset are in the different paragraph. + */ + public @FloatRange(from = 0) float getWidth(@IntRange(from = 0) int start, + @IntRange(from = 0) int end) { + Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset"); + Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset"); + Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset"); + + if (start == end) { + return 0; + } final int paraIndex = findParaIndex(start); final int paraStart = getParagraphStart(paraIndex); final int paraEnd = getParagraphEnd(paraIndex); if (start < paraStart || paraEnd < end) { - throw new RuntimeException("Cannot measured across the paragraph:" + throw new IllegalArgumentException("Cannot measured across the paragraph:" + "para: (" + paraStart + ", " + paraEnd + "), " + "request: (" + start + ", " + end + ")"); } return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart); } - /** @hide */ + /** + * Retrieves the text bounding box for the given range. + * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise + * IllegalArgumentException will be thrown. + * + * @param start the inclusive start offset in the text + * @param end the exclusive end offset in the text + * @param bounds the output rectangle + * @throws IllegalArgumentException if start and end offset are in the different paragraph. + */ public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull Rect bounds) { + Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset"); + Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset"); + Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset"); + Preconditions.checkNotNull(bounds); + if (start == end) { + bounds.set(0, 0, 0, 0); + return; + } final int paraIndex = findParaIndex(start); final int paraStart = getParagraphStart(paraIndex); final int paraEnd = getParagraphEnd(paraIndex); if (start < paraStart || paraEnd < end) { - throw new RuntimeException("Cannot measured across the paragraph:" + throw new IllegalArgumentException("Cannot measured across the paragraph:" + "para: (" + paraStart + ", " + paraEnd + "), " + "request: (" + start + ", " + end + ")"); } - getMeasuredParagraph(paraIndex).getBounds(mParams.mPaint, - start - paraStart, end - paraStart, bounds); + getMeasuredParagraph(paraIndex).getBounds(start - paraStart, end - paraStart, bounds); } /** diff --git a/android/text/Selection.java b/android/text/Selection.java index 34456580..5256e471 100644 --- a/android/text/Selection.java +++ b/android/text/Selection.java @@ -180,7 +180,7 @@ public class Selection { * Remove the selection or cursor, if any, from the text. */ public static final void removeSelection(Spannable text) { - text.removeSpan(SELECTION_START); + text.removeSpan(SELECTION_START, Spanned.SPAN_INTERMEDIATE); text.removeSpan(SELECTION_END); removeMemory(text); } diff --git a/android/text/Spannable.java b/android/text/Spannable.java index 39b78eb0..8315b2aa 100644 --- a/android/text/Spannable.java +++ b/android/text/Spannable.java @@ -46,6 +46,19 @@ extends Spanned public void removeSpan(Object what); /** + * Remove the specified object from the range of text to which it + * was attached, if any. It is OK to remove an object that was never + * attached in the first place. + * + * See {@link Spanned} for an explanation of what the flags mean. + * + * @hide + */ + default void removeSpan(Object what, int flags) { + removeSpan(what); + } + + /** * Factory used by TextView to create new {@link Spannable Spannables}. You can subclass * it to provide something other than {@link SpannableString}. * diff --git a/android/text/SpannableStringBuilder.java b/android/text/SpannableStringBuilder.java index d41dfdcd..41a9c45a 100644 --- a/android/text/SpannableStringBuilder.java +++ b/android/text/SpannableStringBuilder.java @@ -312,7 +312,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable // The following condition indicates that the span would become empty (textIsRemoved || mSpanStarts[i] > start || mSpanEnds[i] < mGapStart)) { mIndexOfSpan.remove(mSpans[i]); - removeSpan(i); + removeSpan(i, 0 /* flags */); return true; } return resolveGap(mSpanStarts[i]) <= end && (i & 1) != 0 && @@ -472,7 +472,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable } // Note: caller is responsible for removing the mIndexOfSpan entry. - private void removeSpan(int i) { + private void removeSpan(int i, int flags) { Object object = mSpans[i]; int start = mSpanStarts[i]; @@ -496,7 +496,9 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable // Invariants must be restored before sending span removed notifications. restoreInvariants(); - sendSpanRemoved(object, start, end); + if ((flags & Spanned.SPAN_INTERMEDIATE) == 0) { + sendSpanRemoved(object, start, end); + } } // Documentation from interface @@ -782,10 +784,19 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable * Remove the specified markup object from the buffer. */ public void removeSpan(Object what) { + removeSpan(what, 0 /* flags */); + } + + /** + * Remove the specified markup object from the buffer. + * + * @hide + */ + public void removeSpan(Object what, int flags) { if (mIndexOfSpan == null) return; Integer i = mIndexOfSpan.remove(what); if (i != null) { - removeSpan(i.intValue()); + removeSpan(i.intValue(), flags); } } diff --git a/android/text/SpannableStringInternal.java b/android/text/SpannableStringInternal.java index 5dd1a52b..bcc2fda8 100644 --- a/android/text/SpannableStringInternal.java +++ b/android/text/SpannableStringInternal.java @@ -249,6 +249,13 @@ import java.lang.reflect.Array; } /* package */ void removeSpan(Object what) { + removeSpan(what, 0 /* flags */); + } + + /** + * @hide + */ + public void removeSpan(Object what, int flags) { int count = mSpanCount; Object[] spans = mSpans; int[] data = mSpanData; @@ -262,11 +269,13 @@ import java.lang.reflect.Array; System.arraycopy(spans, i + 1, spans, i, c); System.arraycopy(data, (i + 1) * COLUMNS, - data, i * COLUMNS, c * COLUMNS); + data, i * COLUMNS, c * COLUMNS); mSpanCount--; - sendSpanRemoved(what, ostart, oend); + if ((flags & Spanned.SPAN_INTERMEDIATE) == 0) { + sendSpanRemoved(what, ostart, oend); + } return; } } diff --git a/android/text/format/Formatter.java b/android/text/format/Formatter.java index ad3b4b6d..de86a66a 100644 --- a/android/text/format/Formatter.java +++ b/android/text/format/Formatter.java @@ -40,6 +40,10 @@ public final class Formatter { public static final int FLAG_SHORTER = 1 << 0; /** {@hide} */ public static final int FLAG_CALCULATE_ROUNDED = 1 << 1; + /** {@hide} */ + public static final int FLAG_SI_UNITS = 1 << 2; + /** {@hide} */ + public static final int FLAG_IEC_UNITS = 1 << 3; /** {@hide} */ public static class BytesResult { @@ -90,7 +94,7 @@ public final class Formatter { if (context == null) { return ""; } - final BytesResult res = formatBytes(context.getResources(), sizeBytes, 0); + final BytesResult res = formatBytes(context.getResources(), sizeBytes, FLAG_SI_UNITS); return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix, res.value, res.units)); } @@ -103,41 +107,43 @@ public final class Formatter { if (context == null) { return ""; } - final BytesResult res = formatBytes(context.getResources(), sizeBytes, FLAG_SHORTER); + final BytesResult res = formatBytes(context.getResources(), sizeBytes, + FLAG_SI_UNITS | FLAG_SHORTER); return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix, res.value, res.units)); } /** {@hide} */ public static BytesResult formatBytes(Resources res, long sizeBytes, int flags) { + final int unit = ((flags & FLAG_IEC_UNITS) != 0) ? 1024 : 1000; final boolean isNegative = (sizeBytes < 0); float result = isNegative ? -sizeBytes : sizeBytes; int suffix = com.android.internal.R.string.byteShort; long mult = 1; if (result > 900) { suffix = com.android.internal.R.string.kilobyteShort; - mult = 1000; - result = result / 1000; + mult = unit; + result = result / unit; } if (result > 900) { suffix = com.android.internal.R.string.megabyteShort; - mult *= 1000; - result = result / 1000; + mult *= unit; + result = result / unit; } if (result > 900) { suffix = com.android.internal.R.string.gigabyteShort; - mult *= 1000; - result = result / 1000; + mult *= unit; + result = result / unit; } if (result > 900) { suffix = com.android.internal.R.string.terabyteShort; - mult *= 1000; - result = result / 1000; + mult *= unit; + result = result / unit; } if (result > 900) { suffix = com.android.internal.R.string.petabyteShort; - mult *= 1000; - result = result / 1000; + mult *= unit; + result = result / unit; } // Note we calculate the rounded long by ourselves, but still let String.format() // compute the rounded value. String.format("%f", 0.1) might not return "0.1" due to diff --git a/android/text/method/LinkMovementMethod.java b/android/text/method/LinkMovementMethod.java index f3323588..e60377b4 100644 --- a/android/text/method/LinkMovementMethod.java +++ b/android/text/method/LinkMovementMethod.java @@ -219,7 +219,7 @@ public class LinkMovementMethod extends ScrollingMovementMethod { links[0].onClick(widget); } else if (action == MotionEvent.ACTION_DOWN) { if (widget.getContext().getApplicationInfo().targetSdkVersion - > Build.VERSION_CODES.O_MR1) { + >= Build.VERSION_CODES.P) { // Selection change will reposition the toolbar. Hide it for a few ms for a // smoother transition. widget.hideFloatingToolbar(HIDE_FLOATING_TOOLBAR_DELAY_MS); diff --git a/android/text/util/Linkify.java b/android/text/util/Linkify.java index 9c6a3f5f..c905f495 100644 --- a/android/text/util/Linkify.java +++ b/android/text/util/Linkify.java @@ -100,13 +100,20 @@ public class Linkify { * take an options mask. Note that this uses the * {@link android.webkit.WebView#findAddress(String) findAddress()} method in * {@link android.webkit.WebView} for finding addresses, which has various - * limitations. + * limitations and has been deprecated. + * @deprecated use {@link android.view.textclassifier.TextClassifier#generateLinks( + * TextLinks.Request)} instead and avoid it even when targeting API levels where no alternative + * is available. */ + @Deprecated public static final int MAP_ADDRESSES = 0x08; /** * Bit mask indicating that all available patterns should be matched in * methods that take an options mask + * <p><strong>Note:</strong></p> {@link #MAP_ADDRESSES} is deprecated. + * Use {@link android.view.textclassifier.TextClassifier#generateLinks(TextLinks.Request)} + * instead and avoid it even when targeting API levels where no alternative is available. */ public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS | MAP_ADDRESSES; diff --git a/android/util/LruCache.java b/android/util/LruCache.java index 52086065..40154880 100644 --- a/android/util/LruCache.java +++ b/android/util/LruCache.java @@ -20,10 +20,6 @@ import java.util.LinkedHashMap; import java.util.Map; /** - * BEGIN LAYOUTLIB CHANGE - * This is a custom version that doesn't use the non standard LinkedHashMap#eldest. - * END LAYOUTLIB CHANGE - * * A cache that holds strong references to a limited number of values. Each time * a value is accessed, it is moved to the head of a queue. When a value is * added to a full cache, the value at the end of that queue is evicted and may @@ -91,9 +87,8 @@ public class LruCache<K, V> { /** * Sets the size of the cache. - * @param maxSize The new maximum size. * - * @hide + * @param maxSize The new maximum size. */ public void resize(int maxSize) { if (maxSize <= 0) { @@ -190,10 +185,13 @@ public class LruCache<K, V> { } /** + * Remove the eldest entries until the total of remaining entries is at or + * below the requested size. + * * @param maxSize the maximum size of the cache before returning. May be -1 - * to evict even 0-sized elements. + * to evict even 0-sized elements. */ - private void trimToSize(int maxSize) { + public void trimToSize(int maxSize) { while (true) { K key; V value; @@ -207,16 +205,7 @@ public class LruCache<K, V> { break; } - // BEGIN LAYOUTLIB CHANGE - // get the last item in the linked list. - // This is not efficient, the goal here is to minimize the changes - // compared to the platform version. - Map.Entry<K, V> toEvict = null; - for (Map.Entry<K, V> entry : map.entrySet()) { - toEvict = entry; - } - // END LAYOUTLIB CHANGE - + Map.Entry<K, V> toEvict = map.eldest(); if (toEvict == null) { break; } diff --git a/android/util/RecurrenceRule.java b/android/util/RecurrenceRule.java index 9f115eba..9c898766 100644 --- a/android/util/RecurrenceRule.java +++ b/android/util/RecurrenceRule.java @@ -149,6 +149,10 @@ public class RecurrenceRule implements Parcelable { } }; + public boolean isRecurring() { + return period != null; + } + @Deprecated public boolean isMonthly() { return start != null @@ -158,7 +162,7 @@ public class RecurrenceRule implements Parcelable { && period.getDays() == 0; } - public Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator() { + public Iterator<Range<ZonedDateTime>> cycleIterator() { if (period != null) { return new RecurringIterator(); } else { @@ -166,7 +170,7 @@ public class RecurrenceRule implements Parcelable { } } - private class NonrecurringIterator implements Iterator<Pair<ZonedDateTime, ZonedDateTime>> { + private class NonrecurringIterator implements Iterator<Range<ZonedDateTime>> { boolean hasNext; public NonrecurringIterator() { @@ -179,13 +183,13 @@ public class RecurrenceRule implements Parcelable { } @Override - public Pair<ZonedDateTime, ZonedDateTime> next() { + public Range<ZonedDateTime> next() { hasNext = false; - return new Pair<>(start, end); + return new Range<>(start, end); } } - private class RecurringIterator implements Iterator<Pair<ZonedDateTime, ZonedDateTime>> { + private class RecurringIterator implements Iterator<Range<ZonedDateTime>> { int i; ZonedDateTime cycleStart; ZonedDateTime cycleEnd; @@ -231,12 +235,12 @@ public class RecurrenceRule implements Parcelable { } @Override - public Pair<ZonedDateTime, ZonedDateTime> next() { + public Range<ZonedDateTime> next() { if (LOGD) Log.d(TAG, "Cycle " + i + " from " + cycleStart + " to " + cycleEnd); - Pair<ZonedDateTime, ZonedDateTime> p = new Pair<>(cycleStart, cycleEnd); + Range<ZonedDateTime> r = new Range<>(cycleStart, cycleEnd); i--; updateCycle(); - return p; + return r; } } diff --git a/android/view/DisplayCutout.java b/android/view/DisplayCutout.java index 66a9c6c0..f59c0b50 100644 --- a/android/view/DisplayCutout.java +++ b/android/view/DisplayCutout.java @@ -31,6 +31,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import android.util.PathParser; import android.util.proto.ProtoOutputStream; @@ -75,15 +76,19 @@ public final class DisplayCutout { false /* copyArguments */); + private static final Pair<Path, DisplayCutout> NULL_PAIR = new Pair<>(null, null); private static final Object CACHE_LOCK = new Object(); + @GuardedBy("CACHE_LOCK") private static String sCachedSpec; @GuardedBy("CACHE_LOCK") private static int sCachedDisplayWidth; @GuardedBy("CACHE_LOCK") + private static int sCachedDisplayHeight; + @GuardedBy("CACHE_LOCK") private static float sCachedDensity; @GuardedBy("CACHE_LOCK") - private static DisplayCutout sCachedCutout; + private static Pair<Path, DisplayCutout> sCachedCutout = NULL_PAIR; private final Rect mSafeInsets; private final Region mBounds; @@ -347,7 +352,7 @@ public final class DisplayCutout { } /** - * Creates an instance according to @android:string/config_mainBuiltInDisplayCutout. + * Creates the bounding path according to @android:string/config_mainBuiltInDisplayCutout. * * @hide */ @@ -357,6 +362,16 @@ public final class DisplayCutout { } /** + * Creates an instance according to @android:string/config_mainBuiltInDisplayCutout. + * + * @hide + */ + public static Path pathFromResources(Resources res, int displayWidth, int displayHeight) { + return pathAndDisplayCutoutFromSpec(res.getString(R.string.config_mainBuiltInDisplayCutout), + displayWidth, displayHeight, res.getDisplayMetrics().density).first; + } + + /** * Creates an instance according to the supplied {@link android.util.PathParser.PathData} spec. * * @hide @@ -364,11 +379,17 @@ public final class DisplayCutout { @VisibleForTesting(visibility = PRIVATE) public static DisplayCutout fromSpec(String spec, int displayWidth, int displayHeight, float density) { + return pathAndDisplayCutoutFromSpec(spec, displayWidth, displayHeight, density).second; + } + + private static Pair<Path, DisplayCutout> pathAndDisplayCutoutFromSpec(String spec, + int displayWidth, int displayHeight, float density) { if (TextUtils.isEmpty(spec)) { - return null; + return NULL_PAIR; } synchronized (CACHE_LOCK) { if (spec.equals(sCachedSpec) && sCachedDisplayWidth == displayWidth + && sCachedDisplayHeight == displayHeight && sCachedDensity == density) { return sCachedCutout; } @@ -398,7 +419,7 @@ public final class DisplayCutout { p = PathParser.createPathFromPathData(spec); } catch (Throwable e) { Log.wtf(TAG, "Could not inflate cutout: ", e); - return null; + return NULL_PAIR; } final Matrix m = new Matrix(); @@ -414,7 +435,7 @@ public final class DisplayCutout { bottomPath = PathParser.createPathFromPathData(bottomSpec); } catch (Throwable e) { Log.wtf(TAG, "Could not inflate bottom cutout: ", e); - return null; + return NULL_PAIR; } // Keep top transform m.postTranslate(0, displayHeight); @@ -422,10 +443,11 @@ public final class DisplayCutout { p.addPath(bottomPath); } - final DisplayCutout result = fromBounds(p); + final Pair<Path, DisplayCutout> result = new Pair<>(p, fromBounds(p)); synchronized (CACHE_LOCK) { sCachedSpec = spec; sCachedDisplayWidth = displayWidth; + sCachedDisplayHeight = displayHeight; sCachedDensity = density; sCachedCutout = result; } diff --git a/android/view/HapticFeedbackConstants.java b/android/view/HapticFeedbackConstants.java index b1479284..db01cea3 100644 --- a/android/view/HapticFeedbackConstants.java +++ b/android/view/HapticFeedbackConstants.java @@ -77,6 +77,55 @@ public class HapticFeedbackConstants { public static final int TEXT_HANDLE_MOVE = 9; /** + * The user unlocked the device + * @hide + */ + public static final int ENTRY_BUMP = 10; + + /** + * The user has moved the dragged object within a droppable area. + * @hide + */ + public static final int DRAG_CROSSING = 11; + + /** + * The user has started a gesture (e.g. on the soft keyboard). + * @hide + */ + public static final int GESTURE_START = 12; + + /** + * The user has finished a gesture (e.g. on the soft keyboard). + * @hide + */ + public static final int GESTURE_END = 13; + + /** + * The user's squeeze crossed the gesture's initiation threshold. + * @hide + */ + public static final int EDGE_SQUEEZE = 14; + + /** + * The user's squeeze crossed the gesture's release threshold. + * @hide + */ + public static final int EDGE_RELEASE = 15; + + /** + * A haptic effect to signal the confirmation or successful completion of a user + * interaction. + * @hide + */ + public static final int CONFIRM = 16; + + /** + * A haptic effect to signal the rejection or failure of a user interaction. + * @hide + */ + public static final int REJECT = 17; + + /** * The phone has booted with safe mode enabled. * This is a private constant. Feel free to renumber as desired. * @hide diff --git a/android/view/SurfaceControl.java b/android/view/SurfaceControl.java index d4610a56..5deee11b 100644 --- a/android/view/SurfaceControl.java +++ b/android/view/SurfaceControl.java @@ -87,6 +87,7 @@ public class SurfaceControl implements Parcelable { private static native void nativeMergeTransaction(long transactionObj, long otherTransactionObj); private static native void nativeSetAnimationTransaction(long transactionObj); + private static native void nativeSetEarlyWakeup(long transactionObj); private static native void nativeSetLayer(long transactionObj, long nativeObject, int zorder); private static native void nativeSetRelativeLayer(long transactionObj, long nativeObject, @@ -1642,6 +1643,19 @@ public class SurfaceControl implements Parcelable { } /** + * Indicate that SurfaceFlinger should wake up earlier than usual as a result of this + * transaction. This should be used when the caller thinks that the scene is complex enough + * that it's likely to hit GL composition, and thus, SurfaceFlinger needs to more time in + * order not to miss frame deadlines. + * <p> + * Corresponds to setting ISurfaceComposer::eEarlyWakeup + */ + public Transaction setEarlyWakeup() { + nativeSetEarlyWakeup(mNativeObject); + return this; + } + + /** * Merge the other transaction into this transaction, clearing the * other transaction as if it had been applied. */ diff --git a/android/view/SurfaceView.java b/android/view/SurfaceView.java index ebb2af45..7e546476 100644 --- a/android/view/SurfaceView.java +++ b/android/view/SurfaceView.java @@ -16,115 +16,1237 @@ package android.view; -import com.android.layoutlib.bridge.MockView; +import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_OVERLAY_SUBLAYER; +import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_SUBLAYER; +import static android.view.WindowManagerPolicyConstants.APPLICATION_PANEL_SUBLAYER; import android.content.Context; +import android.content.res.CompatibilityInfo.Translator; +import android.content.res.Configuration; import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.Region; +import android.os.Build; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.SystemClock; import android.util.AttributeSet; +import android.util.Log; + +import com.android.internal.view.SurfaceCallbackHelper; + +import java.util.ArrayList; +import java.util.concurrent.locks.ReentrantLock; /** - * Mock version of the SurfaceView. - * Only non override public methods from the real SurfaceView have been added in there. - * Methods that take an unknown class as parameter or as return object, have been removed for now. + * Provides a dedicated drawing surface embedded inside of a view hierarchy. + * You can control the format of this surface and, if you like, its size; the + * SurfaceView takes care of placing the surface at the correct location on the + * screen + * + * <p>The surface is Z ordered so that it is behind the window holding its + * SurfaceView; the SurfaceView punches a hole in its window to allow its + * surface to be displayed. The view hierarchy will take care of correctly + * compositing with the Surface any siblings of the SurfaceView that would + * normally appear on top of it. This can be used to place overlays such as + * buttons on top of the Surface, though note however that it can have an + * impact on performance since a full alpha-blended composite will be performed + * each time the Surface changes. + * + * <p> The transparent region that makes the surface visible is based on the + * layout positions in the view hierarchy. If the post-layout transform + * properties are used to draw a sibling view on top of the SurfaceView, the + * view may not be properly composited with the surface. * - * TODO: generate automatically. + * <p>Access to the underlying surface is provided via the SurfaceHolder interface, + * which can be retrieved by calling {@link #getHolder}. * + * <p>The Surface will be created for you while the SurfaceView's window is + * visible; you should implement {@link SurfaceHolder.Callback#surfaceCreated} + * and {@link SurfaceHolder.Callback#surfaceDestroyed} to discover when the + * Surface is created and destroyed as the window is shown and hidden. + * + * <p>One of the purposes of this class is to provide a surface in which a + * secondary thread can render into the screen. If you are going to use it + * this way, you need to be aware of some threading semantics: + * + * <ul> + * <li> All SurfaceView and + * {@link SurfaceHolder.Callback SurfaceHolder.Callback} methods will be called + * from the thread running the SurfaceView's window (typically the main thread + * of the application). They thus need to correctly synchronize with any + * state that is also touched by the drawing thread. + * <li> You must ensure that the drawing thread only touches the underlying + * Surface while it is valid -- between + * {@link SurfaceHolder.Callback#surfaceCreated SurfaceHolder.Callback.surfaceCreated()} + * and + * {@link SurfaceHolder.Callback#surfaceDestroyed SurfaceHolder.Callback.surfaceDestroyed()}. + * </ul> + * + * <p class="note"><strong>Note:</strong> Starting in platform version + * {@link android.os.Build.VERSION_CODES#N}, SurfaceView's window position is + * updated synchronously with other View rendering. This means that translating + * and scaling a SurfaceView on screen will not cause rendering artifacts. Such + * artifacts may occur on previous versions of the platform when its window is + * positioned asynchronously.</p> */ -public class SurfaceView extends MockView { +public class SurfaceView extends View implements ViewRootImpl.WindowStoppedCallback { + private static final String TAG = "SurfaceView"; + private static final boolean DEBUG = false; + + final ArrayList<SurfaceHolder.Callback> mCallbacks + = new ArrayList<SurfaceHolder.Callback>(); + + final int[] mLocation = new int[2]; + + final ReentrantLock mSurfaceLock = new ReentrantLock(); + final Surface mSurface = new Surface(); // Current surface in use + boolean mDrawingStopped = true; + // We use this to track if the application has produced a frame + // in to the Surface. Up until that point, we should be careful not to punch + // holes. + boolean mDrawFinished = false; + + final Rect mScreenRect = new Rect(); + SurfaceSession mSurfaceSession; + + SurfaceControlWithBackground mSurfaceControl; + // In the case of format changes we switch out the surface in-place + // we need to preserve the old one until the new one has drawn. + SurfaceControl mDeferredDestroySurfaceControl; + final Rect mTmpRect = new Rect(); + final Configuration mConfiguration = new Configuration(); + + int mSubLayer = APPLICATION_MEDIA_SUBLAYER; + + boolean mIsCreating = false; + private volatile boolean mRtHandlingPositionUpdates = false; + + private final ViewTreeObserver.OnScrollChangedListener mScrollChangedListener + = new ViewTreeObserver.OnScrollChangedListener() { + @Override + public void onScrollChanged() { + updateSurface(); + } + }; + + private final ViewTreeObserver.OnPreDrawListener mDrawListener = + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + // reposition ourselves where the surface is + mHaveFrame = getWidth() > 0 && getHeight() > 0; + updateSurface(); + return true; + } + }; + + boolean mRequestedVisible = false; + boolean mWindowVisibility = false; + boolean mLastWindowVisibility = false; + boolean mViewVisibility = false; + boolean mWindowStopped = false; + + int mRequestedWidth = -1; + int mRequestedHeight = -1; + /* Set SurfaceView's format to 565 by default to maintain backward + * compatibility with applications assuming this format. + */ + int mRequestedFormat = PixelFormat.RGB_565; + + boolean mHaveFrame = false; + boolean mSurfaceCreated = false; + long mLastLockTime = 0; + + boolean mVisible = false; + int mWindowSpaceLeft = -1; + int mWindowSpaceTop = -1; + int mSurfaceWidth = -1; + int mSurfaceHeight = -1; + int mFormat = -1; + final Rect mSurfaceFrame = new Rect(); + int mLastSurfaceWidth = -1, mLastSurfaceHeight = -1; + private Translator mTranslator; + + private boolean mGlobalListenersAdded; + private boolean mAttachedToWindow; + + private int mSurfaceFlags = SurfaceControl.HIDDEN; + + private int mPendingReportDraws; + + private SurfaceControl.Transaction mRtTransaction = new SurfaceControl.Transaction(); public SurfaceView(Context context) { this(context, null); } public SurfaceView(Context context, AttributeSet attrs) { - this(context, attrs , 0); + this(context, attrs, 0); } - public SurfaceView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); + mRenderNode.requestPositionUpdates(this); + + setWillNotDraw(true); + } + + /** + * Return the SurfaceHolder providing access and control over this + * SurfaceView's underlying surface. + * + * @return SurfaceHolder The holder of the surface. + */ + public SurfaceHolder getHolder() { + return mSurfaceHolder; + } + + private void updateRequestedVisibility() { + mRequestedVisible = mViewVisibility && mWindowVisibility && !mWindowStopped; + } + + /** @hide */ + @Override + public void windowStopped(boolean stopped) { + mWindowStopped = stopped; + updateRequestedVisibility(); + updateSurface(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + getViewRootImpl().addWindowStoppedCallback(this); + mWindowStopped = false; + + mViewVisibility = getVisibility() == VISIBLE; + updateRequestedVisibility(); + + mAttachedToWindow = true; + mParent.requestTransparentRegion(SurfaceView.this); + if (!mGlobalListenersAdded) { + ViewTreeObserver observer = getViewTreeObserver(); + observer.addOnScrollChangedListener(mScrollChangedListener); + observer.addOnPreDrawListener(mDrawListener); + mGlobalListenersAdded = true; + } + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + mWindowVisibility = visibility == VISIBLE; + updateRequestedVisibility(); + updateSurface(); + } + + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + mViewVisibility = visibility == VISIBLE; + boolean newRequestedVisible = mWindowVisibility && mViewVisibility && !mWindowStopped; + if (newRequestedVisible != mRequestedVisible) { + // our base class (View) invalidates the layout only when + // we go from/to the GONE state. However, SurfaceView needs + // to request a re-layout when the visibility changes at all. + // This is needed because the transparent region is computed + // as part of the layout phase, and it changes (obviously) when + // the visibility changes. + requestLayout(); + } + mRequestedVisible = newRequestedVisible; + updateSurface(); + } + + private void performDrawFinished() { + if (mPendingReportDraws > 0) { + mDrawFinished = true; + if (mAttachedToWindow) { + notifyDrawFinished(); + invalidate(); + } + } else { + Log.e(TAG, System.identityHashCode(this) + "finished drawing" + + " but no pending report draw (extra call" + + " to draw completion runnable?)"); + } + } + + void notifyDrawFinished() { + ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot != null) { + viewRoot.pendingDrawFinished(); + } + mPendingReportDraws--; + } + + @Override + protected void onDetachedFromWindow() { + ViewRootImpl viewRoot = getViewRootImpl(); + // It's possible to create a SurfaceView using the default constructor and never + // attach it to a view hierarchy, this is a common use case when dealing with + // OpenGL. A developer will probably create a new GLSurfaceView, and let it manage + // the lifecycle. Instead of attaching it to a view, he/she can just pass + // the SurfaceHolder forward, most live wallpapers do it. + if (viewRoot != null) { + viewRoot.removeWindowStoppedCallback(this); + } + + mAttachedToWindow = false; + if (mGlobalListenersAdded) { + ViewTreeObserver observer = getViewTreeObserver(); + observer.removeOnScrollChangedListener(mScrollChangedListener); + observer.removeOnPreDrawListener(mDrawListener); + mGlobalListenersAdded = false; + } + + while (mPendingReportDraws > 0) { + notifyDrawFinished(); + } + + mRequestedVisible = false; + + updateSurface(); + if (mSurfaceControl != null) { + mSurfaceControl.destroy(); + } + mSurfaceControl = null; + + mHaveFrame = false; + + super.onDetachedFromWindow(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = mRequestedWidth >= 0 + ? resolveSizeAndState(mRequestedWidth, widthMeasureSpec, 0) + : getDefaultSize(0, widthMeasureSpec); + int height = mRequestedHeight >= 0 + ? resolveSizeAndState(mRequestedHeight, heightMeasureSpec, 0) + : getDefaultSize(0, heightMeasureSpec); + setMeasuredDimension(width, height); } + /** @hide */ + @Override + protected boolean setFrame(int left, int top, int right, int bottom) { + boolean result = super.setFrame(left, top, right, bottom); + updateSurface(); + return result; + } + + @Override public boolean gatherTransparentRegion(Region region) { - return false; + if (isAboveParent() || !mDrawFinished) { + return super.gatherTransparentRegion(region); + } + + boolean opaque = true; + if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) { + // this view draws, remove it from the transparent region + opaque = super.gatherTransparentRegion(region); + } else if (region != null) { + int w = getWidth(); + int h = getHeight(); + if (w>0 && h>0) { + getLocationInWindow(mLocation); + // otherwise, punch a hole in the whole hierarchy + int l = mLocation[0]; + int t = mLocation[1]; + region.op(l, t, l+w, t+h, Region.Op.UNION); + } + } + if (PixelFormat.formatHasAlpha(mRequestedFormat)) { + opaque = false; + } + return opaque; + } + + @Override + public void draw(Canvas canvas) { + if (mDrawFinished && !isAboveParent()) { + // draw() is not called when SKIP_DRAW is set + if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) { + // punch a whole in the view-hierarchy below us + canvas.drawColor(0, PorterDuff.Mode.CLEAR); + } + } + super.draw(canvas); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + if (mDrawFinished && !isAboveParent()) { + // draw() is not called when SKIP_DRAW is set + if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { + // punch a whole in the view-hierarchy below us + canvas.drawColor(0, PorterDuff.Mode.CLEAR); + } + } + super.dispatchDraw(canvas); } + /** + * Control whether the surface view's surface is placed on top of another + * regular surface view in the window (but still behind the window itself). + * This is typically used to place overlays on top of an underlying media + * surface view. + * + * <p>Note that this must be set before the surface view's containing + * window is attached to the window manager. + * + * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}. + */ public void setZOrderMediaOverlay(boolean isMediaOverlay) { + mSubLayer = isMediaOverlay + ? APPLICATION_MEDIA_OVERLAY_SUBLAYER : APPLICATION_MEDIA_SUBLAYER; } + /** + * Control whether the surface view's surface is placed on top of its + * window. Normally it is placed behind the window, to allow it to + * (for the most part) appear to composite with the views in the + * hierarchy. By setting this, you cause it to be placed above the + * window. This means that none of the contents of the window this + * SurfaceView is in will be visible on top of its surface. + * + * <p>Note that this must be set before the surface view's containing + * window is attached to the window manager. + * + * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}. + */ public void setZOrderOnTop(boolean onTop) { + if (onTop) { + mSubLayer = APPLICATION_PANEL_SUBLAYER; + } else { + mSubLayer = APPLICATION_MEDIA_SUBLAYER; + } } + /** + * Control whether the surface view's content should be treated as secure, + * preventing it from appearing in screenshots or from being viewed on + * non-secure displays. + * + * <p>Note that this must be set before the surface view's containing + * window is attached to the window manager. + * + * <p>See {@link android.view.Display#FLAG_SECURE} for details. + * + * @param isSecure True if the surface view is secure. + */ public void setSecure(boolean isSecure) { + if (isSecure) { + mSurfaceFlags |= SurfaceControl.SECURE; + } else { + mSurfaceFlags &= ~SurfaceControl.SECURE; + } } - public SurfaceHolder getHolder() { - return mSurfaceHolder; + private void updateOpaqueFlag() { + if (!PixelFormat.formatHasAlpha(mRequestedFormat)) { + mSurfaceFlags |= SurfaceControl.OPAQUE; + } else { + mSurfaceFlags &= ~SurfaceControl.OPAQUE; + } + } + + private Rect getParentSurfaceInsets() { + final ViewRootImpl root = getViewRootImpl(); + if (root == null) { + return null; + } else { + return root.mWindowAttributes.surfaceInsets; + } + } + + /** @hide */ + protected void updateSurface() { + if (!mHaveFrame) { + return; + } + ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot == null || viewRoot.mSurface == null || !viewRoot.mSurface.isValid()) { + return; + } + + mTranslator = viewRoot.mTranslator; + if (mTranslator != null) { + mSurface.setCompatibilityTranslator(mTranslator); + } + + int myWidth = mRequestedWidth; + if (myWidth <= 0) myWidth = getWidth(); + int myHeight = mRequestedHeight; + if (myHeight <= 0) myHeight = getHeight(); + + final boolean formatChanged = mFormat != mRequestedFormat; + final boolean visibleChanged = mVisible != mRequestedVisible; + final boolean creating = (mSurfaceControl == null || formatChanged || visibleChanged) + && mRequestedVisible; + final boolean sizeChanged = mSurfaceWidth != myWidth || mSurfaceHeight != myHeight; + final boolean windowVisibleChanged = mWindowVisibility != mLastWindowVisibility; + boolean redrawNeeded = false; + + if (creating || formatChanged || sizeChanged || visibleChanged || windowVisibleChanged) { + getLocationInWindow(mLocation); + + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + + "Changes: creating=" + creating + + " format=" + formatChanged + " size=" + sizeChanged + + " visible=" + visibleChanged + + " left=" + (mWindowSpaceLeft != mLocation[0]) + + " top=" + (mWindowSpaceTop != mLocation[1])); + + try { + final boolean visible = mVisible = mRequestedVisible; + mWindowSpaceLeft = mLocation[0]; + mWindowSpaceTop = mLocation[1]; + mSurfaceWidth = myWidth; + mSurfaceHeight = myHeight; + mFormat = mRequestedFormat; + mLastWindowVisibility = mWindowVisibility; + + mScreenRect.left = mWindowSpaceLeft; + mScreenRect.top = mWindowSpaceTop; + mScreenRect.right = mWindowSpaceLeft + getWidth(); + mScreenRect.bottom = mWindowSpaceTop + getHeight(); + if (mTranslator != null) { + mTranslator.translateRectInAppWindowToScreen(mScreenRect); + } + + final Rect surfaceInsets = getParentSurfaceInsets(); + mScreenRect.offset(surfaceInsets.left, surfaceInsets.top); + + if (creating) { + mSurfaceSession = new SurfaceSession(viewRoot.mSurface); + mDeferredDestroySurfaceControl = mSurfaceControl; + + updateOpaqueFlag(); + final String name = "SurfaceView - " + viewRoot.getTitle().toString(); + + mSurfaceControl = new SurfaceControlWithBackground( + name, + (mSurfaceFlags & SurfaceControl.OPAQUE) != 0, + new SurfaceControl.Builder(mSurfaceSession) + .setSize(mSurfaceWidth, mSurfaceHeight) + .setFormat(mFormat) + .setFlags(mSurfaceFlags)); + } else if (mSurfaceControl == null) { + return; + } + + boolean realSizeChanged = false; + + mSurfaceLock.lock(); + try { + mDrawingStopped = !visible; + + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + + "Cur surface: " + mSurface); + + SurfaceControl.openTransaction(); + try { + mSurfaceControl.setLayer(mSubLayer); + if (mViewVisibility) { + mSurfaceControl.show(); + } else { + mSurfaceControl.hide(); + } + + // While creating the surface, we will set it's initial + // geometry. Outside of that though, we should generally + // leave it to the RenderThread. + // + // There is one more case when the buffer size changes we aren't yet + // prepared to sync (as even following the transaction applying + // we still need to latch a buffer). + // b/28866173 + if (sizeChanged || creating || !mRtHandlingPositionUpdates) { + mSurfaceControl.setPosition(mScreenRect.left, mScreenRect.top); + mSurfaceControl.setMatrix(mScreenRect.width() / (float) mSurfaceWidth, + 0.0f, 0.0f, + mScreenRect.height() / (float) mSurfaceHeight); + } + if (sizeChanged) { + mSurfaceControl.setSize(mSurfaceWidth, mSurfaceHeight); + } + } finally { + SurfaceControl.closeTransaction(); + } + + if (sizeChanged || creating) { + redrawNeeded = true; + } + + mSurfaceFrame.left = 0; + mSurfaceFrame.top = 0; + if (mTranslator == null) { + mSurfaceFrame.right = mSurfaceWidth; + mSurfaceFrame.bottom = mSurfaceHeight; + } else { + float appInvertedScale = mTranslator.applicationInvertedScale; + mSurfaceFrame.right = (int) (mSurfaceWidth * appInvertedScale + 0.5f); + mSurfaceFrame.bottom = (int) (mSurfaceHeight * appInvertedScale + 0.5f); + } + + final int surfaceWidth = mSurfaceFrame.right; + final int surfaceHeight = mSurfaceFrame.bottom; + realSizeChanged = mLastSurfaceWidth != surfaceWidth + || mLastSurfaceHeight != surfaceHeight; + mLastSurfaceWidth = surfaceWidth; + mLastSurfaceHeight = surfaceHeight; + } finally { + mSurfaceLock.unlock(); + } + + try { + redrawNeeded |= visible && !mDrawFinished; + + SurfaceHolder.Callback callbacks[] = null; + + final boolean surfaceChanged = creating; + if (mSurfaceCreated && (surfaceChanged || (!visible && visibleChanged))) { + mSurfaceCreated = false; + if (mSurface.isValid()) { + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + + "visibleChanged -- surfaceDestroyed"); + callbacks = getSurfaceCallbacks(); + for (SurfaceHolder.Callback c : callbacks) { + c.surfaceDestroyed(mSurfaceHolder); + } + // Since Android N the same surface may be reused and given to us + // again by the system server at a later point. However + // as we didn't do this in previous releases, clients weren't + // necessarily required to clean up properly in + // surfaceDestroyed. This leads to problems for example when + // clients don't destroy their EGL context, and try + // and create a new one on the same surface following reuse. + // Since there is no valid use of the surface in-between + // surfaceDestroyed and surfaceCreated, we force a disconnect, + // so the next connect will always work if we end up reusing + // the surface. + if (mSurface.isValid()) { + mSurface.forceScopedDisconnect(); + } + } + } + + if (creating) { + mSurface.copyFrom(mSurfaceControl); + } + + if (sizeChanged && getContext().getApplicationInfo().targetSdkVersion + < Build.VERSION_CODES.O) { + // Some legacy applications use the underlying native {@link Surface} object + // as a key to whether anything has changed. In these cases, updates to the + // existing {@link Surface} will be ignored when the size changes. + // Therefore, we must explicitly recreate the {@link Surface} in these + // cases. + mSurface.createFrom(mSurfaceControl); + } + + if (visible && mSurface.isValid()) { + if (!mSurfaceCreated && (surfaceChanged || visibleChanged)) { + mSurfaceCreated = true; + mIsCreating = true; + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + + "visibleChanged -- surfaceCreated"); + if (callbacks == null) { + callbacks = getSurfaceCallbacks(); + } + for (SurfaceHolder.Callback c : callbacks) { + c.surfaceCreated(mSurfaceHolder); + } + } + if (creating || formatChanged || sizeChanged + || visibleChanged || realSizeChanged) { + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + + "surfaceChanged -- format=" + mFormat + + " w=" + myWidth + " h=" + myHeight); + if (callbacks == null) { + callbacks = getSurfaceCallbacks(); + } + for (SurfaceHolder.Callback c : callbacks) { + c.surfaceChanged(mSurfaceHolder, mFormat, myWidth, myHeight); + } + } + if (redrawNeeded) { + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + + "surfaceRedrawNeeded"); + if (callbacks == null) { + callbacks = getSurfaceCallbacks(); + } + + mPendingReportDraws++; + viewRoot.drawPending(); + SurfaceCallbackHelper sch = + new SurfaceCallbackHelper(this::onDrawFinished); + sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks); + } + } + } finally { + mIsCreating = false; + if (mSurfaceControl != null && !mSurfaceCreated) { + mSurface.release(); + // If we are not in the stopped state, then the destruction of the Surface + // represents a visual change we need to display, and we should go ahead + // and destroy the SurfaceControl. However if we are in the stopped state, + // we can just leave the Surface around so it can be a part of animations, + // and we let the life-time be tied to the parent surface. + if (!mWindowStopped) { + mSurfaceControl.destroy(); + mSurfaceControl = null; + } + } + } + } catch (Exception ex) { + Log.e(TAG, "Exception configuring surface", ex); + } + if (DEBUG) Log.v( + TAG, "Layout: x=" + mScreenRect.left + " y=" + mScreenRect.top + + " w=" + mScreenRect.width() + " h=" + mScreenRect.height() + + ", frame=" + mSurfaceFrame); + } else { + // Calculate the window position in case RT loses the window + // and we need to fallback to a UI-thread driven position update + getLocationInSurface(mLocation); + final boolean positionChanged = mWindowSpaceLeft != mLocation[0] + || mWindowSpaceTop != mLocation[1]; + final boolean layoutSizeChanged = getWidth() != mScreenRect.width() + || getHeight() != mScreenRect.height(); + if (positionChanged || layoutSizeChanged) { // Only the position has changed + mWindowSpaceLeft = mLocation[0]; + mWindowSpaceTop = mLocation[1]; + // For our size changed check, we keep mScreenRect.width() and mScreenRect.height() + // in view local space. + mLocation[0] = getWidth(); + mLocation[1] = getHeight(); + + mScreenRect.set(mWindowSpaceLeft, mWindowSpaceTop, + mWindowSpaceLeft + mLocation[0], mWindowSpaceTop + mLocation[1]); + + if (mTranslator != null) { + mTranslator.translateRectInAppWindowToScreen(mScreenRect); + } + + if (mSurfaceControl == null) { + return; + } + + if (!isHardwareAccelerated() || !mRtHandlingPositionUpdates) { + try { + if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition UI, " + + "postion = [%d, %d, %d, %d]", System.identityHashCode(this), + mScreenRect.left, mScreenRect.top, + mScreenRect.right, mScreenRect.bottom)); + setParentSpaceRectangle(mScreenRect, -1); + } catch (Exception ex) { + Log.e(TAG, "Exception configuring surface", ex); + } + } + } + } + } + + private void onDrawFinished() { + if (DEBUG) { + Log.i(TAG, System.identityHashCode(this) + " " + + "finishedDrawing"); + } + + if (mDeferredDestroySurfaceControl != null) { + mDeferredDestroySurfaceControl.destroy(); + mDeferredDestroySurfaceControl = null; + } + + runOnUiThread(() -> { + performDrawFinished(); + }); + } + + /** + * A place to over-ride for applying child-surface transactions. + * These can be synchronized with the viewroot surface using deferTransaction. + * + * Called from RenderWorker while UI thread is paused. + * @hide + */ + protected void applyChildSurfaceTransaction_renderWorker(SurfaceControl.Transaction t, + Surface viewRootSurface, long nextViewRootFrameNumber) { + } + + private void applySurfaceTransforms(SurfaceControl surface, Rect position, long frameNumber) { + if (frameNumber > 0) { + final ViewRootImpl viewRoot = getViewRootImpl(); + + mRtTransaction.deferTransactionUntilSurface(surface, viewRoot.mSurface, + frameNumber); + } + + mRtTransaction.setPosition(surface, position.left, position.top); + mRtTransaction.setMatrix(surface, + position.width() / (float) mSurfaceWidth, + 0.0f, 0.0f, + position.height() / (float) mSurfaceHeight); + } + + private void setParentSpaceRectangle(Rect position, long frameNumber) { + final ViewRootImpl viewRoot = getViewRootImpl(); + + applySurfaceTransforms(mSurfaceControl, position, frameNumber); + applySurfaceTransforms(mSurfaceControl.mBackgroundControl, position, frameNumber); + + applyChildSurfaceTransaction_renderWorker(mRtTransaction, viewRoot.mSurface, + frameNumber); + + mRtTransaction.apply(); + } + + private Rect mRTLastReportedPosition = new Rect(); + + /** + * Called by native by a Rendering Worker thread to update the window position + * @hide + */ + public final void updateSurfacePosition_renderWorker(long frameNumber, + int left, int top, int right, int bottom) { + if (mSurfaceControl == null) { + return; + } + + // TODO: This is teensy bit racey in that a brand new SurfaceView moving on + // its 2nd frame if RenderThread is running slowly could potentially see + // this as false, enter the branch, get pre-empted, then this comes along + // and reports a new position, then the UI thread resumes and reports + // its position. This could therefore be de-sync'd in that interval, but + // the synchronization would violate the rule that RT must never block + // on the UI thread which would open up potential deadlocks. The risk of + // a single-frame desync is therefore preferable for now. + mRtHandlingPositionUpdates = true; + if (mRTLastReportedPosition.left == left + && mRTLastReportedPosition.top == top + && mRTLastReportedPosition.right == right + && mRTLastReportedPosition.bottom == bottom) { + return; + } + try { + if (DEBUG) { + Log.d(TAG, String.format("%d updateSurfacePosition RenderWorker, frameNr = %d, " + + "postion = [%d, %d, %d, %d]", System.identityHashCode(this), + frameNumber, left, top, right, bottom)); + } + mRTLastReportedPosition.set(left, top, right, bottom); + setParentSpaceRectangle(mRTLastReportedPosition, frameNumber); + // Now overwrite mRTLastReportedPosition with our values + } catch (Exception ex) { + Log.e(TAG, "Exception from repositionChild", ex); + } + } + + /** + * Called by native on RenderThread to notify that the view is no longer in the + * draw tree. UI thread is blocked at this point. + * @hide + */ + public final void surfacePositionLost_uiRtSync(long frameNumber) { + if (DEBUG) { + Log.d(TAG, String.format("%d windowPositionLost, frameNr = %d", + System.identityHashCode(this), frameNumber)); + } + mRTLastReportedPosition.setEmpty(); + + if (mSurfaceControl == null) { + return; + } + if (mRtHandlingPositionUpdates) { + mRtHandlingPositionUpdates = false; + // This callback will happen while the UI thread is blocked, so we can + // safely access other member variables at this time. + // So do what the UI thread would have done if RT wasn't handling position + // updates. + if (!mScreenRect.isEmpty() && !mScreenRect.equals(mRTLastReportedPosition)) { + try { + if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition, " + + "postion = [%d, %d, %d, %d]", System.identityHashCode(this), + mScreenRect.left, mScreenRect.top, + mScreenRect.right, mScreenRect.bottom)); + setParentSpaceRectangle(mScreenRect, frameNumber); + } catch (Exception ex) { + Log.e(TAG, "Exception configuring surface", ex); + } + } + } + } + + private SurfaceHolder.Callback[] getSurfaceCallbacks() { + SurfaceHolder.Callback callbacks[]; + synchronized (mCallbacks) { + callbacks = new SurfaceHolder.Callback[mCallbacks.size()]; + mCallbacks.toArray(callbacks); + } + return callbacks; } - private SurfaceHolder mSurfaceHolder = new SurfaceHolder() { + private void runOnUiThread(Runnable runnable) { + Handler handler = getHandler(); + if (handler != null && handler.getLooper() != Looper.myLooper()) { + handler.post(runnable); + } else { + runnable.run(); + } + } + + /** + * Check to see if the surface has fixed size dimensions or if the surface's + * dimensions are dimensions are dependent on its current layout. + * + * @return true if the surface has dimensions that are fixed in size + * @hide + */ + public boolean isFixedSize() { + return (mRequestedWidth != -1 || mRequestedHeight != -1); + } + + private boolean isAboveParent() { + return mSubLayer >= 0; + } + + /** + * Set an opaque background color to use with this {@link SurfaceView} when it's being resized + * and size of the content hasn't updated yet. This color will fill the expanded area when the + * view becomes larger. + * @param bgColor An opaque color to fill the background. Alpha component will be ignored. + * @hide + */ + public void setResizeBackgroundColor(int bgColor) { + mSurfaceControl.setBackgroundColor(bgColor); + } + + private final SurfaceHolder mSurfaceHolder = new SurfaceHolder() { + private static final String LOG_TAG = "SurfaceHolder"; @Override public boolean isCreating() { - return false; + return mIsCreating; } @Override public void addCallback(Callback callback) { + synchronized (mCallbacks) { + // This is a linear search, but in practice we'll + // have only a couple callbacks, so it doesn't matter. + if (mCallbacks.contains(callback) == false) { + mCallbacks.add(callback); + } + } } @Override public void removeCallback(Callback callback) { + synchronized (mCallbacks) { + mCallbacks.remove(callback); + } } @Override public void setFixedSize(int width, int height) { + if (mRequestedWidth != width || mRequestedHeight != height) { + mRequestedWidth = width; + mRequestedHeight = height; + requestLayout(); + } } @Override public void setSizeFromLayout() { + if (mRequestedWidth != -1 || mRequestedHeight != -1) { + mRequestedWidth = mRequestedHeight = -1; + requestLayout(); + } } @Override public void setFormat(int format) { + // for backward compatibility reason, OPAQUE always + // means 565 for SurfaceView + if (format == PixelFormat.OPAQUE) + format = PixelFormat.RGB_565; + + mRequestedFormat = format; + if (mSurfaceControl != null) { + updateSurface(); + } } + /** + * @deprecated setType is now ignored. + */ @Override - public void setType(int type) { - } + @Deprecated + public void setType(int type) { } @Override public void setKeepScreenOn(boolean screenOn) { + runOnUiThread(() -> SurfaceView.this.setKeepScreenOn(screenOn)); } + /** + * Gets a {@link Canvas} for drawing into the SurfaceView's Surface + * + * After drawing into the provided {@link Canvas}, the caller must + * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface. + * + * The caller must redraw the entire surface. + * @return A canvas for drawing into the surface. + */ @Override public Canvas lockCanvas() { - return null; + return internalLockCanvas(null, false); + } + + /** + * Gets a {@link Canvas} for drawing into the SurfaceView's Surface + * + * After drawing into the provided {@link Canvas}, the caller must + * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface. + * + * @param inOutDirty A rectangle that represents the dirty region that the caller wants + * to redraw. This function may choose to expand the dirty rectangle if for example + * the surface has been resized or if the previous contents of the surface were + * not available. The caller must redraw the entire dirty region as represented + * by the contents of the inOutDirty rectangle upon return from this function. + * The caller may also pass <code>null</code> instead, in the case where the + * entire surface should be redrawn. + * @return A canvas for drawing into the surface. + */ + @Override + public Canvas lockCanvas(Rect inOutDirty) { + return internalLockCanvas(inOutDirty, false); } @Override - public Canvas lockCanvas(Rect dirty) { + public Canvas lockHardwareCanvas() { + return internalLockCanvas(null, true); + } + + private Canvas internalLockCanvas(Rect dirty, boolean hardware) { + mSurfaceLock.lock(); + + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Locking canvas... stopped=" + + mDrawingStopped + ", surfaceControl=" + mSurfaceControl); + + Canvas c = null; + if (!mDrawingStopped && mSurfaceControl != null) { + try { + if (hardware) { + c = mSurface.lockHardwareCanvas(); + } else { + c = mSurface.lockCanvas(dirty); + } + } catch (Exception e) { + Log.e(LOG_TAG, "Exception locking surface", e); + } + } + + if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Returned canvas: " + c); + if (c != null) { + mLastLockTime = SystemClock.uptimeMillis(); + return c; + } + + // If the Surface is not ready to be drawn, then return null, + // but throttle calls to this function so it isn't called more + // than every 100ms. + long now = SystemClock.uptimeMillis(); + long nextTime = mLastLockTime + 100; + if (nextTime > now) { + try { + Thread.sleep(nextTime-now); + } catch (InterruptedException e) { + } + now = SystemClock.uptimeMillis(); + } + mLastLockTime = now; + mSurfaceLock.unlock(); + return null; } + /** + * Posts the new contents of the {@link Canvas} to the surface and + * releases the {@link Canvas}. + * + * @param canvas The canvas previously obtained from {@link #lockCanvas}. + */ @Override public void unlockCanvasAndPost(Canvas canvas) { + mSurface.unlockCanvasAndPost(canvas); + mSurfaceLock.unlock(); } @Override public Surface getSurface() { - return null; + return mSurface; } @Override public Rect getSurfaceFrame() { - return null; + return mSurfaceFrame; } }; -} + class SurfaceControlWithBackground extends SurfaceControl { + SurfaceControl mBackgroundControl; + private boolean mOpaque = true; + public boolean mVisible = false; + + public SurfaceControlWithBackground(String name, boolean opaque, SurfaceControl.Builder b) + throws Exception { + super(b.setName(name).build()); + + mBackgroundControl = b.setName("Background for -" + name) + .setFormat(OPAQUE) + .setColorLayer(true) + .build(); + mOpaque = opaque; + } + + @Override + public void setAlpha(float alpha) { + super.setAlpha(alpha); + mBackgroundControl.setAlpha(alpha); + } + + @Override + public void setLayer(int zorder) { + super.setLayer(zorder); + // -3 is below all other child layers as SurfaceView never goes below -2 + mBackgroundControl.setLayer(-3); + } + + @Override + public void setPosition(float x, float y) { + super.setPosition(x, y); + mBackgroundControl.setPosition(x, y); + } + + @Override + public void setSize(int w, int h) { + super.setSize(w, h); + mBackgroundControl.setSize(w, h); + } + + @Override + public void setWindowCrop(Rect crop) { + super.setWindowCrop(crop); + mBackgroundControl.setWindowCrop(crop); + } + + @Override + public void setFinalCrop(Rect crop) { + super.setFinalCrop(crop); + mBackgroundControl.setFinalCrop(crop); + } + + @Override + public void setLayerStack(int layerStack) { + super.setLayerStack(layerStack); + mBackgroundControl.setLayerStack(layerStack); + } + + @Override + public void setOpaque(boolean isOpaque) { + super.setOpaque(isOpaque); + mOpaque = isOpaque; + updateBackgroundVisibility(); + } + + @Override + public void setSecure(boolean isSecure) { + super.setSecure(isSecure); + } + + @Override + public void setMatrix(float dsdx, float dtdx, float dsdy, float dtdy) { + super.setMatrix(dsdx, dtdx, dsdy, dtdy); + mBackgroundControl.setMatrix(dsdx, dtdx, dsdy, dtdy); + } + + @Override + public void hide() { + super.hide(); + mVisible = false; + updateBackgroundVisibility(); + } + + @Override + public void show() { + super.show(); + mVisible = true; + updateBackgroundVisibility(); + } + + @Override + public void destroy() { + super.destroy(); + mBackgroundControl.destroy(); + } + + @Override + public void release() { + super.release(); + mBackgroundControl.release(); + } + + @Override + public void setTransparentRegionHint(Region region) { + super.setTransparentRegionHint(region); + mBackgroundControl.setTransparentRegionHint(region); + } + + @Override + public void deferTransactionUntil(IBinder handle, long frame) { + super.deferTransactionUntil(handle, frame); + mBackgroundControl.deferTransactionUntil(handle, frame); + } + + @Override + public void deferTransactionUntil(Surface barrier, long frame) { + super.deferTransactionUntil(barrier, frame); + mBackgroundControl.deferTransactionUntil(barrier, frame); + } + + /** Set the color to fill the background with. */ + private void setBackgroundColor(int bgColor) { + final float[] colorComponents = new float[] { Color.red(bgColor) / 255.f, + Color.green(bgColor) / 255.f, Color.blue(bgColor) / 255.f }; + + SurfaceControl.openTransaction(); + try { + mBackgroundControl.setColor(colorComponents); + } finally { + SurfaceControl.closeTransaction(); + } + } + + void updateBackgroundVisibility() { + if (mOpaque && mVisible) { + mBackgroundControl.show(); + } else { + mBackgroundControl.hide(); + } + } + } +} diff --git a/android/view/ThreadedRenderer.java b/android/view/ThreadedRenderer.java index 5eb7e9cb..e03f5faa 100644 --- a/android/view/ThreadedRenderer.java +++ b/android/view/ThreadedRenderer.java @@ -190,6 +190,10 @@ public final class ThreadedRenderer { */ public static final String DEBUG_FPS_DIVISOR = "debug.hwui.fps_divisor"; + public static int EGL_CONTEXT_PRIORITY_HIGH_IMG = 0x3101; + public static int EGL_CONTEXT_PRIORITY_MEDIUM_IMG = 0x3102; + public static int EGL_CONTEXT_PRIORITY_LOW_IMG = 0x3103; + static { // Try to check OpenGL support early if possible. isAvailable(); @@ -1140,6 +1144,16 @@ public final class ThreadedRenderer { nHackySetRTAnimationsEnabled(divisor <= 1); } + /** + * Changes the OpenGL context priority if IMG_context_priority extension is available. Must be + * called before any OpenGL context is created. + * + * @param priority The priority to use. Must be one of EGL_CONTEXT_PRIORITY_* values. + */ + public static void setContextPriority(int priority) { + nSetContextPriority(priority); + } + /** Not actually public - internal use only. This doc to make lint happy */ public static native void disableVsync(); @@ -1213,4 +1227,5 @@ public final class ThreadedRenderer { private static native void nHackySetRTAnimationsEnabled(boolean enabled); private static native void nSetDebuggingEnabled(boolean enabled); private static native void nSetIsolatedProcess(boolean enabled); + private static native void nSetContextPriority(int priority); } diff --git a/android/view/View.java b/android/view/View.java index 97e11b15..71b60844 100644 --- a/android/view/View.java +++ b/android/view/View.java @@ -697,6 +697,7 @@ import java.util.function.Predicate; * security policy. See also {@link MotionEvent#FLAG_WINDOW_IS_OBSCURED}. * </p> * + * @attr ref android.R.styleable#View_accessibilityHeading * @attr ref android.R.styleable#View_alpha * @attr ref android.R.styleable#View_background * @attr ref android.R.styleable#View_clickable @@ -2955,7 +2956,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * 1 PFLAG3_SCREEN_READER_FOCUSABLE * 1 PFLAG3_AGGREGATED_VISIBLE * 1 PFLAG3_AUTOFILLID_EXPLICITLY_SET - * 1 available + * 1 PFLAG3_ACCESSIBILITY_HEADING * |-------|-------|-------|-------| */ @@ -3252,6 +3253,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private static final int PFLAG3_AUTOFILLID_EXPLICITLY_SET = 0x40000000; + /** + * Indicates if the View is a heading for accessibility purposes + */ + private static final int PFLAG3_ACCESSIBILITY_HEADING = 0x80000000; + /* End of masks for mPrivateFlags3 */ /** @@ -5475,6 +5481,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, case R.styleable.View_outlineAmbientShadowColor: setOutlineAmbientShadowColor(a.getColor(attr, Color.BLACK)); break; + case com.android.internal.R.styleable.View_accessibilityHeading: + setAccessibilityHeading(a.getBoolean(attr, false)); } } @@ -8795,6 +8803,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, info.addAction(AccessibilityAction.ACTION_SHOW_ON_SCREEN); populateAccessibilityNodeInfoDrawingOrderInParent(info); info.setPaneTitle(mAccessibilityPaneTitle); + info.setHeading(isAccessibilityHeading()); } /** @@ -10398,7 +10407,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @param willNotCacheDrawing true if this view does not cache its * drawing, false otherwise + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ + @Deprecated public void setWillNotCacheDrawing(boolean willNotCacheDrawing) { setFlags(willNotCacheDrawing ? WILL_NOT_CACHE_DRAWING : 0, WILL_NOT_CACHE_DRAWING); } @@ -10407,8 +10430,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Returns whether or not this View can cache its drawing or not. * * @return true if this view does not cache its drawing, false otherwise + * + * @deprecated The view drawing cache was largely made obsolete with the introduction of + * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache + * layers are largely unnecessary and can easily result in a net loss in performance due to the + * cost of creating and updating the layer. In the rare cases where caching layers are useful, + * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware + * rendering. For software-rendered snapshots of a small part of the View hierarchy or + * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or + * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these + * software-rendered usages are discouraged and have compatibility issues with hardware-only + * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} + * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback + * reports or unit testing the {@link PixelCopy} API is recommended. */ @ViewDebug.ExportedProperty(category = "drawing") + @Deprecated public boolean willNotCacheDrawing() { return (mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING; } @@ -10754,11 +10791,37 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * accessibility tools. */ public void setScreenReaderFocusable(boolean screenReaderFocusable) { + updatePflags3AndNotifyA11yIfChanged(PFLAG3_SCREEN_READER_FOCUSABLE, screenReaderFocusable); + } + + /** + * Gets whether this view is a heading for accessibility purposes. + * + * @return {@code true} if the view is a heading, {@code false} otherwise. + * + * @attr ref android.R.styleable#View_accessibilityHeading + */ + public boolean isAccessibilityHeading() { + return (mPrivateFlags3 & PFLAG3_ACCESSIBILITY_HEADING) != 0; + } + + /** + * Set if view is a heading for a section of content for accessibility purposes. + * + * @param isHeading {@code true} if the view is a heading, {@code false} otherwise. + * + * @attr ref android.R.styleable#View_accessibilityHeading + */ + public void setAccessibilityHeading(boolean isHeading) { + updatePflags3AndNotifyA11yIfChanged(PFLAG3_ACCESSIBILITY_HEADING, isHeading); + } + + private void updatePflags3AndNotifyA11yIfChanged(int mask, boolean newValue) { int pflags3 = mPrivateFlags3; - if (screenReaderFocusable) { - pflags3 |= PFLAG3_SCREEN_READER_FOCUSABLE; + if (newValue) { + pflags3 |= mask; } else { - pflags3 &= ~PFLAG3_SCREEN_READER_FOCUSABLE; + pflags3 &= ~mask; } if (pflags3 != mPrivateFlags3) { @@ -11763,6 +11826,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return null; } + /** @hide */ + View getSelfOrParentImportantForA11y() { + if (isImportantForAccessibility()) return this; + ViewParent parent = getParentForAccessibility(); + if (parent instanceof View) return (View) parent; + return null; + } + /** * Adds the children of this View relevant for accessibility to the given list * as output. Since some Views are not important for accessibility the added @@ -14978,10 +15049,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) { ensureTransformationInfo(); if (mTransformationInfo.mAlpha != alpha) { - // Report visibility changes, which can affect children, to accessibility - if ((alpha == 0) ^ (mTransformationInfo.mAlpha == 0)) { - notifySubtreeAccessibilityStateChangedIfNeeded(); - } + float oldAlpha = mTransformationInfo.mAlpha; mTransformationInfo.mAlpha = alpha; if (onSetAlpha((int) (alpha * 255))) { mPrivateFlags |= PFLAG_ALPHA_SET; @@ -14993,6 +15061,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, invalidateViewProperty(true, false); mRenderNode.setAlpha(getFinalAlpha()); } + // Report visibility changes, which can affect children, to accessibility + if ((alpha == 0) ^ (oldAlpha == 0)) { + notifySubtreeAccessibilityStateChangedIfNeeded(); + } } } diff --git a/android/view/ViewGroup.java b/android/view/ViewGroup.java index 6002fe51..2ec42c0d 100644 --- a/android/view/ViewGroup.java +++ b/android/view/ViewGroup.java @@ -5692,6 +5692,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } dispatchVisibilityAggregated(isAttachedToWindow() && getWindowVisibility() == VISIBLE && isShown()); + notifySubtreeAccessibilityStateChangedIfNeeded(); } /** diff --git a/android/view/ViewRootImpl.java b/android/view/ViewRootImpl.java index 433c90b3..730c3729 100644 --- a/android/view/ViewRootImpl.java +++ b/android/view/ViewRootImpl.java @@ -3719,7 +3719,7 @@ public final class ViewRootImpl implements ViewParent, checkThread(); if (mView != null) { if (!mView.hasFocus()) { - if (sAlwaysAssignFocus || !isInTouchMode()) { + if (sAlwaysAssignFocus || !mAttachInfo.mInTouchMode) { v.requestFocus(); } } else { @@ -6482,17 +6482,17 @@ public final class ViewRootImpl implements ViewParent, params.type = mOrigWindowType; } } + } - if (mSurface.isValid()) { - params.frameNumber = mSurface.getNextFrameNumber(); - } + long frameNumber = -1; + if (mSurface.isValid()) { + frameNumber = mSurface.getNextFrameNumber(); } - int relayoutResult = mWindowSession.relayout( - mWindow, mSeq, params, + int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params, (int) (mView.getMeasuredWidth() * appScale + 0.5f), - (int) (mView.getMeasuredHeight() * appScale + 0.5f), - viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, + (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility, + insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber, mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets, mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout, mPendingMergedConfiguration, mSurface); @@ -8305,6 +8305,12 @@ public final class ViewRootImpl implements ViewParent, public View mSource; public long mLastEventTimeMillis; + /** + * Override for {@link AccessibilityEvent#originStackTrace} to provide the stack trace + * of the original {@link #runOrPost} call instead of one for sending the delayed event + * from a looper. + */ + public StackTraceElement[] mOrigin; @Override public void run() { @@ -8322,6 +8328,7 @@ public final class ViewRootImpl implements ViewParent, AccessibilityEvent event = AccessibilityEvent.obtain(); event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); event.setContentChangeTypes(mChangeTypes); + if (AccessibilityEvent.DEBUG_ORIGIN) event.originStackTrace = mOrigin; source.sendAccessibilityEventUnchecked(event); } else { mLastEventTimeMillis = 0; @@ -8329,6 +8336,7 @@ public final class ViewRootImpl implements ViewParent, // In any case reset to initial state. source.resetSubtreeAccessibilityStateChanged(); mChangeTypes = 0; + if (AccessibilityEvent.DEBUG_ORIGIN) mOrigin = null; } public void runOrPost(View source, int changeType) { @@ -8352,12 +8360,18 @@ public final class ViewRootImpl implements ViewParent, // If there is no common predecessor, then mSource points to // a removed view, hence in this case always prefer the source. View predecessor = getCommonPredecessor(mSource, source); + if (predecessor != null) { + predecessor = predecessor.getSelfOrParentImportantForA11y(); + } mSource = (predecessor != null) ? predecessor : source; mChangeTypes |= changeType; return; } mSource = source; mChangeTypes = changeType; + if (AccessibilityEvent.DEBUG_ORIGIN) { + mOrigin = Thread.currentThread().getStackTrace(); + } final long timeSinceLastMillis = SystemClock.uptimeMillis() - mLastEventTimeMillis; final long minEventIntevalMillis = ViewConfiguration.getSendRecurringAccessibilityEventsInterval(); diff --git a/android/view/WindowManager.java b/android/view/WindowManager.java index f6181d70..0f5c23f7 100644 --- a/android/view/WindowManager.java +++ b/android/view/WindowManager.java @@ -2438,13 +2438,6 @@ public interface WindowManager extends ViewManager { public long hideTimeoutMilliseconds = -1; /** - * A frame number in which changes requested in this layout will be rendered. - * - * @hide - */ - public long frameNumber = -1; - - /** * The color mode requested by this window. The target display may * not be able to honor the request. When the color mode is not set * to {@link ActivityInfo#COLOR_MODE_DEFAULT}, it might override the @@ -2617,7 +2610,6 @@ public interface WindowManager extends ViewManager { TextUtils.writeToParcel(accessibilityTitle, out, parcelableFlags); out.writeInt(mColorMode); out.writeLong(hideTimeoutMilliseconds); - out.writeLong(frameNumber); } public static final Parcelable.Creator<LayoutParams> CREATOR @@ -2674,7 +2666,6 @@ public interface WindowManager extends ViewManager { accessibilityTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); mColorMode = in.readInt(); hideTimeoutMilliseconds = in.readLong(); - frameNumber = in.readLong(); } @SuppressWarnings({"PointlessBitwiseExpression"}) @@ -2875,10 +2866,6 @@ public interface WindowManager extends ViewManager { changes |= SURFACE_INSETS_CHANGED; } - // The frame number changing is only relevant in the context of other - // changes, and so we don't need to track it with a flag. - frameNumber = o.frameNumber; - if (hasManualSurfaceInsets != o.hasManualSurfaceInsets) { hasManualSurfaceInsets = o.hasManualSurfaceInsets; changes |= SURFACE_INSETS_CHANGED; diff --git a/android/view/WindowManagerGlobal.java b/android/view/WindowManagerGlobal.java index cca66d6b..08c2d0b7 100644 --- a/android/view/WindowManagerGlobal.java +++ b/android/view/WindowManagerGlobal.java @@ -610,6 +610,10 @@ public final class WindowManagerGlobal { ViewRootImpl root = mRoots.get(i); // Client might remove the view by "stopped" event. root.setWindowStopped(stopped); + // Recursively forward stopped state to View's attached + // to this Window rather than the root application token, + // e.g. PopupWindow's. + setStoppedState(root.mAttachInfo.mWindowToken, stopped); } } } diff --git a/android/view/accessibility/AccessibilityEvent.java b/android/view/accessibility/AccessibilityEvent.java index e0f74a7d..7946e9e2 100644 --- a/android/view/accessibility/AccessibilityEvent.java +++ b/android/view/accessibility/AccessibilityEvent.java @@ -201,6 +201,7 @@ import java.util.List; * <em>Properties:</em></br> * <ul> * <li>{@link #getEventType()} - The type of the event.</li> + * <li>{@link #getContentChangeTypes()} - The type of state changes.</li> * <li>{@link #getSource()} - The source info (for registered clients).</li> * <li>{@link #getClassName()} - The class name of the source.</li> * <li>{@link #getPackageName()} - The package name of the source.</li> @@ -388,6 +389,8 @@ import java.util.List; */ public final class AccessibilityEvent extends AccessibilityRecord implements Parcelable { private static final boolean DEBUG = false; + /** @hide */ + public static final boolean DEBUG_ORIGIN = false; /** * Invalid selection/focus position. @@ -748,7 +751,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par private static final int MAX_POOL_SIZE = 10; private static final SynchronizedPool<AccessibilityEvent> sPool = - new SynchronizedPool<AccessibilityEvent>(MAX_POOL_SIZE); + new SynchronizedPool<>(MAX_POOL_SIZE); private @EventType int mEventType; private CharSequence mPackageName; @@ -758,6 +761,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par int mContentChangeTypes; int mWindowChangeTypes; + /** + * The stack trace describing where this event originated from on the app side. + * Only populated if {@link #DEBUG_ORIGIN} is enabled + * Can be inspected(e.g. printed) from an + * {@link android.accessibilityservice.AccessibilityService} to trace where particular events + * are being dispatched from. + * + * @hide + */ + public StackTraceElement[] originStackTrace = null; + private ArrayList<AccessibilityRecord> mRecords; /* @@ -780,6 +794,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par mWindowChangeTypes = event.mWindowChangeTypes; mEventTime = event.mEventTime; mPackageName = event.mPackageName; + if (DEBUG_ORIGIN) originStackTrace = event.originStackTrace; } /** @@ -849,16 +864,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par } /** - * Gets the bit mask of change types signaled by an - * {@link #TYPE_WINDOW_CONTENT_CHANGED} event. A single event may represent - * multiple change types. + * Gets the bit mask of change types signaled by a + * {@link #TYPE_WINDOW_CONTENT_CHANGED} event or {@link #TYPE_WINDOW_STATE_CHANGED}. A single + * event may represent multiple change types. * * @return The bit mask of change types. One or more of: * <ul> - * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION} - * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE} - * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT} - * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED} + * <li>{@link #CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION} + * <li>{@link #CONTENT_CHANGE_TYPE_SUBTREE} + * <li>{@link #CONTENT_CHANGE_TYPE_TEXT} + * <li>{@link #CONTENT_CHANGE_TYPE_PANE_TITLE} + * <li>{@link #CONTENT_CHANGE_TYPE_UNDEFINED} * </ul> */ @ContentChangeTypes @@ -877,6 +893,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par } case CONTENT_CHANGE_TYPE_SUBTREE: return "CONTENT_CHANGE_TYPE_SUBTREE"; case CONTENT_CHANGE_TYPE_TEXT: return "CONTENT_CHANGE_TYPE_TEXT"; + case CONTENT_CHANGE_TYPE_PANE_TITLE: return "CONTENT_CHANGE_TYPE_PANE_TITLE"; case CONTENT_CHANGE_TYPE_UNDEFINED: return "CONTENT_CHANGE_TYPE_UNDEFINED"; default: return Integer.toHexString(type); } @@ -1104,7 +1121,9 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par */ public static AccessibilityEvent obtain() { AccessibilityEvent event = sPool.acquire(); - return (event != null) ? event : new AccessibilityEvent(); + if (event == null) event = new AccessibilityEvent(); + if (DEBUG_ORIGIN) event.originStackTrace = Thread.currentThread().getStackTrace(); + return event; } /** @@ -1142,6 +1161,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par record.recycle(); } } + if (DEBUG_ORIGIN) originStackTrace = null; } /** @@ -1164,7 +1184,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par // Read the records. final int recordCount = parcel.readInt(); if (recordCount > 0) { - mRecords = new ArrayList<AccessibilityRecord>(recordCount); + mRecords = new ArrayList<>(recordCount); for (int i = 0; i < recordCount; i++) { AccessibilityRecord record = AccessibilityRecord.obtain(); readAccessibilityRecordFromParcel(record, parcel); @@ -1172,6 +1192,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par mRecords.add(record); } } + + if (DEBUG_ORIGIN) { + originStackTrace = new StackTraceElement[parcel.readInt()]; + for (int i = 0; i < originStackTrace.length; i++) { + originStackTrace[i] = new StackTraceElement( + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readInt()); + } + } } /** @@ -1227,6 +1258,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par AccessibilityRecord record = mRecords.get(i); writeAccessibilityRecordToParcel(record, parcel, flags); } + + if (DEBUG_ORIGIN) { + if (originStackTrace == null) originStackTrace = Thread.currentThread().getStackTrace(); + parcel.writeInt(originStackTrace.length); + for (StackTraceElement element : originStackTrace) { + parcel.writeString(element.getClassName()); + parcel.writeString(element.getMethodName()); + parcel.writeString(element.getFileName()); + parcel.writeInt(element.getLineNumber()); + } + } } /** @@ -1285,7 +1327,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par } if (!DEBUG_CONCISE_TOSTRING || mWindowChangeTypes != 0) { builder.append("; WindowChangeTypes: ").append( - contentChangeTypesToString(mWindowChangeTypes)); + windowChangeTypesToString(mWindowChangeTypes)); } super.appendTo(builder); if (DEBUG || DEBUG_CONCISE_TOSTRING) { diff --git a/android/view/accessibility/AccessibilityInteractionClient.java b/android/view/accessibility/AccessibilityInteractionClient.java index 72af203e..d60c4819 100644 --- a/android/view/accessibility/AccessibilityInteractionClient.java +++ b/android/view/accessibility/AccessibilityInteractionClient.java @@ -326,12 +326,14 @@ public final class AccessibilityInteractionClient accessibilityWindowId, accessibilityNodeId); if (cachedInfo != null) { if (DEBUG) { - Log.i(LOG_TAG, "Node cache hit"); + Log.i(LOG_TAG, "Node cache hit for " + + idToString(accessibilityWindowId, accessibilityNodeId)); } return cachedInfo; } if (DEBUG) { - Log.i(LOG_TAG, "Node cache miss"); + Log.i(LOG_TAG, "Node cache miss for " + + idToString(accessibilityWindowId, accessibilityNodeId)); } } final int interactionId = mInteractionIdCounter.getAndIncrement(); @@ -368,6 +370,11 @@ public final class AccessibilityInteractionClient return null; } + private static String idToString(int accessibilityWindowId, long accessibilityNodeId) { + return accessibilityWindowId + "/" + + AccessibilityNodeInfo.idToString(accessibilityNodeId); + } + /** * Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in * the window whose id is specified and starts from the node whose accessibility diff --git a/android/view/accessibility/AccessibilityManager.java b/android/view/accessibility/AccessibilityManager.java index 84b40641..cbb23f1a 100644 --- a/android/view/accessibility/AccessibilityManager.java +++ b/android/view/accessibility/AccessibilityManager.java @@ -16,48 +16,156 @@ package android.view.accessibility; +import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_ENABLE_ACCESSIBILITY_VOLUME; + +import android.Manifest; import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.AccessibilityServiceInfo.FeedbackType; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SystemService; +import android.content.ComponentName; import android.content.Context; +import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.os.Binder; import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.Log; +import android.util.SparseArray; import android.view.IWindow; import android.view.View; import android.view.accessibility.AccessibilityEvent.EventType; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IntPair; + +import java.util.ArrayList; import java.util.Collections; import java.util.List; /** - * System level service that serves as an event dispatch for {@link AccessibilityEvent}s. - * Such events are generated when something notable happens in the user interface, + * System level service that serves as an event dispatch for {@link AccessibilityEvent}s, + * and provides facilities for querying the accessibility state of the system. + * Accessibility events are generated when something notable happens in the user interface, * for example an {@link android.app.Activity} starts, the focus or selection of a * {@link android.view.View} changes etc. Parties interested in handling accessibility * events implement and register an accessibility service which extends - * {@code android.accessibilityservice.AccessibilityService}. + * {@link android.accessibilityservice.AccessibilityService}. * * @see AccessibilityEvent - * @see android.content.Context#getSystemService + * @see AccessibilityNodeInfo + * @see android.accessibilityservice.AccessibilityService + * @see Context#getSystemService + * @see Context#ACCESSIBILITY_SERVICE */ -@SuppressWarnings("UnusedDeclaration") +@SystemService(Context.ACCESSIBILITY_SERVICE) public final class AccessibilityManager { + private static final boolean DEBUG = false; + + private static final String LOG_TAG = "AccessibilityManager"; + + /** @hide */ + public static final int STATE_FLAG_ACCESSIBILITY_ENABLED = 0x00000001; + + /** @hide */ + public static final int STATE_FLAG_TOUCH_EXPLORATION_ENABLED = 0x00000002; + + /** @hide */ + public static final int STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED = 0x00000004; + + /** @hide */ + public static final int DALTONIZER_DISABLED = -1; + + /** @hide */ + public static final int DALTONIZER_SIMULATE_MONOCHROMACY = 0; + + /** @hide */ + public static final int DALTONIZER_CORRECT_DEUTERANOMALY = 12; + + /** @hide */ + public static final int AUTOCLICK_DELAY_DEFAULT = 600; + + /** + * Activity action: Launch UI to manage which accessibility service or feature is assigned + * to the navigation bar Accessibility button. + * <p> + * Input: Nothing. + * </p> + * <p> + * Output: Nothing. + * </p> + * + * @hide + */ + @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CHOOSE_ACCESSIBILITY_BUTTON = + "com.android.internal.intent.action.CHOOSE_ACCESSIBILITY_BUTTON"; + + static final Object sInstanceSync = new Object(); + + private static AccessibilityManager sInstance; + + private final Object mLock = new Object(); + + private IAccessibilityManager mService; + + final int mUserId; + + final Handler mHandler; + + final Handler.Callback mCallback; + + boolean mIsEnabled; + + int mRelevantEventTypes = AccessibilityEvent.TYPES_ALL_MASK; + + boolean mIsTouchExplorationEnabled; - private static AccessibilityManager sInstance = new AccessibilityManager(null, null, 0); + boolean mIsHighTextContrastEnabled; + AccessibilityPolicy mAccessibilityPolicy; + + private final ArrayMap<AccessibilityStateChangeListener, Handler> + mAccessibilityStateChangeListeners = new ArrayMap<>(); + + private final ArrayMap<TouchExplorationStateChangeListener, Handler> + mTouchExplorationStateChangeListeners = new ArrayMap<>(); + + private final ArrayMap<HighTextContrastChangeListener, Handler> + mHighTextContrastStateChangeListeners = new ArrayMap<>(); + + private final ArrayMap<AccessibilityServicesStateChangeListener, Handler> + mServicesStateChangeListeners = new ArrayMap<>(); /** - * Listener for the accessibility state. + * Map from a view's accessibility id to the list of request preparers set for that view + */ + private SparseArray<List<AccessibilityRequestPreparer>> mRequestPreparerLists; + + /** + * Listener for the system accessibility state. To listen for changes to the + * accessibility state on the device, implement this interface and register + * it with the system by calling {@link #addAccessibilityStateChangeListener}. */ public interface AccessibilityStateChangeListener { /** - * Called back on change in the accessibility state. + * Called when the accessibility enabled state changes. * * @param enabled Whether accessibility is enabled. */ - public void onAccessibilityStateChanged(boolean enabled); + void onAccessibilityStateChanged(boolean enabled); } /** @@ -73,7 +181,24 @@ public final class AccessibilityManager { * * @param enabled Whether touch exploration is enabled. */ - public void onTouchExplorationStateChanged(boolean enabled); + void onTouchExplorationStateChanged(boolean enabled); + } + + /** + * Listener for changes to the state of accessibility services. Changes include services being + * enabled or disabled, or changes to the {@link AccessibilityServiceInfo} of a running service. + * {@see #addAccessibilityServicesStateChangeListener}. + * + * @hide + */ + public interface AccessibilityServicesStateChangeListener { + + /** + * Called when the state of accessibility services changes. + * + * @param manager The manager that is calling back + */ + void onAccessibilityServicesStateChanged(AccessibilityManager manager); } /** @@ -81,6 +206,8 @@ public final class AccessibilityManager { * the high text contrast state on the device, implement this interface and * register it with the system by calling * {@link #addHighTextContrastStateChangeListener}. + * + * @hide */ public interface HighTextContrastChangeListener { @@ -89,7 +216,7 @@ public final class AccessibilityManager { * * @param enabled Whether high text contrast is enabled. */ - public void onHighTextContrastStateChanged(boolean enabled); + void onHighTextContrastStateChanged(boolean enabled); } /** @@ -148,21 +275,67 @@ public final class AccessibilityManager { private final IAccessibilityManagerClient.Stub mClient = new IAccessibilityManagerClient.Stub() { - public void setState(int state) { - } + @Override + public void setState(int state) { + // We do not want to change this immediately as the application may + // have already checked that accessibility is on and fired an event, + // that is now propagating up the view tree, Hence, if accessibility + // is now off an exception will be thrown. We want to have the exception + // enforcement to guard against apps that fire unnecessary accessibility + // events when accessibility is off. + mHandler.obtainMessage(MyCallback.MSG_SET_STATE, state, 0).sendToTarget(); + } - public void notifyServicesStateChanged() { + @Override + public void notifyServicesStateChanged() { + final ArrayMap<AccessibilityServicesStateChangeListener, Handler> listeners; + synchronized (mLock) { + if (mServicesStateChangeListeners.isEmpty()) { + return; } + listeners = new ArrayMap<>(mServicesStateChangeListeners); + } - public void setRelevantEventTypes(int eventTypes) { - } - }; + int numListeners = listeners.size(); + for (int i = 0; i < numListeners; i++) { + final AccessibilityServicesStateChangeListener listener = + mServicesStateChangeListeners.keyAt(i); + mServicesStateChangeListeners.valueAt(i).post(() -> listener + .onAccessibilityServicesStateChanged(AccessibilityManager.this)); + } + } + + @Override + public void setRelevantEventTypes(int eventTypes) { + mRelevantEventTypes = eventTypes; + } + }; /** * Get an AccessibilityManager instance (create one if necessary). * + * @param context Context in which this manager operates. + * + * @hide */ public static AccessibilityManager getInstance(Context context) { + synchronized (sInstanceSync) { + if (sInstance == null) { + final int userId; + if (Binder.getCallingUid() == Process.SYSTEM_UID + || context.checkCallingOrSelfPermission( + Manifest.permission.INTERACT_ACROSS_USERS) + == PackageManager.PERMISSION_GRANTED + || context.checkCallingOrSelfPermission( + Manifest.permission.INTERACT_ACROSS_USERS_FULL) + == PackageManager.PERMISSION_GRANTED) { + userId = UserHandle.USER_CURRENT; + } else { + userId = context.getUserId(); + } + sInstance = new AccessibilityManager(context, null, userId); + } + } return sInstance; } @@ -170,21 +343,65 @@ public final class AccessibilityManager { * Create an instance. * * @param context A {@link Context}. + * @param service An interface to the backing service. + * @param userId User id under which to run. + * + * @hide */ public AccessibilityManager(Context context, IAccessibilityManager service, int userId) { + // Constructor can't be chained because we can't create an instance of an inner class + // before calling another constructor. + mCallback = new MyCallback(); + mHandler = new Handler(context.getMainLooper(), mCallback); + mUserId = userId; + synchronized (mLock) { + tryConnectToServiceLocked(service); + } + } + + /** + * Create an instance. + * + * @param handler The handler to use + * @param service An interface to the backing service. + * @param userId User id under which to run. + * + * @hide + */ + public AccessibilityManager(Handler handler, IAccessibilityManager service, int userId) { + mCallback = new MyCallback(); + mHandler = handler; + mUserId = userId; + synchronized (mLock) { + tryConnectToServiceLocked(service); + } } + /** + * @hide + */ public IAccessibilityManagerClient getClient() { return mClient; } /** - * Returns if the {@link AccessibilityManager} is enabled. + * @hide + */ + @VisibleForTesting + public Handler.Callback getCallback() { + return mCallback; + } + + /** + * Returns if the accessibility in the system is enabled. * - * @return True if this {@link AccessibilityManager} is enabled, false otherwise. + * @return True if accessibility is enabled, false otherwise. */ public boolean isEnabled() { - return false; + synchronized (mLock) { + return mIsEnabled || (mAccessibilityPolicy != null + && mAccessibilityPolicy.isEnabled(mIsEnabled)); + } } /** @@ -193,7 +410,13 @@ public final class AccessibilityManager { * @return True if touch exploration is enabled, false otherwise. */ public boolean isTouchExplorationEnabled() { - return true; + synchronized (mLock) { + IAccessibilityManager service = getServiceLocked(); + if (service == null) { + return false; + } + return mIsTouchExplorationEnabled; + } } /** @@ -203,47 +426,188 @@ public final class AccessibilityManager { * doing its own rendering and does not rely on the platform rendering pipeline. * </p> * + * @return True if high text contrast is enabled, false otherwise. + * + * @hide */ public boolean isHighTextContrastEnabled() { - return false; + synchronized (mLock) { + IAccessibilityManager service = getServiceLocked(); + if (service == null) { + return false; + } + return mIsHighTextContrastEnabled; + } } /** * Sends an {@link AccessibilityEvent}. - */ - public void sendAccessibilityEvent(AccessibilityEvent event) { - } - - /** - * Returns whether there are observers registered for this event type. If - * this method returns false you shuold not generate events of this type - * to conserve resources. * - * @param type The event type. - * @return Whether the event is being observed. + * @param event The event to send. + * + * @throws IllegalStateException if accessibility is not enabled. + * + * <strong>Note:</strong> The preferred mechanism for sending custom accessibility + * events is through calling + * {@link android.view.ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)} + * instead of this method to allow predecessors to augment/filter events sent by + * their descendants. */ - public boolean isObservedEventType(@AccessibilityEvent.EventType int type) { - return false; + public void sendAccessibilityEvent(AccessibilityEvent event) { + final IAccessibilityManager service; + final int userId; + final AccessibilityEvent dispatchedEvent; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + event.setEventTime(SystemClock.uptimeMillis()); + if (mAccessibilityPolicy != null) { + dispatchedEvent = mAccessibilityPolicy.onAccessibilityEvent(event, + mIsEnabled, mRelevantEventTypes); + if (dispatchedEvent == null) { + return; + } + } else { + dispatchedEvent = event; + } + if (!isEnabled()) { + Looper myLooper = Looper.myLooper(); + if (myLooper == Looper.getMainLooper()) { + throw new IllegalStateException( + "Accessibility off. Did you forget to check that?"); + } else { + // If we're not running on the thread with the main looper, it's possible for + // the state of accessibility to change between checking isEnabled and + // calling this method. So just log the error rather than throwing the + // exception. + Log.e(LOG_TAG, "AccessibilityEvent sent with accessibility disabled"); + return; + } + } + if ((dispatchedEvent.getEventType() & mRelevantEventTypes) == 0) { + if (DEBUG) { + Log.i(LOG_TAG, "Not dispatching irrelevant event: " + dispatchedEvent + + " that is not among " + + AccessibilityEvent.eventTypeToString(mRelevantEventTypes)); + } + return; + } + userId = mUserId; + } + try { + // it is possible that this manager is in the same process as the service but + // client using it is called through Binder from another process. Example: MMS + // app adds a SMS notification and the NotificationManagerService calls this method + long identityToken = Binder.clearCallingIdentity(); + try { + service.sendAccessibilityEvent(dispatchedEvent, userId); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + if (DEBUG) { + Log.i(LOG_TAG, dispatchedEvent + " sent"); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error during sending " + dispatchedEvent + " ", re); + } finally { + if (event != dispatchedEvent) { + event.recycle(); + } + dispatchedEvent.recycle(); + } } /** - * Requests interruption of the accessibility feedback from all accessibility services. + * Requests feedback interruption from all accessibility services. */ public void interrupt() { + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + if (!isEnabled()) { + Looper myLooper = Looper.myLooper(); + if (myLooper == Looper.getMainLooper()) { + throw new IllegalStateException( + "Accessibility off. Did you forget to check that?"); + } else { + // If we're not running on the thread with the main looper, it's possible for + // the state of accessibility to change between checking isEnabled and + // calling this method. So just log the error rather than throwing the + // exception. + Log.e(LOG_TAG, "Interrupt called with accessibility disabled"); + return; + } + } + userId = mUserId; + } + try { + service.interrupt(userId); + if (DEBUG) { + Log.i(LOG_TAG, "Requested interrupt from all services"); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while requesting interrupt from all services. ", re); + } } /** * Returns the {@link ServiceInfo}s of the installed accessibility services. * * @return An unmodifiable list with {@link ServiceInfo}s. + * + * @deprecated Use {@link #getInstalledAccessibilityServiceList()} */ @Deprecated public List<ServiceInfo> getAccessibilityServiceList() { - return Collections.emptyList(); + List<AccessibilityServiceInfo> infos = getInstalledAccessibilityServiceList(); + List<ServiceInfo> services = new ArrayList<>(); + final int infoCount = infos.size(); + for (int i = 0; i < infoCount; i++) { + AccessibilityServiceInfo info = infos.get(i); + services.add(info.getResolveInfo().serviceInfo); + } + return Collections.unmodifiableList(services); } + /** + * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services. + * + * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. + */ public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() { - return Collections.emptyList(); + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return Collections.emptyList(); + } + userId = mUserId; + } + + List<AccessibilityServiceInfo> services = null; + try { + services = service.getInstalledAccessibilityServiceList(userId); + if (DEBUG) { + Log.i(LOG_TAG, "Installed AccessibilityServices " + services); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); + } + if (mAccessibilityPolicy != null) { + services = mAccessibilityPolicy.getInstalledAccessibilityServiceList(services); + } + if (services != null) { + return Collections.unmodifiableList(services); + } else { + return Collections.emptyList(); + } } /** @@ -258,21 +622,52 @@ public final class AccessibilityManager { * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN * @see AccessibilityServiceInfo#FEEDBACK_VISUAL + * @see AccessibilityServiceInfo#FEEDBACK_BRAILLE */ public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList( int feedbackTypeFlags) { - return Collections.emptyList(); + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return Collections.emptyList(); + } + userId = mUserId; + } + + List<AccessibilityServiceInfo> services = null; + try { + services = service.getEnabledAccessibilityServiceList(feedbackTypeFlags, userId); + if (DEBUG) { + Log.i(LOG_TAG, "Installed AccessibilityServices " + services); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); + } + if (mAccessibilityPolicy != null) { + services = mAccessibilityPolicy.getEnabledAccessibilityServiceList( + feedbackTypeFlags, services); + } + if (services != null) { + return Collections.unmodifiableList(services); + } else { + return Collections.emptyList(); + } } /** * Registers an {@link AccessibilityStateChangeListener} for changes in - * the global accessibility state of the system. + * the global accessibility state of the system. Equivalent to calling + * {@link #addAccessibilityStateChangeListener(AccessibilityStateChangeListener, Handler)} + * with a null handler. * * @param listener The listener. - * @return True if successfully registered. + * @return Always returns {@code true}. */ public boolean addAccessibilityStateChangeListener( - AccessibilityStateChangeListener listener) { + @NonNull AccessibilityStateChangeListener listener) { + addAccessibilityStateChangeListener(listener, null); return true; } @@ -286,22 +681,40 @@ public final class AccessibilityManager { * for a callback on the process's main handler. */ public void addAccessibilityStateChangeListener( - @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) {} + @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) { + synchronized (mLock) { + mAccessibilityStateChangeListeners + .put(listener, (handler == null) ? mHandler : handler); + } + } + /** + * Unregisters an {@link AccessibilityStateChangeListener}. + * + * @param listener The listener. + * @return True if the listener was previously registered. + */ public boolean removeAccessibilityStateChangeListener( - AccessibilityStateChangeListener listener) { - return true; + @NonNull AccessibilityStateChangeListener listener) { + synchronized (mLock) { + int index = mAccessibilityStateChangeListeners.indexOfKey(listener); + mAccessibilityStateChangeListeners.remove(listener); + return (index >= 0); + } } /** * Registers a {@link TouchExplorationStateChangeListener} for changes in - * the global touch exploration state of the system. + * the global touch exploration state of the system. Equivalent to calling + * {@link #addTouchExplorationStateChangeListener(TouchExplorationStateChangeListener, Handler)} + * with a null handler. * * @param listener The listener. - * @return True if successfully registered. + * @return Always returns {@code true}. */ public boolean addTouchExplorationStateChangeListener( @NonNull TouchExplorationStateChangeListener listener) { + addTouchExplorationStateChangeListener(listener, null); return true; } @@ -315,17 +728,104 @@ public final class AccessibilityManager { * for a callback on the process's main handler. */ public void addTouchExplorationStateChangeListener( - @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) {} + @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) { + synchronized (mLock) { + mTouchExplorationStateChangeListeners + .put(listener, (handler == null) ? mHandler : handler); + } + } /** * Unregisters a {@link TouchExplorationStateChangeListener}. * * @param listener The listener. - * @return True if successfully unregistered. + * @return True if listener was previously registered. */ public boolean removeTouchExplorationStateChangeListener( @NonNull TouchExplorationStateChangeListener listener) { - return true; + synchronized (mLock) { + int index = mTouchExplorationStateChangeListeners.indexOfKey(listener); + mTouchExplorationStateChangeListeners.remove(listener); + return (index >= 0); + } + } + + /** + * Registers a {@link AccessibilityServicesStateChangeListener}. + * + * @param listener The listener. + * @param handler The handler on which the listener should be called back, or {@code null} + * for a callback on the process's main handler. + * @hide + */ + public void addAccessibilityServicesStateChangeListener( + @NonNull AccessibilityServicesStateChangeListener listener, @Nullable Handler handler) { + synchronized (mLock) { + mServicesStateChangeListeners + .put(listener, (handler == null) ? mHandler : handler); + } + } + + /** + * Unregisters a {@link AccessibilityServicesStateChangeListener}. + * + * @param listener The listener. + * + * @hide + */ + public void removeAccessibilityServicesStateChangeListener( + @NonNull AccessibilityServicesStateChangeListener listener) { + synchronized (mLock) { + mServicesStateChangeListeners.remove(listener); + } + } + + /** + * Registers a {@link AccessibilityRequestPreparer}. + */ + public void addAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) { + if (mRequestPreparerLists == null) { + mRequestPreparerLists = new SparseArray<>(1); + } + int id = preparer.getView().getAccessibilityViewId(); + List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(id); + if (requestPreparerList == null) { + requestPreparerList = new ArrayList<>(1); + mRequestPreparerLists.put(id, requestPreparerList); + } + requestPreparerList.add(preparer); + } + + /** + * Unregisters a {@link AccessibilityRequestPreparer}. + */ + public void removeAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) { + if (mRequestPreparerLists == null) { + return; + } + int viewId = preparer.getView().getAccessibilityViewId(); + List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(viewId); + if (requestPreparerList != null) { + requestPreparerList.remove(preparer); + if (requestPreparerList.isEmpty()) { + mRequestPreparerLists.remove(viewId); + } + } + } + + /** + * Get the preparers that are registered for an accessibility ID + * + * @param id The ID of interest + * @return The list of preparers, or {@code null} if there are none. + * + * @hide + */ + public List<AccessibilityRequestPreparer> getRequestPreparersForAccessibilityId(int id) { + if (mRequestPreparerLists == null) { + return null; + } + return mRequestPreparerLists.get(id); } /** @@ -337,7 +837,12 @@ public final class AccessibilityManager { * @hide */ public void addHighTextContrastStateChangeListener( - @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {} + @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) { + synchronized (mLock) { + mHighTextContrastStateChangeListeners + .put(listener, (handler == null) ? mHandler : handler); + } + } /** * Unregisters a {@link HighTextContrastChangeListener}. @@ -347,7 +852,64 @@ public final class AccessibilityManager { * @hide */ public void removeHighTextContrastStateChangeListener( - @NonNull HighTextContrastChangeListener listener) {} + @NonNull HighTextContrastChangeListener listener) { + synchronized (mLock) { + mHighTextContrastStateChangeListeners.remove(listener); + } + } + + /** + * Sets the {@link AccessibilityPolicy} controlling this manager. + * + * @param policy The policy. + * + * @hide + */ + public void setAccessibilityPolicy(@Nullable AccessibilityPolicy policy) { + synchronized (mLock) { + mAccessibilityPolicy = policy; + } + } + + /** + * Check if the accessibility volume stream is active. + * + * @return True if accessibility volume is active (i.e. some service has requested it). False + * otherwise. + * @hide + */ + public boolean isAccessibilityVolumeStreamActive() { + List<AccessibilityServiceInfo> serviceInfos = + getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK); + for (int i = 0; i < serviceInfos.size(); i++) { + if ((serviceInfos.get(i).flags & FLAG_ENABLE_ACCESSIBILITY_VOLUME) != 0) { + return true; + } + } + return false; + } + + /** + * Report a fingerprint gesture to accessibility. Only available for the system process. + * + * @param keyCode The key code of the gesture + * @return {@code true} if accessibility consumes the event. {@code false} if not. + * @hide + */ + public boolean sendFingerprintGesture(int keyCode) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return false; + } + } + try { + return service.sendFingerprintGesture(keyCode); + } catch (RemoteException e) { + return false; + } + } /** * Sets the current state and notifies listeners, if necessary. @@ -355,14 +917,312 @@ public final class AccessibilityManager { * @param stateFlags The state flags. */ private void setStateLocked(int stateFlags) { + final boolean enabled = (stateFlags & STATE_FLAG_ACCESSIBILITY_ENABLED) != 0; + final boolean touchExplorationEnabled = + (stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0; + final boolean highTextContrastEnabled = + (stateFlags & STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED) != 0; + + final boolean wasEnabled = isEnabled(); + final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled; + final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled; + + // Ensure listeners get current state from isZzzEnabled() calls. + mIsEnabled = enabled; + mIsTouchExplorationEnabled = touchExplorationEnabled; + mIsHighTextContrastEnabled = highTextContrastEnabled; + + if (wasEnabled != isEnabled()) { + notifyAccessibilityStateChanged(); + } + + if (wasTouchExplorationEnabled != touchExplorationEnabled) { + notifyTouchExplorationStateChanged(); + } + + if (wasHighTextContrastEnabled != highTextContrastEnabled) { + notifyHighTextContrastStateChanged(); + } + } + + /** + * Find an installed service with the specified {@link ComponentName}. + * + * @param componentName The name to match to the service. + * + * @return The info corresponding to the installed service, or {@code null} if no such service + * is installed. + * @hide + */ + public AccessibilityServiceInfo getInstalledServiceInfoWithComponentName( + ComponentName componentName) { + final List<AccessibilityServiceInfo> installedServiceInfos = + getInstalledAccessibilityServiceList(); + if ((installedServiceInfos == null) || (componentName == null)) { + return null; + } + for (int i = 0; i < installedServiceInfos.size(); i++) { + if (componentName.equals(installedServiceInfos.get(i).getComponentName())) { + return installedServiceInfos.get(i); + } + } + return null; } + /** + * Adds an accessibility interaction connection interface for a given window. + * @param windowToken The window token to which a connection is added. + * @param connection The connection. + * + * @hide + */ public int addAccessibilityInteractionConnection(IWindow windowToken, - IAccessibilityInteractionConnection connection) { + String packageName, IAccessibilityInteractionConnection connection) { + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return View.NO_ID; + } + userId = mUserId; + } + try { + return service.addAccessibilityInteractionConnection(windowToken, connection, + packageName, userId); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re); + } return View.NO_ID; } + /** + * Removed an accessibility interaction connection interface for a given window. + * @param windowToken The window token to which a connection is removed. + * + * @hide + */ public void removeAccessibilityInteractionConnection(IWindow windowToken) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.removeAccessibilityInteractionConnection(windowToken); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while removing an accessibility interaction connection. ", re); + } } + /** + * Perform the accessibility shortcut if the caller has permission. + * + * @hide + */ + public void performAccessibilityShortcut() { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.performAccessibilityShortcut(); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error performing accessibility shortcut. ", re); + } + } + + /** + * Notifies that the accessibility button in the system's navigation area has been clicked + * + * @hide + */ + public void notifyAccessibilityButtonClicked() { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.notifyAccessibilityButtonClicked(); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while dispatching accessibility button click", re); + } + } + + /** + * Notifies that the visibility of the accessibility button in the system's navigation area + * has changed. + * + * @param shown {@code true} if the accessibility button is visible within the system + * navigation area, {@code false} otherwise + * @hide + */ + public void notifyAccessibilityButtonVisibilityChanged(boolean shown) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.notifyAccessibilityButtonVisibilityChanged(shown); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while dispatching accessibility button visibility change", re); + } + } + + /** + * Set an IAccessibilityInteractionConnection to replace the actions of a picture-in-picture + * window. Intended for use by the System UI only. + * + * @param connection The connection to handle the actions. Set to {@code null} to avoid + * affecting the actions. + * + * @hide + */ + public void setPictureInPictureActionReplacingConnection( + @Nullable IAccessibilityInteractionConnection connection) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.setPictureInPictureActionReplacingConnection(connection); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error setting picture in picture action replacement", re); + } + } + + private IAccessibilityManager getServiceLocked() { + if (mService == null) { + tryConnectToServiceLocked(null); + } + return mService; + } + + private void tryConnectToServiceLocked(IAccessibilityManager service) { + if (service == null) { + IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE); + if (iBinder == null) { + return; + } + service = IAccessibilityManager.Stub.asInterface(iBinder); + } + + try { + final long userStateAndRelevantEvents = service.addClient(mClient, mUserId); + setStateLocked(IntPair.first(userStateAndRelevantEvents)); + mRelevantEventTypes = IntPair.second(userStateAndRelevantEvents); + mService = service; + } catch (RemoteException re) { + Log.e(LOG_TAG, "AccessibilityManagerService is dead", re); + } + } + + /** + * Notifies the registered {@link AccessibilityStateChangeListener}s. + */ + private void notifyAccessibilityStateChanged() { + final boolean isEnabled; + final ArrayMap<AccessibilityStateChangeListener, Handler> listeners; + synchronized (mLock) { + if (mAccessibilityStateChangeListeners.isEmpty()) { + return; + } + isEnabled = isEnabled(); + listeners = new ArrayMap<>(mAccessibilityStateChangeListeners); + } + + final int numListeners = listeners.size(); + for (int i = 0; i < numListeners; i++) { + final AccessibilityStateChangeListener listener = listeners.keyAt(i); + listeners.valueAt(i).post(() -> + listener.onAccessibilityStateChanged(isEnabled)); + } + } + + /** + * Notifies the registered {@link TouchExplorationStateChangeListener}s. + */ + private void notifyTouchExplorationStateChanged() { + final boolean isTouchExplorationEnabled; + final ArrayMap<TouchExplorationStateChangeListener, Handler> listeners; + synchronized (mLock) { + if (mTouchExplorationStateChangeListeners.isEmpty()) { + return; + } + isTouchExplorationEnabled = mIsTouchExplorationEnabled; + listeners = new ArrayMap<>(mTouchExplorationStateChangeListeners); + } + + final int numListeners = listeners.size(); + for (int i = 0; i < numListeners; i++) { + final TouchExplorationStateChangeListener listener = listeners.keyAt(i); + listeners.valueAt(i).post(() -> + listener.onTouchExplorationStateChanged(isTouchExplorationEnabled)); + } + } + + /** + * Notifies the registered {@link HighTextContrastChangeListener}s. + */ + private void notifyHighTextContrastStateChanged() { + final boolean isHighTextContrastEnabled; + final ArrayMap<HighTextContrastChangeListener, Handler> listeners; + synchronized (mLock) { + if (mHighTextContrastStateChangeListeners.isEmpty()) { + return; + } + isHighTextContrastEnabled = mIsHighTextContrastEnabled; + listeners = new ArrayMap<>(mHighTextContrastStateChangeListeners); + } + + final int numListeners = listeners.size(); + for (int i = 0; i < numListeners; i++) { + final HighTextContrastChangeListener listener = listeners.keyAt(i); + listeners.valueAt(i).post(() -> + listener.onHighTextContrastStateChanged(isHighTextContrastEnabled)); + } + } + + /** + * Determines if the accessibility button within the system navigation area is supported. + * + * @return {@code true} if the accessibility button is supported on this device, + * {@code false} otherwise + */ + public static boolean isAccessibilityButtonSupported() { + final Resources res = Resources.getSystem(); + return res.getBoolean(com.android.internal.R.bool.config_showNavigationBar); + } + + private final class MyCallback implements Handler.Callback { + public static final int MSG_SET_STATE = 1; + + @Override + public boolean handleMessage(Message message) { + switch (message.what) { + case MSG_SET_STATE: { + // See comment at mClient + final int state = message.arg1; + synchronized (mLock) { + setStateLocked(state); + } + } break; + } + return true; + } + } } diff --git a/android/view/accessibility/AccessibilityNodeInfo.java b/android/view/accessibility/AccessibilityNodeInfo.java index 4c437dd4..03f1c124 100644 --- a/android/view/accessibility/AccessibilityNodeInfo.java +++ b/android/view/accessibility/AccessibilityNodeInfo.java @@ -3874,6 +3874,24 @@ public class AccessibilityNodeInfo implements Parcelable { | FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS, null); } + /** @hide */ + public static String idToString(long accessibilityId) { + int accessibilityViewId = getAccessibilityViewId(accessibilityId); + int virtualDescendantId = getVirtualDescendantId(accessibilityId); + return virtualDescendantId == AccessibilityNodeProvider.HOST_VIEW_ID + ? idItemToString(accessibilityViewId) + : idItemToString(accessibilityViewId) + ":" + idItemToString(virtualDescendantId); + } + + private static String idItemToString(int item) { + switch (item) { + case ROOT_ITEM_ID: return "ROOT"; + case UNDEFINED_ITEM_ID: return "UNDEFINED"; + case AccessibilityNodeProvider.HOST_VIEW_ID: return "HOST"; + default: return "" + item; + } + } + /** * A class defining an action that can be performed on an {@link AccessibilityNodeInfo}. * Each action has a unique id that is mandatory and optional data. diff --git a/android/view/autofill/AutofillPopupWindow.java b/android/view/autofill/AutofillPopupWindow.java index 1da998d0..a6495d15 100644 --- a/android/view/autofill/AutofillPopupWindow.java +++ b/android/view/autofill/AutofillPopupWindow.java @@ -79,6 +79,11 @@ public class AutofillPopupWindow extends PopupWindow { public AutofillPopupWindow(@NonNull IAutofillWindowPresenter presenter) { mWindowPresenter = new WindowPresenter(presenter); + // We want to show the window as system controlled one so it covers app windows, but it has + // to be an application type (so it's contained inside the application area). + // Hence, we set it to the application type with the highest z-order, which currently + // is TYPE_APPLICATION_ABOVE_SUB_PANEL. + setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL); setTouchModal(false); setOutsideTouchable(true); setInputMethodMode(INPUT_METHOD_NOT_NEEDED); diff --git a/android/view/inputmethod/BaseInputConnection.java b/android/view/inputmethod/BaseInputConnection.java index 5f7a0f78..090e19f9 100644 --- a/android/view/inputmethod/BaseInputConnection.java +++ b/android/view/inputmethod/BaseInputConnection.java @@ -522,7 +522,7 @@ public class BaseInputConnection implements InputConnection { b = tmp; } - if (a == b) return null; + if (a == b || a < 0) return null; if ((flags&GET_TEXT_WITH_STYLES) != 0) { return content.subSequence(a, b); diff --git a/android/view/textclassifier/GenerateLinksLogger.java b/android/view/textclassifier/GenerateLinksLogger.java index 73cf43b8..067513f1 100644 --- a/android/view/textclassifier/GenerateLinksLogger.java +++ b/android/view/textclassifier/GenerateLinksLogger.java @@ -19,13 +19,13 @@ package android.view.textclassifier; import android.annotation.Nullable; import android.metrics.LogMaker; import android.util.ArrayMap; -import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.Preconditions; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Random; @@ -39,6 +39,7 @@ import java.util.UUID; public final class GenerateLinksLogger { private static final String LOG_TAG = "GenerateLinksLogger"; + private static final boolean DEBUG_LOG_ENABLED = false; private static final String ZERO = "0"; private final MetricsLogger mMetricsLogger; @@ -127,7 +128,7 @@ public final class GenerateLinksLogger { } private static void debugLog(LogMaker log) { - if (!Logger.DEBUG_LOG_ENABLED) return; + if (!DEBUG_LOG_ENABLED) return; final String callId = Objects.toString( log.getTaggedData(MetricsEvent.FIELD_LINKIFY_CALL_ID), ""); @@ -142,8 +143,9 @@ public final class GenerateLinksLogger { final int latencyMs = Integer.parseInt( Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_LATENCY), ZERO)); - Log.d(LOG_TAG, String.format("%s:%s %d links (%d/%d chars) %dms %s", callId, entityType, - numLinks, linkLength, textLength, latencyMs, log.getPackageName())); + Log.d(LOG_TAG, + String.format(Locale.US, "%s:%s %d links (%d/%d chars) %dms %s", callId, entityType, + numLinks, linkLength, textLength, latencyMs, log.getPackageName())); } /** Helper class for storing per-entity type statistics. */ diff --git a/android/view/textclassifier/Logger.java b/android/view/textclassifier/Logger.java deleted file mode 100644 index f03906a0..00000000 --- a/android/view/textclassifier/Logger.java +++ /dev/null @@ -1,397 +0,0 @@ -/* - * 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 android.view.textclassifier; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; - -import com.android.internal.util.Preconditions; - -import java.text.BreakIterator; -import java.util.Locale; -import java.util.Objects; - -/** - * A helper for logging TextClassifier related events. - * @hide - */ -public abstract class Logger { - - private static final String LOG_TAG = "Logger"; - /* package */ static final boolean DEBUG_LOG_ENABLED = true; - - private @SelectionEvent.InvocationMethod int mInvocationMethod; - private SelectionEvent mPrevEvent; - private SelectionEvent mSmartEvent; - private SelectionEvent mStartEvent; - - /** - * Logger that does not log anything. - * @hide - */ - public static final Logger DISABLED = new Logger() { - @Override - public void writeEvent(SelectionEvent event) {} - }; - - @Nullable - private final Config mConfig; - - public Logger(Config config) { - mConfig = Preconditions.checkNotNull(config); - } - - private Logger() { - mConfig = null; - } - - /** - * Writes the selection event to a log. - */ - public abstract void writeEvent(@NonNull SelectionEvent event); - - /** - * Returns true if the resultId matches that of a smart selection event (i.e. - * {@link SelectionEvent#EVENT_SMART_SELECTION_SINGLE} or - * {@link SelectionEvent#EVENT_SMART_SELECTION_MULTI}). - * Returns false otherwise. - */ - public boolean isSmartSelection(@NonNull String resultId) { - return false; - } - - /** - * Returns a token iterator for tokenizing text for logging purposes. - */ - public BreakIterator getTokenIterator(@NonNull Locale locale) { - return BreakIterator.getWordInstance(Preconditions.checkNotNull(locale)); - } - - /** - * Logs a "selection started" event. - * - * @param invocationMethod the way the selection was triggered - * @param start the token index of the selected token - */ - public final void logSelectionStartedEvent( - @SelectionEvent.InvocationMethod int invocationMethod, int start) { - if (mConfig == null) { - return; - } - - mInvocationMethod = invocationMethod; - logEvent(new SelectionEvent( - start, start + 1, SelectionEvent.EVENT_SELECTION_STARTED, - TextClassifier.TYPE_UNKNOWN, mInvocationMethod, null, mConfig)); - } - - /** - * Logs a "selection modified" event. - * Use when the user modifies the selection. - * - * @param start the start token (inclusive) index of the selection - * @param end the end token (exclusive) index of the selection - */ - public final void logSelectionModifiedEvent(int start, int end) { - Preconditions.checkArgument(end >= start, "end cannot be less than start"); - - if (mConfig == null) { - return; - } - - logEvent(new SelectionEvent( - start, end, SelectionEvent.EVENT_SELECTION_MODIFIED, - TextClassifier.TYPE_UNKNOWN, mInvocationMethod, null, mConfig)); - } - - /** - * Logs a "selection modified" event. - * Use when the user modifies the selection and the selection's entity type is known. - * - * @param start the start token (inclusive) index of the selection - * @param end the end token (exclusive) index of the selection - * @param classification the TextClassification object returned by the TextClassifier that - * classified the selected text - */ - public final void logSelectionModifiedEvent( - int start, int end, @NonNull TextClassification classification) { - Preconditions.checkArgument(end >= start, "end cannot be less than start"); - Preconditions.checkNotNull(classification); - - if (mConfig == null) { - return; - } - - final String entityType = classification.getEntityCount() > 0 - ? classification.getEntity(0) - : TextClassifier.TYPE_UNKNOWN; - logEvent(new SelectionEvent( - start, end, SelectionEvent.EVENT_SELECTION_MODIFIED, - entityType, mInvocationMethod, classification.getId(), mConfig)); - } - - /** - * Logs a "selection modified" event. - * Use when a TextClassifier modifies the selection. - * - * @param start the start token (inclusive) index of the selection - * @param end the end token (exclusive) index of the selection - * @param selection the TextSelection object returned by the TextClassifier for the - * specified selection - */ - public final void logSelectionModifiedEvent( - int start, int end, @NonNull TextSelection selection) { - Preconditions.checkArgument(end >= start, "end cannot be less than start"); - Preconditions.checkNotNull(selection); - - if (mConfig == null) { - return; - } - - final int eventType; - if (isSmartSelection(selection.getId())) { - eventType = end - start > 1 - ? SelectionEvent.EVENT_SMART_SELECTION_MULTI - : SelectionEvent.EVENT_SMART_SELECTION_SINGLE; - - } else { - eventType = SelectionEvent.EVENT_AUTO_SELECTION; - } - final String entityType = selection.getEntityCount() > 0 - ? selection.getEntity(0) - : TextClassifier.TYPE_UNKNOWN; - logEvent(new SelectionEvent(start, end, eventType, entityType, mInvocationMethod, - selection.getId(), mConfig)); - } - - /** - * Logs an event specifying an action taken on a selection. - * Use when the user clicks on an action to act on the selected text. - * - * @param start the start token (inclusive) index of the selection - * @param end the end token (exclusive) index of the selection - * @param actionType the action that was performed on the selection - */ - public final void logSelectionActionEvent( - int start, int end, @SelectionEvent.ActionType int actionType) { - Preconditions.checkArgument(end >= start, "end cannot be less than start"); - checkActionType(actionType); - - if (mConfig == null) { - return; - } - - logEvent(new SelectionEvent( - start, end, actionType, TextClassifier.TYPE_UNKNOWN, mInvocationMethod, - null, mConfig)); - } - - /** - * Logs an event specifying an action taken on a selection. - * Use when the user clicks on an action to act on the selected text and the selection's - * entity type is known. - * - * @param start the start token (inclusive) index of the selection - * @param end the end token (exclusive) index of the selection - * @param actionType the action that was performed on the selection - * @param classification the TextClassification object returned by the TextClassifier that - * classified the selected text - * - * @throws IllegalArgumentException If actionType is not a valid SelectionEvent actionType - */ - public final void logSelectionActionEvent( - int start, int end, @SelectionEvent.ActionType int actionType, - @NonNull TextClassification classification) { - Preconditions.checkArgument(end >= start, "end cannot be less than start"); - Preconditions.checkNotNull(classification); - checkActionType(actionType); - - if (mConfig == null) { - return; - } - - final String entityType = classification.getEntityCount() > 0 - ? classification.getEntity(0) - : TextClassifier.TYPE_UNKNOWN; - logEvent(new SelectionEvent(start, end, actionType, entityType, mInvocationMethod, - classification.getId(), mConfig)); - } - - private void logEvent(@NonNull SelectionEvent event) { - Preconditions.checkNotNull(event); - - if (event.getEventType() != SelectionEvent.EVENT_SELECTION_STARTED - && mStartEvent == null) { - if (DEBUG_LOG_ENABLED) { - Log.d(LOG_TAG, "Selection session not yet started. Ignoring event"); - } - return; - } - - final long now = System.currentTimeMillis(); - switch (event.getEventType()) { - case SelectionEvent.EVENT_SELECTION_STARTED: - Preconditions.checkArgument(event.getAbsoluteEnd() == event.getAbsoluteStart() + 1); - event.setSessionId(startNewSession()); - mStartEvent = event; - break; - case SelectionEvent.EVENT_SMART_SELECTION_SINGLE: // fall through - case SelectionEvent.EVENT_SMART_SELECTION_MULTI: - mSmartEvent = event; - break; - case SelectionEvent.EVENT_SELECTION_MODIFIED: // fall through - case SelectionEvent.EVENT_AUTO_SELECTION: - if (mPrevEvent != null - && mPrevEvent.getAbsoluteStart() == event.getAbsoluteStart() - && mPrevEvent.getAbsoluteEnd() == event.getAbsoluteEnd()) { - // Selection did not change. Ignore event. - return; - } - break; - default: - // do nothing. - } - - event.setEventTime(now); - if (mStartEvent != null) { - event.setSessionId(mStartEvent.getSessionId()) - .setDurationSinceSessionStart(now - mStartEvent.getEventTime()) - .setStart(event.getAbsoluteStart() - mStartEvent.getAbsoluteStart()) - .setEnd(event.getAbsoluteEnd() - mStartEvent.getAbsoluteStart()); - } - if (mSmartEvent != null) { - event.setResultId(mSmartEvent.getResultId()) - .setSmartStart(mSmartEvent.getAbsoluteStart() - mStartEvent.getAbsoluteStart()) - .setSmartEnd(mSmartEvent.getAbsoluteEnd() - mStartEvent.getAbsoluteStart()); - } - if (mPrevEvent != null) { - event.setDurationSincePreviousEvent(now - mPrevEvent.getEventTime()) - .setEventIndex(mPrevEvent.getEventIndex() + 1); - } - writeEvent(event); - mPrevEvent = event; - - if (event.isTerminal()) { - endSession(); - } - } - - private TextClassificationSessionId startNewSession() { - endSession(); - return new TextClassificationSessionId(); - } - - private void endSession() { - mPrevEvent = null; - mSmartEvent = null; - mStartEvent = null; - } - - /** - * @throws IllegalArgumentException If eventType is not an {@link SelectionEvent.ActionType} - */ - private static void checkActionType(@SelectionEvent.EventType int eventType) - throws IllegalArgumentException { - switch (eventType) { - case SelectionEvent.ACTION_OVERTYPE: // fall through - case SelectionEvent.ACTION_COPY: // fall through - case SelectionEvent.ACTION_PASTE: // fall through - case SelectionEvent.ACTION_CUT: // fall through - case SelectionEvent.ACTION_SHARE: // fall through - case SelectionEvent.ACTION_SMART_SHARE: // fall through - case SelectionEvent.ACTION_DRAG: // fall through - case SelectionEvent.ACTION_ABANDON: // fall through - case SelectionEvent.ACTION_SELECT_ALL: // fall through - case SelectionEvent.ACTION_RESET: // fall through - return; - default: - throw new IllegalArgumentException( - String.format(Locale.US, "%d is not an eventType", eventType)); - } - } - - - /** - * A Logger config. - */ - public static final class Config { - - private final String mPackageName; - private final String mWidgetType; - @Nullable private final String mWidgetVersion; - - /** - * @param context Context of the widget the logger logs for - * @param widgetType a name for the widget being logged for. e.g. - * {@link TextClassifier#WIDGET_TYPE_TEXTVIEW} - * @param widgetVersion a string version info for the widget the logger logs for - */ - public Config( - @NonNull Context context, - @TextClassifier.WidgetType String widgetType, - @Nullable String widgetVersion) { - mPackageName = Preconditions.checkNotNull(context).getPackageName(); - mWidgetType = widgetType; - mWidgetVersion = widgetVersion; - } - - /** - * Returns the package name of the application the logger logs for. - */ - public String getPackageName() { - return mPackageName; - } - - /** - * Returns the name for the widget being logged for. e.g. - * {@link TextClassifier#WIDGET_TYPE_TEXTVIEW}. - */ - public String getWidgetType() { - return mWidgetType; - } - - /** - * Returns string version info for the logger. This is specific to the text classifier. - */ - @Nullable - public String getWidgetVersion() { - return mWidgetVersion; - } - - @Override - public int hashCode() { - return Objects.hash(mPackageName, mWidgetType, mWidgetVersion); - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - - if (!(obj instanceof Config)) { - return false; - } - - final Config other = (Config) obj; - return Objects.equals(mPackageName, other.mPackageName) - && Objects.equals(mWidgetType, other.mWidgetType) - && Objects.equals(mWidgetVersion, other.mWidgetType); - } - } -} diff --git a/android/view/textclassifier/SelectionEvent.java b/android/view/textclassifier/SelectionEvent.java index 1e978ccf..b0735969 100644 --- a/android/view/textclassifier/SelectionEvent.java +++ b/android/view/textclassifier/SelectionEvent.java @@ -150,20 +150,6 @@ public final class SelectionEvent implements Parcelable { mInvocationMethod = invocationMethod; } - SelectionEvent( - int start, int end, - @EventType int eventType, @EntityType String entityType, - @InvocationMethod int invocationMethod, @Nullable String resultId, - Logger.Config config) { - this(start, end, eventType, entityType, invocationMethod, resultId); - Preconditions.checkNotNull(config); - setTextClassificationSessionContext( - new TextClassificationContext.Builder( - config.getPackageName(), config.getWidgetType()) - .setWidgetVersion(config.getWidgetVersion()) - .build()); - } - private SelectionEvent(Parcel in) { mAbsoluteStart = in.readInt(); mAbsoluteEnd = in.readInt(); @@ -362,6 +348,7 @@ public final class SelectionEvent implements Parcelable { case SelectionEvent.ACTION_ABANDON: // fall through case SelectionEvent.ACTION_SELECT_ALL: // fall through case SelectionEvent.ACTION_RESET: // fall through + case SelectionEvent.ACTION_OTHER: // fall through return; default: throw new IllegalArgumentException( @@ -667,4 +654,4 @@ public final class SelectionEvent implements Parcelable { return new SelectionEvent[size]; } }; -}
\ No newline at end of file +} diff --git a/android/view/textclassifier/DefaultLogger.java b/android/view/textclassifier/SelectionSessionLogger.java index 203ca560..f2fb63eb 100644 --- a/android/view/textclassifier/DefaultLogger.java +++ b/android/view/textclassifier/SelectionSessionLogger.java @@ -17,28 +17,29 @@ package android.view.textclassifier; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.metrics.LogMaker; -import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.Preconditions; +import java.text.BreakIterator; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.StringJoiner; /** - * Default Logger. - * Used internally by TextClassifierImpl. + * A helper for logging selection session events. * @hide */ -public final class DefaultLogger extends Logger { +public final class SelectionSessionLogger { - private static final String LOG_TAG = "DefaultLogger"; + private static final String LOG_TAG = "SelectionSessionLogger"; + private static final boolean DEBUG_LOG_ENABLED = false; static final String CLASSIFIER_ID = "androidtc"; private static final int START_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_START; @@ -59,23 +60,16 @@ public final class DefaultLogger extends Logger { private final MetricsLogger mMetricsLogger; - public DefaultLogger(@NonNull Config config) { - super(config); + public SelectionSessionLogger() { mMetricsLogger = new MetricsLogger(); } @VisibleForTesting - public DefaultLogger(@NonNull Config config, @NonNull MetricsLogger metricsLogger) { - super(config); + public SelectionSessionLogger(@NonNull MetricsLogger metricsLogger) { mMetricsLogger = Preconditions.checkNotNull(metricsLogger); } - @Override - public boolean isSmartSelection(@NonNull String signature) { - return CLASSIFIER_ID.equals(SignatureParser.getClassifierId(signature)); - } - - @Override + /** Emits a selection event to the logs. */ public void writeEvent(@NonNull SelectionEvent event) { Preconditions.checkNotNull(event); final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_SESSION) @@ -93,7 +87,7 @@ public final class DefaultLogger extends Logger { .addTaggedData(SMART_END, event.getSmartEnd()) .addTaggedData(EVENT_START, event.getStart()) .addTaggedData(EVENT_END, event.getEnd()) - .addTaggedData(SESSION_ID, event.getSessionId()); + .addTaggedData(SESSION_ID, event.getSessionId().flattenToString()); mMetricsLogger.write(log); debugLog(log); } @@ -225,9 +219,17 @@ public final class DefaultLogger extends Logger { final int eventEnd = Integer.parseInt( Objects.toString(log.getTaggedData(EVENT_END), ZERO)); - Log.d(LOG_TAG, String.format("%2d: %s/%s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)", - index, type, subType, entity, eventStart, eventEnd, smartStart, smartEnd, widget, - model)); + Log.d(LOG_TAG, + String.format(Locale.US, "%2d: %s/%s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)", + index, type, subType, entity, eventStart, eventEnd, smartStart, smartEnd, + widget, model)); + } + + /** + * Returns a token iterator for tokenizing text for logging purposes. + */ + public static BreakIterator getTokenIterator(@NonNull Locale locale) { + return BreakIterator.getWordInstance(Preconditions.checkNotNull(locale)); } /** @@ -260,8 +262,10 @@ public final class DefaultLogger extends Logger { return String.format(Locale.US, "%s|%s|%d", classifierId, modelName, hash); } - static String getClassifierId(String signature) { - Preconditions.checkNotNull(signature); + static String getClassifierId(@Nullable String signature) { + if (signature == null) { + return ""; + } final int end = signature.indexOf("|"); if (end >= 0) { return signature.substring(0, end); @@ -269,8 +273,10 @@ public final class DefaultLogger extends Logger { return ""; } - static String getModelName(String signature) { - Preconditions.checkNotNull(signature); + static String getModelName(@Nullable String signature) { + if (signature == null) { + return ""; + } final int start = signature.indexOf("|") + 1; final int end = signature.indexOf("|", start); if (start >= 1 && end >= start) { @@ -279,8 +285,10 @@ public final class DefaultLogger extends Logger { return ""; } - static int getHash(String signature) { - Preconditions.checkNotNull(signature); + static int getHash(@Nullable String signature) { + if (signature == null) { + return 0; + } final int index1 = signature.indexOf("|"); final int index2 = signature.indexOf("|", index1); if (index2 > 0) { diff --git a/android/view/textclassifier/SystemTextClassifier.java b/android/view/textclassifier/SystemTextClassifier.java index 45fd6bfb..490c3890 100644 --- a/android/view/textclassifier/SystemTextClassifier.java +++ b/android/view/textclassifier/SystemTextClassifier.java @@ -28,7 +28,6 @@ import android.service.textclassifier.ITextClassifierService; import android.service.textclassifier.ITextLinksCallback; import android.service.textclassifier.ITextSelectionCallback; -import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting.Visibility; import com.android.internal.util.Preconditions; @@ -49,13 +48,6 @@ public final class SystemTextClassifier implements TextClassifier { private final TextClassificationConstants mSettings; private final TextClassifier mFallback; private final String mPackageName; - - private final Object mLoggerLock = new Object(); - @GuardedBy("mLoggerLock") - private Logger.Config mLoggerConfig; - @GuardedBy("mLoggerLock") - private Logger mLogger; - @GuardedBy("mLoggerLock") private TextClassificationSessionId mSessionId; public SystemTextClassifier(Context context, TextClassificationConstants settings) @@ -147,27 +139,6 @@ public final class SystemTextClassifier implements TextClassifier { } @Override - public Logger getLogger(@NonNull Logger.Config config) { - Preconditions.checkNotNull(config); - synchronized (mLoggerLock) { - if (mLogger == null || !config.equals(mLoggerConfig)) { - mLoggerConfig = config; - mLogger = new Logger(config) { - @Override - public void writeEvent(SelectionEvent event) { - try { - mManagerService.onSelectionEvent(mSessionId, event); - } catch (RemoteException e) { - Log.e(LOG_TAG, "Error reporting selection event.", e); - } - } - }; - } - } - return mLogger; - } - - @Override public void destroy() { try { if (mSessionId != null) { diff --git a/android/view/textclassifier/TextClassification.java b/android/view/textclassifier/TextClassification.java index 37a5d9a1..96016b44 100644 --- a/android/view/textclassifier/TextClassification.java +++ b/android/view/textclassifier/TextClassification.java @@ -375,13 +375,13 @@ public final class TextClassification implements Parcelable { */ public static final class Builder { - @NonNull private String mText; @NonNull private List<RemoteAction> mActions = new ArrayList<>(); @NonNull private final Map<String, Float> mEntityConfidence = new ArrayMap<>(); - @Nullable Drawable mLegacyIcon; - @Nullable String mLegacyLabel; - @Nullable Intent mLegacyIntent; - @Nullable OnClickListener mLegacyOnClickListener; + @Nullable private String mText; + @Nullable private Drawable mLegacyIcon; + @Nullable private String mLegacyLabel; + @Nullable private Intent mLegacyIntent; + @Nullable private OnClickListener mLegacyOnClickListener; @Nullable private String mId; /** @@ -721,4 +721,67 @@ public final class TextClassification implements Parcelable { mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in); mId = in.readString(); } + + // TODO: Remove once apps can build against the latest sdk. + /** + * Optional input parameters for generating TextClassification. + * @hide + */ + public static final class Options { + + @Nullable private final TextClassificationSessionId mSessionId; + @Nullable private final Request mRequest; + @Nullable private LocaleList mDefaultLocales; + @Nullable private ZonedDateTime mReferenceTime; + + public Options() { + this(null, null); + } + + private Options( + @Nullable TextClassificationSessionId sessionId, @Nullable Request request) { + mSessionId = sessionId; + mRequest = request; + } + + /** Helper to create Options from a Request. */ + public static Options from(TextClassificationSessionId sessionId, Request request) { + final Options options = new Options(sessionId, request); + options.setDefaultLocales(request.getDefaultLocales()); + options.setReferenceTime(request.getReferenceTime()); + return options; + } + + /** @param defaultLocales ordered list of locale preferences. */ + public Options setDefaultLocales(@Nullable LocaleList defaultLocales) { + mDefaultLocales = defaultLocales; + return this; + } + + /** @param referenceTime refrence time used for interpreting relatives dates */ + public Options setReferenceTime(@Nullable ZonedDateTime referenceTime) { + mReferenceTime = referenceTime; + return this; + } + + @Nullable + public LocaleList getDefaultLocales() { + return mDefaultLocales; + } + + @Nullable + public ZonedDateTime getReferenceTime() { + return mReferenceTime; + } + + @Nullable + public Request getRequest() { + return mRequest; + } + + @Nullable + public TextClassificationSessionId getSessionId() { + return mSessionId; + } + } } diff --git a/android/view/textclassifier/TextClassificationSession.java b/android/view/textclassifier/TextClassificationSession.java index e8e300a9..4c641985 100644 --- a/android/view/textclassifier/TextClassificationSession.java +++ b/android/view/textclassifier/TextClassificationSession.java @@ -17,7 +17,6 @@ package android.view.textclassifier; import android.annotation.WorkerThread; -import android.view.textclassifier.DefaultLogger.SignatureParser; import android.view.textclassifier.SelectionEvent.InvocationMethod; import com.android.internal.util.Preconditions; @@ -222,7 +221,8 @@ final class TextClassificationSession implements TextClassifier { } private static boolean isPlatformLocalTextClassifierSmartSelection(String signature) { - return DefaultLogger.CLASSIFIER_ID.equals(SignatureParser.getClassifierId(signature)); + return SelectionSessionLogger.CLASSIFIER_ID.equals( + SelectionSessionLogger.SignatureParser.getClassifierId(signature)); } } } diff --git a/android/view/textclassifier/TextClassifier.java b/android/view/textclassifier/TextClassifier.java index 54261be3..da47bcb1 100644 --- a/android/view/textclassifier/TextClassifier.java +++ b/android/view/textclassifier/TextClassifier.java @@ -41,8 +41,9 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.List; +import java.util.HashSet; import java.util.Map; +import java.util.Set; /** * Interface for providing text classification related features. @@ -208,6 +209,26 @@ public interface TextClassifier { return suggestSelection(request); } + // TODO: Remove once apps can build against the latest sdk. + /** @hide */ + default TextSelection suggestSelection( + @NonNull CharSequence text, + @IntRange(from = 0) int selectionStartIndex, + @IntRange(from = 0) int selectionEndIndex, + @Nullable TextSelection.Options options) { + if (options == null) { + return suggestSelection(new TextSelection.Request.Builder( + text, selectionStartIndex, selectionEndIndex).build()); + } else if (options.getRequest() != null) { + return suggestSelection(options.getRequest()); + } else { + return suggestSelection( + new TextSelection.Request.Builder(text, selectionStartIndex, selectionEndIndex) + .setDefaultLocales(options.getDefaultLocales()) + .build()); + } + } + /** * Classifies the specified text and returns a {@link TextClassification} object that can be * used to generate a widget for handling the classified text. @@ -267,6 +288,26 @@ public interface TextClassifier { return classifyText(request); } + // TODO: Remove once apps can build against the latest sdk. + /** @hide */ + default TextClassification classifyText( + @NonNull CharSequence text, + @IntRange(from = 0) int startIndex, + @IntRange(from = 0) int endIndex, + @Nullable TextClassification.Options options) { + if (options == null) { + return classifyText( + new TextClassification.Request.Builder(text, startIndex, endIndex).build()); + } else if (options.getRequest() != null) { + return classifyText(options.getRequest()); + } else { + return classifyText(new TextClassification.Request.Builder(text, startIndex, endIndex) + .setDefaultLocales(options.getDefaultLocales()) + .setReferenceTime(options.getReferenceTime()) + .build()); + } + } + /** * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with * links information. @@ -288,6 +329,22 @@ public interface TextClassifier { return new TextLinks.Builder(request.getText().toString()).build(); } + // TODO: Remove once apps can build against the latest sdk. + /** @hide */ + default TextLinks generateLinks( + @NonNull CharSequence text, @Nullable TextLinks.Options options) { + if (options == null) { + return generateLinks(new TextLinks.Request.Builder(text).build()); + } else if (options.getRequest() != null) { + return generateLinks(options.getRequest()); + } else { + return generateLinks(new TextLinks.Request.Builder(text) + .setDefaultLocales(options.getDefaultLocales()) + .setEntityConfig(options.getEntityConfig()) + .build()); + } + } + /** * Returns the maximal length of text that can be processed by generateLinks. * @@ -302,18 +359,6 @@ public interface TextClassifier { } /** - * Returns a helper for logging TextClassifier related events. - * - * @param config logger configuration - * @hide - */ - @WorkerThread - default Logger getLogger(@NonNull Logger.Config config) { - Preconditions.checkNotNull(config); - return Logger.DISABLED; - } - - /** * Reports a selection event. * * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should @@ -377,6 +422,12 @@ public interface TextClassifier { /* includedEntityTypes */null, /* excludedEntityTypes */ null); } + // TODO: Remove once apps can build against the latest sdk. + /** @hide */ + public static EntityConfig create(@Nullable Collection<String> hints) { + return createWithHints(hints); + } + /** * Creates an EntityConfig. * @@ -406,6 +457,12 @@ public interface TextClassifier { /* includedEntityTypes */ entityTypes, /* excludedEntityTypes */ null); } + // TODO: Remove once apps can build against the latest sdk. + /** @hide */ + public static EntityConfig createWithEntityList(@Nullable Collection<String> entityTypes) { + return createWithExplicitEntityList(entityTypes); + } + /** * Returns a list of the final set of entities to find. * @@ -413,21 +470,15 @@ public interface TextClassifier { * * This method is intended for use by TextClassifier implementations. */ - public List<String> resolveEntityListModifications(@NonNull Collection<String> entities) { - final ArrayList<String> finalList = new ArrayList<>(); + public Collection<String> resolveEntityListModifications( + @NonNull Collection<String> entities) { + final Set<String> finalSet = new HashSet(); if (mUseHints) { - for (String entity : entities) { - if (!mExcludedEntityTypes.contains(entity)) { - finalList.add(entity); - } - } - } - for (String entity : mIncludedEntityTypes) { - if (!mExcludedEntityTypes.contains(entity) && !finalList.contains(entity)) { - finalList.add(entity); - } + finalSet.addAll(entities); } - return finalList; + finalSet.addAll(mIncludedEntityTypes); + finalSet.removeAll(mExcludedEntityTypes); + return finalSet; } /** @@ -508,7 +559,7 @@ public interface TextClassifier { final String string = request.getText().toString(); final TextLinks.Builder links = new TextLinks.Builder(string); - final List<String> entities = request.getEntityConfig() + final Collection<String> entities = request.getEntityConfig() .resolveEntityListModifications(Collections.emptyList()); if (entities.contains(TextClassifier.TYPE_URL)) { addLinks(links, string, TextClassifier.TYPE_URL); diff --git a/android/view/textclassifier/TextClassifierImpl.java b/android/view/textclassifier/TextClassifierImpl.java index 7e3748ae..22133558 100644 --- a/android/view/textclassifier/TextClassifierImpl.java +++ b/android/view/textclassifier/TextClassifierImpl.java @@ -94,11 +94,7 @@ public final class TextClassifierImpl implements TextClassifier { private final Object mLoggerLock = new Object(); @GuardedBy("mLoggerLock") // Do not access outside this lock. - private Logger.Config mLoggerConfig; - @GuardedBy("mLoggerLock") // Do not access outside this lock. - private Logger mLogger; - @GuardedBy("mLoggerLock") // Do not access outside this lock. - private Logger mLogger2; // This is the new logger. Will replace mLogger. + private SelectionSessionLogger mSessionLogger; private final TextClassificationConstants mSettings; @@ -283,28 +279,14 @@ public final class TextClassifierImpl implements TextClassifier { } } - /** @inheritDoc */ - @Override - public Logger getLogger(@NonNull Logger.Config config) { - Preconditions.checkNotNull(config); - synchronized (mLoggerLock) { - if (mLogger == null || !config.equals(mLoggerConfig)) { - mLoggerConfig = config; - mLogger = new DefaultLogger(config); - } - } - return mLogger; - } - @Override public void onSelectionEvent(SelectionEvent event) { Preconditions.checkNotNull(event); synchronized (mLoggerLock) { - if (mLogger2 == null) { - mLogger2 = new DefaultLogger( - new Logger.Config(mContext, WIDGET_TYPE_UNKNOWN, null)); + if (mSessionLogger == null) { + mSessionLogger = new SelectionSessionLogger(); } - mLogger2.writeEvent(event); + mSessionLogger.writeEvent(event); } } @@ -331,7 +313,7 @@ public final class TextClassifierImpl implements TextClassifier { private String createId(String text, int start, int end) { synchronized (mLock) { - return DefaultLogger.createId(text, start, end, mContext, mModel.getVersion(), + return SelectionSessionLogger.createId(text, start, end, mContext, mModel.getVersion(), mModel.getSupportedLocales()); } } diff --git a/android/view/textclassifier/TextLinks.java b/android/view/textclassifier/TextLinks.java index 17c7b13c..851b2c9b 100644 --- a/android/view/textclassifier/TextLinks.java +++ b/android/view/textclassifier/TextLinks.java @@ -28,6 +28,8 @@ import android.text.Spannable; import android.text.method.MovementMethod; import android.text.style.ClickableSpan; import android.text.style.URLSpan; +import android.text.util.Linkify; +import android.text.util.Linkify.LinkifyMask; import android.view.View; import android.view.textclassifier.TextClassifier.EntityType; import android.widget.TextView; @@ -337,7 +339,7 @@ public final class TextLinks implements Parcelable { /** * @return The config representing the set of entities to look for - * @see #setEntityConfig(TextClassifier.EntityConfig) + * @see Builder#setEntityConfig(TextClassifier.EntityConfig) */ @Nullable public TextClassifier.EntityConfig getEntityConfig() { @@ -607,4 +609,124 @@ public final class TextLinks implements Parcelable { return new TextLinks(mFullText, mLinks); } } + + // TODO: Remove once apps can build against the latest sdk. + /** + * Optional input parameters for generating TextLinks. + * @hide + */ + public static final class Options { + + @Nullable private final TextClassificationSessionId mSessionId; + @Nullable private final Request mRequest; + @Nullable private LocaleList mDefaultLocales; + @Nullable private TextClassifier.EntityConfig mEntityConfig; + private boolean mLegacyFallback; + + private @ApplyStrategy int mApplyStrategy; + private Function<TextLink, TextLinkSpan> mSpanFactory; + + private String mCallingPackageName; + + public Options() { + this(null, null); + } + + private Options( + @Nullable TextClassificationSessionId sessionId, @Nullable Request request) { + mSessionId = sessionId; + mRequest = request; + } + + /** Helper to create Options from a Request. */ + public static Options from(TextClassificationSessionId sessionId, Request request) { + final Options options = new Options(sessionId, request); + options.setDefaultLocales(request.getDefaultLocales()); + options.setEntityConfig(request.getEntityConfig()); + return options; + } + + /** Returns a new options object based on the specified link mask. */ + public static Options fromLinkMask(@LinkifyMask int mask) { + final List<String> entitiesToFind = new ArrayList<>(); + + if ((mask & Linkify.WEB_URLS) != 0) { + entitiesToFind.add(TextClassifier.TYPE_URL); + } + if ((mask & Linkify.EMAIL_ADDRESSES) != 0) { + entitiesToFind.add(TextClassifier.TYPE_EMAIL); + } + if ((mask & Linkify.PHONE_NUMBERS) != 0) { + entitiesToFind.add(TextClassifier.TYPE_PHONE); + } + if ((mask & Linkify.MAP_ADDRESSES) != 0) { + entitiesToFind.add(TextClassifier.TYPE_ADDRESS); + } + + return new Options().setEntityConfig( + TextClassifier.EntityConfig.createWithEntityList(entitiesToFind)); + } + + /** @param defaultLocales ordered list of locale preferences. */ + public Options setDefaultLocales(@Nullable LocaleList defaultLocales) { + mDefaultLocales = defaultLocales; + return this; + } + + /** @param entityConfig definition of which entity types to look for. */ + public Options setEntityConfig(@Nullable TextClassifier.EntityConfig entityConfig) { + mEntityConfig = entityConfig; + return this; + } + + /** @param applyStrategy strategy to use when resolving conflicts. */ + public Options setApplyStrategy(@ApplyStrategy int applyStrategy) { + checkValidApplyStrategy(applyStrategy); + mApplyStrategy = applyStrategy; + return this; + } + + /** @param spanFactory factory for converting TextLink to TextLinkSpan. */ + public Options setSpanFactory(@Nullable Function<TextLink, TextLinkSpan> spanFactory) { + mSpanFactory = spanFactory; + return this; + } + + @Nullable + public LocaleList getDefaultLocales() { + return mDefaultLocales; + } + + @Nullable + public TextClassifier.EntityConfig getEntityConfig() { + return mEntityConfig; + } + + @ApplyStrategy + public int getApplyStrategy() { + return mApplyStrategy; + } + + @Nullable + public Function<TextLink, TextLinkSpan> getSpanFactory() { + return mSpanFactory; + } + + @Nullable + public Request getRequest() { + return mRequest; + } + + @Nullable + public TextClassificationSessionId getSessionId() { + return mSessionId; + } + + private static void checkValidApplyStrategy(int applyStrategy) { + if (applyStrategy != APPLY_STRATEGY_IGNORE && applyStrategy != APPLY_STRATEGY_REPLACE) { + throw new IllegalArgumentException( + "Invalid apply strategy. See TextLinks.ApplyStrategy for options."); + } + } + } } diff --git a/android/view/textclassifier/TextSelection.java b/android/view/textclassifier/TextSelection.java index 939e7176..17687c9e 100644 --- a/android/view/textclassifier/TextSelection.java +++ b/android/view/textclassifier/TextSelection.java @@ -375,4 +375,56 @@ public final class TextSelection implements Parcelable { mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in); mId = in.readString(); } + + + // TODO: Remove once apps can build against the latest sdk. + /** + * Optional input parameters for generating TextSelection. + * @hide + */ + public static final class Options { + + @Nullable private final TextClassificationSessionId mSessionId; + @Nullable private final Request mRequest; + @Nullable private LocaleList mDefaultLocales; + private boolean mDarkLaunchAllowed; + + public Options() { + this(null, null); + } + + private Options( + @Nullable TextClassificationSessionId sessionId, @Nullable Request request) { + mSessionId = sessionId; + mRequest = request; + } + + /** Helper to create Options from a Request. */ + public static Options from(TextClassificationSessionId sessionId, Request request) { + final Options options = new Options(sessionId, request); + options.setDefaultLocales(request.getDefaultLocales()); + return options; + } + + /** @param defaultLocales ordered list of locale preferences. */ + public Options setDefaultLocales(@Nullable LocaleList defaultLocales) { + mDefaultLocales = defaultLocales; + return this; + } + + @Nullable + public LocaleList getDefaultLocales() { + return mDefaultLocales; + } + + @Nullable + public Request getRequest() { + return mRequest; + } + + @Nullable + public TextClassificationSessionId getSessionId() { + return mSessionId; + } + } } diff --git a/android/view/textservice/TextServicesManager.java b/android/view/textservice/TextServicesManager.java index 8e1f2183..21ec42b1 100644 --- a/android/view/textservice/TextServicesManager.java +++ b/android/view/textservice/TextServicesManager.java @@ -1,58 +1,219 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2011 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 + * 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 + * 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. + * 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 android.view.textservice; +import android.annotation.SystemService; +import android.content.Context; import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.ServiceManager.ServiceNotFoundException; +import android.util.Log; import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; +import com.android.internal.textservice.ITextServicesManager; + import java.util.Locale; /** - * A stub class of TextServicesManager for Layout-Lib. + * System API to the overall text services, which arbitrates interaction between applications + * and text services. + * + * The user can change the current text services in Settings. And also applications can specify + * the target text services. + * + * <h3>Architecture Overview</h3> + * + * <p>There are three primary parties involved in the text services + * framework (TSF) architecture:</p> + * + * <ul> + * <li> The <strong>text services manager</strong> as expressed by this class + * is the central point of the system that manages interaction between all + * other parts. It is expressed as the client-side API here which exists + * in each application context and communicates with a global system service + * that manages the interaction across all processes. + * <li> A <strong>text service</strong> implements a particular + * interaction model allowing the client application to retrieve information of text. + * The system binds to the current text service that is in use, causing it to be created and run. + * <li> Multiple <strong>client applications</strong> arbitrate with the text service + * manager for connections to text services. + * </ul> + * + * <h3>Text services sessions</h3> + * <ul> + * <li>The <strong>spell checker session</strong> is one of the text services. + * {@link android.view.textservice.SpellCheckerSession}</li> + * </ul> + * */ +@SystemService(Context.TEXT_SERVICES_MANAGER_SERVICE) public final class TextServicesManager { - private static final TextServicesManager sInstance = new TextServicesManager(); - private static final SpellCheckerInfo[] EMPTY_SPELL_CHECKER_INFO = new SpellCheckerInfo[0]; + private static final String TAG = TextServicesManager.class.getSimpleName(); + private static final boolean DBG = false; + + /** + * A compile time switch to control per-profile spell checker, which is not yet ready. + * @hide + */ + public static final boolean DISABLE_PER_PROFILE_SPELL_CHECKER = true; + + private static TextServicesManager sInstance; + + private final ITextServicesManager mService; + + private TextServicesManager() throws ServiceNotFoundException { + mService = ITextServicesManager.Stub.asInterface( + ServiceManager.getServiceOrThrow(Context.TEXT_SERVICES_MANAGER_SERVICE)); + } /** * Retrieve the global TextServicesManager instance, creating it if it doesn't already exist. * @hide */ public static TextServicesManager getInstance() { - return sInstance; + synchronized (TextServicesManager.class) { + if (sInstance == null) { + try { + sInstance = new TextServicesManager(); + } catch (ServiceNotFoundException e) { + throw new IllegalStateException(e); + } + } + return sInstance; + } } + /** + * Returns the language component of a given locale string. + */ + private static String parseLanguageFromLocaleString(String locale) { + final int idx = locale.indexOf('_'); + if (idx < 0) { + return locale; + } else { + return locale.substring(0, idx); + } + } + + /** + * Get a spell checker session for the specified spell checker + * @param locale the locale for the spell checker. If {@code locale} is null and + * referToSpellCheckerLanguageSettings is true, the locale specified in Settings will be + * returned. If {@code locale} is not null and referToSpellCheckerLanguageSettings is true, + * the locale specified in Settings will be returned only when it is same as {@code locale}. + * Exceptionally, when referToSpellCheckerLanguageSettings is true and {@code locale} is + * only language (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be + * selected. + * @param listener a spell checker session lister for getting results from a spell checker. + * @param referToSpellCheckerLanguageSettings if true, the session for one of enabled + * languages in settings will be returned. + * @return the spell checker session of the spell checker + */ public SpellCheckerSession newSpellCheckerSession(Bundle bundle, Locale locale, SpellCheckerSessionListener listener, boolean referToSpellCheckerLanguageSettings) { - return null; + if (listener == null) { + throw new NullPointerException(); + } + if (!referToSpellCheckerLanguageSettings && locale == null) { + throw new IllegalArgumentException("Locale should not be null if you don't refer" + + " settings."); + } + + if (referToSpellCheckerLanguageSettings && !isSpellCheckerEnabled()) { + return null; + } + + final SpellCheckerInfo sci; + try { + sci = mService.getCurrentSpellChecker(null); + } catch (RemoteException e) { + return null; + } + if (sci == null) { + return null; + } + SpellCheckerSubtype subtypeInUse = null; + if (referToSpellCheckerLanguageSettings) { + subtypeInUse = getCurrentSpellCheckerSubtype(true); + if (subtypeInUse == null) { + return null; + } + if (locale != null) { + final String subtypeLocale = subtypeInUse.getLocale(); + final String subtypeLanguage = parseLanguageFromLocaleString(subtypeLocale); + if (subtypeLanguage.length() < 2 || !locale.getLanguage().equals(subtypeLanguage)) { + return null; + } + } + } else { + final String localeStr = locale.toString(); + for (int i = 0; i < sci.getSubtypeCount(); ++i) { + final SpellCheckerSubtype subtype = sci.getSubtypeAt(i); + final String tempSubtypeLocale = subtype.getLocale(); + final String tempSubtypeLanguage = parseLanguageFromLocaleString(tempSubtypeLocale); + if (tempSubtypeLocale.equals(localeStr)) { + subtypeInUse = subtype; + break; + } else if (tempSubtypeLanguage.length() >= 2 && + locale.getLanguage().equals(tempSubtypeLanguage)) { + subtypeInUse = subtype; + } + } + } + if (subtypeInUse == null) { + return null; + } + final SpellCheckerSession session = new SpellCheckerSession(sci, mService, listener); + try { + mService.getSpellCheckerService(sci.getId(), subtypeInUse.getLocale(), + session.getTextServicesSessionListener(), + session.getSpellCheckerSessionListener(), bundle); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + return session; } /** * @hide */ public SpellCheckerInfo[] getEnabledSpellCheckers() { - return EMPTY_SPELL_CHECKER_INFO; + try { + final SpellCheckerInfo[] retval = mService.getEnabledSpellCheckers(); + if (DBG) { + Log.d(TAG, "getEnabledSpellCheckers: " + (retval != null ? retval.length : "null")); + } + return retval; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** * @hide */ public SpellCheckerInfo getCurrentSpellChecker() { - return null; + try { + // Passing null as a locale for ICS + return mService.getCurrentSpellChecker(null); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** @@ -60,13 +221,22 @@ public final class TextServicesManager { */ public SpellCheckerSubtype getCurrentSpellCheckerSubtype( boolean allowImplicitlySelectedSubtype) { - return null; + try { + // Passing null as a locale until we support multiple enabled spell checker subtypes. + return mService.getCurrentSpellCheckerSubtype(null, allowImplicitlySelectedSubtype); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** * @hide */ public boolean isSpellCheckerEnabled() { - return false; + try { + return mService.isSpellCheckerEnabled(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } diff --git a/android/webkit/FindAddress.java b/android/webkit/FindAddress.java index 31b24273..9183227b 100644 --- a/android/webkit/FindAddress.java +++ b/android/webkit/FindAddress.java @@ -429,20 +429,21 @@ class FindAddress { // At this point we've matched a state; try to match a zip code after it. Matcher zipMatcher = sWordRe.matcher(content); - if (zipMatcher.find(stateMatch.end()) - && isValidZipCode(zipMatcher.group(0), stateMatch)) { - return zipMatcher.end(); + if (zipMatcher.find(stateMatch.end())) { + if (isValidZipCode(zipMatcher.group(0), stateMatch)) { + return zipMatcher.end(); + } + } else { + // The content ends with a state but no zip + // code. This is a legal match according to the + // documentation. N.B. This is equivalent to the + // original c++ implementation, which only allowed + // the zip code to be optional at the end of the + // string, which presumably is a bug. We tried + // relaxing this to work in other places but it + // caused too many false positives. + nonZipMatch = stateMatch.end(); } - // The content ends with a state but no zip - // code. This is a legal match according to the - // documentation. N.B. This differs from the - // original c++ implementation, which only allowed - // the zip code to be optional at the end of the - // string, which presumably is a bug. Now we - // prefer to find a match with a zip code, but - // remember non-zip matches and return them if - // necessary. - nonZipMatch = stateMatch.end(); } } } diff --git a/android/webkit/TracingConfig.java b/android/webkit/TracingConfig.java index d95ca61d..20801684 100644 --- a/android/webkit/TracingConfig.java +++ b/android/webkit/TracingConfig.java @@ -54,37 +54,37 @@ public class TracingConfig { /** * Predefined set of categories typically useful for analyzing WebViews. - * Typically includes android_webview and Java. + * Typically includes "android_webview" and "Java" categories. */ public static final int CATEGORIES_ANDROID_WEBVIEW = 1 << 1; /** * Predefined set of categories typically useful for web developers. - * Typically includes blink, compositor, renderer.scheduler and v8 categories. + * Typically includes "blink", "compositor", "renderer.scheduler" and "v8" categories. */ public static final int CATEGORIES_WEB_DEVELOPER = 1 << 2; /** * Predefined set of categories for analyzing input latency issues. - * Typically includes input, renderer.scheduler categories. + * Typically includes "input", "renderer.scheduler" categories. */ public static final int CATEGORIES_INPUT_LATENCY = 1 << 3; /** * Predefined set of categories for analyzing rendering issues. - * Typically includes blink, compositor and gpu categories. + * Typically includes "blink", "compositor" and "gpu" categories. */ public static final int CATEGORIES_RENDERING = 1 << 4; /** * Predefined set of categories for analyzing javascript and rendering issues. - * Typically includes blink, compositor, gpu, renderer.scheduler and v8 categories. + * Typically includes "blink", "compositor", "gpu", "renderer.scheduler" and "v8" categories. */ public static final int CATEGORIES_JAVASCRIPT_AND_RENDERING = 1 << 5; /** * Predefined set of categories for studying difficult rendering performance problems. - * Typically includes blink, compositor, gpu, renderer.scheduler, v8 and + * Typically includes "blink", "compositor", "gpu", "renderer.scheduler", "v8" and * some other compositor categories which are disabled by default. */ public static final int CATEGORIES_FRAME_VIEWER = 1 << 6; @@ -123,7 +123,9 @@ public class TracingConfig { } /** - * Returns a bitmask of the predefined categories values of this configuration. + * Returns a bitmask of the predefined category sets of this configuration. + * + * @return Bitmask of predefined category sets. */ @PredefinedCategories public int getPredefinedCategories() { @@ -133,7 +135,7 @@ public class TracingConfig { /** * Returns the list of included custom category patterns for this configuration. * - * @return empty list if no custom category patterns are specified. + * @return Empty list if no custom category patterns are specified. */ @NonNull public List<String> getCustomIncludedCategories() { @@ -142,6 +144,8 @@ public class TracingConfig { /** * Returns the tracing mode of this configuration. + * + * @return The tracing mode of this configuration. */ @TracingMode public int getTracingMode() { @@ -150,28 +154,37 @@ public class TracingConfig { /** * Builder used to create {@link TracingConfig} objects. - * + * <p> * Examples: - * new TracingConfig.Builder().build() - * -- creates a configuration with default options: {@link #CATEGORIES_NONE}, - * {@link #RECORD_UNTIL_FULL}. - * new TracingConfig.Builder().addCategories(CATEGORIES_WEB_DEVELOPER).build() - * -- records trace events from the "web developer" predefined category sets. - * new TracingConfig.Builder().addCategories(CATEGORIES_RENDERING, - * CATEGORIES_INPUT_LATENCY).build() - * -- records trace events from the "rendering" and "input latency" predefined - * category sets. - * new TracingConfig.Builder().addCategories("browser").build() - * -- records only the trace events from the "browser" category. - * new TracingConfig.Builder().addCategories("blink*","renderer*").build() - * -- records only the trace events matching the "blink*" and "renderer*" patterns - * (e.g. "blink.animations", "renderer_host" and "renderer.scheduler" categories). - * new TracingConfig.Builder().addCategories(CATEGORIES_WEB_DEVELOPER) + * <pre class="prettyprint"> + * // Create a configuration with default options: {@link #CATEGORIES_NONE}, + * // {@link #RECORD_CONTINUOUSLY}. + * <code>new TracingConfig.Builder().build()</code> + * + * // Record trace events from the "web developer" predefined category sets. + * // Uses a ring buffer (the default {@link #RECORD_CONTINUOUSLY} mode) for + * // internal storage during tracing. + * <code>new TracingConfig.Builder().addCategories(CATEGORIES_WEB_DEVELOPER).build()</code> + * + * // Record trace events from the "rendering" and "input latency" predefined + * // category sets. + * <code>new TracingConfig.Builder().addCategories(CATEGORIES_RENDERING, + * CATEGORIES_INPUT_LATENCY).build()</code> + * + * // Record only the trace events from the "browser" category. + * <code>new TracingConfig.Builder().addCategories("browser").build()</code> + * + * // Record only the trace events matching the "blink*" and "renderer*" patterns + * // (e.g. "blink.animations", "renderer_host" and "renderer.scheduler" categories). + * <code>new TracingConfig.Builder().addCategories("blink*","renderer*").build()</code> + * + * // Record events from the "web developer" predefined category set and events from + * // the "disabled-by-default-v8.gc" category to understand where garbage collection + * // is being triggered. Uses a limited size buffer for internal storage during tracing. + * <code>new TracingConfig.Builder().addCategories(CATEGORIES_WEB_DEVELOPER) * .addCategories("disabled-by-default-v8.gc") - * .setTracingMode(RECORD_CONTINUOUSLY).build() - * -- records events from the "web developer" predefined category set and events from - * the "disabled-by-default-v8.gc" category to understand where garbage collection - * is being triggered. Uses a ring buffer for internal storage during tracing. + * .setTracingMode(RECORD_UNTIL_FULL).build()</code> + * </pre> */ public static class Builder { private @PredefinedCategories int mPredefinedCategories = CATEGORIES_NONE; @@ -185,6 +198,8 @@ public class TracingConfig { /** * Build {@link TracingConfig} using the current settings. + * + * @return The {@link TracingConfig} with the current settings. */ public TracingConfig build() { return new TracingConfig(mPredefinedCategories, mCustomIncludedCategories, @@ -192,16 +207,15 @@ public class TracingConfig { } /** - * Adds categories from a predefined set of categories to be included in the trace output. + * Adds predefined sets of categories to be included in the trace output. + * + * A predefined category set can be one of {@link #CATEGORIES_NONE}, + * {@link #CATEGORIES_ALL}, {@link #CATEGORIES_ANDROID_WEBVIEW}, + * {@link #CATEGORIES_WEB_DEVELOPER}, {@link #CATEGORIES_INPUT_LATENCY}, + * {@link #CATEGORIES_RENDERING}, {@link #CATEGORIES_JAVASCRIPT_AND_RENDERING} or + * {@link #CATEGORIES_FRAME_VIEWER}. * - * @param predefinedCategories list or bitmask of predefined category sets to use: - * {@link #CATEGORIES_NONE}, {@link #CATEGORIES_ALL}, - * {@link #CATEGORIES_ANDROID_WEBVIEW}, - * {@link #CATEGORIES_WEB_DEVELOPER}, - * {@link #CATEGORIES_INPUT_LATENCY}, - * {@link #CATEGORIES_RENDERING}, - * {@link #CATEGORIES_JAVASCRIPT_AND_RENDERING} or - * {@link #CATEGORIES_FRAME_VIEWER}. + * @param predefinedCategories A list or bitmask of predefined category sets. * @return The builder to facilitate chaining. */ public Builder addCategories(@PredefinedCategories int... predefinedCategories) { @@ -215,11 +229,11 @@ public class TracingConfig { * Adds custom categories to be included in trace output. * * Note that the categories are defined by the currently-in-use version of WebView. They - * live in chromium code and are not part of the Android API. See + * live in chromium code and are not part of the Android API. * See <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool"> * chromium documentation on tracing</a> for more details. * - * @param categories a list of category patterns. A category pattern can contain wilcards, + * @param categories A list of category patterns. A category pattern can contain wildcards, * e.g. "blink*" or full category name e.g. "renderer.scheduler". * @return The builder to facilitate chaining. */ @@ -235,7 +249,7 @@ public class TracingConfig { * * Same as {@link #addCategories(String...)} but allows to pass a Collection as a parameter. * - * @param categories a list of category patters. + * @param categories A list of category patterns. * @return The builder to facilitate chaining. */ public Builder addCategories(Collection<String> categories) { @@ -245,8 +259,9 @@ public class TracingConfig { /** * Sets the tracing mode for this configuration. + * When tracingMode is not set explicitly, the default is {@link #RECORD_CONTINUOUSLY}. * - * @param tracingMode tracing mode to use, one of {@link #RECORD_UNTIL_FULL} or + * @param tracingMode The tracing mode to use, one of {@link #RECORD_UNTIL_FULL} or * {@link #RECORD_CONTINUOUSLY}. * @return The builder to facilitate chaining. */ diff --git a/android/webkit/TracingController.java b/android/webkit/TracingController.java index 50068f5a..05c0304e 100644 --- a/android/webkit/TracingController.java +++ b/android/webkit/TracingController.java @@ -35,9 +35,9 @@ import java.util.concurrent.Executor; * Example usage: * <pre class="prettyprint"> * TracingController tracingController = TracingController.getInstance(); - * tracingController.start(new TraceConfig.Builder() + * tracingController.start(new TracingConfig.Builder() * .addCategories(CATEGORIES_WEB_DEVELOPER).build()); - * [..] + * ... * tracingController.stop(new FileOutputStream("trace.json"), * Executors.newSingleThreadExecutor()); * </pre></p> @@ -49,7 +49,7 @@ public abstract class TracingController { * only one TracingController instance for all WebView instances, * however this restriction may be relaxed in a future Android release. * - * @return the default TracingController instance + * @return The default TracingController instance. */ @NonNull public static TracingController getInstance() { @@ -65,8 +65,10 @@ public abstract class TracingController { * using an internal buffer and flushed to the outputStream when * {@link #stop(OutputStream, Executor)} is called. * - * @param tracingConfig configuration options to use for tracing - * @throws IllegalStateException if the system is already tracing. + * @param tracingConfig Configuration options to use for tracing. + * @throws IllegalStateException If the system is already tracing. + * @throws IllegalArgumentException If the configuration is invalid (e.g. + * invalid category pattern or invalid tracing mode). */ public abstract void start(@NonNull TracingConfig tracingConfig); @@ -77,17 +79,22 @@ public abstract class TracingController { * in chunks by invoking {@link java.io.OutputStream#write(byte[])}. On completion * the {@link java.io.OutputStream#close()} method is called. * - * @param outputStream the output steam the tracing data will be sent to. If null + * @param outputStream The output stream the tracing data will be sent to. If null * the tracing data will be discarded. - * @param executor the {@link java.util.concurrent.Executor} on which the - * outputStream #write and #close methods will be invoked. - * @return false if the system was not tracing at the time of the call, true - * otherwise. + * @param executor The {@link java.util.concurrent.Executor} on which the + * outputStream {@link java.io.OutputStream#write(byte[])} and + * {@link java.io.OutputStream#close()} methods will be invoked. + * @return False if the WebView framework was not tracing at the time of the call, + * true otherwise. */ public abstract boolean stop(@Nullable OutputStream outputStream, @NonNull @CallbackExecutor Executor executor); - /** True if the system is tracing */ + /** + * Returns whether the WebView framework is tracing. + * + * @return True if tracing is enabled. + */ public abstract boolean isTracing(); } diff --git a/android/webkit/WebView.java b/android/webkit/WebView.java index 202f2046..10748acd 100644 --- a/android/webkit/WebView.java +++ b/android/webkit/WebView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2006 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. @@ -16,223 +16,3136 @@ package android.webkit; -import com.android.layoutlib.bridge.MockView; - +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.annotation.Widget; import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.res.Configuration; import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; import android.graphics.Picture; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.net.http.SslCertificate; +import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.os.Message; +import android.os.RemoteException; +import android.os.StrictMode; +import android.print.PrintDocumentAdapter; +import android.security.KeyChain; import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.DragEvent; +import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; +import android.view.ViewHierarchyEncoder; +import android.view.ViewStructure; +import android.view.ViewTreeObserver; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; +import android.view.autofill.AutofillValue; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.textclassifier.TextClassifier; +import android.widget.AbsoluteLayout; + +import java.io.BufferedWriter; +import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Map; /** - * Mock version of the WebView. - * Only non override public methods from the real WebView have been added in there. - * Methods that take an unknown class as parameter or as return object, have been removed for now. - * - * TODO: generate automatically. + * <p>A View that displays web pages. This class is the basis upon which you + * can roll your own web browser or simply display some online content within your Activity. + * It uses the WebKit rendering engine to display + * web pages and includes methods to navigate forward and backward + * through a history, zoom in and out, perform text searches and more. + * + * <p>Note that, in order for your Activity to access the Internet and load web pages + * in a WebView, you must add the {@code INTERNET} permissions to your + * Android Manifest file: + * + * <pre> + * {@code <uses-permission android:name="android.permission.INTERNET" />} + * </pre> + * + * <p>This must be a child of the <a + * href="{@docRoot}guide/topics/manifest/manifest-element.html">{@code <manifest>}</a> + * element. + * + * <p>For more information, read + * <a href="{@docRoot}guide/webapps/webview.html">Building Web Apps in WebView</a>. + * + * <h3>Basic usage</h3> + * + * <p>By default, a WebView provides no browser-like widgets, does not + * enable JavaScript and web page errors are ignored. If your goal is only + * to display some HTML as a part of your UI, this is probably fine; + * the user won't need to interact with the web page beyond reading + * it, and the web page won't need to interact with the user. If you + * actually want a full-blown web browser, then you probably want to + * invoke the Browser application with a URL Intent rather than show it + * with a WebView. For example: + * <pre> + * Uri uri = Uri.parse("https://www.example.com"); + * Intent intent = new Intent(Intent.ACTION_VIEW, uri); + * startActivity(intent); + * </pre> + * <p>See {@link android.content.Intent} for more information. + * + * <p>To provide a WebView in your own Activity, include a {@code <WebView>} in your layout, + * or set the entire Activity window as a WebView during {@link + * android.app.Activity#onCreate(Bundle) onCreate()}: + * + * <pre class="prettyprint"> + * WebView webview = new WebView(this); + * setContentView(webview); + * </pre> + * + * <p>Then load the desired web page: + * + * <pre> + * // Simplest usage: note that an exception will NOT be thrown + * // if there is an error loading this page (see below). + * webview.loadUrl("https://example.com/"); + * + * // OR, you can also load from an HTML string: + * String summary = "<html><body>You scored <b>192</b> points.</body></html>"; + * webview.loadData(summary, "text/html", null); + * // ... although note that there are restrictions on what this HTML can do. + * // See {@link #loadData(String,String,String)} and {@link + * #loadDataWithBaseURL(String,String,String,String,String)} for more info. + * // Also see {@link #loadData(String,String,String)} for information on encoding special + * // characters. + * </pre> + * + * <p>A WebView has several customization points where you can add your + * own behavior. These are: + * + * <ul> + * <li>Creating and setting a {@link android.webkit.WebChromeClient} subclass. + * This class is called when something that might impact a + * browser UI happens, for instance, progress updates and + * JavaScript alerts are sent here (see <a + * href="{@docRoot}guide/developing/debug-tasks.html#DebuggingWebPages">Debugging Tasks</a>). + * </li> + * <li>Creating and setting a {@link android.webkit.WebViewClient} subclass. + * It will be called when things happen that impact the + * rendering of the content, eg, errors or form submissions. You + * can also intercept URL loading here (via {@link + * android.webkit.WebViewClient#shouldOverrideUrlLoading(WebView,String) + * shouldOverrideUrlLoading()}).</li> + * <li>Modifying the {@link android.webkit.WebSettings}, such as + * enabling JavaScript with {@link android.webkit.WebSettings#setJavaScriptEnabled(boolean) + * setJavaScriptEnabled()}. </li> + * <li>Injecting Java objects into the WebView using the + * {@link android.webkit.WebView#addJavascriptInterface} method. This + * method allows you to inject Java objects into a page's JavaScript + * context, so that they can be accessed by JavaScript in the page.</li> + * </ul> + * + * <p>Here's a more complicated example, showing error handling, + * settings, and progress notification: + * + * <pre class="prettyprint"> + * // Let's display the progress in the activity title bar, like the + * // browser app does. + * getWindow().requestFeature(Window.FEATURE_PROGRESS); + * + * webview.getSettings().setJavaScriptEnabled(true); + * + * final Activity activity = this; + * webview.setWebChromeClient(new WebChromeClient() { + * public void onProgressChanged(WebView view, int progress) { + * // Activities and WebViews measure progress with different scales. + * // The progress meter will automatically disappear when we reach 100% + * activity.setProgress(progress * 1000); + * } + * }); + * webview.setWebViewClient(new WebViewClient() { + * public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + * Toast.makeText(activity, "Oh no! " + description, Toast.LENGTH_SHORT).show(); + * } + * }); + * + * webview.loadUrl("https://developer.android.com/"); + * </pre> + * + * <h3>Zoom</h3> + * + * <p>To enable the built-in zoom, set + * {@link #getSettings() WebSettings}.{@link WebSettings#setBuiltInZoomControls(boolean)} + * (introduced in API level {@link android.os.Build.VERSION_CODES#CUPCAKE}). + * + * <p class="note"><b>Note:</b> Using zoom if either the height or width is set to + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} may lead to undefined behavior + * and should be avoided. + * + * <h3>Cookie and window management</h3> + * + * <p>For obvious security reasons, your application has its own + * cache, cookie store etc.—it does not share the Browser + * application's data. + * + * <p>By default, requests by the HTML to open new windows are + * ignored. This is {@code true} whether they be opened by JavaScript or by + * the target attribute on a link. You can customize your + * {@link WebChromeClient} to provide your own behavior for opening multiple windows, + * and render them in whatever manner you want. + * + * <p>The standard behavior for an Activity is to be destroyed and + * recreated when the device orientation or any other configuration changes. This will cause + * the WebView to reload the current page. If you don't want that, you + * can set your Activity to handle the {@code orientation} and {@code keyboardHidden} + * changes, and then just leave the WebView alone. It'll automatically + * re-orient itself as appropriate. Read <a + * href="{@docRoot}guide/topics/resources/runtime-changes.html">Handling Runtime Changes</a> for + * more information about how to handle configuration changes during runtime. + * + * + * <h3>Building web pages to support different screen densities</h3> + * + * <p>The screen density of a device is based on the screen resolution. A screen with low density + * has fewer available pixels per inch, where a screen with high density + * has more — sometimes significantly more — pixels per inch. The density of a + * screen is important because, other things being equal, a UI element (such as a button) whose + * height and width are defined in terms of screen pixels will appear larger on the lower density + * screen and smaller on the higher density screen. + * For simplicity, Android collapses all actual screen densities into three generalized densities: + * high, medium, and low. + * <p>By default, WebView scales a web page so that it is drawn at a size that matches the default + * appearance on a medium density screen. So, it applies 1.5x scaling on a high density screen + * (because its pixels are smaller) and 0.75x scaling on a low density screen (because its pixels + * are bigger). + * Starting with API level {@link android.os.Build.VERSION_CODES#ECLAIR}, WebView supports DOM, CSS, + * and meta tag features to help you (as a web developer) target screens with different screen + * densities. + * <p>Here's a summary of the features you can use to handle different screen densities: + * <ul> + * <li>The {@code window.devicePixelRatio} DOM property. The value of this property specifies the + * default scaling factor used for the current device. For example, if the value of {@code + * window.devicePixelRatio} is "1.0", then the device is considered a medium density (mdpi) device + * and default scaling is not applied to the web page; if the value is "1.5", then the device is + * considered a high density device (hdpi) and the page content is scaled 1.5x; if the + * value is "0.75", then the device is considered a low density device (ldpi) and the content is + * scaled 0.75x.</li> + * <li>The {@code -webkit-device-pixel-ratio} CSS media query. Use this to specify the screen + * densities for which this style sheet is to be used. The corresponding value should be either + * "0.75", "1", or "1.5", to indicate that the styles are for devices with low density, medium + * density, or high density screens, respectively. For example: + * <pre> + * <link rel="stylesheet" media="screen and (-webkit-device-pixel-ratio:1.5)" href="hdpi.css" /></pre> + * <p>The {@code hdpi.css} stylesheet is only used for devices with a screen pixel ratio of 1.5, + * which is the high density pixel ratio. + * </li> + * </ul> + * + * <h3>HTML5 Video support</h3> + * + * <p>In order to support inline HTML5 video in your application you need to have hardware + * acceleration turned on. + * + * <h3>Full screen support</h3> + * + * <p>In order to support full screen — for video or other HTML content — you need to set a + * {@link android.webkit.WebChromeClient} and implement both + * {@link WebChromeClient#onShowCustomView(View, WebChromeClient.CustomViewCallback)} + * and {@link WebChromeClient#onHideCustomView()}. If the implementation of either of these two methods is + * missing then the web contents will not be allowed to enter full screen. Optionally you can implement + * {@link WebChromeClient#getVideoLoadingProgressView()} to customize the View displayed whilst a video + * is loading. + * + * <h3>HTML5 Geolocation API support</h3> + * + * <p>For applications targeting Android N and later releases + * (API level > {@link android.os.Build.VERSION_CODES#M}) the geolocation api is only supported on + * secure origins such as https. For such applications requests to geolocation api on non-secure + * origins are automatically denied without invoking the corresponding + * {@link WebChromeClient#onGeolocationPermissionsShowPrompt(String, GeolocationPermissions.Callback)} + * method. + * + * <h3>Layout size</h3> + * <p> + * It is recommended to set the WebView layout height to a fixed value or to + * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} instead of using + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}. + * When using {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} + * for the height none of the WebView's parents should use a + * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} layout height since that could result in + * incorrect sizing of the views. + * + * <p>Setting the WebView's height to {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} + * enables the following behaviors: + * <ul> + * <li>The HTML body layout height is set to a fixed value. This means that elements with a height + * relative to the HTML body may not be sized correctly. </li> + * <li>For applications targeting {@link android.os.Build.VERSION_CODES#KITKAT} and earlier SDKs the + * HTML viewport meta tag will be ignored in order to preserve backwards compatibility. </li> + * </ul> + * + * <p> + * Using a layout width of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} is not + * supported. If such a width is used the WebView will attempt to use the width of the parent + * instead. + * + * <h3>Metrics</h3> + * + * <p> + * WebView may upload anonymous diagnostic data to Google when the user has consented. This data + * helps Google improve WebView. Data is collected on a per-app basis for each app which has + * instantiated a WebView. An individual app can opt out of this feature by putting the following + * tag in its manifest's {@code <application>} element: + * <pre> + * <manifest> + * <application> + * ... + * <meta-data android:name="android.webkit.WebView.MetricsOptOut" + * android:value="true" /> + * </application> + * </manifest> + * </pre> + * <p> + * Data will only be uploaded for a given app if the user has consented AND the app has not opted + * out. + * + * <h3>Safe Browsing</h3> + * + * <p> + * With Safe Browsing, WebView will block malicious URLs and present a warning UI to the user to + * allow them to navigate back safely or proceed to the malicious page. + * <p> + * Safe Browsing is enabled by default on devices which support it. If your app needs to disable + * Safe Browsing for all WebViews, it can do so in the manifest's {@code <application>} element: + * <p> + * <pre> + * <manifest> + * <application> + * ... + * <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" + * android:value="false" /> + * </application> + * </manifest> + * </pre> + * + * <p> + * Otherwise, see {@link WebSettings#setSafeBrowsingEnabled}. * */ -public class WebView extends MockView { +// Implementation notes. +// The WebView is a thin API class that delegates its public API to a backend WebViewProvider +// class instance. WebView extends {@link AbsoluteLayout} for backward compatibility reasons. +// Methods are delegated to the provider implementation: all public API methods introduced in this +// file are fully delegated, whereas public and protected methods from the View base classes are +// only delegated where a specific need exists for them to do so. +@Widget +public class WebView extends AbsoluteLayout + implements ViewTreeObserver.OnGlobalFocusChangeListener, + ViewGroup.OnHierarchyChangeListener, ViewDebug.HierarchyHandler { + + private static final String LOGTAG = "WebView"; + + // Throwing an exception for incorrect thread usage if the + // build target is JB MR2 or newer. Defaults to false, and is + // set in the WebView constructor. + private static volatile boolean sEnforceThreadChecking = false; + + /** + * Transportation object for returning WebView across thread boundaries. + */ + public class WebViewTransport { + private WebView mWebview; + + /** + * Sets the WebView to the transportation object. + * + * @param webview the WebView to transport + */ + public synchronized void setWebView(WebView webview) { + mWebview = webview; + } + + /** + * Gets the WebView object. + * + * @return the transported WebView object + */ + public synchronized WebView getWebView() { + return mWebview; + } + } + + /** + * URI scheme for telephone number. + */ + public static final String SCHEME_TEL = "tel:"; + /** + * URI scheme for email address. + */ + public static final String SCHEME_MAILTO = "mailto:"; + /** + * URI scheme for map address. + */ + public static final String SCHEME_GEO = "geo:0,0?q="; /** - * Construct a new WebView with a Context object. - * @param context A Context object used to access application assets. + * Interface to listen for find results. + */ + public interface FindListener { + /** + * Notifies the listener about progress made by a find operation. + * + * @param activeMatchOrdinal the zero-based ordinal of the currently selected match + * @param numberOfMatches how many matches have been found + * @param isDoneCounting whether the find operation has actually completed. The listener + * may be notified multiple times while the + * operation is underway, and the numberOfMatches + * value should not be considered final unless + * isDoneCounting is {@code true}. + */ + public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, + boolean isDoneCounting); + } + + /** + * Callback interface supplied to {@link #postVisualStateCallback} for receiving + * notifications about the visual state. + */ + public static abstract class VisualStateCallback { + /** + * Invoked when the visual state is ready to be drawn in the next {@link #onDraw}. + * + * @param requestId The identifier passed to {@link #postVisualStateCallback} when this + * callback was posted. + */ + public abstract void onComplete(long requestId); + } + + /** + * Interface to listen for new pictures as they change. + * + * @deprecated This interface is now obsolete. + */ + @Deprecated + public interface PictureListener { + /** + * Used to provide notification that the WebView's picture has changed. + * See {@link WebView#capturePicture} for details of the picture. + * + * @param view the WebView that owns the picture + * @param picture the new picture. Applications targeting + * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} or above + * will always receive a {@code null} Picture. + * @deprecated Deprecated due to internal changes. + */ + @Deprecated + void onNewPicture(WebView view, @Nullable Picture picture); + } + + public static class HitTestResult { + /** + * Default HitTestResult, where the target is unknown. + */ + public static final int UNKNOWN_TYPE = 0; + /** + * @deprecated This type is no longer used. + */ + @Deprecated + public static final int ANCHOR_TYPE = 1; + /** + * HitTestResult for hitting a phone number. + */ + public static final int PHONE_TYPE = 2; + /** + * HitTestResult for hitting a map address. + */ + public static final int GEO_TYPE = 3; + /** + * HitTestResult for hitting an email address. + */ + public static final int EMAIL_TYPE = 4; + /** + * HitTestResult for hitting an HTML::img tag. + */ + public static final int IMAGE_TYPE = 5; + /** + * @deprecated This type is no longer used. + */ + @Deprecated + public static final int IMAGE_ANCHOR_TYPE = 6; + /** + * HitTestResult for hitting a HTML::a tag with src=http. + */ + public static final int SRC_ANCHOR_TYPE = 7; + /** + * HitTestResult for hitting a HTML::a tag with src=http + HTML::img. + */ + public static final int SRC_IMAGE_ANCHOR_TYPE = 8; + /** + * HitTestResult for hitting an edit text area. + */ + public static final int EDIT_TEXT_TYPE = 9; + + private int mType; + private String mExtra; + + /** + * @hide Only for use by WebViewProvider implementations + */ + @SystemApi + public HitTestResult() { + mType = UNKNOWN_TYPE; + } + + /** + * @hide Only for use by WebViewProvider implementations + */ + @SystemApi + public void setType(int type) { + mType = type; + } + + /** + * @hide Only for use by WebViewProvider implementations + */ + @SystemApi + public void setExtra(String extra) { + mExtra = extra; + } + + /** + * Gets the type of the hit test result. See the XXX_TYPE constants + * defined in this class. + * + * @return the type of the hit test result + */ + public int getType() { + return mType; + } + + /** + * Gets additional type-dependant information about the result. See + * {@link WebView#getHitTestResult()} for details. May either be {@code null} + * or contain extra information about this result. + * + * @return additional type-dependant information about the result + */ + @Nullable + public String getExtra() { + return mExtra; + } + } + + /** + * Constructs a new WebView with an Activity Context object. + * + * <p class="note"><b>Note:</b> WebView should always be instantiated with an Activity Context. + * If instantiated with an Application Context, WebView will be unable to provide several + * features, such as JavaScript dialogs and autofill. + * + * @param context an Activity Context to access application assets */ public WebView(Context context) { this(context, null); } /** - * Construct a new WebView with layout parameters. - * @param context A Context object used to access application assets. - * @param attrs An AttributeSet passed to our parent. + * Constructs a new WebView with layout parameters. + * + * @param context an Activity Context to access application assets + * @param attrs an AttributeSet passed to our parent */ public WebView(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.webViewStyle); } /** - * Construct a new WebView with layout parameters and a default style. - * @param context A Context object used to access application assets. - * @param attrs An AttributeSet passed to our parent. - * @param defStyle The default style resource ID. + * Constructs a new WebView with layout parameters and a default style. + * + * @param context an Activity Context to access application assets + * @param attrs an AttributeSet passed to our parent + * @param defStyleAttr an attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + */ + public WebView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * Constructs a new WebView with layout parameters and a default style. + * + * @param context an Activity Context to access application assets + * @param attrs an AttributeSet passed to our parent + * @param defStyleAttr an attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + * @param defStyleRes a resource identifier of a style resource that + * supplies default values for the view, used only if + * defStyleAttr is 0 or can not be found in the theme. Can be 0 + * to not look for defaults. */ - public WebView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + this(context, attrs, defStyleAttr, defStyleRes, null, false); } - - // START FAKE PUBLIC METHODS - + + /** + * Constructs a new WebView with layout parameters and a default style. + * + * @param context an Activity Context to access application assets + * @param attrs an AttributeSet passed to our parent + * @param defStyleAttr an attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + * @param privateBrowsing whether this WebView will be initialized in + * private mode + * + * @deprecated Private browsing is no longer supported directly via + * WebView and will be removed in a future release. Prefer using + * {@link WebSettings}, {@link WebViewDatabase}, {@link CookieManager} + * and {@link WebStorage} for fine-grained control of privacy data. + */ + @Deprecated + public WebView(Context context, AttributeSet attrs, int defStyleAttr, + boolean privateBrowsing) { + this(context, attrs, defStyleAttr, 0, null, privateBrowsing); + } + + /** + * Constructs a new WebView with layout parameters, a default style and a set + * of custom JavaScript interfaces to be added to this WebView at initialization + * time. This guarantees that these interfaces will be available when the JS + * context is initialized. + * + * @param context an Activity Context to access application assets + * @param attrs an AttributeSet passed to our parent + * @param defStyleAttr an attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + * @param javaScriptInterfaces a Map of interface names, as keys, and + * object implementing those interfaces, as + * values + * @param privateBrowsing whether this WebView will be initialized in + * private mode + * @hide This is used internally by dumprendertree, as it requires the JavaScript interfaces to + * be added synchronously, before a subsequent loadUrl call takes effect. + */ + protected WebView(Context context, AttributeSet attrs, int defStyleAttr, + Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) { + this(context, attrs, defStyleAttr, 0, javaScriptInterfaces, privateBrowsing); + } + + /** + * @hide + */ + @SuppressWarnings("deprecation") // for super() call into deprecated base class constructor. + protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, + Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) { + super(context, attrs, defStyleAttr, defStyleRes); + + // WebView is important by default, unless app developer overrode attribute. + if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) { + setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES); + } + + if (context == null) { + throw new IllegalArgumentException("Invalid context argument"); + } + if (mWebViewThread == null) { + throw new RuntimeException( + "WebView cannot be initialized on a thread that has no Looper."); + } + sEnforceThreadChecking = context.getApplicationInfo().targetSdkVersion >= + Build.VERSION_CODES.JELLY_BEAN_MR2; + checkThread(); + + ensureProviderCreated(); + mProvider.init(javaScriptInterfaces, privateBrowsing); + // Post condition of creating a webview is the CookieSyncManager.getInstance() is allowed. + CookieSyncManager.setGetInstanceIsAllowed(); + } + + /** + * Specifies whether the horizontal scrollbar has overlay style. + * + * @deprecated This method has no effect. + * @param overlay {@code true} if horizontal scrollbar should have overlay style + */ + @Deprecated public void setHorizontalScrollbarOverlay(boolean overlay) { } + /** + * Specifies whether the vertical scrollbar has overlay style. + * + * @deprecated This method has no effect. + * @param overlay {@code true} if vertical scrollbar should have overlay style + */ + @Deprecated public void setVerticalScrollbarOverlay(boolean overlay) { } + /** + * Gets whether horizontal scrollbar has overlay style. + * + * @deprecated This method is now obsolete. + * @return {@code true} + */ + @Deprecated public boolean overlayHorizontalScrollbar() { - return false; + // The old implementation defaulted to true, so return true for consistency + return true; } + /** + * Gets whether vertical scrollbar has overlay style. + * + * @deprecated This method is now obsolete. + * @return {@code false} + */ + @Deprecated public boolean overlayVerticalScrollbar() { + // The old implementation defaulted to false, so return false for consistency return false; } + /** + * Gets the visible height (in pixels) of the embedded title bar (if any). + * + * @deprecated This method is now obsolete. + * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} + */ + @Deprecated + public int getVisibleTitleHeight() { + checkThread(); + return mProvider.getVisibleTitleHeight(); + } + + /** + * Gets the SSL certificate for the main top-level page or {@code null} if there is + * no certificate (the site is not secure). + * + * @return the SSL certificate for the main top-level page + */ + @Nullable + public SslCertificate getCertificate() { + checkThread(); + return mProvider.getCertificate(); + } + + /** + * Sets the SSL certificate for the main top-level page. + * + * @deprecated Calling this function has no useful effect, and will be + * ignored in future releases. + */ + @Deprecated + public void setCertificate(SslCertificate certificate) { + checkThread(); + mProvider.setCertificate(certificate); + } + + //------------------------------------------------------------------------- + // Methods called by activity + //------------------------------------------------------------------------- + + /** + * Sets a username and password pair for the specified host. This data is + * used by the WebView to autocomplete username and password fields in web + * forms. Note that this is unrelated to the credentials used for HTTP + * authentication. + * + * @param host the host that required the credentials + * @param username the username for the given host + * @param password the password for the given host + * @see WebViewDatabase#clearUsernamePassword + * @see WebViewDatabase#hasUsernamePassword + * @deprecated Saving passwords in WebView will not be supported in future versions. + */ + @Deprecated public void savePassword(String host, String username, String password) { + checkThread(); + mProvider.savePassword(host, username, password); } + /** + * Stores HTTP authentication credentials for a given host and realm to the {@link WebViewDatabase} + * instance. + * + * @param host the host to which the credentials apply + * @param realm the realm to which the credentials apply + * @param username the username + * @param password the password + * @deprecated Use {@link WebViewDatabase#setHttpAuthUsernamePassword} instead + */ + @Deprecated public void setHttpAuthUsernamePassword(String host, String realm, String username, String password) { + checkThread(); + mProvider.setHttpAuthUsernamePassword(host, realm, username, password); } + /** + * Retrieves HTTP authentication credentials for a given host and realm from the {@link + * WebViewDatabase} instance. + * @param host the host to which the credentials apply + * @param realm the realm to which the credentials apply + * @return the credentials as a String array, if found. The first element + * is the username and the second element is the password. {@code null} if + * no credentials are found. + * @deprecated Use {@link WebViewDatabase#getHttpAuthUsernamePassword} instead + */ + @Deprecated + @Nullable public String[] getHttpAuthUsernamePassword(String host, String realm) { - return null; + checkThread(); + return mProvider.getHttpAuthUsernamePassword(host, realm); } + /** + * Destroys the internal state of this WebView. This method should be called + * after this WebView has been removed from the view system. No other + * methods may be called on this WebView after destroy. + */ public void destroy() { + checkThread(); + mProvider.destroy(); } + /** + * Enables platform notifications of data state and proxy changes. + * Notifications are enabled by default. + * + * @deprecated This method is now obsolete. + * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} + */ + @Deprecated public static void enablePlatformNotifications() { + // noop } + /** + * Disables platform notifications of data state and proxy changes. + * Notifications are enabled by default. + * + * @deprecated This method is now obsolete. + * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} + */ + @Deprecated public static void disablePlatformNotifications() { + // noop + } + + /** + * Used only by internal tests to free up memory. + * + * @hide + */ + public static void freeMemoryForTests() { + getFactory().getStatics().freeMemoryForTests(); + } + + /** + * Informs WebView of the network state. This is used to set + * the JavaScript property window.navigator.isOnline and + * generates the online/offline event as specified in HTML5, sec. 5.7.7 + * + * @param networkUp a boolean indicating if network is available + */ + public void setNetworkAvailable(boolean networkUp) { + checkThread(); + mProvider.setNetworkAvailable(networkUp); + } + + /** + * Saves the state of this WebView used in + * {@link android.app.Activity#onSaveInstanceState}. Please note that this + * method no longer stores the display data for this WebView. The previous + * behavior could potentially leak files if {@link #restoreState} was never + * called. + * + * @param outState the Bundle to store this WebView's state + * @return the same copy of the back/forward list used to save the state, {@code null} if the + * method fails. + */ + @Nullable + public WebBackForwardList saveState(Bundle outState) { + checkThread(); + return mProvider.saveState(outState); + } + + /** + * Saves the current display data to the Bundle given. Used in conjunction + * with {@link #saveState}. + * @param b a Bundle to store the display data + * @param dest the file to store the serialized picture data. Will be + * overwritten with this WebView's picture data. + * @return {@code true} if the picture was successfully saved + * @deprecated This method is now obsolete. + * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} + */ + @Deprecated + public boolean savePicture(Bundle b, final File dest) { + checkThread(); + return mProvider.savePicture(b, dest); + } + + /** + * Restores the display data that was saved in {@link #savePicture}. Used in + * conjunction with {@link #restoreState}. Note that this will not work if + * this WebView is hardware accelerated. + * + * @param b a Bundle containing the saved display data + * @param src the file where the picture data was stored + * @return {@code true} if the picture was successfully restored + * @deprecated This method is now obsolete. + * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} + */ + @Deprecated + public boolean restorePicture(Bundle b, File src) { + checkThread(); + return mProvider.restorePicture(b, src); + } + + /** + * Restores the state of this WebView from the given Bundle. This method is + * intended for use in {@link android.app.Activity#onRestoreInstanceState} + * and should be called to restore the state of this WebView. If + * it is called after this WebView has had a chance to build state (load + * pages, create a back/forward list, etc.) there may be undesirable + * side-effects. Please note that this method no longer restores the + * display data for this WebView. + * + * @param inState the incoming Bundle of state + * @return the restored back/forward list or {@code null} if restoreState failed + */ + @Nullable + public WebBackForwardList restoreState(Bundle inState) { + checkThread(); + return mProvider.restoreState(inState); + } + + /** + * Loads the given URL with the specified additional HTTP headers. + * <p> + * Also see compatibility note on {@link #evaluateJavascript}. + * + * @param url the URL of the resource to load + * @param additionalHttpHeaders the additional headers to be used in the + * HTTP request for this URL, specified as a map from name to + * value. Note that if this map contains any of the headers + * that are set by default by this WebView, such as those + * controlling caching, accept types or the User-Agent, their + * values may be overridden by this WebView's defaults. + */ + public void loadUrl(String url, Map<String, String> additionalHttpHeaders) { + checkThread(); + mProvider.loadUrl(url, additionalHttpHeaders); } + /** + * Loads the given URL. + * <p> + * Also see compatibility note on {@link #evaluateJavascript}. + * + * @param url the URL of the resource to load + */ public void loadUrl(String url) { + checkThread(); + mProvider.loadUrl(url); + } + + /** + * Loads the URL with postData using "POST" method into this WebView. If url + * is not a network URL, it will be loaded with {@link #loadUrl(String)} + * instead, ignoring the postData param. + * + * @param url the URL of the resource to load + * @param postData the data will be passed to "POST" request, which must be + * be "application/x-www-form-urlencoded" encoded. + */ + public void postUrl(String url, byte[] postData) { + checkThread(); + if (URLUtil.isNetworkUrl(url)) { + mProvider.postUrl(url, postData); + } else { + mProvider.loadUrl(url); + } } - public void loadData(String data, String mimeType, String encoding) { + /** + * Loads the given data into this WebView using a 'data' scheme URL. + * <p> + * Note that JavaScript's same origin policy means that script running in a + * page loaded using this method will be unable to access content loaded + * using any scheme other than 'data', including 'http(s)'. To avoid this + * restriction, use {@link + * #loadDataWithBaseURL(String,String,String,String,String) + * loadDataWithBaseURL()} with an appropriate base URL. + * <p> + * The {@code encoding} parameter specifies whether the data is base64 or URL + * encoded. If the data is base64 encoded, the value of the encoding + * parameter must be 'base64'. HTML can be encoded with {@link + * android.util.Base64#encodeToString(byte[],int)} like so: + * <pre> + * String unencodedHtml = + * "<html><body>'%28' is the code for '('</body></html>"; + * String encodedHtml = Base64.encodeToString(unencodedHtml.getBytes(), Base64.NO_PADDING); + * webView.loadData(encodedHtml, "text/html", "base64"); + * </pre> + * <p> + * For all other values of {@code encoding} (including {@code null}) it is assumed that the + * data uses ASCII encoding for octets inside the range of safe URL characters and use the + * standard %xx hex encoding of URLs for octets outside that range. See <a + * href="https://tools.ietf.org/html/rfc3986#section-2.2">RFC 3986</a> for more information. + * <p> + * The {@code mimeType} parameter specifies the format of the data. + * If WebView can't handle the specified MIME type, it will download the data. + * If {@code null}, defaults to 'text/html'. + * <p> + * The 'data' scheme URL formed by this method uses the default US-ASCII + * charset. If you need need to set a different charset, you should form a + * 'data' scheme URL which explicitly specifies a charset parameter in the + * mediatype portion of the URL and call {@link #loadUrl(String)} instead. + * Note that the charset obtained from the mediatype portion of a data URL + * always overrides that specified in the HTML or XML document itself. + * + * @param data a String of data in the given encoding + * @param mimeType the MIME type of the data, e.g. 'text/html'. + * @param encoding the encoding of the data + */ + public void loadData(String data, @Nullable String mimeType, @Nullable String encoding) { + checkThread(); + mProvider.loadData(data, mimeType, encoding); } - public void loadDataWithBaseURL(String baseUrl, String data, - String mimeType, String encoding, String failUrl) { + /** + * Loads the given data into this WebView, using baseUrl as the base URL for + * the content. The base URL is used both to resolve relative URLs and when + * applying JavaScript's same origin policy. The historyUrl is used for the + * history entry. + * <p> + * The {@code mimeType} parameter specifies the format of the data. + * If WebView can't handle the specified MIME type, it will download the data. + * If {@code null}, defaults to 'text/html'. + * <p> + * Note that content specified in this way can access local device files + * (via 'file' scheme URLs) only if baseUrl specifies a scheme other than + * 'http', 'https', 'ftp', 'ftps', 'about' or 'javascript'. + * <p> + * If the base URL uses the data scheme, this method is equivalent to + * calling {@link #loadData(String,String,String) loadData()} and the + * historyUrl is ignored, and the data will be treated as part of a data: URL. + * If the base URL uses any other scheme, then the data will be loaded into + * the WebView as a plain string (i.e. not part of a data URL) and any URL-encoded + * entities in the string will not be decoded. + * <p> + * Note that the baseUrl is sent in the 'Referer' HTTP header when + * requesting subresources (images, etc.) of the page loaded using this method. + * + * @param baseUrl the URL to use as the page's base URL. If {@code null} defaults to + * 'about:blank'. + * @param data a String of data in the given encoding + * @param mimeType the MIME type of the data, e.g. 'text/html'. + * @param encoding the encoding of the data + * @param historyUrl the URL to use as the history entry. If {@code null} defaults + * to 'about:blank'. If non-null, this must be a valid URL. + */ + public void loadDataWithBaseURL(@Nullable String baseUrl, String data, + @Nullable String mimeType, @Nullable String encoding, @Nullable String historyUrl) { + checkThread(); + mProvider.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); } + /** + * Asynchronously evaluates JavaScript in the context of the currently displayed page. + * If non-null, |resultCallback| will be invoked with any result returned from that + * execution. This method must be called on the UI thread and the callback will + * be made on the UI thread. + * <p> + * Compatibility note. Applications targeting {@link android.os.Build.VERSION_CODES#N} or + * later, JavaScript state from an empty WebView is no longer persisted across navigations like + * {@link #loadUrl(String)}. For example, global variables and functions defined before calling + * {@link #loadUrl(String)} will not exist in the loaded page. Applications should use + * {@link #addJavascriptInterface} instead to persist JavaScript objects across navigations. + * + * @param script the JavaScript to execute. + * @param resultCallback A callback to be invoked when the script execution + * completes with the result of the execution (if any). + * May be {@code null} if no notification of the result is required. + */ + public void evaluateJavascript(String script, @Nullable ValueCallback<String> resultCallback) { + checkThread(); + mProvider.evaluateJavaScript(script, resultCallback); + } + + /** + * Saves the current view as a web archive. + * + * @param filename the filename where the archive should be placed + */ + public void saveWebArchive(String filename) { + checkThread(); + mProvider.saveWebArchive(filename); + } + + /** + * Saves the current view as a web archive. + * + * @param basename the filename where the archive should be placed + * @param autoname if {@code false}, takes basename to be a file. If {@code true}, basename + * is assumed to be a directory in which a filename will be + * chosen according to the URL of the current page. + * @param callback called after the web archive has been saved. The + * parameter for onReceiveValue will either be the filename + * under which the file was saved, or {@code null} if saving the + * file failed. + */ + public void saveWebArchive(String basename, boolean autoname, @Nullable ValueCallback<String> + callback) { + checkThread(); + mProvider.saveWebArchive(basename, autoname, callback); + } + + /** + * Stops the current load. + */ public void stopLoading() { + checkThread(); + mProvider.stopLoading(); } + /** + * Reloads the current URL. + */ public void reload() { + checkThread(); + mProvider.reload(); } + /** + * Gets whether this WebView has a back history item. + * + * @return {@code true} if this WebView has a back history item + */ public boolean canGoBack() { - return false; + checkThread(); + return mProvider.canGoBack(); } + /** + * Goes back in the history of this WebView. + */ public void goBack() { + checkThread(); + mProvider.goBack(); } + /** + * Gets whether this WebView has a forward history item. + * + * @return {@code true} if this WebView has a forward history item + */ public boolean canGoForward() { - return false; + checkThread(); + return mProvider.canGoForward(); } + /** + * Goes forward in the history of this WebView. + */ public void goForward() { + checkThread(); + mProvider.goForward(); } + /** + * Gets whether the page can go back or forward the given + * number of steps. + * + * @param steps the negative or positive number of steps to move the + * history + */ public boolean canGoBackOrForward(int steps) { - return false; + checkThread(); + return mProvider.canGoBackOrForward(steps); } + /** + * Goes to the history item that is the number of steps away from + * the current item. Steps is negative if backward and positive + * if forward. + * + * @param steps the number of steps to take back or forward in the back + * forward list + */ public void goBackOrForward(int steps) { + checkThread(); + mProvider.goBackOrForward(steps); } + /** + * Gets whether private browsing is enabled in this WebView. + */ + public boolean isPrivateBrowsingEnabled() { + checkThread(); + return mProvider.isPrivateBrowsingEnabled(); + } + + /** + * Scrolls the contents of this WebView up by half the view size. + * + * @param top {@code true} to jump to the top of the page + * @return {@code true} if the page was scrolled + */ public boolean pageUp(boolean top) { - return false; + checkThread(); + return mProvider.pageUp(top); } - + + /** + * Scrolls the contents of this WebView down by half the page size. + * + * @param bottom {@code true} to jump to bottom of page + * @return {@code true} if the page was scrolled + */ public boolean pageDown(boolean bottom) { - return false; + checkThread(); + return mProvider.pageDown(bottom); } + /** + * Posts a {@link VisualStateCallback}, which will be called when + * the current state of the WebView is ready to be drawn. + * + * <p>Because updates to the DOM are processed asynchronously, updates to the DOM may not + * immediately be reflected visually by subsequent {@link WebView#onDraw} invocations. The + * {@link VisualStateCallback} provides a mechanism to notify the caller when the contents of + * the DOM at the current time are ready to be drawn the next time the {@link WebView} + * draws. + * + * <p>The next draw after the callback completes is guaranteed to reflect all the updates to the + * DOM up to the point at which the {@link VisualStateCallback} was posted, but it may also + * contain updates applied after the callback was posted. + * + * <p>The state of the DOM covered by this API includes the following: + * <ul> + * <li>primitive HTML elements (div, img, span, etc..)</li> + * <li>images</li> + * <li>CSS animations</li> + * <li>WebGL</li> + * <li>canvas</li> + * </ul> + * It does not include the state of: + * <ul> + * <li>the video tag</li> + * </ul> + * + * <p>To guarantee that the {@link WebView} will successfully render the first frame + * after the {@link VisualStateCallback#onComplete} method has been called a set of conditions + * must be met: + * <ul> + * <li>If the {@link WebView}'s visibility is set to {@link View#VISIBLE VISIBLE} then + * the {@link WebView} must be attached to the view hierarchy.</li> + * <li>If the {@link WebView}'s visibility is set to {@link View#INVISIBLE INVISIBLE} + * then the {@link WebView} must be attached to the view hierarchy and must be made + * {@link View#VISIBLE VISIBLE} from the {@link VisualStateCallback#onComplete} method.</li> + * <li>If the {@link WebView}'s visibility is set to {@link View#GONE GONE} then the + * {@link WebView} must be attached to the view hierarchy and its + * {@link AbsoluteLayout.LayoutParams LayoutParams}'s width and height need to be set to fixed + * values and must be made {@link View#VISIBLE VISIBLE} from the + * {@link VisualStateCallback#onComplete} method.</li> + * </ul> + * + * <p>When using this API it is also recommended to enable pre-rasterization if the {@link + * WebView} is off screen to avoid flickering. See {@link WebSettings#setOffscreenPreRaster} for + * more details and do consider its caveats. + * + * @param requestId An id that will be returned in the callback to allow callers to match + * requests with callbacks. + * @param callback The callback to be invoked. + */ + public void postVisualStateCallback(long requestId, VisualStateCallback callback) { + checkThread(); + mProvider.insertVisualStateCallback(requestId, callback); + } + + /** + * Clears this WebView so that onDraw() will draw nothing but white background, + * and onMeasure() will return 0 if MeasureSpec is not MeasureSpec.EXACTLY. + * @deprecated Use WebView.loadUrl("about:blank") to reliably reset the view state + * and release page resources (including any running JavaScript). + */ + @Deprecated public void clearView() { + checkThread(); + mProvider.clearView(); } - + + /** + * Gets a new picture that captures the current contents of this WebView. + * The picture is of the entire document being displayed, and is not + * limited to the area currently displayed by this WebView. Also, the + * picture is a static copy and is unaffected by later changes to the + * content being displayed. + * <p> + * Note that due to internal changes, for API levels between + * {@link android.os.Build.VERSION_CODES#HONEYCOMB} and + * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH} inclusive, the + * picture does not include fixed position elements or scrollable divs. + * <p> + * Note that from {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} the returned picture + * should only be drawn into bitmap-backed Canvas - using any other type of Canvas will involve + * additional conversion at a cost in memory and performance. Also the + * {@link android.graphics.Picture#createFromStream} and + * {@link android.graphics.Picture#writeToStream} methods are not supported on the + * returned object. + * + * @deprecated Use {@link #onDraw} to obtain a bitmap snapshot of the WebView, or + * {@link #saveWebArchive} to save the content to a file. + * + * @return a picture that captures the current contents of this WebView + */ + @Deprecated public Picture capturePicture() { - return null; + checkThread(); + return mProvider.capturePicture(); } + /** + * @deprecated Use {@link #createPrintDocumentAdapter(String)} which requires user + * to provide a print document name. + */ + @Deprecated + public PrintDocumentAdapter createPrintDocumentAdapter() { + checkThread(); + return mProvider.createPrintDocumentAdapter("default"); + } + + /** + * Creates a PrintDocumentAdapter that provides the content of this WebView for printing. + * + * The adapter works by converting the WebView contents to a PDF stream. The WebView cannot + * be drawn during the conversion process - any such draws are undefined. It is recommended + * to use a dedicated off screen WebView for the printing. If necessary, an application may + * temporarily hide a visible WebView by using a custom PrintDocumentAdapter instance + * wrapped around the object returned and observing the onStart and onFinish methods. See + * {@link android.print.PrintDocumentAdapter} for more information. + * + * @param documentName The user-facing name of the printed document. See + * {@link android.print.PrintDocumentInfo} + */ + public PrintDocumentAdapter createPrintDocumentAdapter(String documentName) { + checkThread(); + return mProvider.createPrintDocumentAdapter(documentName); + } + + /** + * Gets the current scale of this WebView. + * + * @return the current scale + * + * @deprecated This method is prone to inaccuracy due to race conditions + * between the web rendering and UI threads; prefer + * {@link WebViewClient#onScaleChanged}. + */ + @Deprecated + @ViewDebug.ExportedProperty(category = "webview") public float getScale() { - return 0; + checkThread(); + return mProvider.getScale(); } + /** + * Sets the initial scale for this WebView. 0 means default. + * The behavior for the default scale depends on the state of + * {@link WebSettings#getUseWideViewPort()} and + * {@link WebSettings#getLoadWithOverviewMode()}. + * If the content fits into the WebView control by width, then + * the zoom is set to 100%. For wide content, the behavior + * depends on the state of {@link WebSettings#getLoadWithOverviewMode()}. + * If its value is {@code true}, the content will be zoomed out to be fit + * by width into the WebView control, otherwise not. + * + * If initial scale is greater than 0, WebView starts with this value + * as initial scale. + * Please note that unlike the scale properties in the viewport meta tag, + * this method doesn't take the screen density into account. + * + * @param scaleInPercent the initial scale in percent + */ public void setInitialScale(int scaleInPercent) { + checkThread(); + mProvider.setInitialScale(scaleInPercent); } + /** + * Invokes the graphical zoom picker widget for this WebView. This will + * result in the zoom widget appearing on the screen to control the zoom + * level of this WebView. + */ public void invokeZoomPicker() { + checkThread(); + mProvider.invokeZoomPicker(); } - public void requestFocusNodeHref(Message hrefMsg) { + /** + * Gets a HitTestResult based on the current cursor node. If a HTML::a + * tag is found and the anchor has a non-JavaScript URL, the HitTestResult + * type is set to SRC_ANCHOR_TYPE and the URL is set in the "extra" field. + * If the anchor does not have a URL or if it is a JavaScript URL, the type + * will be UNKNOWN_TYPE and the URL has to be retrieved through + * {@link #requestFocusNodeHref} asynchronously. If a HTML::img tag is + * found, the HitTestResult type is set to IMAGE_TYPE and the URL is set in + * the "extra" field. A type of + * SRC_IMAGE_ANCHOR_TYPE indicates an anchor with a URL that has an image as + * a child node. If a phone number is found, the HitTestResult type is set + * to PHONE_TYPE and the phone number is set in the "extra" field of + * HitTestResult. If a map address is found, the HitTestResult type is set + * to GEO_TYPE and the address is set in the "extra" field of HitTestResult. + * If an email address is found, the HitTestResult type is set to EMAIL_TYPE + * and the email is set in the "extra" field of HitTestResult. Otherwise, + * HitTestResult type is set to UNKNOWN_TYPE. + */ + public HitTestResult getHitTestResult() { + checkThread(); + return mProvider.getHitTestResult(); } + /** + * Requests the anchor or image element URL at the last tapped point. + * If hrefMsg is {@code null}, this method returns immediately and does not + * dispatch hrefMsg to its target. If the tapped point hits an image, + * an anchor, or an image in an anchor, the message associates + * strings in named keys in its data. The value paired with the key + * may be an empty string. + * + * @param hrefMsg the message to be dispatched with the result of the + * request. The message data contains three keys. "url" + * returns the anchor's href attribute. "title" returns the + * anchor's text. "src" returns the image's src attribute. + */ + public void requestFocusNodeHref(@Nullable Message hrefMsg) { + checkThread(); + mProvider.requestFocusNodeHref(hrefMsg); + } + + /** + * Requests the URL of the image last touched by the user. msg will be sent + * to its target with a String representing the URL as its object. + * + * @param msg the message to be dispatched with the result of the request + * as the data member with "url" as key. The result can be {@code null}. + */ public void requestImageRef(Message msg) { + checkThread(); + mProvider.requestImageRef(msg); } + /** + * Gets the URL for the current page. This is not always the same as the URL + * passed to WebViewClient.onPageStarted because although the load for + * that URL has begun, the current page may not have changed. + * + * @return the URL for the current page + */ + @ViewDebug.ExportedProperty(category = "webview") public String getUrl() { - return null; + checkThread(); + return mProvider.getUrl(); } + /** + * Gets the original URL for the current page. This is not always the same + * as the URL passed to WebViewClient.onPageStarted because although the + * load for that URL has begun, the current page may not have changed. + * Also, there may have been redirects resulting in a different URL to that + * originally requested. + * + * @return the URL that was originally requested for the current page + */ + @ViewDebug.ExportedProperty(category = "webview") + public String getOriginalUrl() { + checkThread(); + return mProvider.getOriginalUrl(); + } + + /** + * Gets the title for the current page. This is the title of the current page + * until WebViewClient.onReceivedTitle is called. + * + * @return the title for the current page + */ + @ViewDebug.ExportedProperty(category = "webview") public String getTitle() { - return null; + checkThread(); + return mProvider.getTitle(); } + /** + * Gets the favicon for the current page. This is the favicon of the current + * page until WebViewClient.onReceivedIcon is called. + * + * @return the favicon for the current page + */ public Bitmap getFavicon() { - return null; + checkThread(); + return mProvider.getFavicon(); + } + + /** + * Gets the touch icon URL for the apple-touch-icon <link> element, or + * a URL on this site's server pointing to the standard location of a + * touch icon. + * + * @hide + */ + public String getTouchIconUrl() { + return mProvider.getTouchIconUrl(); } + /** + * Gets the progress for the current page. + * + * @return the progress for the current page between 0 and 100 + */ public int getProgress() { - return 0; + checkThread(); + return mProvider.getProgress(); } - + + /** + * Gets the height of the HTML content. + * + * @return the height of the HTML content + */ + @ViewDebug.ExportedProperty(category = "webview") public int getContentHeight() { - return 0; + checkThread(); + return mProvider.getContentHeight(); + } + + /** + * Gets the width of the HTML content. + * + * @return the width of the HTML content + * @hide + */ + @ViewDebug.ExportedProperty(category = "webview") + public int getContentWidth() { + return mProvider.getContentWidth(); } + /** + * Pauses all layout, parsing, and JavaScript timers for all WebViews. This + * is a global requests, not restricted to just this WebView. This can be + * useful if the application has been paused. + */ public void pauseTimers() { + checkThread(); + mProvider.pauseTimers(); } + /** + * Resumes all layout, parsing, and JavaScript timers for all WebViews. + * This will resume dispatching all timers. + */ public void resumeTimers() { + checkThread(); + mProvider.resumeTimers(); } - public void clearCache() { + /** + * Does a best-effort attempt to pause any processing that can be paused + * safely, such as animations and geolocation. Note that this call + * does not pause JavaScript. To pause JavaScript globally, use + * {@link #pauseTimers}. + * + * To resume WebView, call {@link #onResume}. + */ + public void onPause() { + checkThread(); + mProvider.onPause(); } + /** + * Resumes a WebView after a previous call to {@link #onPause}. + */ + public void onResume() { + checkThread(); + mProvider.onResume(); + } + + /** + * Gets whether this WebView is paused, meaning onPause() was called. + * Calling onResume() sets the paused state back to {@code false}. + * + * @hide + */ + public boolean isPaused() { + return mProvider.isPaused(); + } + + /** + * Informs this WebView that memory is low so that it can free any available + * memory. + * @deprecated Memory caches are automatically dropped when no longer needed, and in response + * to system memory pressure. + */ + @Deprecated + public void freeMemory() { + checkThread(); + mProvider.freeMemory(); + } + + /** + * Clears the resource cache. Note that the cache is per-application, so + * this will clear the cache for all WebViews used. + * + * @param includeDiskFiles if {@code false}, only the RAM cache is cleared + */ + public void clearCache(boolean includeDiskFiles) { + checkThread(); + mProvider.clearCache(includeDiskFiles); + } + + /** + * Removes the autocomplete popup from the currently focused form field, if + * present. Note this only affects the display of the autocomplete popup, + * it does not remove any saved form data from this WebView's store. To do + * that, use {@link WebViewDatabase#clearFormData}. + */ public void clearFormData() { + checkThread(); + mProvider.clearFormData(); } + /** + * Tells this WebView to clear its internal back/forward list. + */ public void clearHistory() { + checkThread(); + mProvider.clearHistory(); } + /** + * Clears the SSL preferences table stored in response to proceeding with + * SSL certificate errors. + */ public void clearSslPreferences() { + checkThread(); + mProvider.clearSslPreferences(); + } + + /** + * Clears the client certificate preferences stored in response + * to proceeding/cancelling client cert requests. Note that WebView + * automatically clears these preferences when it receives a + * {@link KeyChain#ACTION_STORAGE_CHANGED} intent. The preferences are + * shared by all the WebViews that are created by the embedder application. + * + * @param onCleared A runnable to be invoked when client certs are cleared. + * The runnable will be called in UI thread. + */ + public static void clearClientCertPreferences(@Nullable Runnable onCleared) { + getFactory().getStatics().clearClientCertPreferences(onCleared); } + /** + * Starts Safe Browsing initialization. + * <p> + * URL loads are not guaranteed to be protected by Safe Browsing until after {@code callback} is + * invoked with {@code true}. Safe Browsing is not fully supported on all devices. For those + * devices {@code callback} will receive {@code false}. + * <p> + * This should not be called if Safe Browsing has been disabled by manifest tag or {@link + * WebSettings#setSafeBrowsingEnabled}. This prepares resources used for Safe Browsing. + * <p> + * This should be called with the Application Context (and will always use the Application + * context to do its work regardless). + * + * @param context Application Context. + * @param callback will be called on the UI thread with {@code true} if initialization is + * successful, {@code false} otherwise. + */ + public static void startSafeBrowsing(@NonNull Context context, + @Nullable ValueCallback<Boolean> callback) { + getFactory().getStatics().initSafeBrowsing(context, callback); + } + + /** + * Sets the list of hosts (domain names/IP addresses) that are exempt from SafeBrowsing checks. + * The list is global for all the WebViews. + * <p> + * Each rule should take one of these: + * <table> + * <tr><th> Rule </th> <th> Example </th> <th> Matches Subdomain</th> </tr> + * <tr><td> HOSTNAME </td> <td> example.com </td> <td> Yes </td> </tr> + * <tr><td> .HOSTNAME </td> <td> .example.com </td> <td> No </td> </tr> + * <tr><td> IPV4_LITERAL </td> <td> 192.168.1.1 </td> <td> No </td></tr> + * <tr><td> IPV6_LITERAL_WITH_BRACKETS </td><td>[10:20:30:40:50:60:70:80]</td><td>No</td></tr> + * </table> + * <p> + * All other rules, including wildcards, are invalid. + * <p> + * The correct syntax for hosts is defined by <a + * href="https://tools.ietf.org/html/rfc3986#section-3.2.2">RFC 3986</a>. + * + * @param hosts the list of hosts + * @param callback will be called with {@code true} if hosts are successfully added to the + * whitelist. It will be called with {@code false} if any hosts are malformed. The callback + * will be run on the UI thread + */ + public static void setSafeBrowsingWhitelist(@NonNull List<String> hosts, + @Nullable ValueCallback<Boolean> callback) { + getFactory().getStatics().setSafeBrowsingWhitelist(hosts, callback); + } + + /** + * Returns a URL pointing to the privacy policy for Safe Browsing reporting. + * + * @return the url pointing to a privacy policy document which can be displayed to users. + */ + @NonNull + public static Uri getSafeBrowsingPrivacyPolicyUrl() { + return getFactory().getStatics().getSafeBrowsingPrivacyPolicyUrl(); + } + + /** + * Gets the WebBackForwardList for this WebView. This contains the + * back/forward list for use in querying each item in the history stack. + * This is a copy of the private WebBackForwardList so it contains only a + * snapshot of the current state. Multiple calls to this method may return + * different objects. The object returned from this method will not be + * updated to reflect any new state. + */ + public WebBackForwardList copyBackForwardList() { + checkThread(); + return mProvider.copyBackForwardList(); + + } + + /** + * Registers the listener to be notified as find-on-page operations + * progress. This will replace the current listener. + * + * @param listener an implementation of {@link FindListener} + */ + public void setFindListener(FindListener listener) { + checkThread(); + setupFindListenerIfNeeded(); + mFindListener.mUserFindListener = listener; + } + + /** + * Highlights and scrolls to the next match found by + * {@link #findAllAsync}, wrapping around page boundaries as necessary. + * Notifies any registered {@link FindListener}. If {@link #findAllAsync(String)} + * has not been called yet, or if {@link #clearMatches} has been called since the + * last find operation, this function does nothing. + * + * @param forward the direction to search + * @see #setFindListener + */ + public void findNext(boolean forward) { + checkThread(); + mProvider.findNext(forward); + } + + /** + * Finds all instances of find on the page and highlights them. + * Notifies any registered {@link FindListener}. + * + * @param find the string to find + * @return the number of occurrences of the String "find" that were found + * @deprecated {@link #findAllAsync} is preferred. + * @see #setFindListener + */ + @Deprecated + public int findAll(String find) { + checkThread(); + StrictMode.noteSlowCall("findAll blocks UI: prefer findAllAsync"); + return mProvider.findAll(find); + } + + /** + * Finds all instances of find on the page and highlights them, + * asynchronously. Notifies any registered {@link FindListener}. + * Successive calls to this will cancel any pending searches. + * + * @param find the string to find. + * @see #setFindListener + */ + public void findAllAsync(String find) { + checkThread(); + mProvider.findAllAsync(find); + } + + /** + * Starts an ActionMode for finding text in this WebView. Only works if this + * WebView is attached to the view system. + * + * @param text if non-null, will be the initial text to search for. + * Otherwise, the last String searched for in this WebView will + * be used to start. + * @param showIme if {@code true}, show the IME, assuming the user will begin typing. + * If {@code false} and text is non-null, perform a find all. + * @return {@code true} if the find dialog is shown, {@code false} otherwise + * @deprecated This method does not work reliably on all Android versions; + * implementing a custom find dialog using WebView.findAllAsync() + * provides a more robust solution. + */ + @Deprecated + public boolean showFindDialog(@Nullable String text, boolean showIme) { + checkThread(); + return mProvider.showFindDialog(text, showIme); + } + + /** + * Gets the first substring consisting of the address of a physical + * location. Currently, only addresses in the United States are detected, + * and consist of: + * <ul> + * <li>a house number</li> + * <li>a street name</li> + * <li>a street type (Road, Circle, etc), either spelled out or + * abbreviated</li> + * <li>a city name</li> + * <li>a state or territory, either spelled out or two-letter abbr</li> + * <li>an optional 5 digit or 9 digit zip code</li> + * </ul> + * All names must be correctly capitalized, and the zip code, if present, + * must be valid for the state. The street type must be a standard USPS + * spelling or abbreviation. The state or territory must also be spelled + * or abbreviated using USPS standards. The house number may not exceed + * five digits. + * + * @param addr the string to search for addresses + * @return the address, or if no address is found, {@code null} + * @deprecated this method is superseded by {@link TextClassifier#generateLinks( + * android.view.textclassifier.TextLinks.Request)}. Avoid using this method even when targeting + * API levels where no alternative is available. + */ + @Nullable + @Deprecated public static String findAddress(String addr) { - return null; + if (addr == null) { + throw new NullPointerException("addr is null"); + } + return FindAddress.findAddress(addr); + } + + /** + * For apps targeting the L release, WebView has a new default behavior that reduces + * memory footprint and increases performance by intelligently choosing + * the portion of the HTML document that needs to be drawn. These + * optimizations are transparent to the developers. However, under certain + * circumstances, an App developer may want to disable them: + * <ol> + * <li>When an app uses {@link #onDraw} to do own drawing and accesses portions + * of the page that is way outside the visible portion of the page.</li> + * <li>When an app uses {@link #capturePicture} to capture a very large HTML document. + * Note that capturePicture is a deprecated API.</li> + * </ol> + * Enabling drawing the entire HTML document has a significant performance + * cost. This method should be called before any WebViews are created. + */ + public static void enableSlowWholeDocumentDraw() { + getFactory().getStatics().enableSlowWholeDocumentDraw(); + } + + /** + * Clears the highlighting surrounding text matches created by + * {@link #findAllAsync}. + */ + public void clearMatches() { + checkThread(); + mProvider.clearMatches(); } + /** + * Queries the document to see if it contains any image references. The + * message object will be dispatched with arg1 being set to 1 if images + * were found and 0 if the document does not reference any images. + * + * @param response the message that will be dispatched with the result + */ public void documentHasImages(Message response) { + checkThread(); + mProvider.documentHasImages(response); } + /** + * Sets the WebViewClient that will receive various notifications and + * requests. This will replace the current handler. + * + * @param client an implementation of WebViewClient + * @see #getWebViewClient + */ public void setWebViewClient(WebViewClient client) { + checkThread(); + mProvider.setWebViewClient(client); + } + + /** + * Gets the WebViewClient. + * + * @return the WebViewClient, or a default client if not yet set + * @see #setWebViewClient + */ + public WebViewClient getWebViewClient() { + checkThread(); + return mProvider.getWebViewClient(); } + /** + * Registers the interface to be used when content can not be handled by + * the rendering engine, and should be downloaded instead. This will replace + * the current handler. + * + * @param listener an implementation of DownloadListener + */ public void setDownloadListener(DownloadListener listener) { + checkThread(); + mProvider.setDownloadListener(listener); } + /** + * Sets the chrome handler. This is an implementation of WebChromeClient for + * use in handling JavaScript dialogs, favicons, titles, and the progress. + * This will replace the current handler. + * + * @param client an implementation of WebChromeClient + * @see #getWebChromeClient + */ public void setWebChromeClient(WebChromeClient client) { + checkThread(); + mProvider.setWebChromeClient(client); + } + + /** + * Gets the chrome handler. + * + * @return the WebChromeClient, or {@code null} if not yet set + * @see #setWebChromeClient + */ + @Nullable + public WebChromeClient getWebChromeClient() { + checkThread(); + return mProvider.getWebChromeClient(); + } + + /** + * Sets the Picture listener. This is an interface used to receive + * notifications of a new Picture. + * + * @param listener an implementation of WebView.PictureListener + * @deprecated This method is now obsolete. + */ + @Deprecated + public void setPictureListener(PictureListener listener) { + checkThread(); + mProvider.setPictureListener(listener); + } + + /** + * Injects the supplied Java object into this WebView. The object is + * injected into the JavaScript context of the main frame, using the + * supplied name. This allows the Java object's methods to be + * accessed from JavaScript. For applications targeted to API + * level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} + * and above, only public methods that are annotated with + * {@link android.webkit.JavascriptInterface} can be accessed from JavaScript. + * For applications targeted to API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} or below, + * all public methods (including the inherited ones) can be accessed, see the + * important security note below for implications. + * <p> Note that injected objects will not appear in JavaScript until the page is next + * (re)loaded. JavaScript should be enabled before injecting the object. For example: + * <pre> + * class JsObject { + * {@literal @}JavascriptInterface + * public String toString() { return "injectedObject"; } + * } + * webview.getSettings().setJavaScriptEnabled(true); + * webView.addJavascriptInterface(new JsObject(), "injectedObject"); + * webView.loadData("<!DOCTYPE html><title></title>", "text/html", null); + * webView.loadUrl("javascript:alert(injectedObject.toString())");</pre> + * <p> + * <strong>IMPORTANT:</strong> + * <ul> + * <li> This method can be used to allow JavaScript to control the host + * application. This is a powerful feature, but also presents a security + * risk for apps targeting {@link android.os.Build.VERSION_CODES#JELLY_BEAN} or earlier. + * Apps that target a version later than {@link android.os.Build.VERSION_CODES#JELLY_BEAN} + * are still vulnerable if the app runs on a device running Android earlier than 4.2. + * The most secure way to use this method is to target {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} + * and to ensure the method is called only when running on Android 4.2 or later. + * With these older versions, JavaScript could use reflection to access an + * injected object's public fields. Use of this method in a WebView + * containing untrusted content could allow an attacker to manipulate the + * host application in unintended ways, executing Java code with the + * permissions of the host application. Use extreme care when using this + * method in a WebView which could contain untrusted content.</li> + * <li> JavaScript interacts with Java object on a private, background + * thread of this WebView. Care is therefore required to maintain thread + * safety. + * </li> + * <li> The Java object's fields are not accessible.</li> + * <li> For applications targeted to API level {@link android.os.Build.VERSION_CODES#LOLLIPOP} + * and above, methods of injected Java objects are enumerable from + * JavaScript.</li> + * </ul> + * + * @param object the Java object to inject into this WebView's JavaScript + * context. {@code null} values are ignored. + * @param name the name used to expose the object in JavaScript + */ + public void addJavascriptInterface(Object object, String name) { + checkThread(); + mProvider.addJavascriptInterface(object, name); + } + + /** + * Removes a previously injected Java object from this WebView. Note that + * the removal will not be reflected in JavaScript until the page is next + * (re)loaded. See {@link #addJavascriptInterface}. + * + * @param name the name used to expose the object in JavaScript + */ + public void removeJavascriptInterface(@NonNull String name) { + checkThread(); + mProvider.removeJavascriptInterface(name); + } + + /** + * Creates a message channel to communicate with JS and returns the message + * ports that represent the endpoints of this message channel. The HTML5 message + * channel functionality is described + * <a href="https://html.spec.whatwg.org/multipage/comms.html#messagechannel">here + * </a> + * + * <p>The returned message channels are entangled and already in started state. + * + * @return the two message ports that form the message channel. + */ + public WebMessagePort[] createWebMessageChannel() { + checkThread(); + return mProvider.createWebMessageChannel(); + } + + /** + * Post a message to main frame. The embedded application can restrict the + * messages to a certain target origin. See + * <a href="https://html.spec.whatwg.org/multipage/comms.html#posting-messages"> + * HTML5 spec</a> for how target origin can be used. + * <p> + * A target origin can be set as a wildcard ("*"). However this is not recommended. + * See the page above for security issues. + * + * @param message the WebMessage + * @param targetOrigin the target origin. + */ + public void postWebMessage(WebMessage message, Uri targetOrigin) { + checkThread(); + mProvider.postMessageToMainFrame(message, targetOrigin); + } + + /** + * Gets the WebSettings object used to control the settings for this + * WebView. + * + * @return a WebSettings object that can be used to control this WebView's + * settings + */ + public WebSettings getSettings() { + checkThread(); + return mProvider.getSettings(); + } + + /** + * Enables debugging of web contents (HTML / CSS / JavaScript) + * loaded into any WebViews of this application. This flag can be enabled + * in order to facilitate debugging of web layouts and JavaScript + * code running inside WebViews. Please refer to WebView documentation + * for the debugging guide. + * + * The default is {@code false}. + * + * @param enabled whether to enable web contents debugging + */ + public static void setWebContentsDebuggingEnabled(boolean enabled) { + getFactory().getStatics().setWebContentsDebuggingEnabled(enabled); + } + + /** + * Gets the list of currently loaded plugins. + * + * @return the list of currently loaded plugins + * @deprecated This was used for Gears, which has been deprecated. + * @hide + */ + @Deprecated + public static synchronized PluginList getPluginList() { + return new PluginList(); + } + + /** + * Define the directory used to store WebView data for the current process. + * The provided suffix will be used when constructing data and cache + * directory paths. If this API is not called, no suffix will be used. + * Each directory can be used by only one process in the application. If more + * than one process in an app wishes to use WebView, only one process can use + * the default directory, and other processes must call this API to define + * a unique suffix. + * <p> + * This means that different processes in the same application cannot directly + * share WebView-related data, since the data directories must be distinct. + * Applications that use this API may have to explicitly pass data between + * processes. For example, login cookies may have to be copied from one + * process's cookie jar to the other using {@link CookieManager} if both + * processes' WebViews are intended to be logged in. + * <p> + * Most applications should simply ensure that all components of the app + * that rely on WebView are in the same process, to avoid needing multiple + * data directories. The {@link #disableWebView} method can be used to ensure + * that the other processes do not use WebView by accident in this case. + * <p> + * This API must be called before any instances of WebView are created in + * this process and before any other methods in the android.webkit package + * are called by this process. + * + * @param suffix The directory name suffix to be used for the current + * process. Must not contain a path separator. + * @throws IllegalStateException if WebView has already been initialized + * in the current process. + * @throws IllegalArgumentException if the suffix contains a path separator. + */ + public static void setDataDirectorySuffix(String suffix) { + WebViewFactory.setDataDirectorySuffix(suffix); } - public void addJavascriptInterface(Object obj, String interfaceName) { + /** + * Indicate that the current process does not intend to use WebView, and + * that an exception should be thrown if a WebView is created or any other + * methods in the android.webkit package are used. + * <p> + * Applications with multiple processes may wish to call this in processes + * that are not intended to use WebView to avoid accidentally incurring + * the memory usage of initializing WebView in long-lived processes that + * have no need for it, and to prevent potential data directory conflicts + * (see {@link #setDataDirectorySuffix}). + * <p> + * For example, an audio player application with one process for its + * activities and another process for its playback service may wish to call + * this method in the playback service's {@link android.app.Service#onCreate}. + * + * @throws IllegalStateException if WebView has already been initialized + * in the current process. + */ + public static void disableWebView() { + WebViewFactory.disableWebView(); } + + /** + * @deprecated This was used for Gears, which has been deprecated. + * @hide + */ + @Deprecated + public void refreshPlugins(boolean reloadOpenPages) { + checkThread(); + } + + /** + * Puts this WebView into text selection mode. Do not rely on this + * functionality; it will be deprecated in the future. + * + * @deprecated This method is now obsolete. + * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} + */ + @Deprecated + public void emulateShiftHeld() { + checkThread(); + } + + /** + * @deprecated WebView no longer needs to implement + * ViewGroup.OnHierarchyChangeListener. This method does nothing now. + */ + @Override + // Cannot add @hide as this can always be accessed via the interface. + @Deprecated + public void onChildViewAdded(View parent, View child) {} + + /** + * @deprecated WebView no longer needs to implement + * ViewGroup.OnHierarchyChangeListener. This method does nothing now. + */ + @Override + // Cannot add @hide as this can always be accessed via the interface. + @Deprecated + public void onChildViewRemoved(View p, View child) {} + + /** + * @deprecated WebView should not have implemented + * ViewTreeObserver.OnGlobalFocusChangeListener. This method does nothing now. + */ + @Override + // Cannot add @hide as this can always be accessed via the interface. + @Deprecated + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + } + + /** + * @deprecated Only the default case, {@code true}, will be supported in a future version. + */ + @Deprecated + public void setMapTrackballToArrowKeys(boolean setMap) { + checkThread(); + mProvider.setMapTrackballToArrowKeys(setMap); + } + + + public void flingScroll(int vx, int vy) { + checkThread(); + mProvider.flingScroll(vx, vy); + } + + /** + * Gets the zoom controls for this WebView, as a separate View. The caller + * is responsible for inserting this View into the layout hierarchy. + * <p/> + * API level {@link android.os.Build.VERSION_CODES#CUPCAKE} introduced + * built-in zoom mechanisms for the WebView, as opposed to these separate + * zoom controls. The built-in mechanisms are preferred and can be enabled + * using {@link WebSettings#setBuiltInZoomControls}. + * + * @deprecated the built-in zoom mechanisms are preferred + * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} + */ + @Deprecated public View getZoomControls() { - return null; + checkThread(); + return mProvider.getZoomControls(); + } + + /** + * Gets whether this WebView can be zoomed in. + * + * @return {@code true} if this WebView can be zoomed in + * + * @deprecated This method is prone to inaccuracy due to race conditions + * between the web rendering and UI threads; prefer + * {@link WebViewClient#onScaleChanged}. + */ + @Deprecated + public boolean canZoomIn() { + checkThread(); + return mProvider.canZoomIn(); + } + + /** + * Gets whether this WebView can be zoomed out. + * + * @return {@code true} if this WebView can be zoomed out + * + * @deprecated This method is prone to inaccuracy due to race conditions + * between the web rendering and UI threads; prefer + * {@link WebViewClient#onScaleChanged}. + */ + @Deprecated + public boolean canZoomOut() { + checkThread(); + return mProvider.canZoomOut(); + } + + /** + * Performs a zoom operation in this WebView. + * + * @param zoomFactor the zoom factor to apply. The zoom factor will be clamped to the WebView's + * zoom limits. This value must be in the range 0.01 to 100.0 inclusive. + */ + public void zoomBy(float zoomFactor) { + checkThread(); + if (zoomFactor < 0.01) + throw new IllegalArgumentException("zoomFactor must be greater than 0.01."); + if (zoomFactor > 100.0) + throw new IllegalArgumentException("zoomFactor must be less than 100."); + mProvider.zoomBy(zoomFactor); } + /** + * Performs zoom in in this WebView. + * + * @return {@code true} if zoom in succeeds, {@code false} if no zoom changes + */ public boolean zoomIn() { - return false; + checkThread(); + return mProvider.zoomIn(); } + /** + * Performs zoom out in this WebView. + * + * @return {@code true} if zoom out succeeds, {@code false} if no zoom changes + */ public boolean zoomOut() { - return false; + checkThread(); + return mProvider.zoomOut(); + } + + /** + * @deprecated This method is now obsolete. + * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} + */ + @Deprecated + public void debugDump() { + checkThread(); + } + + /** + * See {@link ViewDebug.HierarchyHandler#dumpViewHierarchyWithProperties(BufferedWriter, int)} + * @hide + */ + @Override + public void dumpViewHierarchyWithProperties(BufferedWriter out, int level) { + mProvider.dumpViewHierarchyWithProperties(out, level); + } + + /** + * See {@link ViewDebug.HierarchyHandler#findHierarchyView(String, int)} + * @hide + */ + @Override + public View findHierarchyView(String className, int hashCode) { + return mProvider.findHierarchyView(className, hashCode); + } + + /** @hide */ + @IntDef(prefix = { "RENDERER_PRIORITY_" }, value = { + RENDERER_PRIORITY_WAIVED, + RENDERER_PRIORITY_BOUND, + RENDERER_PRIORITY_IMPORTANT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface RendererPriority {} + + /** + * The renderer associated with this WebView is bound with + * {@link Context#BIND_WAIVE_PRIORITY}. At this priority level + * {@link WebView} renderers will be strong targets for out of memory + * killing. + * + * Use with {@link #setRendererPriorityPolicy}. + */ + public static final int RENDERER_PRIORITY_WAIVED = 0; + /** + * The renderer associated with this WebView is bound with + * the default priority for services. + * + * Use with {@link #setRendererPriorityPolicy}. + */ + public static final int RENDERER_PRIORITY_BOUND = 1; + /** + * The renderer associated with this WebView is bound with + * {@link Context#BIND_IMPORTANT}. + * + * Use with {@link #setRendererPriorityPolicy}. + */ + public static final int RENDERER_PRIORITY_IMPORTANT = 2; + + /** + * Set the renderer priority policy for this {@link WebView}. The + * priority policy will be used to determine whether an out of + * process renderer should be considered to be a target for OOM + * killing. + * + * Because a renderer can be associated with more than one + * WebView, the final priority it is computed as the maximum of + * any attached WebViews. When a WebView is destroyed it will + * cease to be considerered when calculating the renderer + * priority. Once no WebViews remain associated with the renderer, + * the priority of the renderer will be reduced to + * {@link #RENDERER_PRIORITY_WAIVED}. + * + * The default policy is to set the priority to + * {@link #RENDERER_PRIORITY_IMPORTANT} regardless of visibility, + * and this should not be changed unless the caller also handles + * renderer crashes with + * {@link WebViewClient#onRenderProcessGone}. Any other setting + * will result in WebView renderers being killed by the system + * more aggressively than the application. + * + * @param rendererRequestedPriority the minimum priority at which + * this WebView desires the renderer process to be bound. + * @param waivedWhenNotVisible if {@code true}, this flag specifies that + * when this WebView is not visible, it will be treated as + * if it had requested a priority of + * {@link #RENDERER_PRIORITY_WAIVED}. + */ + public void setRendererPriorityPolicy( + @RendererPriority int rendererRequestedPriority, + boolean waivedWhenNotVisible) { + mProvider.setRendererPriorityPolicy(rendererRequestedPriority, waivedWhenNotVisible); + } + + /** + * Get the requested renderer priority for this WebView. + * + * @return the requested renderer priority policy. + */ + @RendererPriority + public int getRendererRequestedPriority() { + return mProvider.getRendererRequestedPriority(); + } + + /** + * Return whether this WebView requests a priority of + * {@link #RENDERER_PRIORITY_WAIVED} when not visible. + * + * @return whether this WebView requests a priority of + * {@link #RENDERER_PRIORITY_WAIVED} when not visible. + */ + public boolean getRendererPriorityWaivedWhenNotVisible() { + return mProvider.getRendererPriorityWaivedWhenNotVisible(); + } + + /** + * Sets the {@link TextClassifier} for this WebView. + */ + public void setTextClassifier(@Nullable TextClassifier textClassifier) { + mProvider.setTextClassifier(textClassifier); + } + + /** + * Returns the {@link TextClassifier} used by this WebView. + * If no TextClassifier has been set, this WebView uses the default set by the system. + */ + @NonNull + public TextClassifier getTextClassifier() { + return mProvider.getTextClassifier(); + } + + /** + * Returns the {@link ClassLoader} used to load internal WebView classes. + * This method is meant for use by the WebView Support Library, there is no reason to use this + * method otherwise. + */ + @NonNull + public static ClassLoader getWebViewClassLoader() { + return getFactory().getWebViewClassLoader(); + } + + /** + * Returns the {@link Looper} corresponding to the thread on which WebView calls must be made. + */ + @NonNull + public Looper getWebViewLooper() { + return mWebViewThread; + } + + //------------------------------------------------------------------------- + // Interface for WebView providers + //------------------------------------------------------------------------- + + /** + * Gets the WebViewProvider. Used by providers to obtain the underlying + * implementation, e.g. when the application responds to + * WebViewClient.onCreateWindow() request. + * + * @hide WebViewProvider is not public API. + */ + @SystemApi + public WebViewProvider getWebViewProvider() { + return mProvider; + } + + /** + * Callback interface, allows the provider implementation to access non-public methods + * and fields, and make super-class calls in this WebView instance. + * @hide Only for use by WebViewProvider implementations + */ + @SystemApi + public class PrivateAccess { + // ---- Access to super-class methods ---- + public int super_getScrollBarStyle() { + return WebView.super.getScrollBarStyle(); + } + + public void super_scrollTo(int scrollX, int scrollY) { + WebView.super.scrollTo(scrollX, scrollY); + } + + public void super_computeScroll() { + WebView.super.computeScroll(); + } + + public boolean super_onHoverEvent(MotionEvent event) { + return WebView.super.onHoverEvent(event); + } + + public boolean super_performAccessibilityAction(int action, Bundle arguments) { + return WebView.super.performAccessibilityActionInternal(action, arguments); + } + + public boolean super_performLongClick() { + return WebView.super.performLongClick(); + } + + public boolean super_setFrame(int left, int top, int right, int bottom) { + return WebView.super.setFrame(left, top, right, bottom); + } + + public boolean super_dispatchKeyEvent(KeyEvent event) { + return WebView.super.dispatchKeyEvent(event); + } + + public boolean super_onGenericMotionEvent(MotionEvent event) { + return WebView.super.onGenericMotionEvent(event); + } + + public boolean super_requestFocus(int direction, Rect previouslyFocusedRect) { + return WebView.super.requestFocus(direction, previouslyFocusedRect); + } + + public void super_setLayoutParams(ViewGroup.LayoutParams params) { + WebView.super.setLayoutParams(params); + } + + public void super_startActivityForResult(Intent intent, int requestCode) { + WebView.super.startActivityForResult(intent, requestCode); + } + + // ---- Access to non-public methods ---- + public void overScrollBy(int deltaX, int deltaY, + int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, + int maxOverScrollX, int maxOverScrollY, + boolean isTouchEvent) { + WebView.this.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, + maxOverScrollX, maxOverScrollY, isTouchEvent); + } + + public void awakenScrollBars(int duration) { + WebView.this.awakenScrollBars(duration); + } + + public void awakenScrollBars(int duration, boolean invalidate) { + WebView.this.awakenScrollBars(duration, invalidate); + } + + public float getVerticalScrollFactor() { + return WebView.this.getVerticalScrollFactor(); + } + + public float getHorizontalScrollFactor() { + return WebView.this.getHorizontalScrollFactor(); + } + + public void setMeasuredDimension(int measuredWidth, int measuredHeight) { + WebView.this.setMeasuredDimension(measuredWidth, measuredHeight); + } + + public void onScrollChanged(int l, int t, int oldl, int oldt) { + WebView.this.onScrollChanged(l, t, oldl, oldt); + } + + public int getHorizontalScrollbarHeight() { + return WebView.this.getHorizontalScrollbarHeight(); + } + + public void super_onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar, + int l, int t, int r, int b) { + WebView.super.onDrawVerticalScrollBar(canvas, scrollBar, l, t, r, b); + } + + // ---- Access to (non-public) fields ---- + /** Raw setter for the scroll X value, without invoking onScrollChanged handlers etc. */ + public void setScrollXRaw(int scrollX) { + WebView.this.mScrollX = scrollX; + } + + /** Raw setter for the scroll Y value, without invoking onScrollChanged handlers etc. */ + public void setScrollYRaw(int scrollY) { + WebView.this.mScrollY = scrollY; + } + + } + + //------------------------------------------------------------------------- + // Package-private internal stuff + //------------------------------------------------------------------------- + + // Only used by android.webkit.FindActionModeCallback. + void setFindDialogFindListener(FindListener listener) { + checkThread(); + setupFindListenerIfNeeded(); + mFindListener.mFindDialogFindListener = listener; + } + + // Only used by android.webkit.FindActionModeCallback. + void notifyFindDialogDismissed() { + checkThread(); + mProvider.notifyFindDialogDismissed(); + } + + //------------------------------------------------------------------------- + // Private internal stuff + //------------------------------------------------------------------------- + + private WebViewProvider mProvider; + + /** + * In addition to the FindListener that the user may set via the WebView.setFindListener + * API, FindActionModeCallback will register it's own FindListener. We keep them separate + * via this class so that the two FindListeners can potentially exist at once. + */ + private class FindListenerDistributor implements FindListener { + private FindListener mFindDialogFindListener; + private FindListener mUserFindListener; + + @Override + public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, + boolean isDoneCounting) { + if (mFindDialogFindListener != null) { + mFindDialogFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, + isDoneCounting); + } + + if (mUserFindListener != null) { + mUserFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, + isDoneCounting); + } + } + } + private FindListenerDistributor mFindListener; + + private void setupFindListenerIfNeeded() { + if (mFindListener == null) { + mFindListener = new FindListenerDistributor(); + mProvider.setFindListener(mFindListener); + } + } + + private void ensureProviderCreated() { + checkThread(); + if (mProvider == null) { + // As this can get called during the base class constructor chain, pass the minimum + // number of dependencies here; the rest are deferred to init(). + mProvider = getFactory().createWebView(this, new PrivateAccess()); + } + } + + private static WebViewFactoryProvider getFactory() { + return WebViewFactory.getProvider(); + } + + private final Looper mWebViewThread = Looper.myLooper(); + + private void checkThread() { + // Ignore mWebViewThread == null because this can be called during in the super class + // constructor, before this class's own constructor has even started. + if (mWebViewThread != null && Looper.myLooper() != mWebViewThread) { + Throwable throwable = new Throwable( + "A WebView method was called on thread '" + + Thread.currentThread().getName() + "'. " + + "All WebView methods must be called on the same thread. " + + "(Expected Looper " + mWebViewThread + " called on " + Looper.myLooper() + + ", FYI main Looper is " + Looper.getMainLooper() + ")"); + Log.w(LOGTAG, Log.getStackTraceString(throwable)); + StrictMode.onWebViewMethodCalledOnWrongThread(throwable); + + if (sEnforceThreadChecking) { + throw new RuntimeException(throwable); + } + } + } + + //------------------------------------------------------------------------- + // Override View methods + //------------------------------------------------------------------------- + + // TODO: Add a test that enumerates all methods in ViewDelegte & ScrollDelegate, and ensures + // there's a corresponding override (or better, caller) for each of them in here. + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mProvider.getViewDelegate().onAttachedToWindow(); + } + + /** @hide */ + @Override + protected void onDetachedFromWindowInternal() { + mProvider.getViewDelegate().onDetachedFromWindow(); + super.onDetachedFromWindowInternal(); + } + + /** @hide */ + @Override + public void onMovedToDisplay(int displayId, Configuration config) { + mProvider.getViewDelegate().onMovedToDisplay(displayId, config); + } + + @Override + public void setLayoutParams(ViewGroup.LayoutParams params) { + mProvider.getViewDelegate().setLayoutParams(params); + } + + @Override + public void setOverScrollMode(int mode) { + super.setOverScrollMode(mode); + // This method may be called in the constructor chain, before the WebView provider is + // created. + ensureProviderCreated(); + mProvider.getViewDelegate().setOverScrollMode(mode); + } + + @Override + public void setScrollBarStyle(int style) { + mProvider.getViewDelegate().setScrollBarStyle(style); + super.setScrollBarStyle(style); + } + + @Override + protected int computeHorizontalScrollRange() { + return mProvider.getScrollDelegate().computeHorizontalScrollRange(); + } + + @Override + protected int computeHorizontalScrollOffset() { + return mProvider.getScrollDelegate().computeHorizontalScrollOffset(); + } + + @Override + protected int computeVerticalScrollRange() { + return mProvider.getScrollDelegate().computeVerticalScrollRange(); + } + + @Override + protected int computeVerticalScrollOffset() { + return mProvider.getScrollDelegate().computeVerticalScrollOffset(); + } + + @Override + protected int computeVerticalScrollExtent() { + return mProvider.getScrollDelegate().computeVerticalScrollExtent(); + } + + @Override + public void computeScroll() { + mProvider.getScrollDelegate().computeScroll(); + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + return mProvider.getViewDelegate().onHoverEvent(event); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + return mProvider.getViewDelegate().onTouchEvent(event); + } + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + return mProvider.getViewDelegate().onGenericMotionEvent(event); + } + + @Override + public boolean onTrackballEvent(MotionEvent event) { + return mProvider.getViewDelegate().onTrackballEvent(event); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return mProvider.getViewDelegate().onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return mProvider.getViewDelegate().onKeyUp(keyCode, event); + } + + @Override + public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + return mProvider.getViewDelegate().onKeyMultiple(keyCode, repeatCount, event); + } + + /* + TODO: These are not currently implemented in WebViewClassic, but it seems inconsistent not + to be delegating them too. + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + return mProvider.getViewDelegate().onKeyPreIme(keyCode, event); + } + @Override + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return mProvider.getViewDelegate().onKeyLongPress(keyCode, event); + } + @Override + public boolean onKeyShortcut(int keyCode, KeyEvent event) { + return mProvider.getViewDelegate().onKeyShortcut(keyCode, event); + } + */ + + @Override + public AccessibilityNodeProvider getAccessibilityNodeProvider() { + AccessibilityNodeProvider provider = + mProvider.getViewDelegate().getAccessibilityNodeProvider(); + return provider == null ? super.getAccessibilityNodeProvider() : provider; + } + + @Deprecated + @Override + public boolean shouldDelayChildPressedState() { + return mProvider.getViewDelegate().shouldDelayChildPressedState(); + } + + @Override + public CharSequence getAccessibilityClassName() { + return WebView.class.getName(); + } + + @Override + public void onProvideVirtualStructure(ViewStructure structure) { + mProvider.getViewDelegate().onProvideVirtualStructure(structure); + } + + /** + * {@inheritDoc} + * + * <p>The {@link ViewStructure} traditionally represents a {@link View}, while for web pages + * it represent HTML nodes. Hence, it's necessary to "map" the HTML properties in a way that is + * understood by the {@link android.service.autofill.AutofillService} implementations: + * + * <ol> + * <li>Only the HTML nodes inside a {@code FORM} are generated. + * <li>The source of the HTML is set using {@link ViewStructure#setWebDomain(String)} in the + * node representing the WebView. + * <li>If a web page has multiple {@code FORM}s, only the data for the current form is + * represented—if the user taps a field from another form, then the current autofill + * context is canceled (by calling {@link android.view.autofill.AutofillManager#cancel()} and + * a new context is created for that {@code FORM}. + * <li>Similarly, if the page has {@code IFRAME} nodes, they are not initially represented in + * the view structure until the user taps a field from a {@code FORM} inside the + * {@code IFRAME}, in which case it would be treated the same way as multiple forms described + * above, except that the {@link ViewStructure#setWebDomain(String) web domain} of the + * {@code FORM} contains the {@code src} attribute from the {@code IFRAME} node. + * <li>The W3C autofill field ({@code autocomplete} tag attribute) maps to + * {@link ViewStructure#setAutofillHints(String[])}. + * <li>If the view is editable, the {@link ViewStructure#setAutofillType(int)} and + * {@link ViewStructure#setAutofillValue(AutofillValue)} must be set. + * <li>The {@code placeholder} attribute maps to {@link ViewStructure#setHint(CharSequence)}. + * <li>Other HTML attributes can be represented through + * {@link ViewStructure#setHtmlInfo(android.view.ViewStructure.HtmlInfo)}. + * </ol> + * + * <p>If the WebView implementation can determine that the value of a field was set statically + * (for example, not through Javascript), it should also call + * {@code structure.setDataIsSensitive(false)}. + * + * <p>For example, an HTML form with 2 fields for username and password: + * + * <pre class="prettyprint"> + * <label>Username:</label> + * <input type="text" name="username" id="user" value="Type your username" autocomplete="username" placeholder="Email or username"> + * <label>Password:</label> + * <input type="password" name="password" id="pass" autocomplete="current-password" placeholder="Password"> + * </pre> + * + * <p>Would map to: + * + * <pre class="prettyprint"> + * int index = structure.addChildCount(2); + * ViewStructure username = structure.newChild(index); + * username.setAutofillId(structure.getAutofillId(), 1); // id 1 - first child + * username.setAutofillHints("username"); + * username.setHtmlInfo(username.newHtmlInfoBuilder("input") + * .addAttribute("type", "text") + * .addAttribute("name", "username") + * .addAttribute("label", "Username:") + * .build()); + * username.setHint("Email or username"); + * username.setAutofillType(View.AUTOFILL_TYPE_TEXT); + * username.setAutofillValue(AutofillValue.forText("Type your username")); + * // Value of the field is not sensitive because it was created statically and not changed. + * username.setDataIsSensitive(false); + * + * ViewStructure password = structure.newChild(index + 1); + * username.setAutofillId(structure, 2); // id 2 - second child + * password.setAutofillHints("current-password"); + * password.setHtmlInfo(password.newHtmlInfoBuilder("input") + * .addAttribute("type", "password") + * .addAttribute("name", "password") + * .addAttribute("label", "Password:") + * .build()); + * password.setHint("Password"); + * password.setAutofillType(View.AUTOFILL_TYPE_TEXT); + * </pre> + */ + @Override + public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) { + mProvider.getViewDelegate().onProvideAutofillVirtualStructure(structure, flags); + } + + @Override + public void autofill(SparseArray<AutofillValue>values) { + mProvider.getViewDelegate().autofill(values); + } + + @Override + public boolean isVisibleToUserForAutofill(int virtualId) { + return mProvider.getViewDelegate().isVisibleToUserForAutofill(virtualId); + } + + /** @hide */ + @Override + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); + mProvider.getViewDelegate().onInitializeAccessibilityNodeInfo(info); + } + + /** @hide */ + @Override + public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { + super.onInitializeAccessibilityEventInternal(event); + mProvider.getViewDelegate().onInitializeAccessibilityEvent(event); + } + + /** @hide */ + @Override + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { + return mProvider.getViewDelegate().performAccessibilityAction(action, arguments); + } + + /** @hide */ + @Override + protected void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar, + int l, int t, int r, int b) { + mProvider.getViewDelegate().onDrawVerticalScrollBar(canvas, scrollBar, l, t, r, b); + } + + @Override + protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { + mProvider.getViewDelegate().onOverScrolled(scrollX, scrollY, clampedX, clampedY); + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + mProvider.getViewDelegate().onWindowVisibilityChanged(visibility); + } + + @Override + protected void onDraw(Canvas canvas) { + mProvider.getViewDelegate().onDraw(canvas); + } + + @Override + public boolean performLongClick() { + return mProvider.getViewDelegate().performLongClick(); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + mProvider.getViewDelegate().onConfigurationChanged(newConfig); + } + + /** + * Creates a new InputConnection for an InputMethod to interact with the WebView. + * This is similar to {@link View#onCreateInputConnection} but note that WebView + * calls InputConnection methods on a thread other than the UI thread. + * If these methods are overridden, then the overriding methods should respect + * thread restrictions when calling View methods or accessing data. + */ + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return mProvider.getViewDelegate().onCreateInputConnection(outAttrs); + } + + @Override + public boolean onDragEvent(DragEvent event) { + return mProvider.getViewDelegate().onDragEvent(event); + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + // This method may be called in the constructor chain, before the WebView provider is + // created. + ensureProviderCreated(); + mProvider.getViewDelegate().onVisibilityChanged(changedView, visibility); + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + mProvider.getViewDelegate().onWindowFocusChanged(hasWindowFocus); + super.onWindowFocusChanged(hasWindowFocus); + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + mProvider.getViewDelegate().onFocusChanged(focused, direction, previouslyFocusedRect); + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + /** @hide */ + @Override + protected boolean setFrame(int left, int top, int right, int bottom) { + return mProvider.getViewDelegate().setFrame(left, top, right, bottom); + } + + @Override + protected void onSizeChanged(int w, int h, int ow, int oh) { + super.onSizeChanged(w, h, ow, oh); + mProvider.getViewDelegate().onSizeChanged(w, h, ow, oh); + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + mProvider.getViewDelegate().onScrollChanged(l, t, oldl, oldt); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + return mProvider.getViewDelegate().dispatchKeyEvent(event); + } + + @Override + public boolean requestFocus(int direction, Rect previouslyFocusedRect) { + return mProvider.getViewDelegate().requestFocus(direction, previouslyFocusedRect); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + mProvider.getViewDelegate().onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { + return mProvider.getViewDelegate().requestChildRectangleOnScreen(child, rect, immediate); + } + + @Override + public void setBackgroundColor(int color) { + mProvider.getViewDelegate().setBackgroundColor(color); + } + + @Override + public void setLayerType(int layerType, Paint paint) { + super.setLayerType(layerType, paint); + mProvider.getViewDelegate().setLayerType(layerType, paint); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + mProvider.getViewDelegate().preDispatchDraw(canvas); + super.dispatchDraw(canvas); + } + + @Override + public void onStartTemporaryDetach() { + super.onStartTemporaryDetach(); + mProvider.getViewDelegate().onStartTemporaryDetach(); + } + + @Override + public void onFinishTemporaryDetach() { + super.onFinishTemporaryDetach(); + mProvider.getViewDelegate().onFinishTemporaryDetach(); + } + + @Override + public Handler getHandler() { + return mProvider.getViewDelegate().getHandler(super.getHandler()); + } + + @Override + public View findFocus() { + return mProvider.getViewDelegate().findFocus(super.findFocus()); + } + + /** + * If WebView has already been loaded into the current process this method will return the + * package that was used to load it. Otherwise, the package that would be used if the WebView + * was loaded right now will be returned; this does not cause WebView to be loaded, so this + * information may become outdated at any time. + * The WebView package changes either when the current WebView package is updated, disabled, or + * uninstalled. It can also be changed through a Developer Setting. + * If the WebView package changes, any app process that has loaded WebView will be killed. The + * next time the app starts and loads WebView it will use the new WebView package instead. + * @return the current WebView package, or {@code null} if there is none. + */ + @Nullable + public static PackageInfo getCurrentWebViewPackage() { + PackageInfo webviewPackage = WebViewFactory.getLoadedPackageInfo(); + if (webviewPackage != null) { + return webviewPackage; + } + + IWebViewUpdateService service = WebViewFactory.getUpdateService(); + if (service == null) { + return null; + } + try { + return service.getCurrentWebViewPackage(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Receive the result from a previous call to {@link #startActivityForResult(Intent, int)}. + * + * @param requestCode The integer request code originally supplied to + * startActivityForResult(), allowing you to identify who this + * result came from. + * @param resultCode The integer result code returned by the child activity + * through its setResult(). + * @param data An Intent, which can return result data to the caller + * (various data can be attached to Intent "extras"). + * @hide + */ + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + mProvider.getViewDelegate().onActivityResult(requestCode, resultCode, data); + } + + @Override + public boolean onCheckIsTextEditor() { + return mProvider.getViewDelegate().onCheckIsTextEditor(); + } + + /** @hide */ + @Override + protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) { + super.encodeProperties(encoder); + + checkThread(); + encoder.addProperty("webview:contentHeight", mProvider.getContentHeight()); + encoder.addProperty("webview:contentWidth", mProvider.getContentWidth()); + encoder.addProperty("webview:scale", mProvider.getScale()); + encoder.addProperty("webview:title", mProvider.getTitle()); + encoder.addProperty("webview:url", mProvider.getUrl()); + encoder.addProperty("webview:originalUrl", mProvider.getOriginalUrl()); } } diff --git a/android/widget/Editor.java b/android/widget/Editor.java index 99467265..dac100a4 100644 --- a/android/widget/Editor.java +++ b/android/widget/Editor.java @@ -39,6 +39,7 @@ import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; +import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; @@ -4837,14 +4838,48 @@ public class Editor { return true; } - private boolean handleOverlapsMagnifier() { - final int handleY = mContainer.getDecorViewLayoutParams().y; - final int magnifierBottomWhenAtWindowTop = - mTextView.getRootWindowInsets().getSystemWindowInsetTop() - + mMagnifierAnimator.mMagnifier.getHeight(); - return handleY <= magnifierBottomWhenAtWindowTop; + private boolean handleOverlapsMagnifier(@NonNull final HandleView handle, + @NonNull final Rect magnifierRect) { + final PopupWindow window = handle.mContainer; + if (!window.hasDecorView()) { + return false; + } + final Rect handleRect = new Rect( + window.getDecorViewLayoutParams().x, + window.getDecorViewLayoutParams().y, + window.getDecorViewLayoutParams().x + window.getContentView().getWidth(), + window.getDecorViewLayoutParams().y + window.getContentView().getHeight()); + return Rect.intersects(handleRect, magnifierRect); + } + + private @Nullable HandleView getOtherSelectionHandle() { + final SelectionModifierCursorController controller = getSelectionController(); + if (controller == null || !controller.isActive()) { + return null; + } + return controller.mStartHandle != this + ? controller.mStartHandle + : controller.mEndHandle; } + private final Magnifier.Callback mHandlesVisibilityCallback = new Magnifier.Callback() { + @Override + public void onOperationComplete() { + final Point magnifierTopLeft = mMagnifierAnimator.mMagnifier.getWindowCoords(); + if (magnifierTopLeft == null) { + return; + } + final Rect magnifierRect = new Rect(magnifierTopLeft.x, magnifierTopLeft.y, + magnifierTopLeft.x + mMagnifierAnimator.mMagnifier.getWidth(), + magnifierTopLeft.y + mMagnifierAnimator.mMagnifier.getHeight()); + setVisible(!handleOverlapsMagnifier(HandleView.this, magnifierRect)); + final HandleView otherHandle = getOtherSelectionHandle(); + if (otherHandle != null) { + otherHandle.setVisible(!handleOverlapsMagnifier(otherHandle, magnifierRect)); + } + } + }; + protected final void updateMagnifier(@NonNull final MotionEvent event) { if (mMagnifierAnimator == null) { return; @@ -4858,12 +4893,8 @@ public class Editor { mRenderCursorRegardlessTiming = true; mTextView.invalidateCursorPath(); suspendBlink(); - // Hide handle if it overlaps the magnifier. - if (handleOverlapsMagnifier()) { - setVisible(false); - } else { - setVisible(true); - } + mMagnifierAnimator.mMagnifier + .setOnOperationCompleteCallback(mHandlesVisibilityCallback); mMagnifierAnimator.show(showPosInView.x, showPosInView.y); } else { @@ -4877,6 +4908,10 @@ public class Editor { mRenderCursorRegardlessTiming = false; resumeBlink(); setVisible(true); + final HandleView otherHandle = getOtherSelectionHandle(); + if (otherHandle != null) { + otherHandle.setVisible(true); + } } } @@ -6031,7 +6066,9 @@ public class Editor { mSwitchedLines = false; final int selectionStart = mTextView.getSelectionStart(); final int selectionEnd = mTextView.getSelectionEnd(); - if (selectionStart > selectionEnd) { + if (selectionStart < 0 || selectionEnd < 0) { + Selection.removeSelection((Spannable) mTextView.getText()); + } else if (selectionStart > selectionEnd) { Selection.setSelection((Spannable) mTextView.getText(), selectionEnd, selectionStart); } diff --git a/android/widget/ImageView.java b/android/widget/ImageView.java index 4b951fa1..13729874 100644 --- a/android/widget/ImageView.java +++ b/android/widget/ImageView.java @@ -817,8 +817,6 @@ public class ImageView extends View { if (mScaleType != scaleType) { mScaleType = scaleType; - setWillNotCacheDrawing(mScaleType == ScaleType.CENTER); - requestLayout(); invalidate(); } diff --git a/android/widget/LinearLayout.java b/android/widget/LinearLayout.java index d32e93c7..40f9652c 100644 --- a/android/widget/LinearLayout.java +++ b/android/widget/LinearLayout.java @@ -217,6 +217,17 @@ public class LinearLayout extends ViewGroup { private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED; + /** + * Signals that compatibility booleans have been initialized according to + * target SDK versions. + */ + private static boolean sCompatibilityDone = false; + + /** + * Behavior change in P; always remeasure weighted children, regardless of excess space. + */ + private static boolean sRemeasureWeightedChildren = true; + public LinearLayout(Context context) { this(context, null); } @@ -232,6 +243,15 @@ public class LinearLayout extends ViewGroup { public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); + if (!sCompatibilityDone && context != null) { + final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion; + + // Older apps only remeasure non-zero children + sRemeasureWeightedChildren = targetSdkVersion >= Build.VERSION_CODES.P; + + sCompatibilityDone = true; + } + final TypedArray a = context.obtainStyledAttributes( attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes); @@ -917,7 +937,8 @@ public class LinearLayout extends ViewGroup { // measurement on any children, we need to measure them now. int remainingExcess = heightSize - mTotalLength + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace); - if (skippedMeasure || totalWeight > 0.0f) { + if (skippedMeasure + || ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) { float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight; mTotalLength = 0; @@ -1300,7 +1321,8 @@ public class LinearLayout extends ViewGroup { // measurement on any children, we need to measure them now. int remainingExcess = widthSize - mTotalLength + (mAllowInconsistentMeasurement ? 0 : usedExcessSpace); - if (skippedMeasure || totalWeight > 0.0f) { + if (skippedMeasure + || ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) { float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight; maxAscent[0] = maxAscent[1] = maxAscent[2] = maxAscent[3] = -1; diff --git a/android/widget/Magnifier.java b/android/widget/Magnifier.java index 5eb66999..cb362e65 100644 --- a/android/widget/Magnifier.java +++ b/android/widget/Magnifier.java @@ -233,6 +233,17 @@ public final class Magnifier { return mZoom; } + /** + * @hide + */ + @Nullable + public Point getWindowCoords() { + if (mWindow == null) { + return null; + } + return new Point(mWindow.mLastDrawContentPositionX, mWindow.mLastDrawContentPositionY); + } + @Nullable private Surface getValidViewSurface() { // TODO: deduplicate this against the first part of #performPixelCopy @@ -374,8 +385,11 @@ public final class Magnifier { private final Runnable mMagnifierUpdater; // The handler where the magnifier updater jobs will be post'd. private final Handler mHandler; - // The callback to be run after the next draw. Only used for testing. + // The callback to be run after the next draw. private Callback mCallback; + // The position of the magnifier content when the last draw was requested. + private int mLastDrawContentPositionX; + private int mLastDrawContentPositionY; // Members below describe the state of the magnifier. Reads/writes to them // have to be synchronized between the UI thread and the thread that handles @@ -598,6 +612,8 @@ public final class Magnifier { callback = null; } + mLastDrawContentPositionX = mWindowPositionX + mOffsetX; + mLastDrawContentPositionY = mWindowPositionY + mOffsetY; mFrameDrawScheduled = false; } diff --git a/android/widget/SelectionActionModeHelper.java b/android/widget/SelectionActionModeHelper.java index b3327a70..1f2b90a1 100644 --- a/android/widget/SelectionActionModeHelper.java +++ b/android/widget/SelectionActionModeHelper.java @@ -33,9 +33,9 @@ import android.text.Spannable; import android.text.TextUtils; import android.util.Log; import android.view.ActionMode; -import android.view.textclassifier.Logger; import android.view.textclassifier.SelectionEvent; import android.view.textclassifier.SelectionEvent.InvocationMethod; +import android.view.textclassifier.SelectionSessionLogger; import android.view.textclassifier.TextClassification; import android.view.textclassifier.TextClassificationConstants; import android.view.textclassifier.TextClassificationManager; @@ -663,7 +663,6 @@ public final class SelectionActionModeHelper { private static final String LOG_TAG = "SelectionMetricsLogger"; private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\s+"); - private final Logger mLogger; private final boolean mEditTextLogger; private final BreakIterator mTokenIterator; @@ -673,10 +672,8 @@ public final class SelectionActionModeHelper { SelectionMetricsLogger(TextView textView) { Preconditions.checkNotNull(textView); - mLogger = textView.getTextClassifier().getLogger( - new Logger.Config(textView.getContext(), getWidetType(textView), null)); mEditTextLogger = textView.isTextEditable(); - mTokenIterator = mLogger.getTokenIterator(textView.getTextLocale()); + mTokenIterator = SelectionSessionLogger.getTokenIterator(textView.getTextLocale()); } @TextClassifier.WidgetType @@ -702,8 +699,6 @@ public final class SelectionActionModeHelper { } mTokenIterator.setText(mText); mStartIndex = index; - mLogger.logSelectionStartedEvent(invocationMethod, 0); - // TODO: Remove the above legacy logging. mClassificationSession = classificationSession; mClassificationSession.onSelectionEvent( SelectionEvent.createSelectionStartedEvent(invocationMethod, 0)); @@ -720,27 +715,18 @@ public final class SelectionActionModeHelper { Preconditions.checkArgumentInRange(end, start, mText.length(), "end"); int[] wordIndices = getWordDelta(start, end); if (selection != null) { - mLogger.logSelectionModifiedEvent( - wordIndices[0], wordIndices[1], selection); - // TODO: Remove the above legacy logging. if (mClassificationSession != null) { mClassificationSession.onSelectionEvent( SelectionEvent.createSelectionModifiedEvent( wordIndices[0], wordIndices[1], selection)); } } else if (classification != null) { - mLogger.logSelectionModifiedEvent( - wordIndices[0], wordIndices[1], classification); - // TODO: Remove the above legacy logging. if (mClassificationSession != null) { mClassificationSession.onSelectionEvent( SelectionEvent.createSelectionModifiedEvent( wordIndices[0], wordIndices[1], classification)); } } else { - mLogger.logSelectionModifiedEvent( - wordIndices[0], wordIndices[1]); - // TODO: Remove the above legacy logging. if (mClassificationSession != null) { mClassificationSession.onSelectionEvent( SelectionEvent.createSelectionModifiedEvent( @@ -762,18 +748,12 @@ public final class SelectionActionModeHelper { Preconditions.checkArgumentInRange(end, start, mText.length(), "end"); int[] wordIndices = getWordDelta(start, end); if (classification != null) { - mLogger.logSelectionActionEvent( - wordIndices[0], wordIndices[1], action, classification); - // TODO: Remove the above legacy logging. if (mClassificationSession != null) { mClassificationSession.onSelectionEvent( SelectionEvent.createSelectionActionEvent( wordIndices[0], wordIndices[1], action, classification)); } } else { - mLogger.logSelectionActionEvent( - wordIndices[0], wordIndices[1], action); - // TODO: Remove the above legacy logging. if (mClassificationSession != null) { mClassificationSession.onSelectionEvent( SelectionEvent.createSelectionActionEvent( @@ -989,7 +969,7 @@ public final class SelectionActionModeHelper { mHot = true; trimText(); final TextSelection selection; - if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.O_MR1) { + if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) { final TextSelection.Request request = new TextSelection.Request.Builder( mTrimmedText, mRelativeStart, mRelativeEnd) .setDefaultLocales(mDefaultLocales) @@ -1043,7 +1023,7 @@ public final class SelectionActionModeHelper { trimText(); final TextClassification classification; - if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.O_MR1) { + if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) { final TextClassification.Request request = new TextClassification.Request.Builder( mTrimmedText, mRelativeStart, mRelativeEnd) diff --git a/android/widget/TextClock.java b/android/widget/TextClock.java index 53318c99..d8a9ccad 100644 --- a/android/widget/TextClock.java +++ b/android/widget/TextClock.java @@ -408,6 +408,15 @@ public class TextClock extends TextView { } /** + * Update the displayed time if necessary and invalidate the view. + * @hide + */ + public void refresh() { + onTimeChanged(); + invalidate(); + } + + /** * Indicates whether the system is currently using the 24-hour mode. * * When the system is in 24-hour mode, this view will use the pattern diff --git a/android/widget/TextView.java b/android/widget/TextView.java index 11db6b65..7b9ecca0 100644 --- a/android/widget/TextView.java +++ b/android/widget/TextView.java @@ -317,7 +317,6 @@ import java.util.function.Supplier; * @attr ref android.R.styleable#TextView_autoSizeMaxTextSize * @attr ref android.R.styleable#TextView_autoSizeStepGranularity * @attr ref android.R.styleable#TextView_autoSizePresetSizes - * @attr ref android.R.styleable#TextView_accessibilityHeading */ @RemoteView public class TextView extends View implements ViewTreeObserver.OnPreDrawListener { @@ -417,7 +416,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private int mCurTextColor; private int mCurHintTextColor; private boolean mFreezesText; - private boolean mIsAccessibilityHeading; private Editable.Factory mEditableFactory = Editable.Factory.getInstance(); private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance(); @@ -1294,8 +1292,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case com.android.internal.R.styleable.TextView_lineHeight: lineHeight = a.getDimensionPixelSize(attr, -1); break; - case com.android.internal.R.styleable.TextView_accessibilityHeading: - mIsAccessibilityHeading = a.getBoolean(attr, false); } } @@ -5213,32 +5209,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * Gets whether this view is a heading for accessibility purposes. - * - * @return {@code true} if the view is a heading, {@code false} otherwise. - * - * @attr ref android.R.styleable#TextView_accessibilityHeading - */ - public boolean isAccessibilityHeading() { - return mIsAccessibilityHeading; - } - - /** - * Set if view is a heading for a section of content for accessibility purposes. - * - * @param isHeading {@code true} if the view is a heading, {@code false} otherwise. - * - * @attr ref android.R.styleable#TextView_accessibilityHeading - */ - public void setAccessibilityHeading(boolean isHeading) { - if (isHeading != mIsAccessibilityHeading) { - mIsAccessibilityHeading = isHeading; - notifyViewAccessibilityStateChangedIfNeeded( - AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); - } - } - - /** * Convenience method to append the specified text to the TextView's * display buffer, upgrading it to {@link android.widget.TextView.BufferType#EDITABLE} * if it was not already editable. @@ -9380,7 +9350,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int selectionStart = getSelectionStart(); final int selectionEnd = getSelectionEnd(); - return selectionStart >= 0 && selectionStart != selectionEnd; + return selectionStart >= 0 && selectionEnd > 0 && selectionStart != selectionEnd; } String getSelectedText() { @@ -10833,7 +10803,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener info.setText(getTextForAccessibility()); info.setHintText(mHint); info.setShowingHintText(isShowingHint()); - info.setHeading(mIsAccessibilityHeading); if (mBufferType == BufferType.EDITABLE) { info.setEditable(true); |