diff options
367 files changed, 20102 insertions, 6447 deletions
diff --git a/Android.bp b/Android.bp index 9ffdd1d6c8fe..15188ece0a6d 100644 --- a/Android.bp +++ b/Android.bp @@ -203,7 +203,6 @@ filegroup { name: "framework-non-updatable-sources", srcs: [ // Java/AIDL sources under frameworks/base - ":framework-appsearch-sources", ":framework-blobstore-sources", ":framework-core-sources", ":framework-drm-sources", @@ -262,6 +261,7 @@ filegroup { filegroup { name: "framework-updatable-sources", srcs: [ + ":framework-appsearch-sources", ":framework-sdkext-sources", ":framework-statsd-sources", ":updatable-media-srcs", @@ -427,6 +427,7 @@ java_library { defaults: ["framework-defaults"], srcs: [":framework-non-updatable-sources"], libs: [ + "framework-appsearch-stubs", // TODO(b/146167933): Use framework-statsd-stubs "framework-statsd", "framework-wifi-stubs", @@ -466,6 +467,7 @@ java_library { installable: false, // this lib is a build-only library static_libs: [ "framework-minus-apex", + "framework-appsearch", // TODO(b/146218515): should be framework-appsearch-stubs "framework-sdkext-stubs-systemapi", // TODO(b/146167933): Use framework-statsd-stubs instead. "framework-statsd", @@ -1658,6 +1660,8 @@ filegroup { "core/java/android/util/LocalLog.java", "core/java/android/util/TimeUtils.java", "core/java/com/android/internal/os/SomeArgs.java", + "core/java/com/android/internal/util/AsyncChannel.java", + "core/java/com/android/internal/util/BitwiseInputStream.java", "core/java/com/android/internal/util/FastXmlSerializer.java", "core/java/com/android/internal/util/HexDump.java", "core/java/com/android/internal/util/IState.java", diff --git a/apex/Android.bp b/apex/Android.bp index 85d72c76fb48..56f7db2c8dad 100644 --- a/apex/Android.bp +++ b/apex/Android.bp @@ -26,6 +26,9 @@ mainline_stubs_args = "--hide Typo " + "--hide UnavailableSymbol " +// TODO: remove this server classes are cleaned up. +mainline_stubs_args += "--hide-package com.android.server " + stubs_defaults { name: "framework-module-stubs-defaults-publicapi", args: mainline_stubs_args, diff --git a/apex/appsearch/Android.bp b/apex/appsearch/Android.bp index bcdcc7da8070..b014fdcb3df3 100644 --- a/apex/appsearch/Android.bp +++ b/apex/appsearch/Android.bp @@ -14,9 +14,11 @@ apex { name: "com.android.appsearch", - manifest: "apex_manifest.json", - + java_libs: [ + "framework-appsearch", + "service-appsearch", + ], key: "com.android.appsearch.key", certificate: ":com.android.appsearch.certificate", } diff --git a/apex/appsearch/framework/Android.bp b/apex/appsearch/framework/Android.bp index 0a65f7320d8a..3dc5a2c10b6b 100644 --- a/apex/appsearch/framework/Android.bp +++ b/apex/appsearch/framework/Android.bp @@ -23,17 +23,52 @@ filegroup { java_library { name: "framework-appsearch", - installable: false, - sdk_version: "core_platform", - srcs: [ - ":framework-appsearch-sources", - ], - aidl: { - export_include_dirs: [ - "java", - ], - }, + installable: true, + sdk_version: "core_platform", // TODO(b/146218515) should be core_current + srcs: [":framework-appsearch-sources"], libs: [ - "framework-minus-apex", + "framework-minus-apex", // TODO(b/146218515) should be framework-system-stubs ], } + +metalava_appsearch_docs_args = + "--hide-package com.android.server " + + "--error UnhiddenSystemApi " + + "--hide RequiresPermission " + + "--hide MissingPermission " + + "--hide BroadcastBehavior " + + "--hide HiddenSuperclass " + + "--hide DeprecationMismatch " + + "--hide UnavailableSymbol " + + "--hide SdkConstant " + + "--hide HiddenTypeParameter " + + "--hide Todo --hide Typo " + + "--hide HiddenTypedefConstant " + + "--show-annotation android.annotation.SystemApi " + +droidstubs { + name: "framework-appsearch-stubs-srcs", + srcs: [ + ":framework-annotations", + ":framework-appsearch-sources", + ], + aidl: { + include_dirs: ["frameworks/base/core/java"], + }, + args: metalava_appsearch_docs_args, + sdk_version: "core_current", + libs: ["android_system_stubs_current"], +} + +java_library { + name: "framework-appsearch-stubs", + srcs: [":framework-appsearch-stubs-srcs"], + aidl: { + export_include_dirs: [ + "java", + ], + }, + sdk_version: "core_current", + libs: ["android_system_stubs_current"], + installable: false, +} diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManagerFrameworkInitializer.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManagerFrameworkInitializer.java index fcebe3d9cf3c..02cc967e7daf 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManagerFrameworkInitializer.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManagerFrameworkInitializer.java @@ -15,19 +15,25 @@ */ package android.app.appsearch; +import android.annotation.SystemApi; import android.app.SystemServiceRegistry; import android.content.Context; /** - * This is where the AppSearchManagerService wrapper is registered. + * Class holding initialization code for the AppSearch module. * - * TODO(b/142567528): add comments when implement this class * @hide */ +@SystemApi public class AppSearchManagerFrameworkInitializer { + private AppSearchManagerFrameworkInitializer() {} /** - * TODO(b/142567528): add comments when implement this class + * Called by {@link SystemServiceRegistry}'s static initializer and registers all AppSearch + * services to {@link Context}, so that {@link Context#getSystemService} can return them. + * + * @throws IllegalStateException if this is called from anywhere besides + * {@link SystemServiceRegistry} */ public static void initialize() { SystemServiceRegistry.registerStaticService( diff --git a/apex/sdkext/Android.bp b/apex/sdkext/Android.bp index b9071f8b72b0..5369a96d965c 100644 --- a/apex/sdkext/Android.bp +++ b/apex/sdkext/Android.bp @@ -23,6 +23,7 @@ apex { java_libs: [ "framework-sdkext" ], prebuilts: [ "com.android.sdkext.ldconfig", + "cur_sdkinfo", "derive_sdk.rc", ], key: "com.android.sdkext.key", @@ -51,3 +52,33 @@ prebuilt_etc { filename: "ld.config.txt", installable: false, } + +python_binary_host { + name: "gen_sdkinfo", + srcs: [ + "sdk.proto", + "gen_sdkinfo.py", + ], + proto: { + canonical_path_from_root: false, + }, + version: { + py3: { + embedded_launcher: true, + }, + }, +} + +gensrcs { + name: "cur_sdkinfo_src", + srcs: [""], + tools: [ "gen_sdkinfo" ], + cmd: "$(location) -v 0 -o $(out)", +} + +prebuilt_etc { + name: "cur_sdkinfo", + src: ":cur_sdkinfo_src", + filename: "sdkinfo.binarypb", + installable: false, +} diff --git a/apex/sdkext/framework/java/android/os/ext/SdkExtensions.java b/apex/sdkext/framework/java/android/os/ext/SdkExtensions.java index d3b9397d45f2..a8a7effa9b6c 100644 --- a/apex/sdkext/framework/java/android/os/ext/SdkExtensions.java +++ b/apex/sdkext/framework/java/android/os/ext/SdkExtensions.java @@ -60,7 +60,7 @@ public class SdkExtensions { */ public static int getExtensionVersion(@SdkVersion int sdk) { if (sdk < VERSION_CODES.R) { - throw new IllegalArgumentException(); + throw new IllegalArgumentException(String.valueOf(sdk) + " does not have extensions"); } return R_EXTENSION_INT; } diff --git a/apex/sdkext/gen_sdkinfo.py b/apex/sdkext/gen_sdkinfo.py new file mode 100644 index 000000000000..5af478ba7fe6 --- /dev/null +++ b/apex/sdkext/gen_sdkinfo.py @@ -0,0 +1,19 @@ +import sdk_pb2 +import sys + +if __name__ == '__main__': + argv = sys.argv[1:] + if not len(argv) == 4 or sorted([argv[0], argv[2]]) != ['-o', '-v']: + print('usage: gen_sdkinfo -v <version> -o <output-file>') + sys.exit(1) + + for i in range(len(argv)): + if sys.argv[i] == '-o': + filename = sys.argv[i+1] + if sys.argv[i] == '-v': + version = int(sys.argv[i+1]) + + proto = sdk_pb2.SdkVersion() + proto.version = version + with open(filename, 'wb') as f: + f.write(proto.SerializeToString()) diff --git a/apex/sdkext/sdk.proto b/apex/sdkext/sdk.proto new file mode 100644 index 000000000000..d15b93552ff4 --- /dev/null +++ b/apex/sdkext/sdk.proto @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2019 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. + */ + +syntax = "proto3"; +package com.android.sdkext.proto; + +option java_outer_classname = "SdkProto"; +option optimize_for = LITE_RUNTIME; + +message SdkVersion { + int32 version = 1; +} diff --git a/api/current.txt b/api/current.txt index 1254a8526eb0..52dd1a16cc93 100644 --- a/api/current.txt +++ b/api/current.txt @@ -17246,6 +17246,7 @@ package android.hardware.camera2 { field public static final int REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR = 1; // 0x1 field public static final int REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME = 12; // 0xc field public static final int REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING = 10; // 0xa + field public static final int REQUEST_AVAILABLE_CAPABILITIES_OFFLINE_PROCESSING = 15; // 0xf field public static final int REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING = 4; // 0x4 field public static final int REQUEST_AVAILABLE_CAPABILITIES_RAW = 3; // 0x3 field public static final int REQUEST_AVAILABLE_CAPABILITIES_READ_SENSOR_SETTINGS = 5; // 0x5 @@ -22652,6 +22653,7 @@ package android.inputmethodservice { method public void onConfigureWindow(android.view.Window, boolean, boolean); method public android.view.View onCreateCandidatesView(); method public android.view.View onCreateExtractTextView(); + method @Nullable public android.view.inputmethod.InlineSuggestionsRequest onCreateInlineSuggestionsRequest(); method public android.inputmethodservice.AbstractInputMethodService.AbstractInputMethodImpl onCreateInputMethodInterface(); method public android.inputmethodservice.AbstractInputMethodService.AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface(); method public android.view.View onCreateInputView(); @@ -22668,6 +22670,7 @@ package android.inputmethodservice { method public void onFinishInput(); method public void onFinishInputView(boolean); method public void onInitializeInterface(); + method public boolean onInlineSuggestionsResponse(@NonNull android.view.inputmethod.InlineSuggestionsResponse); method public boolean onKeyDown(int, android.view.KeyEvent); method public boolean onKeyLongPress(int, android.view.KeyEvent); method public boolean onKeyMultiple(int, int, android.view.KeyEvent); @@ -35028,6 +35031,7 @@ package android.os { method public void dump(@NonNull java.io.FileDescriptor, @Nullable String[]) throws android.os.RemoteException; method public void dumpAsync(@NonNull java.io.FileDescriptor, @Nullable String[]) throws android.os.RemoteException; method @Nullable public String getInterfaceDescriptor() throws android.os.RemoteException; + method public static int getSuggestedMaxIpcSizeBytes(); method public boolean isBinderAlive(); method public void linkToDeath(@NonNull android.os.IBinder.DeathRecipient, int) throws android.os.RemoteException; method public boolean pingBinder(); @@ -35224,6 +35228,7 @@ package android.os { method public void readMap(@NonNull java.util.Map, @Nullable ClassLoader); method @Nullable public <T extends android.os.Parcelable> T readParcelable(@Nullable ClassLoader); method @Nullable public android.os.Parcelable[] readParcelableArray(@Nullable ClassLoader); + method @Nullable public android.os.Parcelable.Creator<?> readParcelableCreator(@Nullable ClassLoader); method @NonNull public <T extends android.os.Parcelable> java.util.List<T> readParcelableList(@NonNull java.util.List<T>, @Nullable ClassLoader); method @Nullable public android.os.PersistableBundle readPersistableBundle(); method @Nullable public android.os.PersistableBundle readPersistableBundle(@Nullable ClassLoader); @@ -35271,6 +35276,7 @@ package android.os { method public void writeNoException(); method public void writeParcelable(@Nullable android.os.Parcelable, int); method public <T extends android.os.Parcelable> void writeParcelableArray(@Nullable T[], int); + method public void writeParcelableCreator(@NonNull android.os.Parcelable); method public <T extends android.os.Parcelable> void writeParcelableList(@Nullable java.util.List<T>, int); method public void writePersistableBundle(@Nullable android.os.PersistableBundle); method public void writeSerializable(@Nullable java.io.Serializable); @@ -36021,7 +36027,8 @@ package android.os.storage { method @WorkerThread public long getCacheSizeBytes(@NonNull java.util.UUID) throws java.io.IOException; method public String getMountedObbPath(String); method @NonNull public android.os.storage.StorageVolume getPrimaryStorageVolume(); - method @Nullable public android.os.storage.StorageVolume getStorageVolume(java.io.File); + method @NonNull public java.util.List<android.os.storage.StorageVolume> getRecentStorageVolumes(); + method @Nullable public android.os.storage.StorageVolume getStorageVolume(@NonNull java.io.File); method @NonNull public android.os.storage.StorageVolume getStorageVolume(@NonNull android.net.Uri); method @NonNull public java.util.List<android.os.storage.StorageVolume> getStorageVolumes(); method @NonNull public java.util.UUID getUuidForPath(@NonNull java.io.File) throws java.io.IOException; @@ -36047,6 +36054,8 @@ package android.os.storage { method @NonNull public android.content.Intent createOpenDocumentTreeIntent(); method public int describeContents(); method public String getDescription(android.content.Context); + method @Nullable public java.io.File getDirectory(); + method @Nullable public String getMediaStoreVolumeName(); method public String getState(); method @Nullable public String getUuid(); method public boolean isEmulated(); @@ -38709,19 +38718,21 @@ package android.provider { public final class MediaStore { ctor public MediaStore(); + method @NonNull public static android.app.PendingIntent createDeleteRequest(@NonNull android.content.ContentResolver, @NonNull java.util.Collection<android.net.Uri>); + method @NonNull public static android.app.PendingIntent createFavoriteRequest(@NonNull android.content.ContentResolver, @NonNull java.util.Collection<android.net.Uri>, boolean); + method @NonNull public static android.app.PendingIntent createTrashRequest(@NonNull android.content.ContentResolver, @NonNull java.util.Collection<android.net.Uri>, boolean); + method @NonNull public static android.app.PendingIntent createWriteRequest(@NonNull android.content.ContentResolver, @NonNull java.util.Collection<android.net.Uri>); method @Nullable public static android.net.Uri getDocumentUri(@NonNull android.content.Context, @NonNull android.net.Uri); method @NonNull public static java.util.Set<java.lang.String> getExternalVolumeNames(@NonNull android.content.Context); method public static android.net.Uri getMediaScannerUri(); method @Nullable public static android.net.Uri getMediaUri(@NonNull android.content.Context, @NonNull android.net.Uri); + method @NonNull public static java.util.Set<java.lang.String> getRecentExternalVolumeNames(@NonNull android.content.Context); method public static boolean getRequireOriginal(@NonNull android.net.Uri); method @NonNull public static String getVersion(@NonNull android.content.Context); method @NonNull public static String getVersion(@NonNull android.content.Context, @NonNull String); method @NonNull public static String getVolumeName(@NonNull android.net.Uri); method @Deprecated @NonNull public static android.net.Uri setIncludePending(@NonNull android.net.Uri); method @NonNull public static android.net.Uri setRequireOriginal(@NonNull android.net.Uri); - method public static void trash(@NonNull android.content.Context, @NonNull android.net.Uri); - method public static void trash(@NonNull android.content.Context, @NonNull android.net.Uri, long); - method public static void untrash(@NonNull android.content.Context, @NonNull android.net.Uri); field public static final String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE"; field public static final String ACTION_IMAGE_CAPTURE_SECURE = "android.media.action.IMAGE_CAPTURE_SECURE"; field public static final String ACTION_REVIEW = "android.provider.action.REVIEW"; @@ -38981,6 +38992,7 @@ package android.provider { method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long); method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long, long); method @Deprecated public static android.net.Uri getContentUri(String); + method @Deprecated @NonNull public static android.util.Size getKindSize(int); method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, int, android.graphics.BitmapFactory.Options); method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, long, int, android.graphics.BitmapFactory.Options); method @Deprecated public static final android.database.Cursor query(android.content.ContentResolver, android.net.Uri, String[]); @@ -39063,6 +39075,7 @@ package android.provider { method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long); method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long, long); method @Deprecated public static android.net.Uri getContentUri(String); + method @Deprecated @NonNull public static android.util.Size getKindSize(int); method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, int, android.graphics.BitmapFactory.Options); method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, long, int, android.graphics.BitmapFactory.Options); field @Deprecated public static final String DATA = "_data"; @@ -41569,7 +41582,8 @@ package android.service.autofill { method @NonNull public java.util.List<android.service.autofill.FillContext> getFillContexts(); method public int getFlags(); method public int getId(); - method public void writeToParcel(android.os.Parcel, int); + method @Nullable public android.view.inputmethod.InlineSuggestionsRequest getInlineSuggestionsRequest(); + method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.service.autofill.FillRequest> CREATOR; field public static final int FLAG_COMPATIBILITY_MODE_REQUEST = 2; // 0x2 field public static final int FLAG_MANUAL_REQUEST = 1; // 0x1 @@ -41586,6 +41600,7 @@ package android.service.autofill { public static final class FillResponse.Builder { ctor public FillResponse.Builder(); method @NonNull public android.service.autofill.FillResponse.Builder addDataset(@Nullable android.service.autofill.Dataset); + method @NonNull public android.service.autofill.FillResponse.Builder addInlineSuggestionSlice(@NonNull android.app.slice.Slice); method @NonNull public android.service.autofill.FillResponse build(); method @NonNull public android.service.autofill.FillResponse.Builder disableAutofill(long); method @NonNull public android.service.autofill.FillResponse.Builder setAuthentication(@NonNull android.view.autofill.AutofillId[], @Nullable android.content.IntentSender, @Nullable android.widget.RemoteViews); @@ -43891,6 +43906,7 @@ package android.telecom { field public static final String EVENT_CALL_PULL_FAILED = "android.telecom.event.CALL_PULL_FAILED"; field public static final String EVENT_CALL_REMOTELY_HELD = "android.telecom.event.CALL_REMOTELY_HELD"; field public static final String EVENT_CALL_REMOTELY_UNHELD = "android.telecom.event.CALL_REMOTELY_UNHELD"; + field public static final String EVENT_CALL_SWITCH_FAILED = "android.telecom.event.CALL_SWITCH_FAILED"; field public static final String EVENT_MERGE_COMPLETE = "android.telecom.event.MERGE_COMPLETE"; field public static final String EVENT_MERGE_START = "android.telecom.event.MERGE_START"; field public static final String EVENT_ON_HOLD_TONE_END = "android.telecom.event.ON_HOLD_TONE_END"; @@ -44600,6 +44616,7 @@ package android.telephony { method public void notifyConfigChangedForSubId(int); field public static final String ACTION_CARRIER_CONFIG_CHANGED = "android.telephony.action.CARRIER_CONFIG_CHANGED"; field public static final int DATA_CYCLE_THRESHOLD_DISABLED = -2; // 0xfffffffe + field public static final int DATA_CYCLE_USE_PLATFORM_DEFAULT = -1; // 0xffffffff field public static final String EXTRA_SLOT_INDEX = "android.telephony.extra.SLOT_INDEX"; field public static final String EXTRA_SUBSCRIPTION_INDEX = "android.telephony.extra.SUBSCRIPTION_INDEX"; field public static final String KEY_5G_NR_SSRSRP_THRESHOLDS_INT_ARRAY = "5g_nr_ssrsrp_thresholds_int_array"; @@ -44610,6 +44627,7 @@ package android.telephony { field public static final String KEY_ALLOW_ADD_CALL_DURING_VIDEO_CALL_BOOL = "allow_add_call_during_video_call"; field public static final String KEY_ALLOW_EMERGENCY_NUMBERS_IN_CALL_LOG_BOOL = "allow_emergency_numbers_in_call_log_bool"; field public static final String KEY_ALLOW_EMERGENCY_VIDEO_CALLS_BOOL = "allow_emergency_video_calls_bool"; + field public static final String KEY_ALLOW_HOLDING_VIDEO_CALL_BOOL = "allow_holding_video_call"; field public static final String KEY_ALLOW_HOLD_CALL_DURING_EMERGENCY_BOOL = "allow_hold_call_during_emergency_bool"; field public static final String KEY_ALLOW_LOCAL_DTMF_TONES_BOOL = "allow_local_dtmf_tones_bool"; field public static final String KEY_ALLOW_MERGE_WIFI_CALLS_WHEN_VOWIFI_OFF_BOOL = "allow_merge_wifi_calls_when_vowifi_off_bool"; @@ -44659,11 +44677,16 @@ package android.telephony { field public static final String KEY_CI_ACTION_ON_SYS_UPDATE_EXTRA_STRING = "ci_action_on_sys_update_extra_string"; field public static final String KEY_CI_ACTION_ON_SYS_UPDATE_EXTRA_VAL_STRING = "ci_action_on_sys_update_extra_val_string"; field public static final String KEY_CI_ACTION_ON_SYS_UPDATE_INTENT_STRING = "ci_action_on_sys_update_intent_string"; - field public static final String KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING = "config_ims_package_override_string"; + field public static final String KEY_CONFIG_IMS_MMTEL_PACKAGE_OVERRIDE_STRING = "config_ims_mmtel_package_override_string"; + field @Deprecated public static final String KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING = "config_ims_package_override_string"; + field public static final String KEY_CONFIG_IMS_RCS_PACKAGE_OVERRIDE_STRING = "config_ims_rcs_package_override_string"; field public static final String KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING = "config_plans_package_override_string"; field public static final String KEY_CONFIG_TELEPHONY_USE_OWN_NUMBER_FOR_VOICEMAIL_BOOL = "config_telephony_use_own_number_for_voicemail_bool"; field public static final String KEY_CSP_ENABLED_BOOL = "csp_enabled_bool"; + field public static final String KEY_DATA_LIMIT_NOTIFICATION_BOOL = "data_limit_notification_bool"; field public static final String KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG = "data_limit_threshold_bytes_long"; + field public static final String KEY_DATA_RAPID_NOTIFICATION_BOOL = "data_rapid_notification_bool"; + field public static final String KEY_DATA_WARNING_NOTIFICATION_BOOL = "data_warning_notification_bool"; field public static final String KEY_DATA_WARNING_THRESHOLD_BYTES_LONG = "data_warning_threshold_bytes_long"; field public static final String KEY_DEFAULT_SIM_CALL_MANAGER_STRING = "default_sim_call_manager_string"; field public static final String KEY_DEFAULT_VM_NUMBER_ROAMING_AND_IMS_UNREGISTERED_STRING = "default_vm_number_roaming_and_ims_unregistered_string"; @@ -45223,6 +45246,7 @@ package android.telephony { method public void onDataConnectionStateChanged(int); method public void onDataConnectionStateChanged(int, int); method public void onMessageWaitingIndicatorChanged(boolean); + method @RequiresPermission("android.permission.MODIFY_PHONE_STATE") public void onPreciseDataConnectionStateChanged(@NonNull android.telephony.PreciseDataConnectionState); method public void onServiceStateChanged(android.telephony.ServiceState); method @Deprecated public void onSignalStrengthChanged(int); method public void onSignalStrengthsChanged(android.telephony.SignalStrength); @@ -45237,12 +45261,23 @@ package android.telephony { field public static final int LISTEN_EMERGENCY_NUMBER_LIST = 16777216; // 0x1000000 field public static final int LISTEN_MESSAGE_WAITING_INDICATOR = 4; // 0x4 field public static final int LISTEN_NONE = 0; // 0x0 + field @RequiresPermission("android.permission.MODIFY_PHONE_STATE") public static final int LISTEN_PRECISE_DATA_CONNECTION_STATE = 4096; // 0x1000 field public static final int LISTEN_SERVICE_STATE = 1; // 0x1 field @Deprecated public static final int LISTEN_SIGNAL_STRENGTH = 2; // 0x2 field public static final int LISTEN_SIGNAL_STRENGTHS = 256; // 0x100 field public static final int LISTEN_USER_MOBILE_DATA_STATE = 524288; // 0x80000 } + public final class PreciseDataConnectionState implements android.os.Parcelable { + method public int describeContents(); + method public int getLastCauseCode(); + method @Nullable public android.net.LinkProperties getLinkProperties(); + method public int getNetworkType(); + method public int getState(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.telephony.PreciseDataConnectionState> CREATOR; + } + public final class RadioAccessSpecifier implements android.os.Parcelable { ctor public RadioAccessSpecifier(int, int[], int[]); method public int describeContents(); @@ -45755,6 +45790,7 @@ package android.telephony { field public static final int DATA_CONNECTED = 2; // 0x2 field public static final int DATA_CONNECTING = 1; // 0x1 field public static final int DATA_DISCONNECTED = 0; // 0x0 + field public static final int DATA_DISCONNECTING = 4; // 0x4 field public static final int DATA_SUSPENDED = 3; // 0x3 field public static final int DATA_UNKNOWN = -1; // 0xffffffff field public static final String EXTRA_CALL_VOICEMAIL_INTENT = "android.telephony.extra.CALL_VOICEMAIL_INTENT"; @@ -51081,6 +51117,9 @@ package android.view { method public boolean dispatchUnhandledMove(android.view.View, int); method protected void dispatchVisibilityChanged(@NonNull android.view.View, int); method public void dispatchWindowFocusChanged(boolean); + method public void dispatchWindowInsetsAnimationFinished(@NonNull android.view.WindowInsetsAnimationCallback.InsetsAnimation); + method @NonNull public android.view.WindowInsets dispatchWindowInsetsAnimationProgress(@NonNull android.view.WindowInsets); + method @NonNull public android.view.WindowInsetsAnimationCallback.AnimationBounds dispatchWindowInsetsAnimationStarted(@NonNull android.view.WindowInsetsAnimationCallback.InsetsAnimation, @NonNull android.view.WindowInsetsAnimationCallback.AnimationBounds); method public void dispatchWindowSystemUiVisiblityChanged(int); method public void dispatchWindowVisibilityChanged(int); method @CallSuper public void draw(android.graphics.Canvas); @@ -51261,6 +51300,7 @@ package android.view { method @android.view.ViewDebug.ExportedProperty(category="layout") public final int getWidth(); method protected int getWindowAttachCount(); method public android.view.WindowId getWindowId(); + method @Nullable public android.view.WindowInsetsController getWindowInsetsController(); method public int getWindowSystemUiVisibility(); method public android.os.IBinder getWindowToken(); method public int getWindowVisibility(); @@ -51599,6 +51639,7 @@ package android.view { method public void setVisibility(int); method @Deprecated public void setWillNotCacheDrawing(boolean); method public void setWillNotDraw(boolean); + method public void setWindowInsetsAnimationCallback(@Nullable android.view.WindowInsetsAnimationCallback); method public void setX(float); method public void setY(float); method public void setZ(float); @@ -52485,6 +52526,7 @@ package android.view { method public android.transition.Transition getExitTransition(); method protected final int getFeatures(); method protected final int getForcedWindowFlags(); + method @Nullable public android.view.WindowInsetsController getInsetsController(); method @NonNull public abstract android.view.LayoutInflater getLayoutInflater(); method protected final int getLocalFeatures(); method public android.media.session.MediaController getMediaController(); @@ -52700,7 +52742,9 @@ package android.view { method @NonNull public android.view.WindowInsets consumeStableInsets(); method @NonNull public android.view.WindowInsets consumeSystemWindowInsets(); method @Nullable public android.view.DisplayCutout getDisplayCutout(); + method @NonNull public android.graphics.Insets getInsets(int); method @NonNull public android.graphics.Insets getMandatorySystemGestureInsets(); + method @NonNull public android.graphics.Insets getMaxInsets(int) throws java.lang.IllegalArgumentException; method public int getStableInsetBottom(); method public int getStableInsetLeft(); method public int getStableInsetRight(); @@ -52719,6 +52763,7 @@ package android.view { method @NonNull public android.view.WindowInsets inset(@IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0) int); method public boolean isConsumed(); method public boolean isRound(); + method public boolean isVisible(int); method @Deprecated @NonNull public android.view.WindowInsets replaceSystemWindowInsets(int, int, int, int); method @Deprecated @NonNull public android.view.WindowInsets replaceSystemWindowInsets(android.graphics.Rect); } @@ -52728,11 +52773,78 @@ package android.view { ctor public WindowInsets.Builder(@NonNull android.view.WindowInsets); method @NonNull public android.view.WindowInsets build(); method @NonNull public android.view.WindowInsets.Builder setDisplayCutout(@Nullable android.view.DisplayCutout); + method @NonNull public android.view.WindowInsets.Builder setInsets(int, @NonNull android.graphics.Insets); method @NonNull public android.view.WindowInsets.Builder setMandatorySystemGestureInsets(@NonNull android.graphics.Insets); + method @NonNull public android.view.WindowInsets.Builder setMaxInsets(int, @NonNull android.graphics.Insets) throws java.lang.IllegalArgumentException; method @NonNull public android.view.WindowInsets.Builder setStableInsets(@NonNull android.graphics.Insets); method @NonNull public android.view.WindowInsets.Builder setSystemGestureInsets(@NonNull android.graphics.Insets); method @NonNull public android.view.WindowInsets.Builder setSystemWindowInsets(@NonNull android.graphics.Insets); method @NonNull public android.view.WindowInsets.Builder setTappableElementInsets(@NonNull android.graphics.Insets); + method @NonNull public android.view.WindowInsets.Builder setVisible(int, boolean); + } + + public static final class WindowInsets.Type { + method public static int all(); + method public static int captionBar(); + method public static int ime(); + method public static int mandatorySystemGestures(); + method public static int navigationBars(); + method public static int statusBars(); + method public static int systemBars(); + method public static int systemGestures(); + method public static int tappableElement(); + method public static int windowDecor(); + } + + public interface WindowInsetsAnimationCallback { + method public default void onFinished(@NonNull android.view.WindowInsetsAnimationCallback.InsetsAnimation); + method @NonNull public android.view.WindowInsets onProgress(@NonNull android.view.WindowInsets); + method @NonNull public default android.view.WindowInsetsAnimationCallback.AnimationBounds onStarted(@NonNull android.view.WindowInsetsAnimationCallback.InsetsAnimation, @NonNull android.view.WindowInsetsAnimationCallback.AnimationBounds); + } + + public static final class WindowInsetsAnimationCallback.AnimationBounds { + ctor public WindowInsetsAnimationCallback.AnimationBounds(@NonNull android.graphics.Insets, @NonNull android.graphics.Insets); + method @NonNull public android.graphics.Insets getLowerBound(); + method @NonNull public android.graphics.Insets getUpperBound(); + method @NonNull public android.view.WindowInsetsAnimationCallback.AnimationBounds inset(@NonNull android.graphics.Insets); + } + + public static final class WindowInsetsAnimationCallback.InsetsAnimation { + ctor public WindowInsetsAnimationCallback.InsetsAnimation(int, @Nullable android.view.animation.Interpolator, long); + method @FloatRange(from=0.0f, to=1.0f) public float getAlpha(); + method public long getDurationMillis(); + method @FloatRange(from=0.0f, to=1.0f) public float getFraction(); + method public float getInterpolatedFraction(); + method @Nullable public android.view.animation.Interpolator getInterpolator(); + method public int getTypeMask(); + method public void setDuration(long); + method public void setFraction(@FloatRange(from=0.0f, to=1.0f) float); + } + + public interface WindowInsetsAnimationControlListener { + method public void onCancelled(); + method public void onReady(@NonNull android.view.WindowInsetsAnimationController, int); + } + + public interface WindowInsetsAnimationController { + method public void finish(boolean); + method public float getCurrentAlpha(); + method @FloatRange(from=0.0f, to=1.0f) public float getCurrentFraction(); + method @NonNull public android.graphics.Insets getCurrentInsets(); + method @NonNull public android.graphics.Insets getHiddenStateInsets(); + method @NonNull public android.graphics.Insets getShownStateInsets(); + method public int getTypes(); + method public void setInsetsAndAlpha(@Nullable android.graphics.Insets, @FloatRange(from=0.0f, to=1.0f) float, @FloatRange(from=0.0f, to=1.0f) float); + } + + public interface WindowInsetsController { + method public default void controlInputMethodAnimation(long, @NonNull android.view.WindowInsetsAnimationControlListener); + method public default void hideInputMethod(); + method public void setSystemBarsAppearance(int); + method public void setSystemBarsBehavior(int); + method public default void showInputMethod(); + field public static final int APPEARANCE_LIGHT_NAVIGATION_BARS = 16; // 0x10 + field public static final int APPEARANCE_LIGHT_STATUS_BARS = 8; // 0x8 } public interface WindowManager extends android.view.ViewManager { diff --git a/api/removed.txt b/api/removed.txt index 1a2f43450d7b..8b30d0a5cf39 100644 --- a/api/removed.txt +++ b/api/removed.txt @@ -436,11 +436,12 @@ package android.provider { } public final class MediaStore { - method @Deprecated @NonNull public static android.net.Uri createPending(@NonNull android.content.Context, @NonNull android.provider.MediaStore.PendingParams); method @Deprecated @NonNull public static java.util.Set<java.lang.String> getAllVolumeNames(@NonNull android.content.Context); method @Deprecated public static boolean getIncludePending(@NonNull android.net.Uri); - method @Deprecated @NonNull public static android.provider.MediaStore.PendingSession openPending(@NonNull android.content.Context, @NonNull android.net.Uri); method @Deprecated @NonNull public static android.net.Uri setIncludeTrashed(@NonNull android.net.Uri); + method @Deprecated public static void trash(@NonNull android.content.Context, @NonNull android.net.Uri); + method @Deprecated public static void trash(@NonNull android.content.Context, @NonNull android.net.Uri, long); + method @Deprecated public static void untrash(@NonNull android.content.Context, @NonNull android.net.Uri); } public static interface MediaStore.Audio.AudioColumns extends android.provider.MediaStore.MediaColumns { @@ -470,22 +471,6 @@ package android.provider { field @Deprecated public static final String GROUP_ID = "group_id"; } - @Deprecated public static class MediaStore.PendingParams { - ctor public MediaStore.PendingParams(@NonNull android.net.Uri, @NonNull String, @NonNull String); - method public void setDownloadUri(@Nullable android.net.Uri); - method public void setRefererUri(@Nullable android.net.Uri); - method public void setRelativePath(@Nullable String); - } - - @Deprecated public static class MediaStore.PendingSession implements java.lang.AutoCloseable { - method public void abandon(); - method public void close(); - method public void notifyProgress(@IntRange(from=0, to=100) int); - method @NonNull public android.os.ParcelFileDescriptor open() throws java.io.FileNotFoundException; - method @NonNull public java.io.OutputStream openOutputStream() throws java.io.FileNotFoundException; - method @NonNull public android.net.Uri publish(); - } - public static interface MediaStore.Video.VideoColumns extends android.provider.MediaStore.MediaColumns { field public static final String ALBUM = "album"; field public static final String ARTIST = "artist"; diff --git a/api/system-current.txt b/api/system-current.txt index ebcda3d25fb3..11089277a067 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -546,10 +546,12 @@ package android.app { method public final android.os.IBinder onBind(android.content.Intent); method @Deprecated public void onGetInstantAppIntentFilter(@Nullable int[], @NonNull String, @NonNull android.app.InstantAppResolverService.InstantAppResolutionCallback); method @Deprecated public void onGetInstantAppIntentFilter(@NonNull android.content.Intent, @Nullable int[], @NonNull String, @NonNull android.app.InstantAppResolverService.InstantAppResolutionCallback); - method public void onGetInstantAppIntentFilter(@NonNull android.content.Intent, @Nullable int[], @NonNull android.os.UserHandle, @NonNull String, @NonNull android.app.InstantAppResolverService.InstantAppResolutionCallback); + method @Deprecated public void onGetInstantAppIntentFilter(@NonNull android.content.Intent, @Nullable int[], @NonNull android.os.UserHandle, @NonNull String, @NonNull android.app.InstantAppResolverService.InstantAppResolutionCallback); + method public void onGetInstantAppIntentFilter(@NonNull android.content.pm.InstantAppRequestInfo, @NonNull android.app.InstantAppResolverService.InstantAppResolutionCallback); method @Deprecated public void onGetInstantAppResolveInfo(@Nullable int[], @NonNull String, @NonNull android.app.InstantAppResolverService.InstantAppResolutionCallback); method @Deprecated public void onGetInstantAppResolveInfo(@NonNull android.content.Intent, @Nullable int[], @NonNull String, @NonNull android.app.InstantAppResolverService.InstantAppResolutionCallback); - method public void onGetInstantAppResolveInfo(@NonNull android.content.Intent, @Nullable int[], @NonNull android.os.UserHandle, @NonNull String, @NonNull android.app.InstantAppResolverService.InstantAppResolutionCallback); + method @Deprecated public void onGetInstantAppResolveInfo(@NonNull android.content.Intent, @Nullable int[], @NonNull android.os.UserHandle, @NonNull String, @NonNull android.app.InstantAppResolverService.InstantAppResolutionCallback); + method public void onGetInstantAppResolveInfo(@NonNull android.content.pm.InstantAppRequestInfo, @NonNull android.app.InstantAppResolverService.InstantAppResolutionCallback); } public static final class InstantAppResolverService.InstantAppResolutionCallback { @@ -809,6 +811,14 @@ package android.app.admin { } +package android.app.appsearch { + + public class AppSearchManagerFrameworkInitializer { + method public static void initialize(); + } + +} + package android.app.assist { public static class AssistStructure.ViewNode { @@ -1350,6 +1360,14 @@ package android.bluetooth { field public static final int OPTIONAL_CODECS_SUPPORT_UNKNOWN = -1; // 0xffffffff } + public final class BluetoothA2dpSink implements android.bluetooth.BluetoothProfile { + method public void finalize(); + method @RequiresPermission(android.Manifest.permission.BLUETOOTH) public int getConnectionPolicy(@Nullable android.bluetooth.BluetoothDevice); + method @RequiresPermission(android.Manifest.permission.BLUETOOTH) public boolean isAudioPlaying(@Nullable android.bluetooth.BluetoothDevice); + method @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean setConnectionPolicy(@Nullable android.bluetooth.BluetoothDevice, int); + field @RequiresPermission(android.Manifest.permission.BLUETOOTH) public static final String ACTION_CONNECTION_STATE_CHANGED = "android.bluetooth.a2dp-sink.profile.action.CONNECTION_STATE_CHANGED"; + } + public final class BluetoothAdapter { method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public boolean addOnMetadataChangedListener(@NonNull android.bluetooth.BluetoothDevice, @NonNull java.util.concurrent.Executor, @NonNull android.bluetooth.BluetoothAdapter.OnMetadataChangedListener); method @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN) public boolean connectAllEnabledProfiles(@NonNull android.bluetooth.BluetoothDevice); @@ -1920,6 +1938,18 @@ package android.content.pm { field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.InstantAppIntentFilter> CREATOR; } + public final class InstantAppRequestInfo implements android.os.Parcelable { + ctor public InstantAppRequestInfo(@NonNull android.content.Intent, @Nullable int[], @NonNull android.os.UserHandle, boolean, @NonNull String); + method public int describeContents(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.InstantAppRequestInfo> CREATOR; + field @Nullable public final int[] hostDigestPrefix; + field @NonNull public final android.content.Intent intent; + field public final boolean isRequesterInstantApp; + field @NonNull public final String token; + field @NonNull public final android.os.UserHandle userHandle; + } + public final class InstantAppResolveInfo implements android.os.Parcelable { ctor public InstantAppResolveInfo(@NonNull android.content.pm.InstantAppResolveInfo.InstantAppDigest, @Nullable String, @Nullable java.util.List<android.content.pm.InstantAppIntentFilter>, int); ctor public InstantAppResolveInfo(@NonNull android.content.pm.InstantAppResolveInfo.InstantAppDigest, @Nullable String, @Nullable java.util.List<android.content.pm.InstantAppIntentFilter>, long, @Nullable android.os.Bundle); @@ -4173,18 +4203,20 @@ package android.media.audiopolicy { package android.media.session { public final class MediaSessionManager { - method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.session.MediaSessionManager.Callback); + method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void addOnMediaKeyEventDispatchedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.session.MediaSessionManager.OnMediaKeyEventDispatchedListener); + method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void addOnMediaKeyEventSessionChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.session.MediaSessionManager.OnMediaKeyEventSessionChangedListener); + method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void removeOnMediaKeyEventDispatchedListener(@NonNull android.media.session.MediaSessionManager.OnMediaKeyEventDispatchedListener); + method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void removeOnMediaKeyEventSessionChangedListener(@NonNull android.media.session.MediaSessionManager.OnMediaKeyEventSessionChangedListener); method @RequiresPermission(android.Manifest.permission.SET_MEDIA_KEY_LISTENER) public void setOnMediaKeyListener(android.media.session.MediaSessionManager.OnMediaKeyListener, @Nullable android.os.Handler); method @RequiresPermission(android.Manifest.permission.SET_VOLUME_KEY_LONG_PRESS_LISTENER) public void setOnVolumeKeyLongPressListener(android.media.session.MediaSessionManager.OnVolumeKeyLongPressListener, @Nullable android.os.Handler); - method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void unregisterCallback(@NonNull android.media.session.MediaSessionManager.Callback); } - public abstract static class MediaSessionManager.Callback { - ctor public MediaSessionManager.Callback(); - method public abstract void onAddressedPlayerChanged(android.media.session.MediaSession.Token); - method public abstract void onAddressedPlayerChanged(android.content.ComponentName); - method public abstract void onMediaKeyEventDispatched(android.view.KeyEvent, android.media.session.MediaSession.Token); - method public abstract void onMediaKeyEventDispatched(android.view.KeyEvent, android.content.ComponentName); + public static interface MediaSessionManager.OnMediaKeyEventDispatchedListener { + method public default void onMediaKeyEventDispatched(@NonNull android.view.KeyEvent, @NonNull String, @NonNull android.media.session.MediaSession.Token); + } + + public static interface MediaSessionManager.OnMediaKeyEventSessionChangedListener { + method public default void onMediaKeyEventSessionChanged(@NonNull String, @Nullable android.media.session.MediaSession.Token); } public static interface MediaSessionManager.OnMediaKeyListener { @@ -4538,6 +4570,14 @@ package android.net { method public void onUpstreamChanged(@Nullable android.net.Network); } + public class InvalidPacketException extends java.lang.Exception { + ctor public InvalidPacketException(int); + field public static final int ERROR_INVALID_IP_ADDRESS = -21; // 0xffffffeb + field public static final int ERROR_INVALID_LENGTH = -23; // 0xffffffe9 + field public static final int ERROR_INVALID_PORT = -22; // 0xffffffea + field public final int error; + } + public final class IpConfiguration implements android.os.Parcelable { ctor public IpConfiguration(); ctor public IpConfiguration(@NonNull android.net.IpConfiguration); @@ -4588,6 +4628,15 @@ package android.net { method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public android.net.IpSecTransform buildTunnelModeTransform(@NonNull java.net.InetAddress, @NonNull android.net.IpSecManager.SecurityParameterIndex) throws java.io.IOException, android.net.IpSecManager.ResourceUnavailableException, android.net.IpSecManager.SpiUnavailableException; } + public class KeepalivePacketData { + ctor protected KeepalivePacketData(@NonNull java.net.InetAddress, int, @NonNull java.net.InetAddress, int, @NonNull byte[]) throws android.net.InvalidPacketException; + method @NonNull public byte[] getPacket(); + field @NonNull public final java.net.InetAddress dstAddress; + field public final int dstPort; + field @NonNull public final java.net.InetAddress srcAddress; + field public final int srcPort; + } + public class LinkAddress implements android.os.Parcelable { ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int, int, int); ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int); @@ -4867,7 +4916,7 @@ package android.net.ipsec.ike { method @NonNull public java.util.List<android.net.LinkAddress> getInternalAddresses(); method @NonNull public java.util.List<java.net.InetAddress> getInternalDhcpServers(); method @NonNull public java.util.List<java.net.InetAddress> getInternalDnsServers(); - method @NonNull public java.util.List<android.net.LinkAddress> getInternalSubnets(); + method @NonNull public java.util.List<android.net.IpPrefix> getInternalSubnets(); method @NonNull public java.util.List<android.net.ipsec.ike.IkeTrafficSelector> getOutboundTrafficSelectors(); } @@ -4936,6 +4985,7 @@ package android.net.ipsec.ike { public final class IkeSessionConfiguration { ctor public IkeSessionConfiguration(); method @NonNull public String getRemoteApplicationVersion(); + method @NonNull public java.util.List<byte[]> getRemoteVendorIDs(); method public boolean isIkeExtensionEnabled(int); field public static final int EXTENSION_TYPE_FRAGMENTATION = 1; // 0x1 field public static final int EXTENSION_TYPE_MOBIKE = 2; // 0x2 @@ -4955,9 +5005,9 @@ package android.net.ipsec.ike { ctor public IkeSessionParams.Builder(); method @NonNull public android.net.ipsec.ike.IkeSessionParams.Builder addSaProposal(@NonNull android.net.ipsec.ike.IkeSaProposal); method @NonNull public android.net.ipsec.ike.IkeSessionParams build(); - method @NonNull public android.net.ipsec.ike.IkeSessionParams.Builder setAuthDigitalSignature(@NonNull java.security.cert.X509Certificate, @NonNull java.security.cert.X509Certificate, @NonNull java.security.PrivateKey); - method @NonNull public android.net.ipsec.ike.IkeSessionParams.Builder setAuthDigitalSignature(@NonNull java.security.cert.X509Certificate, @NonNull java.security.cert.X509Certificate, @NonNull java.util.List<java.security.cert.X509Certificate>, @NonNull java.security.PrivateKey); - method @NonNull public android.net.ipsec.ike.IkeSessionParams.Builder setAuthEap(@NonNull java.security.cert.X509Certificate, @NonNull android.net.eap.EapSessionConfig); + method @NonNull public android.net.ipsec.ike.IkeSessionParams.Builder setAuthDigitalSignature(@Nullable java.security.cert.X509Certificate, @NonNull java.security.cert.X509Certificate, @NonNull java.security.PrivateKey); + method @NonNull public android.net.ipsec.ike.IkeSessionParams.Builder setAuthDigitalSignature(@Nullable java.security.cert.X509Certificate, @NonNull java.security.cert.X509Certificate, @NonNull java.util.List<java.security.cert.X509Certificate>, @NonNull java.security.PrivateKey); + method @NonNull public android.net.ipsec.ike.IkeSessionParams.Builder setAuthEap(@Nullable java.security.cert.X509Certificate, @NonNull android.net.eap.EapSessionConfig); method @NonNull public android.net.ipsec.ike.IkeSessionParams.Builder setAuthPsk(@NonNull byte[]); method @NonNull public android.net.ipsec.ike.IkeSessionParams.Builder setLocalIdentification(@NonNull android.net.ipsec.ike.IkeIdentification); method @NonNull public android.net.ipsec.ike.IkeSessionParams.Builder setRemoteIdentification(@NonNull android.net.ipsec.ike.IkeIdentification); @@ -4975,7 +5025,7 @@ package android.net.ipsec.ike { } public static class IkeSessionParams.IkeAuthDigitalSignRemoteConfig extends android.net.ipsec.ike.IkeSessionParams.IkeAuthConfig { - method @NonNull public java.security.cert.X509Certificate getRemoteCaCert(); + method @Nullable public java.security.cert.X509Certificate getRemoteCaCert(); } public static class IkeSessionParams.IkeAuthEapConfig extends android.net.ipsec.ike.IkeSessionParams.IkeAuthConfig { @@ -5039,12 +5089,10 @@ package android.net.ipsec.ike { ctor public TunnelModeChildSessionParams.Builder(); method @NonNull public android.net.ipsec.ike.TunnelModeChildSessionParams.Builder addInboundTrafficSelectors(@NonNull android.net.ipsec.ike.IkeTrafficSelector); method @NonNull public android.net.ipsec.ike.TunnelModeChildSessionParams.Builder addInternalAddressRequest(int); - method @NonNull public android.net.ipsec.ike.TunnelModeChildSessionParams.Builder addInternalAddressRequest(@NonNull java.net.InetAddress, int); + method @NonNull public android.net.ipsec.ike.TunnelModeChildSessionParams.Builder addInternalAddressRequest(@NonNull java.net.Inet4Address); + method @NonNull public android.net.ipsec.ike.TunnelModeChildSessionParams.Builder addInternalAddressRequest(@NonNull java.net.Inet6Address, int); method @NonNull public android.net.ipsec.ike.TunnelModeChildSessionParams.Builder addInternalDhcpServerRequest(int); - method @NonNull public android.net.ipsec.ike.TunnelModeChildSessionParams.Builder addInternalDhcpServerRequest(@NonNull java.net.InetAddress); method @NonNull public android.net.ipsec.ike.TunnelModeChildSessionParams.Builder addInternalDnsServerRequest(int); - method @NonNull public android.net.ipsec.ike.TunnelModeChildSessionParams.Builder addInternalDnsServerRequest(@NonNull java.net.InetAddress); - method @NonNull public android.net.ipsec.ike.TunnelModeChildSessionParams.Builder addInternalSubnetRequest(int); method @NonNull public android.net.ipsec.ike.TunnelModeChildSessionParams.Builder addOutboundTrafficSelectors(@NonNull android.net.ipsec.ike.IkeTrafficSelector); method @NonNull public android.net.ipsec.ike.TunnelModeChildSessionParams.Builder addSaProposal(@NonNull android.net.ipsec.ike.ChildSaProposal); method @NonNull public android.net.ipsec.ike.TunnelModeChildSessionParams build(); @@ -5068,9 +5116,6 @@ package android.net.ipsec.ike { public static interface TunnelModeChildSessionParams.ConfigRequestIpv4Netmask extends android.net.ipsec.ike.TunnelModeChildSessionParams.ConfigRequest { } - public static interface TunnelModeChildSessionParams.ConfigRequestIpv4Subnet extends android.net.ipsec.ike.TunnelModeChildSessionParams.ConfigRequest { - } - public static interface TunnelModeChildSessionParams.ConfigRequestIpv6Address extends android.net.ipsec.ike.TunnelModeChildSessionParams.ConfigRequest { method @Nullable public java.net.Inet6Address getAddress(); method public int getPrefixLength(); @@ -5080,9 +5125,6 @@ package android.net.ipsec.ike { method @Nullable public java.net.Inet6Address getAddress(); } - public static interface TunnelModeChildSessionParams.ConfigRequestIpv6Subnet extends android.net.ipsec.ike.TunnelModeChildSessionParams.ConfigRequest { - } - } package android.net.ipsec.ike.exceptions { @@ -6323,6 +6365,7 @@ package android.os { } public class Environment { + method @NonNull public static java.util.Collection<java.io.File> getInternalMediaDirectories(); method @NonNull public static java.io.File getOdmDirectory(); method @NonNull public static java.io.File getOemDirectory(); method @NonNull public static java.io.File getProductDirectory(); @@ -7211,8 +7254,8 @@ package android.provider { } public final class MediaStore { - method @NonNull @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) public static java.util.Set<java.lang.String> getRecentExternalVolumeNames(@NonNull android.content.Context); - method @NonNull @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) public static java.util.Collection<java.io.File> getVolumeScanPaths(@NonNull String) throws java.io.FileNotFoundException; + method @NonNull public static android.net.Uri scanFile(@NonNull android.content.ContentResolver, @NonNull java.io.File); + method public static void scanVolume(@NonNull android.content.ContentResolver, @NonNull String); } public abstract class SearchIndexableData { @@ -8005,6 +8048,7 @@ package android.service.notification { field public static final String KEY_CONTEXTUAL_ACTIONS = "key_contextual_actions"; field public static final String KEY_IMPORTANCE = "key_importance"; field public static final String KEY_PEOPLE = "key_people"; + field public static final String KEY_RANKING_SCORE = "key_ranking_score"; field public static final String KEY_SNOOZE_CRITERIA = "key_snooze_criteria"; field public static final String KEY_TEXT_REPLIES = "key_text_replies"; field public static final String KEY_USER_SENTIMENT = "key_user_sentiment"; @@ -9173,6 +9217,7 @@ package android.telephony { method public int getSleepTimeMillis(); method public long getTimestamp(); method @NonNull public java.util.List<android.telephony.ModemActivityInfo.TransmitPower> getTransmitPowerInfo(); + method public boolean isValid(); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.telephony.ModemActivityInfo> CREATOR; field public static final int TX_POWER_LEVELS = 5; // 0x5 @@ -9286,7 +9331,6 @@ package android.telephony { method public void onOutgoingEmergencyCall(@NonNull android.telephony.emergency.EmergencyNumber); method public void onOutgoingEmergencySms(@NonNull android.telephony.emergency.EmergencyNumber); method @RequiresPermission("android.permission.READ_PRECISE_PHONE_STATE") public void onPreciseCallStateChanged(@NonNull android.telephony.PreciseCallState); - method @RequiresPermission("android.permission.READ_PRECISE_PHONE_STATE") public void onPreciseDataConnectionStateChanged(@NonNull android.telephony.PreciseDataConnectionState); method public void onRadioPowerStateChanged(int); method public void onSrvccStateChanged(int); method public void onVoiceActivationStateChanged(int); @@ -9296,7 +9340,6 @@ package android.telephony { field @RequiresPermission(android.Manifest.permission.READ_ACTIVE_EMERGENCY_SESSION) public static final int LISTEN_OUTGOING_EMERGENCY_CALL = 268435456; // 0x10000000 field @RequiresPermission(android.Manifest.permission.READ_ACTIVE_EMERGENCY_SESSION) public static final int LISTEN_OUTGOING_EMERGENCY_SMS = 536870912; // 0x20000000 field @RequiresPermission("android.permission.READ_PRECISE_PHONE_STATE") public static final int LISTEN_PRECISE_CALL_STATE = 2048; // 0x800 - field @RequiresPermission("android.permission.READ_PRECISE_PHONE_STATE") public static final int LISTEN_PRECISE_DATA_CONNECTION_STATE = 4096; // 0x1000 field public static final int LISTEN_RADIO_POWER_STATE_CHANGED = 8388608; // 0x800000 field @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static final int LISTEN_SRVCC_STATE_CHANGED = 16384; // 0x4000 field public static final int LISTEN_VOICE_ACTIVATION_STATE = 131072; // 0x20000 @@ -9322,13 +9365,12 @@ package android.telephony { } public final class PreciseDataConnectionState implements android.os.Parcelable { - method public int describeContents(); - method @Nullable public String getDataConnectionApn(); - method public int getDataConnectionApnTypeBitMask(); - method public int getDataConnectionFailCause(); - method public int getDataConnectionState(); - method public void writeToParcel(android.os.Parcel, int); - field @NonNull public static final android.os.Parcelable.Creator<android.telephony.PreciseDataConnectionState> CREATOR; + method @Deprecated @NonNull public String getDataConnectionApn(); + method @Deprecated public int getDataConnectionApnTypeBitMask(); + method @Deprecated public int getDataConnectionFailCause(); + method @Deprecated @Nullable public android.net.LinkProperties getDataConnectionLinkProperties(); + method @Deprecated public int getDataConnectionNetworkType(); + method @Deprecated public int getDataConnectionState(); } public final class PreciseDisconnectCause { @@ -9567,6 +9609,7 @@ package android.telephony { public final class SmsManager { method public boolean disableCellBroadcastRange(int, int, int); method public boolean enableCellBroadcastRange(int, int, int); + method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getSmsCapacityOnIcc(); method public void sendMultipartTextMessage(@NonNull String, @NonNull String, @NonNull java.util.List<java.lang.String>, @Nullable java.util.List<android.app.PendingIntent>, @Nullable java.util.List<android.app.PendingIntent>, @NonNull String); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void sendMultipartTextMessageWithoutPersisting(String, String, java.util.List<java.lang.String>, java.util.List<android.app.PendingIntent>, java.util.List<android.app.PendingIntent>); } @@ -9637,9 +9680,6 @@ package android.telephony { public class TelephonyManager { method @Deprecated @RequiresPermission(android.Manifest.permission.CALL_PHONE) public void call(String, String); - method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void carrierActionReportDefaultNetworkStatus(int, boolean); - method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void carrierActionResetAll(int); - method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void carrierActionSetRadioEnabled(int, boolean); method public int checkCarrierPrivilegesForPackage(String); method public int checkCarrierPrivilegesForPackageAnyPhone(String); method public void dial(String); @@ -9713,9 +9753,11 @@ package android.telephony { method public boolean needsOtaServiceProvisioning(); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void notifyOtaEmergencyNumberDbInstalled(); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean rebootRadio(); + method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void reportDefaultNetworkStatus(boolean); method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.MODIFY_PHONE_STATE}) public void requestCellInfoUpdate(@NonNull android.os.WorkSource, @NonNull java.util.concurrent.Executor, @NonNull android.telephony.TelephonyManager.CellInfoCallback); method public void requestModemActivityInfo(@NonNull android.os.ResultReceiver); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void requestNumberVerification(@NonNull android.telephony.PhoneNumberRange, long, @NonNull java.util.concurrent.Executor, @NonNull android.telephony.NumberVerificationCallback); + method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void resetAllCarrierActions(); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void resetCarrierKeysForImsiEncryption(); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean resetRadioConfig(); method @RequiresPermission(android.Manifest.permission.CONNECTIVITY_INTERNAL) public void resetSettings(); @@ -9729,6 +9771,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setOpportunisticNetworkState(boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setPreferredNetworkTypeBitmask(long); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setRadio(boolean); + method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setRadioEnabled(boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setRadioPower(boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setSimPowerState(int); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setSimPowerStateForSlot(int, int); diff --git a/api/test-current.txt b/api/test-current.txt index 3bdd4790372b..219258ef50b0 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -697,6 +697,7 @@ package android.content { public abstract class Context { method @NonNull public android.content.Context createContextAsUser(@NonNull android.os.UserHandle, int); method @NonNull public android.content.Context createPackageContextAsUser(@NonNull String, int, @NonNull android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException; + method @NonNull public java.io.File getCrateDir(@NonNull String); method public abstract android.view.Display getDisplay(); method public abstract int getDisplayId(); method public android.os.UserHandle getUser(); @@ -2467,14 +2468,9 @@ package android.provider { } public final class MediaStore { - method @RequiresPermission(android.Manifest.permission.CLEAR_APP_USER_DATA) public static void deleteContributedMedia(android.content.Context, String, android.os.UserHandle) throws java.io.IOException; - method @RequiresPermission(android.Manifest.permission.CLEAR_APP_USER_DATA) public static long getContributedMediaSize(android.content.Context, String, android.os.UserHandle) throws java.io.IOException; - method @NonNull public static java.io.File getVolumePath(@NonNull String) throws java.io.FileNotFoundException; - method @NonNull @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) public static java.util.Collection<java.io.File> getVolumeScanPaths(@NonNull String) throws java.io.FileNotFoundException; - method public static android.net.Uri scanFile(android.content.Context, java.io.File); - method public static android.net.Uri scanFileFromShell(android.content.Context, java.io.File); - method public static void scanVolume(android.content.Context, java.io.File); - method public static void waitForIdle(android.content.Context); + method @NonNull public static android.net.Uri scanFile(@NonNull android.content.ContentResolver, @NonNull java.io.File); + method public static void scanVolume(@NonNull android.content.ContentResolver, @NonNull String); + method public static void waitForIdle(@NonNull android.content.ContentResolver); } public final class Settings { @@ -2832,6 +2828,7 @@ package android.service.notification { field @NonNull public static final android.os.Parcelable.Creator<android.service.notification.Adjustment> CREATOR; field public static final String KEY_CONTEXTUAL_ACTIONS = "key_contextual_actions"; field public static final String KEY_IMPORTANCE = "key_importance"; + field public static final String KEY_RANKING_SCORE = "key_ranking_score"; field public static final String KEY_SNOOZE_CRITERIA = "key_snooze_criteria"; field public static final String KEY_TEXT_REPLIES = "key_text_replies"; field public static final String KEY_USER_SENTIMENT = "key_user_sentiment"; @@ -3162,6 +3159,10 @@ package android.telephony { field @RequiresPermission("android.permission.READ_ACTIVE_EMERGENCY_SESSION") public static final int LISTEN_OUTGOING_EMERGENCY_SMS = 536870912; // 0x20000000 } + public final class PreciseDataConnectionState implements android.os.Parcelable { + ctor @Deprecated public PreciseDataConnectionState(int, int, int, @NonNull String, @Nullable android.net.LinkProperties, int); + } + public class ServiceState implements android.os.Parcelable { method public void addNetworkRegistrationInfo(android.telephony.NetworkRegistrationInfo); method public void setCdmaSystemAndNetworkId(int, int); diff --git a/cmds/statsd/Android.bp b/cmds/statsd/Android.bp index 484f82330055..afff61497157 100644 --- a/cmds/statsd/Android.bp +++ b/cmds/statsd/Android.bp @@ -146,6 +146,7 @@ cc_defaults { "libprotoutil", "libservices", "libstatslog", + "libstatsmetadata", "libstatssocket", "libsysutils", "libtimestats_proto", @@ -153,6 +154,63 @@ cc_defaults { ], } +// ================ +// libstatsmetadata +// ================ + +genrule { + name: "atoms_info.h", + tools: ["stats-log-api-gen"], + cmd: "$(location stats-log-api-gen) --atomsInfoHeader $(genDir)/atoms_info.h", + out: [ + "atoms_info.h", + ], +} + +genrule { + name: "atoms_info.cpp", + tools: ["stats-log-api-gen"], + cmd: "$(location stats-log-api-gen) --atomsInfoCpp $(genDir)/atoms_info.cpp", + out: [ + "atoms_info.cpp", + ], +} + +cc_library_shared { + name: "libstatsmetadata", + host_supported: true, + generated_sources: [ + "atoms_info.cpp", + ], + generated_headers: [ + "atoms_info.h", + ], + cflags: [ + "-Wall", + "-Werror", + ], + export_generated_headers: [ + "atoms_info.h", + ], + shared_libs: [ + "libcutils", + "libstatslog", + ], + target: { + android: { + shared_libs: [ + "libutils", + ], + }, + host: { + static_libs: [ + "libutils", + ], + }, + }, +} + + // ========= // statsd // ========= diff --git a/cmds/statsd/src/FieldValue.cpp b/cmds/statsd/src/FieldValue.cpp index 84a06070e431..a545fc5718d0 100644 --- a/cmds/statsd/src/FieldValue.cpp +++ b/cmds/statsd/src/FieldValue.cpp @@ -18,8 +18,8 @@ #include "Log.h" #include "FieldValue.h" #include "HashableDimensionKey.h" +#include "atoms_info.h" #include "math.h" -#include "statslog.h" namespace android { namespace os { diff --git a/cmds/statsd/src/StatsLogProcessor.cpp b/cmds/statsd/src/StatsLogProcessor.cpp index 98d41c22d540..34818145a922 100644 --- a/cmds/statsd/src/StatsLogProcessor.cpp +++ b/cmds/statsd/src/StatsLogProcessor.cpp @@ -27,6 +27,7 @@ #include <utils/SystemClock.h> #include "android-base/stringprintf.h" +#include "atoms_info.h" #include "external/StatsPullerManager.h" #include "guardrail/StatsdStats.h" #include "metrics/CountMetricProducer.h" diff --git a/cmds/statsd/src/external/PowerStatsPuller.cpp b/cmds/statsd/src/external/PowerStatsPuller.cpp index b142caca3acc..dc69b78f0329 100644 --- a/cmds/statsd/src/external/PowerStatsPuller.cpp +++ b/cmds/statsd/src/external/PowerStatsPuller.cpp @@ -22,6 +22,7 @@ #include <vector> #include "PowerStatsPuller.h" +#include "statslog.h" #include "stats_log_util.h" using android::hardware::hidl_vec; diff --git a/cmds/statsd/src/external/puller_util.cpp b/cmds/statsd/src/external/puller_util.cpp index 53fa6301a836..031c43740d9d 100644 --- a/cmds/statsd/src/external/puller_util.cpp +++ b/cmds/statsd/src/external/puller_util.cpp @@ -18,8 +18,8 @@ #include "Log.h" #include "StatsPullerManager.h" +#include "atoms_info.h" #include "puller_util.h" -#include "statslog.h" namespace android { namespace os { diff --git a/cmds/statsd/src/guardrail/StatsdStats.h b/cmds/statsd/src/guardrail/StatsdStats.h index 23d2aceb4fc3..564b9ee8051c 100644 --- a/cmds/statsd/src/guardrail/StatsdStats.h +++ b/cmds/statsd/src/guardrail/StatsdStats.h @@ -16,7 +16,7 @@ #pragma once #include "config/ConfigKey.h" -#include "statslog.h" +#include "atoms_info.h" #include <gtest/gtest_prod.h> #include <log/log_time.h> diff --git a/cmds/statsd/src/metrics/MetricsManager.cpp b/cmds/statsd/src/metrics/MetricsManager.cpp index 464cec36d5e3..088f607ecfce 100644 --- a/cmds/statsd/src/metrics/MetricsManager.cpp +++ b/cmds/statsd/src/metrics/MetricsManager.cpp @@ -23,6 +23,7 @@ #include <utils/SystemClock.h> #include "CountMetricProducer.h" +#include "atoms_info.h" #include "condition/CombinationConditionTracker.h" #include "condition/SimpleConditionTracker.h" #include "guardrail/StatsdStats.h" diff --git a/cmds/statsd/src/metrics/metrics_manager_util.cpp b/cmds/statsd/src/metrics/metrics_manager_util.cpp index 9131802c83e7..2ad8217c45d4 100644 --- a/cmds/statsd/src/metrics/metrics_manager_util.cpp +++ b/cmds/statsd/src/metrics/metrics_manager_util.cpp @@ -22,6 +22,7 @@ #include <inttypes.h> +#include "atoms_info.h" #include "condition/CombinationConditionTracker.h" #include "condition/SimpleConditionTracker.h" #include "condition/StateConditionTracker.h" @@ -36,7 +37,6 @@ #include "metrics/ValueMetricProducer.h" #include "state/StateManager.h" #include "stats_util.h" -#include "statslog.h" using std::set; using std::string; diff --git a/cmds/statsd/src/state/StateTracker.h b/cmds/statsd/src/state/StateTracker.h index e65325a52b98..7453370f25fd 100644 --- a/cmds/statsd/src/state/StateTracker.h +++ b/cmds/statsd/src/state/StateTracker.h @@ -15,7 +15,7 @@ */ #pragma once -#include <statslog.h> +#include <atoms_info.h> #include <utils/RefBase.h> #include "HashableDimensionKey.h" #include "logd/LogEvent.h" diff --git a/cmds/statsd/src/stats_log_util.h b/cmds/statsd/src/stats_log_util.h index 0a86363a7090..f3e94331a23e 100644 --- a/cmds/statsd/src/stats_log_util.h +++ b/cmds/statsd/src/stats_log_util.h @@ -19,9 +19,9 @@ #include <android/util/ProtoOutputStream.h> #include "FieldValue.h" #include "HashableDimensionKey.h" +#include "atoms_info.h" #include "frameworks/base/cmds/statsd/src/statsd_config.pb.h" #include "guardrail/StatsdStats.h" -#include "statslog.h" namespace android { namespace os { diff --git a/cmds/statsd/tests/external/GpuStatsPuller_test.cpp b/cmds/statsd/tests/external/GpuStatsPuller_test.cpp index e91fb0d4b27c..6abdfa3dbb24 100644 --- a/cmds/statsd/tests/external/GpuStatsPuller_test.cpp +++ b/cmds/statsd/tests/external/GpuStatsPuller_test.cpp @@ -24,6 +24,7 @@ #include <log/log.h> #include "src/external/GpuStatsPuller.h" +#include "statslog.h" #ifdef __ANDROID__ diff --git a/cmds/statsd/tests/external/SurfaceflingerStatsPuller_test.cpp b/cmds/statsd/tests/external/SurfaceflingerStatsPuller_test.cpp index 5c9636fa99cc..5b7a30d4a5aa 100644 --- a/cmds/statsd/tests/external/SurfaceflingerStatsPuller_test.cpp +++ b/cmds/statsd/tests/external/SurfaceflingerStatsPuller_test.cpp @@ -18,6 +18,7 @@ #define LOG_TAG "SurfaceflingerStatsPuller_test" #include "src/external/SurfaceflingerStatsPuller.h" +#include "statslog.h" #include <gtest/gtest.h> #include <log/log.h> diff --git a/cmds/statsd/tests/external/puller_util_test.cpp b/cmds/statsd/tests/external/puller_util_test.cpp index 266ea351b5d4..673082834e18 100644 --- a/cmds/statsd/tests/external/puller_util_test.cpp +++ b/cmds/statsd/tests/external/puller_util_test.cpp @@ -17,6 +17,7 @@ #include <gtest/gtest.h> #include <stdio.h> #include <vector> +#include "statslog.h" #include "../metrics/metrics_test_helper.h" #ifdef __ANDROID__ diff --git a/cmds/svc/src/com/android/commands/svc/DataCommand.java b/cmds/svc/src/com/android/commands/svc/DataCommand.java index 35510cfd38b1..b4dbd1d41bc1 100644 --- a/cmds/svc/src/com/android/commands/svc/DataCommand.java +++ b/cmds/svc/src/com/android/commands/svc/DataCommand.java @@ -16,12 +16,16 @@ package com.android.commands.svc; -import android.os.ServiceManager; -import android.os.RemoteException; -import android.content.Context; -import com.android.internal.telephony.ITelephony; - +/** + * @deprecated Please use adb shell cmd phone data enabled/disable instead. + */ +@Deprecated public class DataCommand extends Svc.Command { + + private static final String DECPRECATED_MESSAGE = + "adb shell svc data enable/disable is deprecated;" + + "please use adb shell cmd phone data enable/disable instead."; + public DataCommand() { super("data"); } @@ -33,36 +37,10 @@ public class DataCommand extends Svc.Command { public String longHelp() { return shortHelp() + "\n" + "\n" - + "usage: svc data [enable|disable]\n" - + " Turn mobile data on or off.\n\n"; + + DECPRECATED_MESSAGE; } public void run(String[] args) { - boolean validCommand = false; - if (args.length >= 2) { - boolean flag = false; - if ("enable".equals(args[1])) { - flag = true; - validCommand = true; - } else if ("disable".equals(args[1])) { - flag = false; - validCommand = true; - } - if (validCommand) { - ITelephony phoneMgr - = ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE)); - try { - if (flag) { - phoneMgr.enableDataConnectivity(); - } else - phoneMgr.disableDataConnectivity(); - } - catch (RemoteException e) { - System.err.println("Mobile data operation failed: " + e); - } - return; - } - } - System.err.println(longHelp()); + System.err.println(DECPRECATED_MESSAGE); } } diff --git a/core/java/android/accessibilityservice/AccessibilityGestureEvent.java b/core/java/android/accessibilityservice/AccessibilityGestureEvent.java index 8b62e2f83cff..d82b151e9ce9 100644 --- a/core/java/android/accessibilityservice/AccessibilityGestureEvent.java +++ b/core/java/android/accessibilityservice/AccessibilityGestureEvent.java @@ -17,6 +17,8 @@ package android.accessibilityservice; +import static android.accessibilityservice.AccessibilityService.GESTURE_DOUBLE_TAP; +import static android.accessibilityservice.AccessibilityService.GESTURE_DOUBLE_TAP_AND_HOLD; import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN; import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN_AND_LEFT; import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN_AND_RIGHT; @@ -58,6 +60,8 @@ public final class AccessibilityGestureEvent implements Parcelable { /** @hide */ @IntDef(prefix = { "GESTURE_" }, value = { + GESTURE_DOUBLE_TAP, + GESTURE_DOUBLE_TAP_AND_HOLD, GESTURE_SWIPE_UP, GESTURE_SWIPE_UP_AND_LEFT, GESTURE_SWIPE_UP_AND_DOWN, diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 47fdcdeafce9..0f619c8d1f76 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -300,6 +300,18 @@ public abstract class AccessibilityService extends Service { public static final int GESTURE_SWIPE_DOWN_AND_RIGHT = 16; /** + * The user has performed a double tap gesture on the touch screen. + * @hide + */ + public static final int GESTURE_DOUBLE_TAP = 17; + + /** + * The user has performed a double tap and hold gesture on the touch screen. + * @hide + */ + public static final int GESTURE_DOUBLE_TAP_AND_HOLD = 18; + + /** * The {@link Intent} that must be declared as handled by the service. */ public static final String SERVICE_INTERFACE = diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java index f4e465ab3adb..0f10c3980021 100644 --- a/core/java/android/accounts/AccountManager.java +++ b/core/java/android/accounts/AccountManager.java @@ -1943,35 +1943,6 @@ public class AccountManager { } /** - * @hide - * Removes the shared account. - * @param account the account to remove - * @param user the user to remove the account from - * @return - */ - public boolean removeSharedAccount(final Account account, UserHandle user) { - try { - boolean val = mService.removeSharedAccountAsUser(account, user.getIdentifier()); - return val; - } catch (RemoteException re) { - throw re.rethrowFromSystemServer(); - } - } - - /** - * @hide - * @param user - * @return - */ - public Account[] getSharedAccounts(UserHandle user) { - try { - return mService.getSharedAccountsAsUser(user.getIdentifier()); - } catch (RemoteException re) { - throw re.rethrowFromSystemServer(); - } - } - - /** * Confirms that the user knows the password for an account to make extra * sure they are the owner of the account. The user-entered password can * be supplied directly, otherwise the authenticator for this account type diff --git a/core/java/android/accounts/IAccountManager.aidl b/core/java/android/accounts/IAccountManager.aidl index 4cf0a2089fe5..012713891d11 100644 --- a/core/java/android/accounts/IAccountManager.aidl +++ b/core/java/android/accounts/IAccountManager.aidl @@ -80,14 +80,11 @@ interface IAccountManager { String authTokenType); /* Shared accounts */ - Account[] getSharedAccountsAsUser(int userId); - boolean removeSharedAccountAsUser(in Account account, int userId); void addSharedAccountsFromParentUser(int parentUserId, int userId, String opPackageName); /* Account renaming. */ void renameAccount(in IAccountManagerResponse response, in Account accountToRename, String newName); String getPreviousName(in Account account); - boolean renameSharedAccountAsUser(in Account accountToRename, String newName, int userId); /* Add account in two steps. */ void startAddAccountSession(in IAccountManagerResponse response, String accountType, diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 46f88d5c81e4..155e93f9be19 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -99,6 +99,7 @@ import java.io.InputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteOrder; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Objects; import java.util.concurrent.Executor; @@ -251,6 +252,8 @@ class ContextImpl extends Context { @GuardedBy("mSync") private File mFilesDir; @GuardedBy("mSync") + private File mCratesDir; + @GuardedBy("mSync") private File mNoBackupFilesDir; @GuardedBy("mSync") private File mCacheDir; @@ -702,6 +705,24 @@ class ContextImpl extends Context { } @Override + public File getCrateDir(@NonNull String crateId) { + Preconditions.checkArgument(FileUtils.isValidExtFilename(crateId), "invalidated crateId"); + final Path cratesRootPath = getDataDir().toPath().resolve("crates"); + final Path absoluteNormalizedCratePath = cratesRootPath.resolve(crateId) + .toAbsolutePath().normalize(); + + synchronized (mSync) { + if (mCratesDir == null) { + mCratesDir = cratesRootPath.toFile(); + } + ensurePrivateDirExists(mCratesDir); + } + + File cratedDir = absoluteNormalizedCratePath.toFile(); + return ensurePrivateDirExists(cratedDir); + } + + @Override public File getNoBackupFilesDir() { synchronized (mSync) { if (mNoBackupFilesDir == null) { diff --git a/core/java/android/app/DownloadManager.java b/core/java/android/app/DownloadManager.java index 80c9ba2a9c48..eb50581f3319 100644 --- a/core/java/android/app/DownloadManager.java +++ b/core/java/android/app/DownloadManager.java @@ -1314,8 +1314,8 @@ public class DownloadManager { // TODO: DownloadProvider.update() should take care of updating corresponding // MediaProvider entries. - MediaStore.scanFile(context, before); - MediaStore.scanFile(context, after); + MediaStore.scanFile(mResolver, before); + MediaStore.scanFile(mResolver, after); final ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_TITLE, displayName); diff --git a/core/java/android/app/IInstantAppResolver.aidl b/core/java/android/app/IInstantAppResolver.aidl index 7318762cfc63..8618fbb3bada 100644 --- a/core/java/android/app/IInstantAppResolver.aidl +++ b/core/java/android/app/IInstantAppResolver.aidl @@ -16,15 +16,13 @@ package android.app; -import android.content.Intent; +import android.content.pm.InstantAppRequestInfo; import android.os.IRemoteCallback; /** @hide */ oneway interface IInstantAppResolver { - void getInstantAppResolveInfoList(in Intent sanitizedIntent, in int[] hostDigestPrefix, - int userId, String token, int sequence, IRemoteCallback callback); - - void getInstantAppIntentFilterList(in Intent sanitizedIntent, in int[] hostDigestPrefix, - int userId, String token, IRemoteCallback callback); + void getInstantAppResolveInfoList(in InstantAppRequestInfo request, int sequence, + IRemoteCallback callback); + void getInstantAppIntentFilterList(in InstantAppRequestInfo request, IRemoteCallback callback); } diff --git a/core/java/android/app/InstantAppResolverService.java b/core/java/android/app/InstantAppResolverService.java index a7be5421adb2..a413c606b76f 100644 --- a/core/java/android/app/InstantAppResolverService.java +++ b/core/java/android/app/InstantAppResolverService.java @@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.annotation.SystemApi; import android.content.Context; import android.content.Intent; +import android.content.pm.InstantAppRequestInfo; import android.content.pm.InstantAppResolveInfo; import android.os.Build; import android.os.Bundle; @@ -59,8 +60,9 @@ public abstract class InstantAppResolverService extends Service { * Called to retrieve resolve info for instant applications immediately. * * @param digestPrefix The hash prefix of the instant app's domain. - * @deprecated Should implement {@link #onGetInstantAppResolveInfo(Intent, int[], UserHandle, - * String, InstantAppResolutionCallback)}. + * + * @deprecated Should implement {@link #onGetInstantAppResolveInfo(InstantAppRequestInfo, + * InstantAppResolutionCallback)} */ @Deprecated public void onGetInstantAppResolveInfo(@Nullable int[] digestPrefix, @NonNull String token, @@ -73,8 +75,9 @@ public abstract class InstantAppResolverService extends Service { * sources. * * @param digestPrefix The hash prefix of the instant app's domain. - * @deprecated Should implement {@link #onGetInstantAppIntentFilter(Intent, int[], UserHandle, - * String, InstantAppResolutionCallback)}. + * + * @deprecated Should implement {@link #onGetInstantAppIntentFilter(InstantAppRequestInfo, + * InstantAppResolutionCallback)} */ @Deprecated public void onGetInstantAppIntentFilter(@Nullable int[] digestPrefix, @NonNull String token, @@ -103,8 +106,8 @@ public abstract class InstantAppResolverService extends Service { * * @see InstantAppResolveInfo * - * @deprecated Should implement {@link #onGetInstantAppResolveInfo(Intent, int[], UserHandle, - * String, InstantAppResolutionCallback)}. + * @deprecated Should implement {@link #onGetInstantAppResolveInfo(InstantAppRequestInfo, + * InstantAppResolutionCallback)} */ @Deprecated public void onGetInstantAppResolveInfo(@NonNull Intent sanitizedIntent, @@ -134,8 +137,8 @@ public abstract class InstantAppResolverService extends Service { * {@link Intent#EXTRA_INSTANT_APP_TOKEN}. * @param callback The {@link InstantAppResolutionCallback} to provide results to. * - * @deprecated Should implement {@link #onGetInstantAppIntentFilter(Intent, int[], UserHandle, - * String, InstantAppResolutionCallback)}. + * @deprecated Should implement {@link #onGetInstantAppIntentFilter(InstantAppRequestInfo, + * InstantAppResolutionCallback)} */ @Deprecated public void onGetInstantAppIntentFilter(@NonNull Intent sanitizedIntent, @@ -170,7 +173,11 @@ public abstract class InstantAppResolverService extends Service { * @param callback The {@link InstantAppResolutionCallback} to provide results to. * * @see InstantAppResolveInfo + * + * @deprecated Should implement {@link #onGetInstantAppResolveInfo(InstantAppRequestInfo, + * InstantAppResolutionCallback */ + @Deprecated public void onGetInstantAppResolveInfo(@NonNull Intent sanitizedIntent, @Nullable int[] hostDigestPrefix, @NonNull UserHandle userHandle, @NonNull String token, @NonNull InstantAppResolutionCallback callback) { @@ -193,7 +200,11 @@ public abstract class InstantAppResolverService extends Service { * Intent, int[], UserHandle, String, InstantAppResolutionCallback)} and provided * to the currently visible installer via {@link Intent#EXTRA_INSTANT_APP_TOKEN}. * @param callback The {@link InstantAppResolutionCallback} to provide results to. + * + * @deprecated Should implement {@link #onGetInstantAppIntentFilter(InstantAppRequestInfo, + * InstantAppResolutionCallback)} */ + @Deprecated public void onGetInstantAppIntentFilter(@NonNull Intent sanitizedIntent, @Nullable int[] hostDigestPrefix, @NonNull UserHandle userHandle, @NonNull String token, @NonNull InstantAppResolutionCallback callback) { @@ -202,6 +213,41 @@ public abstract class InstantAppResolverService extends Service { } /** + * Called to retrieve resolve info for instant applications immediately. The response will be + * ignored if not provided within a reasonable time. {@link InstantAppResolveInfo}s provided + * in response to this method may be partial to request a second phase of resolution which will + * result in a subsequent call to {@link #onGetInstantAppIntentFilter(InstantAppRequestInfo, + * InstantAppResolutionCallback)} + * + * @param request The parameters for this resolution request + * @param callback The {@link InstantAppResolutionCallback} to provide results to. + * + * @see InstantAppResolveInfo + */ + public void onGetInstantAppResolveInfo(@NonNull InstantAppRequestInfo request, + @NonNull InstantAppResolutionCallback callback) { + // If not overridden, forward to the old method. + onGetInstantAppResolveInfo(request.intent, request.hostDigestPrefix, request.userHandle, + request.token, callback); + } + + /** + * Called to retrieve intent filters for potentially matching instant applications. Unlike + * {@link #onGetInstantAppResolveInfo(InstantAppRequestInfo, InstantAppResolutionCallback)}, + * the response may take as long as necessary to respond. All {@link InstantAppResolveInfo}s + * provided in response to this method must be completely populated. + * + * @param request The parameters for this resolution request + * @param callback The {@link InstantAppResolutionCallback} to provide results to. + */ + public void onGetInstantAppIntentFilter(@NonNull InstantAppRequestInfo request, + @NonNull InstantAppResolutionCallback callback) { + // If not overridden, forward to the old method. + onGetInstantAppIntentFilter(request.intent, request.hostDigestPrefix, request.userHandle, + request.token, callback); + } + + /** * Returns a {@link Looper} to perform service operations on. */ Looper getLooper() { @@ -218,35 +264,29 @@ public abstract class InstantAppResolverService extends Service { public final IBinder onBind(Intent intent) { return new IInstantAppResolver.Stub() { @Override - public void getInstantAppResolveInfoList(Intent sanitizedIntent, int[] digestPrefix, - int userId, String token, int sequence, IRemoteCallback callback) { + public void getInstantAppResolveInfoList(InstantAppRequestInfo request, int sequence, + IRemoteCallback callback) { if (DEBUG_INSTANT) { - Slog.v(TAG, "[" + token + "] Phase1 called; posting"); + Slog.v(TAG, "[" + request.token + "] Phase1 called; posting"); } final SomeArgs args = SomeArgs.obtain(); - args.arg1 = callback; - args.arg2 = digestPrefix; - args.arg3 = userId; - args.arg4 = token; - args.arg5 = sanitizedIntent; - mHandler.obtainMessage(ServiceHandler.MSG_GET_INSTANT_APP_RESOLVE_INFO, - sequence, 0, args).sendToTarget(); + args.arg1 = request; + args.arg2 = callback; + mHandler.obtainMessage(ServiceHandler.MSG_GET_INSTANT_APP_RESOLVE_INFO, sequence, + 0, args).sendToTarget(); } @Override - public void getInstantAppIntentFilterList(Intent sanitizedIntent, - int[] digestPrefix, int userId, String token, IRemoteCallback callback) { + public void getInstantAppIntentFilterList(InstantAppRequestInfo request, + IRemoteCallback callback) { if (DEBUG_INSTANT) { - Slog.v(TAG, "[" + token + "] Phase2 called; posting"); + Slog.v(TAG, "[" + request.token + "] Phase2 called; posting"); } final SomeArgs args = SomeArgs.obtain(); - args.arg1 = callback; - args.arg2 = digestPrefix; - args.arg3 = userId; - args.arg4 = token; - args.arg5 = sanitizedIntent; - mHandler.obtainMessage(ServiceHandler.MSG_GET_INSTANT_APP_INTENT_FILTER, - args).sendToTarget(); + args.arg1 = request; + args.arg2 = callback; + mHandler.obtainMessage(ServiceHandler.MSG_GET_INSTANT_APP_INTENT_FILTER, args) + .sendToTarget(); } }; } @@ -287,36 +327,33 @@ public abstract class InstantAppResolverService extends Service { switch (action) { case MSG_GET_INSTANT_APP_RESOLVE_INFO: { final SomeArgs args = (SomeArgs) message.obj; - final IRemoteCallback callback = (IRemoteCallback) args.arg1; - final int[] digestPrefix = (int[]) args.arg2; - final int userId = (int) args.arg3; - final String token = (String) args.arg4; - final Intent intent = (Intent) args.arg5; + final InstantAppRequestInfo request = (InstantAppRequestInfo) args.arg1; + final IRemoteCallback callback = (IRemoteCallback) args.arg2; + args.recycle(); final int sequence = message.arg1; if (DEBUG_INSTANT) { - Slog.d(TAG, "[" + token + "] Phase1 request;" - + " prefix: " + Arrays.toString(digestPrefix) - + ", userId: " + userId); + Slog.d(TAG, "[" + request.token + "] Phase1 request;" + + " prefix: " + Arrays.toString(request.hostDigestPrefix) + + ", userId: " + request.userHandle.getIdentifier()); } - onGetInstantAppResolveInfo(intent, digestPrefix, UserHandle.of(userId), token, + onGetInstantAppResolveInfo(request, new InstantAppResolutionCallback(sequence, callback)); } break; case MSG_GET_INSTANT_APP_INTENT_FILTER: { final SomeArgs args = (SomeArgs) message.obj; - final IRemoteCallback callback = (IRemoteCallback) args.arg1; - final int[] digestPrefix = (int[]) args.arg2; - final int userId = (int) args.arg3; - final String token = (String) args.arg4; - final Intent intent = (Intent) args.arg5; + final InstantAppRequestInfo request = (InstantAppRequestInfo) args.arg1; + final IRemoteCallback callback = (IRemoteCallback) args.arg2; + args.recycle(); if (DEBUG_INSTANT) { - Slog.d(TAG, "[" + token + "] Phase2 request;" - + " prefix: " + Arrays.toString(digestPrefix) - + ", userId: " + userId); + Slog.d(TAG, "[" + request.token + "] Phase2 request;" + + " prefix: " + Arrays.toString(request.hostDigestPrefix) + + ", userId: " + request.userHandle.getIdentifier()); } - onGetInstantAppIntentFilter(intent, digestPrefix, UserHandle.of(userId), token, + onGetInstantAppIntentFilter(request, new InstantAppResolutionCallback(-1 /*sequence*/, callback)); - } break; + } + break; default: { throw new IllegalArgumentException("Unknown message: " + action); diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index a1305da0a502..ce21db335615 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -152,6 +152,8 @@ import android.os.Vibrator; import android.os.health.SystemHealthManager; import android.os.image.DynamicSystemManager; import android.os.image.IDynamicSystemService; +import android.os.incremental.IIncrementalManagerNative; +import android.os.incremental.IncrementalManager; import android.os.storage.StorageManager; import android.permission.PermissionControllerManager; import android.permission.PermissionManager; @@ -1225,6 +1227,20 @@ public final class SystemServiceRegistry { Context.DATA_LOADER_MANAGER_SERVICE); return new DataLoaderManager(IDataLoaderManager.Stub.asInterface(b)); }}); + //TODO(b/136132412): refactor this: 1) merge IIncrementalManager.aidl and + //IIncrementalManagerNative.aidl, 2) implement the binder interface in + //IncrementalManagerService.java, 3) use JNI to call native functions + registerService(Context.INCREMENTAL_SERVICE, IncrementalManager.class, + new CachedServiceFetcher<IncrementalManager>() { + @Override + public IncrementalManager createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(Context.INCREMENTAL_SERVICE); + if (b == null) { + return null; + } + return new IncrementalManager( + IIncrementalManagerNative.Stub.asInterface(b)); + }}); //CHECKSTYLE:ON IndentationCheck sInitializing = true; diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 6e7ead1a4b60..7332978852c4 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -1349,6 +1349,9 @@ public class DevicePolicyManager { * Broadcast action: send when any policy admin changes a policy. * This is generally used to find out when a new policy is in effect. * + * If the profile owner of an organization-owned managed profile changes some user + * restriction explicitly on the parent user, this broadcast will <em>not</em> be + * sent to the parent user. * @hide */ @UnsupportedAppUsage @@ -7958,18 +7961,23 @@ public class DevicePolicyManager { * <p> * The calling device admin must be a profile or device owner; if it is not, a security * exception will be thrown. + * <p> + * The profile owner of an organization-owned managed profile may invoke this method on + * the {@link DevicePolicyManager} instance it obtained from + * {@link #getParentProfileInstance(ComponentName)}, for enforcing device-wide restrictions. + * <p> + * See the constants in {@link android.os.UserManager} for the list of restrictions that can + * be enforced device-wide. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. - * @param key The key of the restriction. See the constants in {@link android.os.UserManager} - * for the list of keys. + * @param key The key of the restriction. * @throws SecurityException if {@code admin} is not a device or profile owner. */ public void addUserRestriction(@NonNull ComponentName admin, @UserManager.UserRestrictionKey String key) { - throwIfParentInstance("addUserRestriction"); if (mService != null) { try { - mService.setUserRestriction(admin, key, true); + mService.setUserRestriction(admin, key, true, mParentInstance); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -7981,18 +7989,22 @@ public class DevicePolicyManager { * <p> * The calling device admin must be a profile or device owner; if it is not, a security * exception will be thrown. + * <p> + * The profile owner of an organization-owned managed profile may invoke this method on + * the {@link DevicePolicyManager} instance it obtained from + * {@link #getParentProfileInstance(ComponentName)}, for clearing device-wide restrictions. + * <p> + * See the constants in {@link android.os.UserManager} for the list of restrictions. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. - * @param key The key of the restriction. See the constants in {@link android.os.UserManager} - * for the list of keys. + * @param key The key of the restriction. * @throws SecurityException if {@code admin} is not a device or profile owner. */ public void clearUserRestriction(@NonNull ComponentName admin, @UserManager.UserRestrictionKey String key) { - throwIfParentInstance("clearUserRestriction"); if (mService != null) { try { - mService.setUserRestriction(admin, key, false); + mService.setUserRestriction(admin, key, false, mParentInstance); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -8006,16 +8018,20 @@ public class DevicePolicyManager { * The target user may have more restrictions set by the system or other device owner / profile * owner. To get all the user restrictions currently set, use * {@link UserManager#getUserRestrictions()}. + * <p> + * The profile owner of an organization-owned managed profile may invoke this method on + * the {@link DevicePolicyManager} instance it obtained from + * {@link #getParentProfileInstance(ComponentName)}, for retrieving device-wide restrictions + * it previously set with {@link #addUserRestriction(ComponentName, String)}. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @throws SecurityException if {@code admin} is not a device or profile owner. */ public @NonNull Bundle getUserRestrictions(@NonNull ComponentName admin) { - throwIfParentInstance("getUserRestrictions"); Bundle ret = null; if (mService != null) { try { - ret = mService.getUserRestrictions(admin); + ret = mService.getUserRestrictions(admin, mParentInstance); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 949e8ab165bd..9c82ff6f51ac 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -208,8 +208,8 @@ interface IDevicePolicyManager { void setRestrictionsProvider(in ComponentName who, in ComponentName provider); ComponentName getRestrictionsProvider(int userHandle); - void setUserRestriction(in ComponentName who, in String key, boolean enable); - Bundle getUserRestrictions(in ComponentName who); + void setUserRestriction(in ComponentName who, in String key, boolean enable, boolean parent); + Bundle getUserRestrictions(in ComponentName who, boolean parent); void addCrossProfileIntentFilter(in ComponentName admin, in IntentFilter filter, int flags); void clearCrossProfileIntentFilters(in ComponentName admin); diff --git a/core/java/android/bluetooth/BluetoothA2dpSink.java b/core/java/android/bluetooth/BluetoothA2dpSink.java index cf3367602aa9..c6957e14116b 100755 --- a/core/java/android/bluetooth/BluetoothA2dpSink.java +++ b/core/java/android/bluetooth/BluetoothA2dpSink.java @@ -17,7 +17,9 @@ package android.bluetooth; import android.Manifest; +import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.UnsupportedAppUsage; import android.content.Context; @@ -39,6 +41,7 @@ import java.util.List; * * @hide */ +@SystemApi public final class BluetoothA2dpSink implements BluetoothProfile { private static final String TAG = "BluetoothA2dpSink"; private static final boolean DBG = true; @@ -59,71 +62,14 @@ public final class BluetoothA2dpSink implements BluetoothProfile { * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}. * - * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to - * receive. + * @hide */ + @SystemApi + @SuppressLint("ActionValue") + @RequiresPermission(Manifest.permission.BLUETOOTH) public static final String ACTION_CONNECTION_STATE_CHANGED = "android.bluetooth.a2dp-sink.profile.action.CONNECTION_STATE_CHANGED"; - /** - * Intent used to broadcast the change in the Playing state of the A2DP Sink - * profile. - * - * <p>This intent will have 3 extras: - * <ul> - * <li> {@link #EXTRA_STATE} - The current state of the profile. </li> - * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile. </li> - * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li> - * </ul> - * - * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of - * {@link #STATE_PLAYING}, {@link #STATE_NOT_PLAYING}, - * - * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to - * receive. - */ - public static final String ACTION_PLAYING_STATE_CHANGED = - "android.bluetooth.a2dp-sink.profile.action.PLAYING_STATE_CHANGED"; - - /** - * A2DP sink device is streaming music. This state can be one of - * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of - * {@link #ACTION_PLAYING_STATE_CHANGED} intent. - */ - public static final int STATE_PLAYING = 10; - - /** - * A2DP sink device is NOT streaming music. This state can be one of - * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of - * {@link #ACTION_PLAYING_STATE_CHANGED} intent. - */ - public static final int STATE_NOT_PLAYING = 11; - - /** - * Intent used to broadcast the change in the Playing state of the A2DP Sink - * profile. - * - * <p>This intent will have 3 extras: - * <ul> - * <li> {@link #EXTRA_AUDIO_CONFIG} - The audio configuration for the remote device. </li> - * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li> - * </ul> - * - * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to - * receive. - */ - public static final String ACTION_AUDIO_CONFIG_CHANGED = - "android.bluetooth.a2dp-sink.profile.action.AUDIO_CONFIG_CHANGED"; - - /** - * Extra for the {@link #ACTION_AUDIO_CONFIG_CHANGED} intent. - * - * This extra represents the current audio configuration of the A2DP source device. - * {@see BluetoothAudioConfig} - */ - public static final String EXTRA_AUDIO_CONFIG = - "android.bluetooth.a2dp-sink.profile.extra.AUDIO_CONFIG"; - private BluetoothAdapter mAdapter; private final BluetoothProfileConnector<IBluetoothA2dpSink> mProfileConnector = new BluetoothProfileConnector(this, BluetoothProfile.A2DP_SINK, @@ -170,13 +116,11 @@ public final class BluetoothA2dpSink implements BluetoothProfile { * the state. Users can get the connection state of the profile * from this intent. * - * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} - * permission. - * * @param device Remote Bluetooth Device * @return false on immediate error, true otherwise * @hide */ + @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) public boolean connect(BluetoothDevice device) { if (DBG) log("connect(" + device + ")"); final IBluetoothA2dpSink service = getService(); @@ -210,14 +154,12 @@ public final class BluetoothA2dpSink implements BluetoothProfile { * {@link #STATE_DISCONNECTING} can be used to distinguish between the * two scenarios. * - * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} - * permission. - * * @param device Remote Bluetooth Device * @return false on immediate error, true otherwise * @hide */ @UnsupportedAppUsage + @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) public boolean disconnect(BluetoothDevice device) { if (DBG) log("disconnect(" + device + ")"); final IBluetoothA2dpSink service = getService(); @@ -235,6 +177,8 @@ public final class BluetoothA2dpSink implements BluetoothProfile { /** * {@inheritDoc} + * + * @hide */ @Override public List<BluetoothDevice> getConnectedDevices() { @@ -254,6 +198,8 @@ public final class BluetoothA2dpSink implements BluetoothProfile { /** * {@inheritDoc} + * + * @hide */ @Override public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { @@ -273,6 +219,8 @@ public final class BluetoothA2dpSink implements BluetoothProfile { /** * {@inheritDoc} + * + * @hide */ @Override public int getConnectionState(BluetoothDevice device) { @@ -300,6 +248,8 @@ public final class BluetoothA2dpSink implements BluetoothProfile { * @return audio configuration for the device, or null * * {@see BluetoothAudioConfig} + * + * @hide */ public BluetoothAudioConfig getAudioConfig(BluetoothDevice device) { if (VDBG) log("getAudioConfig(" + device + ")"); @@ -347,7 +297,7 @@ public final class BluetoothA2dpSink implements BluetoothProfile { */ @SystemApi @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) - public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) { + public boolean setConnectionPolicy(@Nullable BluetoothDevice device, int connectionPolicy) { if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")"); final IBluetoothA2dpSink service = getService(); if (service != null && isEnabled() && isValidDevice(device)) { @@ -395,7 +345,7 @@ public final class BluetoothA2dpSink implements BluetoothProfile { */ @SystemApi @RequiresPermission(Manifest.permission.BLUETOOTH) - public int getConnectionPolicy(BluetoothDevice device) { + public int getConnectionPolicy(@Nullable BluetoothDevice device) { if (VDBG) log("getConnectionPolicy(" + device + ")"); final IBluetoothA2dpSink service = getService(); if (service != null && isEnabled() && isValidDevice(device)) { @@ -411,13 +361,16 @@ public final class BluetoothA2dpSink implements BluetoothProfile { } /** - * Check if A2DP profile is streaming music. - * - * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. + * Check if audio is playing on the bluetooth device (A2DP profile is streaming music). * * @param device BluetoothDevice device + * @return true if audio is playing (A2dp is streaming music), false otherwise + * + * @hide */ - public boolean isA2dpPlaying(BluetoothDevice device) { + @SystemApi + @RequiresPermission(Manifest.permission.BLUETOOTH) + public boolean isAudioPlaying(@Nullable BluetoothDevice device) { final IBluetoothA2dpSink service = getService(); if (service != null && isEnabled() && isValidDevice(device)) { try { @@ -448,9 +401,9 @@ public final class BluetoothA2dpSink implements BluetoothProfile { return "connected"; case STATE_DISCONNECTING: return "disconnecting"; - case STATE_PLAYING: + case BluetoothA2dp.STATE_PLAYING: return "playing"; - case STATE_NOT_PLAYING: + case BluetoothA2dp.STATE_NOT_PLAYING: return "not playing"; default: return "<unknown state " + state + ">"; diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 808f2160109d..24b5061bf27b 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -1060,6 +1060,31 @@ public abstract class Context { public abstract File getFilesDir(); /** + * Returns the absolute path to the directory that is related to the crate on the filesystem. + * <p> + * The crateId require a validated file name. It can't contain any "..", ".", + * {@link File#separatorChar} etc.. + * </p> + * <p> + * The returned path may change over time if the calling app is moved to an + * adopted storage device, so only relative paths should be persisted. + * </p> + * <p> + * No additional permissions are required for the calling app to read or + * write files under the returned path. + *</p> + * + * @param crateId the relative validated file name under {@link Context#getDataDir()}/crates + * @return the crate directory file. + * @hide + */ + @NonNull + @TestApi + public File getCrateDir(@NonNull String crateId) { + throw new RuntimeException("Not implemented. Must override in a subclass."); + } + + /** * Returns the absolute path to the directory on the filesystem similar to * {@link #getFilesDir()}. The difference is that files placed under this * directory will be excluded from automatic backup to remote storage. See @@ -4336,6 +4361,15 @@ public abstract class Context { public static final String SOUND_TRIGGER_SERVICE = "soundtrigger"; /** + * Use with {@link #getSystemService(String)} to access the + * {@link com.android.server.soundtrigger_middleware.SoundTriggerMiddlewareService}. + * + * @hide + * @see #getSystemService(String) + */ + public static final String SOUND_TRIGGER_MIDDLEWARE_SERVICE = "soundtrigger_middleware"; + + /** * Official published name of the (internal) permission service. * * @see #getSystemService(String) diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index d6442e28439f..d1b5135e959d 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -252,6 +252,16 @@ public class ContextWrapper extends Context { return mBase.getFilesDir(); } + /** + * {@inheritDoc Context#getCrateDir()} + * @hide + */ + @NonNull + @Override + public File getCrateDir(@NonNull String cratedId) { + return mBase.getCrateDir(cratedId); + } + @Override public File getNoBackupFilesDir() { return mBase.getNoBackupFilesDir(); diff --git a/core/java/android/content/pm/AuxiliaryResolveInfo.java b/core/java/android/content/pm/AuxiliaryResolveInfo.java index 202df50dda6f..7d07e1dc6361 100644 --- a/core/java/android/content/pm/AuxiliaryResolveInfo.java +++ b/core/java/android/content/pm/AuxiliaryResolveInfo.java @@ -46,17 +46,21 @@ public final class AuxiliaryResolveInfo { public final Intent failureIntent; /** The matching filters for this resolve info. */ public final List<AuxiliaryFilter> filters; + /** Stored {@link InstantAppRequest#hostDigestPrefixSecure} to prevent re-generation */ + public final int[] hostDigestPrefixSecure; /** Create a response for installing an instant application. */ public AuxiliaryResolveInfo(@NonNull String token, boolean needsPhase2, @Nullable Intent failureIntent, - @Nullable List<AuxiliaryFilter> filters) { + @Nullable List<AuxiliaryFilter> filters, + @Nullable int[] hostDigestPrefix) { this.token = token; this.needsPhaseTwo = needsPhase2; this.failureIntent = failureIntent; this.filters = filters; this.installFailureActivity = null; + this.hostDigestPrefixSecure = hostDigestPrefix; } /** Create a response for installing a split on demand. */ @@ -69,6 +73,7 @@ public final class AuxiliaryResolveInfo { this.token = null; this.needsPhaseTwo = false; this.failureIntent = failureIntent; + this.hostDigestPrefixSecure = null; } /** Create a response for installing a split on demand. */ @@ -126,4 +131,4 @@ public final class AuxiliaryResolveInfo { + ", splitName='" + splitName + '\'' + '}'; } } -}
\ No newline at end of file +} diff --git a/core/java/android/content/pm/BaseParceledListSlice.java b/core/java/android/content/pm/BaseParceledListSlice.java index 4178309450bd..ffbca1609557 100644 --- a/core/java/android/content/pm/BaseParceledListSlice.java +++ b/core/java/android/content/pm/BaseParceledListSlice.java @@ -47,7 +47,7 @@ abstract class BaseParceledListSlice<T> implements Parcelable { * TODO get this number from somewhere else. For now set it to a quarter of * the 1MB limit. */ - private static final int MAX_IPC_SIZE = IBinder.MAX_IPC_SIZE; + private static final int MAX_IPC_SIZE = IBinder.getSuggestedMaxIpcSizeBytes(); private final List<T> mList; diff --git a/core/java/android/content/pm/InstantAppRequest.java b/core/java/android/content/pm/InstantAppRequest.java index 361d4e4acb3b..f692db1f0443 100644 --- a/core/java/android/content/pm/InstantAppRequest.java +++ b/core/java/android/content/pm/InstantAppRequest.java @@ -16,9 +16,10 @@ package android.content.pm; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Intent; import android.os.Bundle; -import android.text.TextUtils; /** * Information needed to make an instant application resolution request. @@ -34,32 +35,40 @@ public final class InstantAppRequest { public final String resolvedType; /** The name of the package requesting the instant application */ public final String callingPackage; + /** Whether or not the requesting package was an instant app */ + public final boolean isRequesterInstantApp; /** ID of the user requesting the instant application */ public final int userId; /** * Optional extra bundle provided by the source application to the installer for additional - * verification. */ + * verification. + */ public final Bundle verificationBundle; /** Whether resolution occurs because an application is starting */ public final boolean resolveForStart; - /** The instant app digest for this request */ - public final InstantAppResolveInfo.InstantAppDigest digest; + /** + * The hash prefix of an instant app's domain or null if no host is defined. + * Secure version that should be carried through for external use. + */ + @Nullable + public final int[] hostDigestPrefixSecure; + /** A unique identifier */ + @NonNull + public final String token; public InstantAppRequest(AuxiliaryResolveInfo responseObj, Intent origIntent, - String resolvedType, String callingPackage, int userId, Bundle verificationBundle, - boolean resolveForStart) { + String resolvedType, String callingPackage, boolean isRequesterInstantApp, + int userId, Bundle verificationBundle, boolean resolveForStart, + @Nullable int[] hostDigestPrefixSecure, @NonNull String token) { this.responseObj = responseObj; this.origIntent = origIntent; this.resolvedType = resolvedType; this.callingPackage = callingPackage; + this.isRequesterInstantApp = isRequesterInstantApp; this.userId = userId; this.verificationBundle = verificationBundle; this.resolveForStart = resolveForStart; - if (origIntent.getData() != null && !TextUtils.isEmpty(origIntent.getData().getHost())) { - digest = new InstantAppResolveInfo.InstantAppDigest( - origIntent.getData().getHost(), 5 /*maxDigests*/); - } else { - digest = InstantAppResolveInfo.InstantAppDigest.UNDEFINED; - } + this.hostDigestPrefixSecure = hostDigestPrefixSecure; + this.token = token; } } diff --git a/core/java/android/content/pm/InstantAppRequestInfo.aidl b/core/java/android/content/pm/InstantAppRequestInfo.aidl new file mode 100644 index 000000000000..0f94220bc5da --- /dev/null +++ b/core/java/android/content/pm/InstantAppRequestInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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; + +parcelable InstantAppRequestInfo; diff --git a/core/java/android/content/pm/InstantAppRequestInfo.java b/core/java/android/content/pm/InstantAppRequestInfo.java new file mode 100644 index 000000000000..83d55361ff2a --- /dev/null +++ b/core/java/android/content/pm/InstantAppRequestInfo.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2019 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.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.Intent; +import android.os.Parcelable; +import android.os.UserHandle; + +import com.android.internal.util.DataClass; + +/** + * Information exposed to {@link android.app.InstantAppResolverService} to complete + * an instant application resolution request. + * @hide + */ +@SystemApi +@DataClass(genParcelable = true, genConstructor = true, genAidl = true) +public final class InstantAppRequestInfo implements Parcelable { + + /** + * The sanitized {@link Intent} used for resolution. A sanitized Intent is an intent with + * potential PII removed from the original intent. Fields removed include extras and the + * host + path of the data, if defined. + */ + @NonNull + public final Intent intent; + + /** The hash prefix of the instant app's domain or null if no host is defined. */ + @Nullable + public final int[] hostDigestPrefix; + + /** The user requesting the instant application */ + @NonNull + public final UserHandle userHandle; + + /** Whether or not the requesting package was an instant app itself */ + public final boolean isRequesterInstantApp; + + /** A unique identifier */ + @NonNull + public final String token; + + + + // Code below generated by codegen v1.0.13. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/content/pm/InstantAppRequestInfo.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + /** + * Creates a new InstantAppRequestInfo. + * + * @param intent + * The sanitized {@link Intent} used for resolution. A sanitized Intent is an intent with + * potential PII removed from the original intent. Fields removed include extras and the + * host + path of the data, if defined. + * @param hostDigestPrefix + * The hash prefix of the instant app's domain or null if no host is defined. + * @param userHandle + * The user requesting the instant application + * @param isRequesterInstantApp + * Whether or not the requesting package was an instant app itself + * @param token + * A unique identifier + */ + @DataClass.Generated.Member + public InstantAppRequestInfo( + @NonNull Intent intent, + @Nullable int[] hostDigestPrefix, + @NonNull UserHandle userHandle, + boolean isRequesterInstantApp, + @NonNull String token) { + this.intent = intent; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, intent); + this.hostDigestPrefix = hostDigestPrefix; + this.userHandle = userHandle; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, userHandle); + this.isRequesterInstantApp = isRequesterInstantApp; + this.token = token; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, token); + + // onConstructed(); // You can define this method to get a callback + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + byte flg = 0; + if (isRequesterInstantApp) flg |= 0x8; + if (hostDigestPrefix != null) flg |= 0x2; + dest.writeByte(flg); + dest.writeTypedObject(intent, flags); + if (hostDigestPrefix != null) dest.writeIntArray(hostDigestPrefix); + dest.writeTypedObject(userHandle, flags); + dest.writeString(token); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ InstantAppRequestInfo(@NonNull android.os.Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + byte flg = in.readByte(); + boolean _isRequesterInstantApp = (flg & 0x8) != 0; + Intent _intent = (Intent) in.readTypedObject(Intent.CREATOR); + int[] _hostDigestPrefix = (flg & 0x2) == 0 ? null : in.createIntArray(); + UserHandle _userHandle = (UserHandle) in.readTypedObject(UserHandle.CREATOR); + String _token = in.readString(); + + this.intent = _intent; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, intent); + this.hostDigestPrefix = _hostDigestPrefix; + this.userHandle = _userHandle; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, userHandle); + this.isRequesterInstantApp = _isRequesterInstantApp; + this.token = _token; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, token); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<InstantAppRequestInfo> CREATOR + = new Parcelable.Creator<InstantAppRequestInfo>() { + @Override + public InstantAppRequestInfo[] newArray(int size) { + return new InstantAppRequestInfo[size]; + } + + @Override + public InstantAppRequestInfo createFromParcel(@NonNull android.os.Parcel in) { + return new InstantAppRequestInfo(in); + } + }; + + @DataClass.Generated( + time = 1574373347443L, + codegenVersion = "1.0.13", + sourceFile = "frameworks/base/core/java/android/content/pm/InstantAppRequestInfo.java", + inputSignatures = "public final @android.annotation.NonNull android.content.Intent intent\npublic final @android.annotation.Nullable int[] hostDigestPrefix\npublic final @android.annotation.NonNull android.os.UserHandle userHandle\npublic final boolean isRequesterInstantApp\npublic final @android.annotation.NonNull java.lang.String token\nclass InstantAppRequestInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genConstructor=true, genAidl=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 94af5416aa8d..0d1f4046e70e 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1513,16 +1513,6 @@ public abstract class PackageManager { public static final int DELETE_DONT_KILL_APP = 0x00000008; /** - * Flag parameter for {@link #deletePackage} to indicate that any - * contributed media should also be deleted during this uninstall. The - * meaning of "contributed" means it won't automatically be deleted when the - * app is uninstalled. - * - * @hide - */ - public static final int DELETE_CONTRIBUTED_MEDIA = 0x00000010; - - /** * Flag parameter for {@link #deletePackage} to indicate that package deletion * should be chatty. * diff --git a/core/java/android/content/pm/parsing/AndroidPackage.java b/core/java/android/content/pm/parsing/AndroidPackage.java index 515185eaaf57..35df47431a91 100644 --- a/core/java/android/content/pm/parsing/AndroidPackage.java +++ b/core/java/android/content/pm/parsing/AndroidPackage.java @@ -229,6 +229,11 @@ public interface AndroidPackage extends Parcelable { String getOverlayTargetName(); + /** + * Map of overlayable name to actor name. + */ + Map<String, String> getOverlayables(); + // TODO(b/135203078): Does this and getAppInfoPackageName have to be separate methods? // The refactor makes them the same value with no known consequences, so should be redundant. String getPackageName(); diff --git a/core/java/android/content/pm/parsing/ApkParseUtils.java b/core/java/android/content/pm/parsing/ApkParseUtils.java index 0deb2ab6747b..773231682fe4 100644 --- a/core/java/android/content/pm/parsing/ApkParseUtils.java +++ b/core/java/android/content/pm/parsing/ApkParseUtils.java @@ -52,6 +52,7 @@ import android.content.pm.permission.SplitPermissionInfoParcelable; import android.content.pm.split.DefaultSplitAssetLoader; import android.content.pm.split.SplitAssetDependencyLoader; import android.content.pm.split.SplitAssetLoader; +import android.content.res.ApkAssets; import android.content.res.AssetManager; import android.content.res.Configuration; import android.content.res.Resources; @@ -92,6 +93,7 @@ import java.security.PublicKey; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Set; /** @hide */ @@ -287,8 +289,23 @@ public class ApkParseUtils { + result.getErrorMessage()); } - return result.getResultAndNull() - .setVolumeUuid(volumeUuid) + ParsingPackage pkg = result.getResultAndNull(); + ApkAssets apkAssets = assets.getApkAssets()[0]; + if (apkAssets.definesOverlayable()) { + SparseArray<String> packageNames = assets.getAssignedPackageIdentifiers(); + int size = packageNames.size(); + for (int index = 0; index < size; index++) { + String packageName = packageNames.get(index); + Map<String, String> overlayableToActor = assets.getOverlayableMap(packageName); + if (overlayableToActor != null && !overlayableToActor.isEmpty()) { + for (String overlayable : overlayableToActor.keySet()) { + pkg.addOverlayable(overlayable, overlayableToActor.get(overlayable)); + } + } + } + } + + return pkg.setVolumeUuid(volumeUuid) .setApplicationVolumeUuid(volumeUuid) .setSigningDetails(SigningDetails.UNKNOWN); } catch (PackageParserException e) { diff --git a/core/java/android/content/pm/parsing/PackageImpl.java b/core/java/android/content/pm/parsing/PackageImpl.java index 377279e750c6..0e736d522c10 100644 --- a/core/java/android/content/pm/parsing/PackageImpl.java +++ b/core/java/android/content/pm/parsing/PackageImpl.java @@ -18,6 +18,8 @@ package android.content.pm.parsing; import static android.os.Build.VERSION_CODES.DONUT; +import static java.util.Collections.emptyMap; + import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Intent; @@ -55,11 +57,13 @@ import android.util.SparseArray; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.CollectionUtils; import com.android.server.SystemConfig; import java.security.PublicKey; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -126,6 +130,7 @@ public final class PackageImpl implements ParsingPackage, ParsedPackage, Android private String overlayCategory; private int overlayPriority; private boolean overlayIsStatic; + private Map<String, String> overlayables = emptyMap(); private String staticSharedLibName; private long staticSharedLibVersion; @@ -475,7 +480,7 @@ public final class PackageImpl implements ParsingPackage, ParsedPackage, Android @Override public Map<String, ArraySet<PublicKey>> getKeySetMapping() { - return keySetMapping == null ? Collections.emptyMap() : keySetMapping; + return keySetMapping == null ? emptyMap() : keySetMapping; } @Override @@ -773,6 +778,13 @@ public final class PackageImpl implements ParsingPackage, ParsedPackage, Android } @Override + public ParsingPackage addOverlayable(String overlayableName, String actorName) { + this.overlayables = CollectionUtils.add(this.overlayables, + TextUtils.safeIntern(overlayableName), TextUtils.safeIntern(actorName)); + return this; + } + + @Override public PackageImpl addAdoptPermission(String adoptPermission) { this.adoptPermissions = ArrayUtils.add(this.adoptPermissions, adoptPermission); return this; @@ -2125,6 +2137,11 @@ public final class PackageImpl implements ParsingPackage, ParsedPackage, Android } @Override + public Map<String, String> getOverlayables() { + return overlayables; + } + + @Override public boolean isOverlayIsStatic() { return overlayIsStatic; } @@ -2291,7 +2308,7 @@ public final class PackageImpl implements ParsingPackage, ParsedPackage, Android appInfo.metaData = appMetaData; appInfo.minAspectRatio = minAspectRatio; appInfo.minSdkVersion = minSdkVersion; - appInfo.name = name; + appInfo.name = className; if (appInfo.name != null) { appInfo.name = appInfo.name.trim(); } @@ -2957,6 +2974,7 @@ public final class PackageImpl implements ParsingPackage, ParsedPackage, Android dest.writeString(this.overlayCategory); dest.writeInt(this.overlayPriority); dest.writeBoolean(this.overlayIsStatic); + dest.writeMap(this.overlayables); dest.writeString(this.staticSharedLibName); dest.writeLong(this.staticSharedLibVersion); dest.writeStringList(this.libraryNames); @@ -3100,6 +3118,8 @@ public final class PackageImpl implements ParsingPackage, ParsedPackage, Android this.overlayCategory = in.readString(); this.overlayPriority = in.readInt(); this.overlayIsStatic = in.readBoolean(); + this.overlayables = new HashMap<>(); + in.readMap(overlayables, boot); this.staticSharedLibName = TextUtils.safeIntern(in.readString()); this.staticSharedLibVersion = in.readLong(); this.libraryNames = in.createStringArrayList(); diff --git a/core/java/android/content/pm/parsing/ParsingPackage.java b/core/java/android/content/pm/parsing/ParsingPackage.java index 43c1f6e335b0..aff1b2e05eaf 100644 --- a/core/java/android/content/pm/parsing/ParsingPackage.java +++ b/core/java/android/content/pm/parsing/ParsingPackage.java @@ -62,6 +62,8 @@ public interface ParsingPackage extends AndroidPackage { ParsingPackage addOriginalPackage(String originalPackage); + ParsingPackage addOverlayable(String overlayableName, String actorName); + ParsingPackage addPermission(ParsedPermission permission); ParsingPackage addPermissionGroup(ParsedPermissionGroup permissionGroup); diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java index 48d88678f721..39c1daca1bfd 100644 --- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java +++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java @@ -823,6 +823,7 @@ public class SQLiteQueryBuilder { switch (token.toUpperCase(Locale.US)) { case "COLLATE": case "ASC": case "DESC": case "BINARY": case "RTRIM": case "NOCASE": + case "LOCALIZED": case "UNICODE": return; } throw new IllegalArgumentException("Invalid token " + token); diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index 1f29d1a22325..c84c4a762796 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -935,7 +935,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri /** * <p>List of the maximum number of regions that can be used for metering in * auto-exposure (AE), auto-white balance (AWB), and auto-focus (AF); - * this corresponds to the the maximum number of elements in + * this corresponds to the maximum number of elements in * {@link CaptureRequest#CONTROL_AE_REGIONS android.control.aeRegions}, {@link CaptureRequest#CONTROL_AWB_REGIONS android.control.awbRegions}, * and {@link CaptureRequest#CONTROL_AF_REGIONS android.control.afRegions}.</p> * <p><b>Range of valid values:</b><br></p> @@ -955,7 +955,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri /** * <p>The maximum number of metering regions that can be used by the auto-exposure (AE) * routine.</p> - * <p>This corresponds to the the maximum allowed number of elements in + * <p>This corresponds to the maximum allowed number of elements in * {@link CaptureRequest#CONTROL_AE_REGIONS android.control.aeRegions}.</p> * <p><b>Range of valid values:</b><br> * Value will be >= 0. For FULL-capability devices, this @@ -973,7 +973,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri /** * <p>The maximum number of metering regions that can be used by the auto-white balance (AWB) * routine.</p> - * <p>This corresponds to the the maximum allowed number of elements in + * <p>This corresponds to the maximum allowed number of elements in * {@link CaptureRequest#CONTROL_AWB_REGIONS android.control.awbRegions}.</p> * <p><b>Range of valid values:</b><br> * Value will be >= 0.</p> @@ -989,7 +989,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri /** * <p>The maximum number of metering regions that can be used by the auto-focus (AF) routine.</p> - * <p>This corresponds to the the maximum allowed number of elements in + * <p>This corresponds to the maximum allowed number of elements in * {@link CaptureRequest#CONTROL_AF_REGIONS android.control.afRegions}.</p> * <p><b>Range of valid values:</b><br> * Value will be >= 0. For FULL-capability devices, this @@ -1987,6 +1987,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME MONOCHROME}</li> * <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_SECURE_IMAGE_DATA SECURE_IMAGE_DATA}</li> * <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_SYSTEM_CAMERA SYSTEM_CAMERA}</li> + * <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_OFFLINE_PROCESSING OFFLINE_PROCESSING}</li> * </ul></p> * <p>This key is available on all devices.</p> * @@ -2006,6 +2007,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * @see #REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME * @see #REQUEST_AVAILABLE_CAPABILITIES_SECURE_IMAGE_DATA * @see #REQUEST_AVAILABLE_CAPABILITIES_SYSTEM_CAMERA + * @see #REQUEST_AVAILABLE_CAPABILITIES_OFFLINE_PROCESSING */ @PublicKey @NonNull diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java index 799c71683d8a..f2a7abd60d85 100644 --- a/core/java/android/hardware/camera2/CameraMetadata.java +++ b/core/java/android/hardware/camera2/CameraMetadata.java @@ -1004,6 +1004,51 @@ public abstract class CameraMetadata<TKey> { */ public static final int REQUEST_AVAILABLE_CAPABILITIES_SYSTEM_CAMERA = 14; + /** + * <p>The camera device supports the OFFLINE_PROCESSING use case.</p> + * <p>With OFFLINE_PROCESSING capability, the application can switch an ongoing + * capture session to offline mode by calling the + * CameraCaptureSession#switchToOffline method and specify streams to be kept in offline + * mode. The camera will then stop currently active repeating requests, prepare for + * some requests to go into offline mode, and return an offline session object. After + * the switchToOffline call returns, the original capture session is in closed state as + * if the CameraCaptureSession#close method has been called. + * In the offline mode, all inflight requests will continue to be processed in the + * background, and the application can immediately close the camera or create a new + * capture session without losing those requests' output images and capture results.</p> + * <p>While the camera device is processing offline requests, it + * might not be able to support all stream configurations it can support + * without offline requests. When that happens, the createCaptureSession + * method call will fail. The following stream configurations are guaranteed to work + * without hitting the resource busy exception:</p> + * <ul> + * <li>One ongoing offline session: target one output surface of YUV or + * JPEG format, any resolution.</li> + * <li>The active camera capture session:<ol> + * <li>One preview surface (SurfaceView or SurfaceTexture) up to 1920 width</li> + * <li>One YUV ImageReader surface up to 1920 width</li> + * <li>One Jpeg ImageReader, any resolution: the camera device is + * allowed to slow down JPEG output speed by 50% if there is any ongoing offline + * session.</li> + * <li>One ImageWriter surface of private format, any resolution if the device supports + * PRIVATE_REPROCESSING capability</li> + * </ol> + * </li> + * <li>Alternatively, the active camera session above can be replaced by an legacy + * {@link android.hardware.Camera Camera} with the following parameter settings:<ol> + * <li>Preview size up to 1920 width</li> + * <li>Preview callback size up to 1920 width</li> + * <li>Video size up to 1920 width</li> + * <li>Picture size, any resolution: the camera device is + * allowed to slow down JPEG output speed by 50% if there is any ongoing offline + * session.</li> + * </ol> + * </li> + * </ul> + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + */ + public static final int REQUEST_AVAILABLE_CAPABILITIES_OFFLINE_PROCESSING = 15; + // // Enumeration values for CameraCharacteristics#SCALER_CROPPING_TYPE // diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index bcbc33780ec4..41435c9b6a40 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -29,6 +29,7 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.hardware.camera2.ICameraDeviceCallbacks; import android.hardware.camera2.ICameraDeviceUser; +import android.hardware.camera2.ICameraOfflineSession; import android.hardware.camera2.TotalCaptureResult; import android.hardware.camera2.params.InputConfiguration; import android.hardware.camera2.params.OutputConfiguration; @@ -866,6 +867,38 @@ public class CameraDeviceImpl extends CameraDevice } } + public void switchToOffline(ICameraDeviceCallbacks cbs, Surface[] offlineOutputs) + throws CameraAccessException { + if ((offlineOutputs == null) || (offlineOutputs.length == 0)) { + throw new IllegalArgumentException("Invalid offline outputs!"); + } + if (cbs == null) { + throw new IllegalArgumentException("Invalid device callbacks!"); + } + + ICameraOfflineSession offlineSession = null; + synchronized(mInterfaceLock) { + int streamId = -1; + for (Surface surface : offlineOutputs) { + for (int i = 0; i < mConfiguredOutputs.size(); i++) { + if (surface == mConfiguredOutputs.valueAt(i).getSurface()) { + streamId = mConfiguredOutputs.keyAt(i); + break; + } + } + if (streamId == -1) { + throw new IllegalArgumentException("Offline surface is not part of this" + + " session"); + } + } + + offlineSession = mRemoteDevice.switchToOffline(cbs, + offlineOutputs); + // TODO: Initialize CameraOfflineSession wrapper, clear 'mConfiguredOutputs', + // and update request tracking + } + } + public void tearDown(Surface surface) throws CameraAccessException { if (surface == null) throw new IllegalArgumentException("Surface is null"); diff --git a/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java b/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java index 3660f29f68d1..397417ba5b4f 100644 --- a/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java +++ b/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java @@ -28,6 +28,8 @@ import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.ICameraDeviceUser; +import android.hardware.camera2.ICameraDeviceCallbacks; +import android.hardware.camera2.ICameraOfflineSession; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; @@ -248,6 +250,17 @@ public class ICameraDeviceUserWrapper { } } + public ICameraOfflineSession switchToOffline(ICameraDeviceCallbacks cbs, + Surface[] offlineOutputs) + throws CameraAccessException { + try { + return mRemoteDevice.switchToOffline(cbs, offlineOutputs); + } catch (Throwable t) { + CameraManager.throwAsPublicException(t); + throw new UnsupportedOperationException("Unexpected exception", t); + } + } + public void finalizeOutputConfigurations(int streamId, OutputConfiguration deferredConfig) throws CameraAccessException { try { diff --git a/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java b/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java index 5d1435ab4c8d..6ab0c294f5e4 100644 --- a/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java +++ b/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java @@ -24,6 +24,7 @@ import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.ICameraDeviceCallbacks; import android.hardware.camera2.ICameraDeviceUser; +import android.hardware.camera2.ICameraOfflineSession; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.impl.CaptureResultExtras; import android.hardware.camera2.impl.PhysicalCaptureResultInfo; @@ -789,6 +790,12 @@ public class CameraDeviceUserShim implements ICameraDeviceUser { } @Override + public ICameraOfflineSession switchToOffline(ICameraDeviceCallbacks cbs, + Surface[] offlineOutputs) { + throw new UnsupportedOperationException("Legacy device does not support switchToOffline"); + } + + @Override public IBinder asBinder() { // This is solely intended to be used for in-process binding. return null; diff --git a/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java b/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java index 6eaf54bd0d4d..23f18a80caf8 100644 --- a/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java +++ b/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java @@ -1163,7 +1163,7 @@ public final class MandatoryStreamCombination { if (orderedPreviewSizes != null) { for (Size size : orderedPreviewSizes) { if ((mDisplaySize.getWidth() >= size.getWidth()) && - (mDisplaySize.getWidth() >= size.getHeight())) { + (mDisplaySize.getHeight() >= size.getHeight())) { return size; } } diff --git a/core/java/android/hardware/soundtrigger/ConversionUtil.java b/core/java/android/hardware/soundtrigger/ConversionUtil.java new file mode 100644 index 000000000000..8231c58a105e --- /dev/null +++ b/core/java/android/hardware/soundtrigger/ConversionUtil.java @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2019 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.soundtrigger; + +import android.hardware.soundtrigger.ModelParams; +import android.media.AudioFormat; +import android.media.audio.common.AudioConfig; +import android.media.soundtrigger_middleware.ConfidenceLevel; +import android.media.soundtrigger_middleware.ModelParameterRange; +import android.media.soundtrigger_middleware.Phrase; +import android.media.soundtrigger_middleware.PhraseRecognitionEvent; +import android.media.soundtrigger_middleware.PhraseRecognitionExtra; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionConfig; +import android.media.soundtrigger_middleware.RecognitionEvent; +import android.media.soundtrigger_middleware.RecognitionMode; +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; +import android.media.soundtrigger_middleware.SoundTriggerModuleProperties; + +import android.annotation.Nullable; + +import java.util.Arrays; +import java.util.UUID; + +/** @hide */ +class ConversionUtil { + public static SoundTrigger.ModuleProperties aidl2apiModuleDescriptor( + SoundTriggerModuleDescriptor aidlDesc) { + SoundTriggerModuleProperties properties = aidlDesc.properties; + return new SoundTrigger.ModuleProperties( + aidlDesc.handle, + properties.implementor, + properties.description, + properties.uuid, + properties.version, + properties.maxSoundModels, + properties.maxKeyPhrases, + properties.maxUsers, + aidl2apiRecognitionModes(properties.recognitionModes), + properties.captureTransition, + properties.maxBufferMs, + properties.concurrentCapture, + properties.powerConsumptionMw, + properties.triggerInEvent + ); + } + + public static int aidl2apiRecognitionModes(int aidlModes) { + int result = 0; + if ((aidlModes & RecognitionMode.VOICE_TRIGGER) != 0) { + result |= SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER; + } + if ((aidlModes & RecognitionMode.USER_IDENTIFICATION) != 0) { + result |= SoundTrigger.RECOGNITION_MODE_USER_IDENTIFICATION; + } + if ((aidlModes & RecognitionMode.USER_AUTHENTICATION) != 0) { + result |= SoundTrigger.RECOGNITION_MODE_USER_AUTHENTICATION; + } + if ((aidlModes & RecognitionMode.GENERIC_TRIGGER) != 0) { + result |= SoundTrigger.RECOGNITION_MODE_GENERIC; + } + return result; + } + + public static int api2aidlRecognitionModes(int apiModes) { + int result = 0; + if ((apiModes & SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER) != 0) { + result |= RecognitionMode.VOICE_TRIGGER; + } + if ((apiModes & SoundTrigger.RECOGNITION_MODE_USER_IDENTIFICATION) != 0) { + result |= RecognitionMode.USER_IDENTIFICATION; + } + if ((apiModes & SoundTrigger.RECOGNITION_MODE_USER_AUTHENTICATION) != 0) { + result |= RecognitionMode.USER_AUTHENTICATION; + } + if ((apiModes & SoundTrigger.RECOGNITION_MODE_GENERIC) != 0) { + result |= RecognitionMode.GENERIC_TRIGGER; + } + return result; + } + + + public static SoundModel api2aidlGenericSoundModel(SoundTrigger.GenericSoundModel apiModel) { + return api2aidlSoundModel(apiModel); + } + + public static SoundModel api2aidlSoundModel(SoundTrigger.SoundModel apiModel) { + SoundModel aidlModel = new SoundModel(); + aidlModel.type = apiModel.type; + aidlModel.uuid = api2aidlUuid(apiModel.uuid); + aidlModel.vendorUuid = api2aidlUuid(apiModel.vendorUuid); + aidlModel.data = Arrays.copyOf(apiModel.data, apiModel.data.length); + return aidlModel; + } + + public static String api2aidlUuid(UUID apiUuid) { + return apiUuid.toString(); + } + + public static PhraseSoundModel api2aidlPhraseSoundModel( + SoundTrigger.KeyphraseSoundModel apiModel) { + PhraseSoundModel aidlModel = new PhraseSoundModel(); + aidlModel.common = api2aidlSoundModel(apiModel); + aidlModel.phrases = new Phrase[apiModel.keyphrases.length]; + for (int i = 0; i < apiModel.keyphrases.length; ++i) { + aidlModel.phrases[i] = api2aidlPhrase(apiModel.keyphrases[i]); + } + return aidlModel; + } + + public static Phrase api2aidlPhrase(SoundTrigger.Keyphrase apiPhrase) { + Phrase aidlPhrase = new Phrase(); + aidlPhrase.id = apiPhrase.id; + aidlPhrase.recognitionModes = api2aidlRecognitionModes(apiPhrase.recognitionModes); + aidlPhrase.users = Arrays.copyOf(apiPhrase.users, apiPhrase.users.length); + aidlPhrase.locale = apiPhrase.locale; + aidlPhrase.text = apiPhrase.text; + return aidlPhrase; + } + + public static RecognitionConfig api2aidlRecognitionConfig( + SoundTrigger.RecognitionConfig apiConfig) { + RecognitionConfig aidlConfig = new RecognitionConfig(); + aidlConfig.captureRequested = apiConfig.captureRequested; + // apiConfig.allowMultipleTriggers is ignored by the lower layers. + aidlConfig.phraseRecognitionExtras = + new PhraseRecognitionExtra[apiConfig.keyphrases.length]; + for (int i = 0; i < apiConfig.keyphrases.length; ++i) { + aidlConfig.phraseRecognitionExtras[i] = api2aidlPhraseRecognitionExtra( + apiConfig.keyphrases[i]); + } + aidlConfig.data = Arrays.copyOf(apiConfig.data, apiConfig.data.length); + return aidlConfig; + } + + public static PhraseRecognitionExtra api2aidlPhraseRecognitionExtra( + SoundTrigger.KeyphraseRecognitionExtra apiExtra) { + PhraseRecognitionExtra aidlExtra = new PhraseRecognitionExtra(); + aidlExtra.id = apiExtra.id; + aidlExtra.recognitionModes = api2aidlRecognitionModes(apiExtra.recognitionModes); + aidlExtra.confidenceLevel = apiExtra.coarseConfidenceLevel; + aidlExtra.levels = new ConfidenceLevel[apiExtra.confidenceLevels.length]; + for (int i = 0; i < apiExtra.confidenceLevels.length; ++i) { + aidlExtra.levels[i] = api2aidlConfidenceLevel(apiExtra.confidenceLevels[i]); + } + return aidlExtra; + } + + public static SoundTrigger.KeyphraseRecognitionExtra aidl2apiPhraseRecognitionExtra( + PhraseRecognitionExtra aidlExtra) { + SoundTrigger.ConfidenceLevel[] apiLevels = + new SoundTrigger.ConfidenceLevel[aidlExtra.levels.length]; + for (int i = 0; i < aidlExtra.levels.length; ++i) { + apiLevels[i] = aidl2apiConfidenceLevel(aidlExtra.levels[i]); + } + return new SoundTrigger.KeyphraseRecognitionExtra(aidlExtra.id, + aidl2apiRecognitionModes(aidlExtra.recognitionModes), + aidlExtra.confidenceLevel, apiLevels); + } + + public static ConfidenceLevel api2aidlConfidenceLevel( + SoundTrigger.ConfidenceLevel apiLevel) { + ConfidenceLevel aidlLevel = new ConfidenceLevel(); + aidlLevel.levelPercent = apiLevel.confidenceLevel; + aidlLevel.userId = apiLevel.userId; + return aidlLevel; + } + + public static SoundTrigger.ConfidenceLevel aidl2apiConfidenceLevel( + ConfidenceLevel apiLevel) { + return new SoundTrigger.ConfidenceLevel(apiLevel.userId, apiLevel.levelPercent); + } + + public static SoundTrigger.RecognitionEvent aidl2apiRecognitionEvent( + int modelHandle, RecognitionEvent aidlEvent) { + return new SoundTrigger.GenericRecognitionEvent( + aidlEvent.status, + modelHandle, aidlEvent.captureAvailable, aidlEvent.captureSession, + aidlEvent.captureDelayMs, aidlEvent.capturePreambleMs, aidlEvent.triggerInData, + aidl2apiAudioFormat(aidlEvent.audioConfig), aidlEvent.data); + } + + public static SoundTrigger.RecognitionEvent aidl2apiPhraseRecognitionEvent( + int modelHandle, + PhraseRecognitionEvent aidlEvent) { + SoundTrigger.KeyphraseRecognitionExtra[] apiExtras = + new SoundTrigger.KeyphraseRecognitionExtra[aidlEvent.phraseExtras.length]; + for (int i = 0; i < aidlEvent.phraseExtras.length; ++i) { + apiExtras[i] = aidl2apiPhraseRecognitionExtra(aidlEvent.phraseExtras[i]); + } + return new SoundTrigger.KeyphraseRecognitionEvent(aidlEvent.common.status, modelHandle, + aidlEvent.common.captureAvailable, + aidlEvent.common.captureSession, aidlEvent.common.captureDelayMs, + aidlEvent.common.capturePreambleMs, aidlEvent.common.triggerInData, + aidl2apiAudioFormat(aidlEvent.common.audioConfig), aidlEvent.common.data, + apiExtras); + } + + public static AudioFormat aidl2apiAudioFormat(AudioConfig audioConfig) { + AudioFormat.Builder apiBuilder = new AudioFormat.Builder(); + apiBuilder.setSampleRate(audioConfig.sampleRateHz); + apiBuilder.setChannelMask(aidl2apiChannelInMask(audioConfig.channelMask)); + apiBuilder.setEncoding(aidl2apiEncoding(audioConfig.format)); + return apiBuilder.build(); + } + + public static int aidl2apiEncoding(int aidlFormat) { + switch (aidlFormat) { + case android.media.audio.common.AudioFormat.PCM + | android.media.audio.common.AudioFormat.PCM_SUB_16_BIT: + return AudioFormat.ENCODING_PCM_16BIT; + + case android.media.audio.common.AudioFormat.PCM + | android.media.audio.common.AudioFormat.PCM_SUB_8_BIT: + return AudioFormat.ENCODING_PCM_8BIT; + + case android.media.audio.common.AudioFormat.PCM + | android.media.audio.common.AudioFormat.PCM_SUB_FLOAT: + case android.media.audio.common.AudioFormat.PCM + | android.media.audio.common.AudioFormat.PCM_SUB_8_24_BIT: + case android.media.audio.common.AudioFormat.PCM + | android.media.audio.common.AudioFormat.PCM_SUB_24_BIT_PACKED: + case android.media.audio.common.AudioFormat.PCM + | android.media.audio.common.AudioFormat.PCM_SUB_32_BIT: + return AudioFormat.ENCODING_PCM_FLOAT; + + case android.media.audio.common.AudioFormat.AC3: + return AudioFormat.ENCODING_AC3; + + case android.media.audio.common.AudioFormat.E_AC3: + return AudioFormat.ENCODING_E_AC3; + + case android.media.audio.common.AudioFormat.DTS: + return AudioFormat.ENCODING_DTS; + + case android.media.audio.common.AudioFormat.DTS_HD: + return AudioFormat.ENCODING_DTS_HD; + + case android.media.audio.common.AudioFormat.MP3: + return AudioFormat.ENCODING_MP3; + + case android.media.audio.common.AudioFormat.AAC + | android.media.audio.common.AudioFormat.AAC_SUB_LC: + return AudioFormat.ENCODING_AAC_LC; + + case android.media.audio.common.AudioFormat.AAC + | android.media.audio.common.AudioFormat.AAC_SUB_HE_V1: + return AudioFormat.ENCODING_AAC_HE_V1; + + case android.media.audio.common.AudioFormat.AAC + | android.media.audio.common.AudioFormat.AAC_SUB_HE_V2: + return AudioFormat.ENCODING_AAC_HE_V2; + + case android.media.audio.common.AudioFormat.IEC61937: + return AudioFormat.ENCODING_IEC61937; + + case android.media.audio.common.AudioFormat.DOLBY_TRUEHD: + return AudioFormat.ENCODING_DOLBY_TRUEHD; + + case android.media.audio.common.AudioFormat.AAC + | android.media.audio.common.AudioFormat.AAC_SUB_ELD: + return AudioFormat.ENCODING_AAC_ELD; + + case android.media.audio.common.AudioFormat.AAC + | android.media.audio.common.AudioFormat.AAC_SUB_XHE: + return AudioFormat.ENCODING_AAC_XHE; + + case android.media.audio.common.AudioFormat.AC4: + return AudioFormat.ENCODING_AC4; + + case android.media.audio.common.AudioFormat.E_AC3 + | android.media.audio.common.AudioFormat.E_AC3_SUB_JOC: + return AudioFormat.ENCODING_E_AC3_JOC; + + case android.media.audio.common.AudioFormat.MAT: + case android.media.audio.common.AudioFormat.MAT + | android.media.audio.common.AudioFormat.MAT_SUB_1_0: + case android.media.audio.common.AudioFormat.MAT + | android.media.audio.common.AudioFormat.MAT_SUB_2_0: + case android.media.audio.common.AudioFormat.MAT + | android.media.audio.common.AudioFormat.MAT_SUB_2_1: + return AudioFormat.ENCODING_DOLBY_MAT; + + case android.media.audio.common.AudioFormat.DEFAULT: + return AudioFormat.ENCODING_DEFAULT; + + default: + return AudioFormat.ENCODING_INVALID; + } + } + + public static int api2aidlModelParameter(int apiParam) { + switch (apiParam) { + case ModelParams.THRESHOLD_FACTOR: + return android.media.soundtrigger_middleware.ModelParameter.THRESHOLD_FACTOR; + default: + return android.media.soundtrigger_middleware.ModelParameter.INVALID; + } + } + + public static int aidl2apiChannelInMask(int aidlMask) { + // We're assuming AudioFormat.CHANNEL_IN_* constants are kept in sync with + // android.media.audio.common.AudioChannelMask. + return aidlMask; + } + + public static SoundTrigger.ModelParamRange aidl2apiModelParameterRange( + @Nullable ModelParameterRange aidlRange) { + if (aidlRange == null) { + return null; + } + return new SoundTrigger.ModelParamRange(aidlRange.minInclusive, aidlRange.maxInclusive); + } +} diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java index 86f3eec4109e..5484df416e79 100644 --- a/core/java/android/hardware/soundtrigger/SoundTrigger.java +++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java @@ -22,18 +22,29 @@ import static android.system.OsConstants.ENOSYS; import static android.system.OsConstants.EPERM; import static android.system.OsConstants.EPIPE; +import static java.util.Objects.requireNonNull; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.UnsupportedAppUsage; import android.app.ActivityThread; +import android.content.Context; import android.media.AudioFormat; +import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; +import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.UUID; /** @@ -44,6 +55,7 @@ import java.util.UUID; */ @SystemApi public class SoundTrigger { + private static final String TAG = "SoundTrigger"; private SoundTrigger() { } @@ -119,15 +131,15 @@ public class SoundTrigger { * recognition callback event */ public final boolean returnsTriggerInEvent; - ModuleProperties(int id, String implementor, String description, - String uuid, int version, int maxSoundModels, int maxKeyphrases, + ModuleProperties(int id, @NonNull String implementor, @NonNull String description, + @NonNull String uuid, int version, int maxSoundModels, int maxKeyphrases, int maxUsers, int recognitionModes, boolean supportsCaptureTransition, int maxBufferMs, boolean supportsConcurrentCapture, int powerConsumptionMw, boolean returnsTriggerInEvent) { this.id = id; - this.implementor = implementor; - this.description = description; - this.uuid = UUID.fromString(uuid); + this.implementor = requireNonNull(implementor); + this.description = requireNonNull(description); + this.uuid = UUID.fromString(requireNonNull(uuid)); this.version = version; this.maxSoundModels = maxSoundModels; this.maxKeyphrases = maxKeyphrases; @@ -231,6 +243,7 @@ public class SoundTrigger { /** Unique sound model identifier */ @UnsupportedAppUsage + @NonNull public final UUID uuid; /** Sound model type (e.g. TYPE_KEYPHRASE); */ @@ -238,17 +251,20 @@ public class SoundTrigger { /** Unique sound model vendor identifier */ @UnsupportedAppUsage + @NonNull public final UUID vendorUuid; /** Opaque data. For use by vendor implementation and enrollment application */ @UnsupportedAppUsage + @NonNull public final byte[] data; - public SoundModel(UUID uuid, UUID vendorUuid, int type, byte[] data) { - this.uuid = uuid; - this.vendorUuid = vendorUuid; + public SoundModel(@NonNull UUID uuid, @Nullable UUID vendorUuid, int type, + @Nullable byte[] data) { + this.uuid = requireNonNull(uuid); + this.vendorUuid = vendorUuid != null ? vendorUuid : new UUID(0, 0); this.type = type; - this.data = data; + this.data = data != null ? data : new byte[0]; } @Override @@ -271,8 +287,6 @@ public class SoundTrigger { if (!(obj instanceof SoundModel)) return false; SoundModel other = (SoundModel) obj; - if (!Arrays.equals(data, other.data)) - return false; if (type != other.type) return false; if (uuid == null) { @@ -285,6 +299,8 @@ public class SoundTrigger { return false; } else if (!vendorUuid.equals(other.vendorUuid)) return false; + if (!Arrays.equals(data, other.data)) + return false; return true; } } @@ -306,24 +322,28 @@ public class SoundTrigger { /** Locale of the keyphrase. JAVA Locale string e.g en_US */ @UnsupportedAppUsage + @NonNull public final String locale; /** Key phrase text */ @UnsupportedAppUsage + @NonNull public final String text; /** Users this key phrase has been trained for. countains sound trigger specific user IDs * derived from system user IDs {@link android.os.UserHandle#getIdentifier()}. */ @UnsupportedAppUsage + @NonNull public final int[] users; @UnsupportedAppUsage - public Keyphrase(int id, int recognitionModes, String locale, String text, int[] users) { + public Keyphrase(int id, int recognitionModes, @NonNull String locale, @NonNull String text, + @Nullable int[] users) { this.id = id; this.recognitionModes = recognitionModes; - this.locale = locale; - this.text = text; - this.users = users; + this.locale = requireNonNull(locale); + this.text = requireNonNull(text); + this.users = users != null ? users : new int[0]; } public static final @android.annotation.NonNull Parcelable.Creator<Keyphrase> CREATOR @@ -427,13 +447,15 @@ public class SoundTrigger { public static class KeyphraseSoundModel extends SoundModel implements Parcelable { /** Key phrases in this sound model */ @UnsupportedAppUsage + @NonNull public final Keyphrase[] keyphrases; // keyword phrases in model @UnsupportedAppUsage public KeyphraseSoundModel( - UUID uuid, UUID vendorUuid, byte[] data, Keyphrase[] keyphrases) { + @NonNull UUID uuid, @NonNull UUID vendorUuid, @Nullable byte[] data, + @Nullable Keyphrase[] keyphrases) { super(uuid, vendorUuid, TYPE_KEYPHRASE, data); - this.keyphrases = keyphrases; + this.keyphrases = keyphrases != null ? keyphrases : new Keyphrase[0]; } public static final @android.annotation.NonNull Parcelable.Creator<KeyphraseSoundModel> CREATOR @@ -528,7 +550,8 @@ public class SoundTrigger { }; @UnsupportedAppUsage - public GenericSoundModel(UUID uuid, UUID vendorUuid, byte[] data) { + public GenericSoundModel(@NonNull UUID uuid, @NonNull UUID vendorUuid, + @Nullable byte[] data) { super(uuid, vendorUuid, TYPE_GENERIC_SOUND, data); } @@ -648,6 +671,12 @@ public class SoundTrigger { * @hide */ public static final int RECOGNITION_MODE_USER_AUTHENTICATION = 0x4; + /** + * Generic (non-speech) recognition. + * + * @hide + */ + public static final int RECOGNITION_MODE_GENERIC = 0x8; /** * Status codes for {@link RecognitionEvent} @@ -739,6 +768,7 @@ public class SoundTrigger { * * @hide */ + @NonNull public final AudioFormat captureFormat; /** * Opaque data for use by system applications who know about voice engine internals, @@ -747,13 +777,14 @@ public class SoundTrigger { * @hide */ @UnsupportedAppUsage + @NonNull public final byte[] data; /** @hide */ @UnsupportedAppUsage public RecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, int captureSession, int captureDelayMs, int capturePreambleMs, - boolean triggerInData, AudioFormat captureFormat, byte[] data) { + boolean triggerInData, @NonNull AudioFormat captureFormat, @Nullable byte[] data) { this.status = status; this.soundModelHandle = soundModelHandle; this.captureAvailable = captureAvailable; @@ -761,8 +792,8 @@ public class SoundTrigger { this.captureDelayMs = captureDelayMs; this.capturePreambleMs = capturePreambleMs; this.triggerInData = triggerInData; - this.captureFormat = captureFormat; - this.data = data; + this.captureFormat = requireNonNull(captureFormat); + this.data = data != null ? data : new byte[0]; } /** @@ -965,19 +996,21 @@ public class SoundTrigger { /** List of all keyphrases in the sound model for which recognition should be performed with * options for each keyphrase. */ @UnsupportedAppUsage + @NonNull public final KeyphraseRecognitionExtra keyphrases[]; /** Opaque data for use by system applications who know about voice engine internals, * typically during enrollment. */ @UnsupportedAppUsage + @NonNull public final byte[] data; @UnsupportedAppUsage public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers, - KeyphraseRecognitionExtra[] keyphrases, byte[] data) { + @Nullable KeyphraseRecognitionExtra[] keyphrases, @Nullable byte[] data) { this.captureRequested = captureRequested; this.allowMultipleTriggers = allowMultipleTriggers; - this.keyphrases = keyphrases; - this.data = data; + this.keyphrases = keyphrases != null ? keyphrases : new KeyphraseRecognitionExtra[0]; + this.data = data != null ? data : new byte[0]; } public static final @android.annotation.NonNull Parcelable.Creator<RecognitionConfig> CREATOR @@ -1126,15 +1159,17 @@ public class SoundTrigger { /** Confidence levels for all users recognized (KeyphraseRecognitionEvent) or to * be recognized (RecognitionConfig) */ @UnsupportedAppUsage + @NonNull public final ConfidenceLevel[] confidenceLevels; @UnsupportedAppUsage public KeyphraseRecognitionExtra(int id, int recognitionModes, int coarseConfidenceLevel, - ConfidenceLevel[] confidenceLevels) { + @Nullable ConfidenceLevel[] confidenceLevels) { this.id = id; this.recognitionModes = recognitionModes; this.coarseConfidenceLevel = coarseConfidenceLevel; - this.confidenceLevels = confidenceLevels; + this.confidenceLevels = + confidenceLevels != null ? confidenceLevels : new ConfidenceLevel[0]; } public static final @android.annotation.NonNull Parcelable.Creator<KeyphraseRecognitionExtra> CREATOR @@ -1217,16 +1252,18 @@ public class SoundTrigger { public static class KeyphraseRecognitionEvent extends RecognitionEvent implements Parcelable { /** Indicates if the key phrase is present in the buffered audio available for capture */ @UnsupportedAppUsage + @NonNull public final KeyphraseRecognitionExtra[] keyphraseExtras; @UnsupportedAppUsage public KeyphraseRecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, int captureSession, int captureDelayMs, int capturePreambleMs, - boolean triggerInData, AudioFormat captureFormat, byte[] data, - KeyphraseRecognitionExtra[] keyphraseExtras) { + boolean triggerInData, @NonNull AudioFormat captureFormat, @Nullable byte[] data, + @Nullable KeyphraseRecognitionExtra[] keyphraseExtras) { super(status, soundModelHandle, captureAvailable, captureSession, captureDelayMs, capturePreambleMs, triggerInData, captureFormat, data); - this.keyphraseExtras = keyphraseExtras; + this.keyphraseExtras = + keyphraseExtras != null ? keyphraseExtras : new KeyphraseRecognitionExtra[0]; } public static final @android.annotation.NonNull Parcelable.Creator<KeyphraseRecognitionEvent> CREATOR @@ -1343,8 +1380,8 @@ public class SoundTrigger { @UnsupportedAppUsage public GenericRecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, int captureSession, int captureDelayMs, - int capturePreambleMs, boolean triggerInData, AudioFormat captureFormat, - byte[] data) { + int capturePreambleMs, boolean triggerInData, @NonNull AudioFormat captureFormat, + @Nullable byte[] data) { super(status, soundModelHandle, captureAvailable, captureSession, captureDelayMs, capturePreambleMs, triggerInData, captureFormat, data); @@ -1408,13 +1445,14 @@ public class SoundTrigger { /** The updated sound model handle */ public final int soundModelHandle; /** New sound model data */ + @NonNull public final byte[] data; @UnsupportedAppUsage - SoundModelEvent(int status, int soundModelHandle, byte[] data) { + SoundModelEvent(int status, int soundModelHandle, @Nullable byte[] data) { this.status = status; this.soundModelHandle = soundModelHandle; - this.data = data; + this.data = data != null ? data : new byte[0]; } public static final @android.annotation.NonNull Parcelable.Creator<SoundModelEvent> CREATOR @@ -1498,8 +1536,9 @@ public class SoundTrigger { * @hide */ public static final int SERVICE_STATE_DISABLED = 1; - - /** + private static Object mServiceLock = new Object(); + private static ISoundTriggerMiddlewareService mService; + /** * @return returns current package name. */ static String getCurrentOpPackageName() { @@ -1523,25 +1562,22 @@ public class SoundTrigger { * @hide */ @UnsupportedAppUsage - public static int listModules(ArrayList<ModuleProperties> modules) { - return listModules(getCurrentOpPackageName(), modules); + public static int listModules(@NonNull ArrayList<ModuleProperties> modules) { + try { + SoundTriggerModuleDescriptor[] descs = getService().listModules(); + modules.clear(); + modules.ensureCapacity(descs.length); + for (SoundTriggerModuleDescriptor desc : descs) { + modules.add(ConversionUtil.aidl2apiModuleDescriptor(desc)); + } + return STATUS_OK; + } catch (RemoteException e) { + Log.e(TAG, "Exception caught", e); + return STATUS_DEAD_OBJECT; + } } /** - * Returns a list of descriptors for all hardware modules loaded. - * @param opPackageName - * @param modules A ModuleProperties array where the list will be returned. - * @return - {@link #STATUS_OK} in case of success - * - {@link #STATUS_ERROR} in case of unspecified error - * - {@link #STATUS_PERMISSION_DENIED} if the caller does not have system permission - * - {@link #STATUS_NO_INIT} if the native service cannot be reached - * - {@link #STATUS_BAD_VALUE} if modules is null - * - {@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails - */ - private static native int listModules(String opPackageName, - ArrayList<ModuleProperties> modules); - - /** * Get an interface on a hardware module to control sound models and recognition on * this module. * @param moduleId Sound module system identifier {@link ModuleProperties#id}. mandatory. @@ -1553,14 +1589,40 @@ public class SoundTrigger { * @hide */ @UnsupportedAppUsage - public static SoundTriggerModule attachModule(int moduleId, - StatusListener listener, - Handler handler) { - if (listener == null) { + public static @NonNull SoundTriggerModule attachModule(int moduleId, + @NonNull StatusListener listener, + @Nullable Handler handler) { + Looper looper = handler != null ? handler.getLooper() : Looper.getMainLooper(); + try { + return new SoundTriggerModule(getService(), moduleId, listener, looper); + } catch (RemoteException e) { + Log.e(TAG, "", e); return null; } - SoundTriggerModule module = new SoundTriggerModule(moduleId, listener, handler); - return module; + } + + private static ISoundTriggerMiddlewareService getService() { + synchronized (mServiceLock) { + while (true) { + IBinder binder = null; + try { + binder = + ServiceManager.getServiceOrThrow( + Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE); + binder.linkToDeath(() -> { + synchronized (mServiceLock) { + mService = null; + } + }, 0); + mService = ISoundTriggerMiddlewareService.Stub.asInterface(binder); + break; + } catch (Exception e) { + Log.e(TAG, "Failed to bind to soundtrigger service", e); + } + } + return mService; + } + } /** diff --git a/core/java/android/hardware/soundtrigger/SoundTriggerModule.java b/core/java/android/hardware/soundtrigger/SoundTriggerModule.java index b16ef5c43346..7cf560019239 100644 --- a/core/java/android/hardware/soundtrigger/SoundTriggerModule.java +++ b/core/java/android/hardware/soundtrigger/SoundTriggerModule.java @@ -16,14 +16,23 @@ package android.hardware.soundtrigger; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UnsupportedAppUsage; -import android.hardware.soundtrigger.SoundTrigger.ModelParamRange; +import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; +import android.media.soundtrigger_middleware.ISoundTriggerModule; +import android.media.soundtrigger_middleware.ModelParameterRange; +import android.media.soundtrigger_middleware.PhraseRecognitionEvent; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionEvent; +import android.media.soundtrigger_middleware.SoundModel; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.os.Message; - -import java.lang.ref.WeakReference; +import android.os.RemoteException; +import android.util.Log; /** * The SoundTriggerModule provides APIs to control sound models and sound detection @@ -32,39 +41,47 @@ import java.lang.ref.WeakReference; * @hide */ public class SoundTriggerModule { - @UnsupportedAppUsage - private long mNativeContext; - - @UnsupportedAppUsage - private int mId; - private NativeEventHandlerDelegate mEventHandlerDelegate; + private static final String TAG = "SoundTriggerModule"; - // to be kept in sync with core/jni/android_hardware_SoundTrigger.cpp private static final int EVENT_RECOGNITION = 1; private static final int EVENT_SERVICE_DIED = 2; - private static final int EVENT_SOUNDMODEL = 3; - private static final int EVENT_SERVICE_STATE_CHANGE = 4; + private static final int EVENT_SERVICE_STATE_CHANGE = 3; + @UnsupportedAppUsage + private int mId; + private EventHandlerDelegate mEventHandlerDelegate; + private ISoundTriggerModule mService; - SoundTriggerModule(int moduleId, SoundTrigger.StatusListener listener, Handler handler) { + SoundTriggerModule(@NonNull ISoundTriggerMiddlewareService service, + int moduleId, @NonNull SoundTrigger.StatusListener listener, @NonNull Looper looper) + throws RemoteException { mId = moduleId; - mEventHandlerDelegate = new NativeEventHandlerDelegate(listener, handler); - native_setup(SoundTrigger.getCurrentOpPackageName(), - new WeakReference<SoundTriggerModule>(this)); + mEventHandlerDelegate = new EventHandlerDelegate(listener, looper); + mService = service.attach(moduleId, mEventHandlerDelegate); + mService.asBinder().linkToDeath(mEventHandlerDelegate, 0); } - private native void native_setup(String opPackageName, Object moduleThis); @Override protected void finalize() { - native_finalize(); + detach(); } - private native void native_finalize(); /** * Detach from this module. The {@link SoundTrigger.StatusListener} callback will not be called * anymore and associated resources will be released. - * */ + * All models must have been unloaded prior to detaching. + */ @UnsupportedAppUsage - public native void detach(); + public synchronized void detach() { + try { + if (mService != null) { + mService.asBinder().unlinkToDeath(mEventHandlerDelegate, 0); + mService.detach(); + mService = null; + } + } catch (Exception e) { + handleException(e); + } + } /** * Load a {@link SoundTrigger.SoundModel} to the hardware. A sound model must be loaded in @@ -82,7 +99,26 @@ public class SoundTriggerModule { * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence */ @UnsupportedAppUsage - public native int loadSoundModel(SoundTrigger.SoundModel model, int[] soundModelHandle); + public synchronized int loadSoundModel(@NonNull SoundTrigger.SoundModel model, + @NonNull int[] soundModelHandle) { + try { + if (model instanceof SoundTrigger.GenericSoundModel) { + SoundModel aidlModel = ConversionUtil.api2aidlGenericSoundModel( + (SoundTrigger.GenericSoundModel) model); + soundModelHandle[0] = mService.loadModel(aidlModel); + return SoundTrigger.STATUS_OK; + } + if (model instanceof SoundTrigger.KeyphraseSoundModel) { + PhraseSoundModel aidlModel = ConversionUtil.api2aidlPhraseSoundModel( + (SoundTrigger.KeyphraseSoundModel) model); + soundModelHandle[0] = mService.loadPhraseModel(aidlModel); + return SoundTrigger.STATUS_OK; + } + return SoundTrigger.STATUS_BAD_VALUE; + } catch (Exception e) { + return handleException(e); + } + } /** * Unload a {@link SoundTrigger.SoundModel} and abort any pendiong recognition @@ -97,7 +133,14 @@ public class SoundTriggerModule { * service fails */ @UnsupportedAppUsage - public native int unloadSoundModel(int soundModelHandle); + public synchronized int unloadSoundModel(int soundModelHandle) { + try { + mService.unloadModel(soundModelHandle); + return SoundTrigger.STATUS_OK; + } catch (Exception e) { + return handleException(e); + } + } /** * Start listening to all key phrases in a {@link SoundTrigger.SoundModel}. @@ -117,7 +160,16 @@ public class SoundTriggerModule { * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence */ @UnsupportedAppUsage - public native int startRecognition(int soundModelHandle, SoundTrigger.RecognitionConfig config); + public synchronized int startRecognition(int soundModelHandle, + SoundTrigger.RecognitionConfig config) { + try { + mService.startRecognition(soundModelHandle, + ConversionUtil.api2aidlRecognitionConfig(config)); + return SoundTrigger.STATUS_OK; + } catch (Exception e) { + return handleException(e); + } + } /** * Stop listening to all key phrases in a {@link SoundTrigger.SoundModel} @@ -133,12 +185,20 @@ public class SoundTriggerModule { * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence */ @UnsupportedAppUsage - public native int stopRecognition(int soundModelHandle); + public synchronized int stopRecognition(int soundModelHandle) { + try { + mService.stopRecognition(soundModelHandle); + return SoundTrigger.STATUS_OK; + } catch (Exception e) { + return handleException(e); + } + } /** * Get the current state of a {@link SoundTrigger.SoundModel}. - * The state will be returned asynchronously as a {@link SoundTrigger#RecognitionEvent} - * in the callback registered in the {@link SoundTrigger.startRecognition} method. + * The state will be returned asynchronously as a {@link SoundTrigger.RecognitionEvent} + * in the callback registered in the + * {@link SoundTrigger#attachModule(int, SoundTrigger.StatusListener, Handler)} method. * @param soundModelHandle The sound model handle indicating which model's state to return * @return - {@link SoundTrigger#STATUS_OK} in case of success * - {@link SoundTrigger#STATUS_ERROR} in case of unspecified error @@ -150,46 +210,71 @@ public class SoundTriggerModule { * service fails * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence */ - public native int getModelState(int soundModelHandle); + public synchronized int getModelState(int soundModelHandle) { + try { + mService.forceRecognitionEvent(soundModelHandle); + return SoundTrigger.STATUS_OK; + } catch (Exception e) { + return handleException(e); + } + } /** * Set a model specific {@link ModelParams} with the given value. This - * parameter will keep its value for the duration the model is loaded regardless of starting and + * parameter will keep its value for the duration the model is loaded regardless of starting + * and * stopping recognition. Once the model is unloaded, the value will be lost. - * {@link SoundTriggerModule#isParameterSupported} should be checked first before calling this - * method. + * {@link SoundTriggerModule#queryParameter(int, int)} should be checked first before calling + * this method. * * @param soundModelHandle handle of model to apply parameter - * @param modelParam {@link ModelParams} - * @param value Value to set + * @param modelParam {@link ModelParams} + * @param value Value to set * @return - {@link SoundTrigger#STATUS_OK} in case of success - * - {@link SoundTrigger#STATUS_NO_INIT} if the native service cannot be reached - * - {@link SoundTrigger#STATUS_BAD_VALUE} invalid input parameter - * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence or - * if API is not supported by HAL + * - {@link SoundTrigger#STATUS_NO_INIT} if the native service cannot be reached + * - {@link SoundTrigger#STATUS_BAD_VALUE} invalid input parameter + * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence or + * if API is not supported by HAL */ - public native int setParameter(int soundModelHandle, - @ModelParams int modelParam, int value); + public synchronized int setParameter(int soundModelHandle, @ModelParams int modelParam, + int value) { + try { + mService.setModelParameter(soundModelHandle, + ConversionUtil.api2aidlModelParameter(modelParam), value); + return SoundTrigger.STATUS_OK; + } catch (Exception e) { + return handleException(e); + } + } /** * Get a model specific {@link ModelParams}. This parameter will keep its value * for the duration the model is loaded regardless of starting and stopping recognition. * Once the model is unloaded, the value will be lost. If the value is not set, a default * value is returned. See {@link ModelParams} for parameter default values. - * {@link SoundTriggerModule#isParameterSupported} should be checked first before + * {@link SoundTriggerModule#queryParameter(int, int)} should be checked first before * calling this method. Otherwise, an exception can be thrown. * * @param soundModelHandle handle of model to get parameter - * @param modelParam {@link ModelParams} + * @param modelParam {@link ModelParams} * @return value of parameter * @throws UnsupportedOperationException if hal or model do not support this API. - * {@link SoundTriggerModule#isParameterSupported} should be checked first. - * @throws IllegalArgumentException if invalid model handle or parameter is passed. - * {@link SoundTriggerModule#isParameterSupported} should be checked first. + * {@link SoundTriggerModule#queryParameter(int, int)} + * should + * be checked first. + * @throws IllegalArgumentException if invalid model handle or parameter is passed. + * {@link SoundTriggerModule#queryParameter(int, int)} + * should be checked first. */ - public native int getParameter(int soundModelHandle, - @ModelParams int modelParam) - throws UnsupportedOperationException, IllegalArgumentException; + public synchronized int getParameter(int soundModelHandle, @ModelParams int modelParam) + throws UnsupportedOperationException, IllegalArgumentException { + try { + return mService.getModelParameter(soundModelHandle, + ConversionUtil.api2aidlModelParameter(modelParam)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } /** * Determine if parameter control is supported for the given model handle. @@ -197,85 +282,98 @@ public class SoundTriggerModule { * {@link SoundTriggerModule#getParameter}. * * @param soundModelHandle handle of model to get parameter - * @param modelParam {@link ModelParams} + * @param modelParam {@link ModelParams} * @return supported range of parameter, null if not supported */ @Nullable - public native ModelParamRange queryParameter(int soundModelHandle, @ModelParams int modelParam); + public synchronized SoundTrigger.ModelParamRange queryParameter(int soundModelHandle, + @ModelParams int modelParam) { + try { + return ConversionUtil.aidl2apiModelParameterRange(mService.queryModelParameterSupport( + soundModelHandle, + ConversionUtil.api2aidlModelParameter(modelParam))); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private int handleException(Exception e) { + Log.e(TAG, "", e); + if (e instanceof NullPointerException) { + return SoundTrigger.STATUS_NO_INIT; + } + if (e instanceof RemoteException) { + return SoundTrigger.STATUS_DEAD_OBJECT; + } + if (e instanceof IllegalArgumentException) { + return SoundTrigger.STATUS_BAD_VALUE; + } + if (e instanceof IllegalStateException) { + return SoundTrigger.STATUS_INVALID_OPERATION; + } + return SoundTrigger.STATUS_ERROR; + } - private class NativeEventHandlerDelegate { + private class EventHandlerDelegate extends ISoundTriggerCallback.Stub implements + IBinder.DeathRecipient { private final Handler mHandler; - NativeEventHandlerDelegate(final SoundTrigger.StatusListener listener, - Handler handler) { - // find the looper for our new event handler - Looper looper; - if (handler != null) { - looper = handler.getLooper(); - } else { - looper = Looper.getMainLooper(); - } + EventHandlerDelegate(@NonNull final SoundTrigger.StatusListener listener, + @NonNull Looper looper) { // construct the event handler with this looper - if (looper != null) { - // implement the event handler delegate - mHandler = new Handler(looper) { - @Override - public void handleMessage(Message msg) { - switch(msg.what) { + // implement the event handler delegate + mHandler = new Handler(looper) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { case EVENT_RECOGNITION: - if (listener != null) { - listener.onRecognition( - (SoundTrigger.RecognitionEvent)msg.obj); - } - break; - case EVENT_SOUNDMODEL: - if (listener != null) { - listener.onSoundModelUpdate( - (SoundTrigger.SoundModelEvent)msg.obj); - } + listener.onRecognition( + (SoundTrigger.RecognitionEvent) msg.obj); break; case EVENT_SERVICE_STATE_CHANGE: - if (listener != null) { - listener.onServiceStateChange(msg.arg1); - } + listener.onServiceStateChange(msg.arg1); break; case EVENT_SERVICE_DIED: - if (listener != null) { - listener.onServiceDied(); - } + listener.onServiceDied(); break; default: + Log.e(TAG, "Unknown message: " + msg.toString()); break; - } } - }; - } else { - mHandler = null; - } + } + }; } - Handler handler() { - return mHandler; + @Override + public synchronized void onRecognition(int handle, RecognitionEvent event) + throws RemoteException { + Message m = mHandler.obtainMessage(EVENT_RECOGNITION, + ConversionUtil.aidl2apiRecognitionEvent(handle, event)); + mHandler.sendMessage(m); } - } - @SuppressWarnings("unused") - @UnsupportedAppUsage - private static void postEventFromNative(Object module_ref, - int what, int arg1, int arg2, Object obj) { - SoundTriggerModule module = (SoundTriggerModule)((WeakReference)module_ref).get(); - if (module == null) { - return; + @Override + public synchronized void onPhraseRecognition(int handle, PhraseRecognitionEvent event) + throws RemoteException { + Message m = mHandler.obtainMessage(EVENT_RECOGNITION, + ConversionUtil.aidl2apiPhraseRecognitionEvent(handle, event)); + mHandler.sendMessage(m); } - NativeEventHandlerDelegate delegate = module.mEventHandlerDelegate; - if (delegate != null) { - Handler handler = delegate.handler(); - if (handler != null) { - Message m = handler.obtainMessage(what, arg1, arg2, obj); - handler.sendMessage(m); - } + @Override + public synchronized void onRecognitionAvailabilityChange(boolean available) + throws RemoteException { + Message m = mHandler.obtainMessage(EVENT_SERVICE_STATE_CHANGE, + available ? SoundTrigger.SERVICE_STATE_ENABLED + : SoundTrigger.SERVICE_STATE_DISABLED); + mHandler.sendMessage(m); + } + + @Override + public synchronized void binderDied() { + Message m = mHandler.obtainMessage(EVENT_SERVICE_DIED); + mHandler.sendMessage(m); } } } diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java index a47f601033cf..62f0196693ea 100644 --- a/core/java/android/inputmethodservice/IInputMethodWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java @@ -19,6 +19,7 @@ package android.inputmethodservice; import android.annotation.BinderThread; import android.annotation.MainThread; import android.annotation.UnsupportedAppUsage; +import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.os.Binder; @@ -28,6 +29,7 @@ import android.os.RemoteException; import android.os.ResultReceiver; import android.util.Log; import android.view.InputChannel; +import android.view.autofill.AutofillId; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputBinding; import android.view.inputmethod.InputConnection; @@ -39,6 +41,7 @@ import android.view.inputmethod.InputMethodSubtype; import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; import com.android.internal.os.HandlerCaller; import com.android.internal.os.SomeArgs; +import com.android.internal.view.IInlineSuggestionsRequestCallback; import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethod; import com.android.internal.view.IInputMethodSession; @@ -72,6 +75,7 @@ class IInputMethodWrapper extends IInputMethod.Stub private static final int DO_SHOW_SOFT_INPUT = 60; private static final int DO_HIDE_SOFT_INPUT = 70; private static final int DO_CHANGE_INPUTMETHOD_SUBTYPE = 80; + private static final int DO_CREATE_INLINE_SUGGESTIONS_REQUEST = 90; final WeakReference<AbstractInputMethodService> mTarget; final Context mContext; @@ -225,6 +229,11 @@ class IInputMethodWrapper extends IInputMethod.Stub case DO_CHANGE_INPUTMETHOD_SUBTYPE: inputMethod.changeInputMethodSubtype((InputMethodSubtype)msg.obj); return; + case DO_CREATE_INLINE_SUGGESTIONS_REQUEST: + SomeArgs args = (SomeArgs) msg.obj; + inputMethod.onCreateInlineSuggestionsRequest((ComponentName) args.arg1, + (AutofillId) args.arg2, (IInlineSuggestionsRequestCallback) args.arg3); + return; } Log.w(TAG, "Unhandled message code: " + msg.what); } @@ -267,6 +276,15 @@ class IInputMethodWrapper extends IInputMethod.Stub @BinderThread @Override + public void onCreateInlineSuggestionsRequest(ComponentName componentName, AutofillId autofillId, + IInlineSuggestionsRequestCallback cb) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageOOO(DO_CREATE_INLINE_SUGGESTIONS_REQUEST, componentName, + autofillId, cb)); + } + + @BinderThread + @Override public void bindInput(InputBinding binding) { if (mIsUnbindIssued != null) { Log.e(TAG, "bindInput must be paired with unbindInput."); diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 156bcfe147f7..7da7dc120dcb 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -23,6 +23,8 @@ import static android.view.ViewRootImpl.NEW_INSETS_MODE_NONE; import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_ONLY_DRAW_BOTTOM_BAR_BACKGROUND; +import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; + import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.AnyThread; @@ -35,6 +37,7 @@ import android.annotation.Nullable; import android.annotation.UnsupportedAppUsage; import android.app.ActivityManager; import android.app.Dialog; +import android.content.ComponentName; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -47,6 +50,8 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; import android.os.ResultReceiver; import android.os.SystemClock; import android.provider.Settings; @@ -70,11 +75,14 @@ import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; import android.view.animation.AnimationUtils; +import android.view.autofill.AutofillId; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InlineSuggestionsRequest; +import android.view.inputmethod.InlineSuggestionsResponse; import android.view.inputmethod.InputBinding; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputContentInfo; @@ -91,11 +99,14 @@ import com.android.internal.inputmethod.IInputContentUriToken; import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; import com.android.internal.inputmethod.InputMethodPrivilegedOperations; import com.android.internal.inputmethod.InputMethodPrivilegedOperationsRegistry; +import com.android.internal.view.IInlineSuggestionsRequestCallback; +import com.android.internal.view.IInlineSuggestionsResponseCallback; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; import java.util.Collections; /** @@ -436,6 +447,14 @@ public class InputMethodService extends AbstractInputMethodService { final Insets mTmpInsets = new Insets(); final int[] mTmpLocation = new int[2]; + @Nullable + private InlineSuggestionsRequestInfo mInlineSuggestionsRequestInfo = null; + + @Nullable + private InlineSuggestionsResponseCallbackImpl mInlineSuggestionsResponseCallback = null; + + private final Handler mHandler = new Handler(Looper.getMainLooper(), null, true); + final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = info -> { onComputeInsets(mTmpInsets); if (isExtractViewShown()) { @@ -495,6 +514,18 @@ public class InputMethodService extends AbstractInputMethodService { /** * {@inheritDoc} + * @hide + */ + @MainThread + @Override + public void onCreateInlineSuggestionsRequest(ComponentName componentName, + AutofillId autofillId, IInlineSuggestionsRequestCallback cb) { + Log.d(TAG, "InputMethodService received onCreateInlineSuggestionsRequest()"); + handleOnCreateInlineSuggestionsRequest(componentName, autofillId, cb); + } + + /** + * {@inheritDoc} */ @MainThread @Override @@ -670,6 +701,103 @@ public class InputMethodService extends AbstractInputMethodService { } } + // TODO(b/137800469): Add detailed docs explaining the inline suggestions process. + /** + * Returns an {@link InlineSuggestionsRequest} to be sent to Autofill. + * + * <p>Should be implemented by subclasses.</p> + */ + public @Nullable InlineSuggestionsRequest onCreateInlineSuggestionsRequest() { + return null; + } + + /** + * Called when Autofill responds back with {@link InlineSuggestionsResponse} containing + * inline suggestions. + * + * <p>Should be implemented by subclasses.</p> + * + * @param response {@link InlineSuggestionsResponse} passed back by Autofill. + * @return Whether the IME will use and render the inline suggestions. + */ + public boolean onInlineSuggestionsResponse(@NonNull InlineSuggestionsResponse response) { + return false; + } + + /** + * Returns whether inline suggestions are enabled on this service. + * + * TODO(b/137800469): check XML for value. + */ + private boolean isInlineSuggestionsEnabled() { + return true; + } + + /** + * Sends an {@link InlineSuggestionsRequest} obtained from + * {@link #onCreateInlineSuggestionsRequest()} to the current Autofill Session through + * {@link IInlineSuggestionsRequestCallback#onInlineSuggestionsRequest}. + */ + private void makeInlineSuggestionsRequest() { + if (mInlineSuggestionsRequestInfo == null) { + Log.w(TAG, "makeInlineSuggestionsRequest() called with null requestInfo cache"); + return; + } + + final IInlineSuggestionsRequestCallback requestCallback = + mInlineSuggestionsRequestInfo.mCallback; + try { + final InlineSuggestionsRequest request = onCreateInlineSuggestionsRequest(); + if (request == null) { + Log.w(TAG, "onCreateInlineSuggestionsRequest() returned null request"); + requestCallback.onInlineSuggestionsUnsupported(); + } else { + if (mInlineSuggestionsResponseCallback == null) { + mInlineSuggestionsResponseCallback = + new InlineSuggestionsResponseCallbackImpl(this, + mInlineSuggestionsRequestInfo.mComponentName, + mInlineSuggestionsRequestInfo.mFocusedId); + } + requestCallback.onInlineSuggestionsRequest(request, + mInlineSuggestionsResponseCallback); + } + } catch (RemoteException e) { + Log.w(TAG, "makeInlinedSuggestionsRequest() remote exception:" + e); + } + } + + private void handleOnCreateInlineSuggestionsRequest(@NonNull ComponentName componentName, + @NonNull AutofillId autofillId, @NonNull IInlineSuggestionsRequestCallback callback) { + mInlineSuggestionsRequestInfo = new InlineSuggestionsRequestInfo(componentName, autofillId, + callback); + + if (!isInlineSuggestionsEnabled()) { + try { + callback.onInlineSuggestionsUnsupported(); + } catch (RemoteException e) { + Log.w(TAG, "handleMakeInlineSuggestionsRequest() RemoteException:" + e); + } + return; + } + + if (!mInputStarted) { + Log.w(TAG, "onStartInput() not called yet"); + return; + } + + makeInlineSuggestionsRequest(); + } + + private void handleOnInlineSuggestionsResponse(@NonNull ComponentName componentName, + @NonNull AutofillId autofillId, @NonNull InlineSuggestionsResponse response) { + if (!mInlineSuggestionsRequestInfo.validate(componentName)) { + Log.d(TAG, "Response component=" + componentName + " differs from request component=" + + mInlineSuggestionsRequestInfo.mComponentName + ", ignoring response"); + return; + } + onInlineSuggestionsResponse(response); + } + private void notifyImeHidden() { setImeWindowStatus(IME_ACTIVE | IME_INVISIBLE, mBackDisposition); onPreRenderedWindowVisibilityChanged(false /* setVisible */); @@ -688,6 +816,63 @@ public class InputMethodService extends AbstractInputMethodService { } /** + * Internal implementation of {@link IInlineSuggestionsResponseCallback}. + */ + private static final class InlineSuggestionsResponseCallbackImpl + extends IInlineSuggestionsResponseCallback.Stub { + private final WeakReference<InputMethodService> mInputMethodService; + + private final ComponentName mRequestComponentName; + private final AutofillId mRequestAutofillId; + + private InlineSuggestionsResponseCallbackImpl(InputMethodService inputMethodService, + ComponentName componentName, AutofillId autofillId) { + mInputMethodService = new WeakReference<>(inputMethodService); + mRequestComponentName = componentName; + mRequestAutofillId = autofillId; + } + + @Override + public void onInlineSuggestionsResponse(InlineSuggestionsResponse response) + throws RemoteException { + final InputMethodService service = mInputMethodService.get(); + if (service != null) { + service.mHandler.sendMessage(obtainMessage( + InputMethodService::handleOnInlineSuggestionsResponse, service, + mRequestComponentName, mRequestAutofillId, response)); + } + } + } + + /** + * Information about incoming requests from Autofill Frameworks for inline suggestions. + */ + private static final class InlineSuggestionsRequestInfo { + final ComponentName mComponentName; + final AutofillId mFocusedId; + final IInlineSuggestionsRequestCallback mCallback; + + InlineSuggestionsRequestInfo(ComponentName componentName, AutofillId focusedId, + IInlineSuggestionsRequestCallback callback) { + this.mComponentName = componentName; + this.mFocusedId = focusedId; + this.mCallback = callback; + } + + /** + * Returns whether the cached {@link ComponentName} matches the passed in activity. + */ + public boolean validate(ComponentName componentName) { + final boolean result = componentName.equals(mComponentName); + if (!result) { + Log.d(TAG, "Cached request info ComponentName=" + mComponentName + + " differs from received ComponentName=" + componentName); + } + return result; + } + } + + /** * Concrete implementation of * {@link AbstractInputMethodService.AbstractInputMethodSessionImpl} that provides * all of the standard behavior for an input method session. diff --git a/core/java/android/net/InvalidPacketException.java b/core/java/android/net/InvalidPacketException.java new file mode 100644 index 000000000000..909998d4562c --- /dev/null +++ b/core/java/android/net/InvalidPacketException.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2019 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; + +import android.annotation.IntDef; +import android.annotation.SystemApi; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Thrown when a packet is invalid. + * @hide + */ +@SystemApi +public class InvalidPacketException extends Exception { + public final int error; + + // Must match SocketKeepalive#ERROR_INVALID_IP_ADDRESS. + /** Invalid IP address. */ + public static final int ERROR_INVALID_IP_ADDRESS = -21; + + // Must match SocketKeepalive#ERROR_INVALID_PORT. + /** Invalid port number. */ + public static final int ERROR_INVALID_PORT = -22; + + // Must match SocketKeepalive#ERROR_INVALID_LENGTH. + /** Invalid packet length. */ + public static final int ERROR_INVALID_LENGTH = -23; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "ERROR_" }, value = { + ERROR_INVALID_IP_ADDRESS, + ERROR_INVALID_PORT, + ERROR_INVALID_LENGTH + }) + public @interface ErrorCode {} + + /** + * This packet is invalid. + * See the error code for details. + */ + public InvalidPacketException(@ErrorCode final int error) { + this.error = error; + } +} diff --git a/core/java/android/net/KeepalivePacketData.java b/core/java/android/net/KeepalivePacketData.java index 9b8b7322cd23..2b8b7e69dec9 100644 --- a/core/java/android/net/KeepalivePacketData.java +++ b/core/java/android/net/KeepalivePacketData.java @@ -16,13 +16,13 @@ package android.net; -import static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS; -import static android.net.SocketKeepalive.ERROR_INVALID_PORT; +import static android.net.InvalidPacketException.ERROR_INVALID_IP_ADDRESS; +import static android.net.InvalidPacketException.ERROR_INVALID_PORT; -import android.net.SocketKeepalive.InvalidPacketException; +import android.annotation.NonNull; +import android.annotation.SystemApi; import android.net.util.IpUtils; import android.os.Parcel; -import android.os.Parcelable; import android.util.Log; import java.net.InetAddress; @@ -33,13 +33,16 @@ import java.net.InetAddress; * * @hide */ -public class KeepalivePacketData implements Parcelable { +@SystemApi +public class KeepalivePacketData { private static final String TAG = "KeepalivePacketData"; /** Source IP address */ + @NonNull public final InetAddress srcAddress; /** Destination IP address */ + @NonNull public final InetAddress dstAddress; /** Source port */ @@ -51,13 +54,14 @@ public class KeepalivePacketData implements Parcelable { /** Packet data. A raw byte string of packet data, not including the link-layer header. */ private final byte[] mPacket; - protected static final int IPV4_HEADER_LENGTH = 20; - protected static final int UDP_HEADER_LENGTH = 8; - // This should only be constructed via static factory methods, such as - // nattKeepalivePacket - protected KeepalivePacketData(InetAddress srcAddress, int srcPort, - InetAddress dstAddress, int dstPort, byte[] data) throws InvalidPacketException { + // nattKeepalivePacket. + /** + * A holding class for data necessary to build a keepalive packet. + */ + protected KeepalivePacketData(@NonNull InetAddress srcAddress, int srcPort, + @NonNull InetAddress dstAddress, int dstPort, + @NonNull byte[] data) throws InvalidPacketException { this.srcAddress = srcAddress; this.dstAddress = dstAddress; this.srcPort = srcPort; @@ -78,16 +82,12 @@ public class KeepalivePacketData implements Parcelable { } } + @NonNull public byte[] getPacket() { return mPacket.clone(); } - /* Parcelable Implementation */ - public int describeContents() { - return 0; - } - - /** Write to parcel */ + /** @hide */ public void writeToParcel(Parcel out, int flags) { out.writeString(srcAddress.getHostAddress()); out.writeString(dstAddress.getHostAddress()); @@ -96,6 +96,7 @@ public class KeepalivePacketData implements Parcelable { out.writeByteArray(mPacket); } + /** @hide */ protected KeepalivePacketData(Parcel in) { srcAddress = NetworkUtils.numericToInetAddress(in.readString()); dstAddress = NetworkUtils.numericToInetAddress(in.readString()); @@ -103,17 +104,4 @@ public class KeepalivePacketData implements Parcelable { dstPort = in.readInt(); mPacket = in.createByteArray(); } - - /** Parcelable Creator */ - public static final @android.annotation.NonNull Parcelable.Creator<KeepalivePacketData> CREATOR = - new Parcelable.Creator<KeepalivePacketData>() { - public KeepalivePacketData createFromParcel(Parcel in) { - return new KeepalivePacketData(in); - } - - public KeepalivePacketData[] newArray(int size) { - return new KeepalivePacketData[size]; - } - }; - } diff --git a/core/java/android/net/NattKeepalivePacketData.java b/core/java/android/net/NattKeepalivePacketData.java index a77c244d6b40..3fb52f12a88d 100644 --- a/core/java/android/net/NattKeepalivePacketData.java +++ b/core/java/android/net/NattKeepalivePacketData.java @@ -19,7 +19,6 @@ package android.net; import static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS; import static android.net.SocketKeepalive.ERROR_INVALID_PORT; -import android.net.SocketKeepalive.InvalidPacketException; import android.net.util.IpUtils; import android.os.Parcel; import android.os.Parcelable; @@ -32,6 +31,9 @@ import java.nio.ByteOrder; /** @hide */ public final class NattKeepalivePacketData extends KeepalivePacketData implements Parcelable { + private static final int IPV4_HEADER_LENGTH = 20; + private static final int UDP_HEADER_LENGTH = 8; + // This should only be constructed via static factory methods, such as // nattKeepalivePacket private NattKeepalivePacketData(InetAddress srcAddress, int srcPort, diff --git a/core/java/android/net/SocketKeepalive.java b/core/java/android/net/SocketKeepalive.java index ec73866a647d..fb224fbe1318 100644 --- a/core/java/android/net/SocketKeepalive.java +++ b/core/java/android/net/SocketKeepalive.java @@ -147,17 +147,6 @@ public abstract class SocketKeepalive implements AutoCloseable { } } - /** - * This packet is invalid. - * See the error code for details. - * @hide - */ - public static class InvalidPacketException extends ErrorCodeException { - public InvalidPacketException(final int error) { - super(error); - } - } - @NonNull final IConnectivityManager mService; @NonNull final Network mNetwork; @NonNull final ParcelFileDescriptor mPfd; diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index a92237b9f17f..61da5e67e57b 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -34,6 +34,8 @@ import android.text.TextUtils; import android.util.Log; import java.io.File; +import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedList; /** @@ -528,6 +530,22 @@ public class Environment { } /** + * Return locations where media files (such as ringtones, notification + * sounds, or alarm sounds) may be located on internal storage. These are + * typically indexed under {@link MediaStore#VOLUME_INTERNAL}. + * + * @hide + */ + @SystemApi + public static @NonNull Collection<File> getInternalMediaDirectories() { + final ArrayList<File> res = new ArrayList<>(); + res.add(new File(Environment.getRootDirectory(), "media")); + res.add(new File(Environment.getOemDirectory(), "media")); + res.add(new File(Environment.getProductDirectory(), "media")); + return res; + } + + /** * Return the primary shared/external storage directory. This directory may * not currently be accessible if it has been mounted by the user on their * computer, has been removed from the device, or some other problem has diff --git a/core/java/android/os/IBinder.java b/core/java/android/os/IBinder.java index 12bce8a305f2..ed980f3049b3 100644 --- a/core/java/android/os/IBinder.java +++ b/core/java/android/os/IBinder.java @@ -182,6 +182,14 @@ public interface IBinder { public static final int MAX_IPC_SIZE = 64 * 1024; /** + * Limit that should be placed on IPC sizes, in bytes, to keep them safely under the transaction + * buffer limit. + */ + static int getSuggestedMaxIpcSizeBytes() { + return MAX_IPC_SIZE; + } + + /** * Get the canonical name of the interface supported by this binder. */ public @Nullable String getInterfaceDescriptor() throws RemoteException; diff --git a/core/java/android/os/IVibratorService.aidl b/core/java/android/os/IVibratorService.aidl index 1456ff7e6c5e..6b881fecad56 100644 --- a/core/java/android/os/IVibratorService.aidl +++ b/core/java/android/os/IVibratorService.aidl @@ -24,6 +24,7 @@ interface IVibratorService { boolean hasVibrator(); boolean hasAmplitudeControl(); + boolean setAlwaysOnEffect(int id, in VibrationEffect effect, in AudioAttributes attributes); void vibrate(int uid, String opPkg, in VibrationEffect effect, in AudioAttributes attributes, String reason, IBinder token); void cancelVibrate(IBinder token); diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 9eb6445d5931..339397b90afa 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -1815,8 +1815,12 @@ public final class Parcel { p.writeToParcel(this, parcelableFlags); } - /** @hide */ - @UnsupportedAppUsage + /** + * Flatten the name of the class of the Parcelable into this Parcel. + * + * @param p The Parcelable object to be written. + * @see #readParcelableCreator + */ public final void writeParcelableCreator(@NonNull Parcelable p) { String name = p.getClass().getName(); writeString(name); @@ -3011,8 +3015,19 @@ public final class Parcel { return (T) creator.createFromParcel(this); } - /** @hide */ - @UnsupportedAppUsage + /** + * Read and return a Parcelable.Creator from the parcel. The given class loader will be used to + * load the {@link Parcelable.Creator}. If it is null, the default class loader will be used. + * + * @param loader A ClassLoader from which to instantiate the {@link Parcelable.Creator} + * object, or null for the default class loader. + * @return the previously written {@link Parcelable.Creator}, or null if a null Creator was + * written. + * @throws BadParcelableException Throws BadParcelableException if there was an error trying to + * read the {@link Parcelable.Creator}. + * + * @see #writeParcelableCreator + */ @Nullable public final Parcelable.Creator<?> readParcelableCreator(@Nullable ClassLoader loader) { String name = readString(); diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 725e0fb901f2..5e478b555fca 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -44,70 +44,10 @@ import java.util.concurrent.Executor; * <p> * <b>Device battery life will be significantly affected by the use of this API.</b> * Do not acquire {@link WakeLock}s unless you really need them, use the minimum levels - * possible, and be sure to release them as soon as possible. - * </p><p> - * The primary API you'll use is {@link #newWakeLock(int, String) newWakeLock()}. - * This will create a {@link PowerManager.WakeLock} object. You can then use methods - * on the wake lock object to control the power state of the device. - * </p><p> - * In practice it's quite simple: - * {@samplecode - * PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - * PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "My Tag"); - * wl.acquire(); - * ..screen will stay on during this section.. - * wl.release(); - * } - * </p><p> - * The following wake lock levels are defined, with varying effects on system power. - * <i>These levels are mutually exclusive - you may only specify one of them.</i> + * possible, and be sure to release them as soon as possible. In most cases, + * you'll want to use + * {@link android.view.WindowManager.LayoutParams#FLAG_KEEP_SCREEN_ON} instead. * - * <table> - * <tr><th>Flag Value</th> - * <th>CPU</th> <th>Screen</th> <th>Keyboard</th></tr> - * - * <tr><td>{@link #PARTIAL_WAKE_LOCK}</td> - * <td>On*</td> <td>Off</td> <td>Off</td> - * </tr> - * - * <tr><td>{@link #SCREEN_DIM_WAKE_LOCK}</td> - * <td>On</td> <td>Dim</td> <td>Off</td> - * </tr> - * - * <tr><td>{@link #SCREEN_BRIGHT_WAKE_LOCK}</td> - * <td>On</td> <td>Bright</td> <td>Off</td> - * </tr> - * - * <tr><td>{@link #FULL_WAKE_LOCK}</td> - * <td>On</td> <td>Bright</td> <td>Bright</td> - * </tr> - * </table> - * </p><p> - * *<i>If you hold a partial wake lock, the CPU will continue to run, regardless of any - * display timeouts or the state of the screen and even after the user presses the power button. - * In all other wake locks, the CPU will run, but the user can still put the device to sleep - * using the power button.</i> - * </p><p> - * In addition, you can add two more flags, which affect behavior of the screen only. - * <i>These flags have no effect when combined with a {@link #PARTIAL_WAKE_LOCK}.</i></p> - * - * <table> - * <tr><th>Flag Value</th> <th>Description</th></tr> - * - * <tr><td>{@link #ACQUIRE_CAUSES_WAKEUP}</td> - * <td>Normal wake locks don't actually turn on the illumination. Instead, they cause - * the illumination to remain on once it turns on (e.g. from user activity). This flag - * will force the screen and/or keyboard to turn on immediately, when the WakeLock is - * acquired. A typical use would be for notifications which are important for the user to - * see immediately.</td> - * </tr> - * - * <tr><td>{@link #ON_AFTER_RELEASE}</td> - * <td>If this flag is set, the user activity timer will be reset when the WakeLock is - * released, causing the illumination to remain on a bit longer. This can be used to - * reduce flicker if you are cycling between wake lock conditions.</td> - * </tr> - * </table> * <p> * Any application using a WakeLock must request the {@code android.permission.WAKE_LOCK} * permission in an {@code <uses-permission>} element of the application's manifest. @@ -931,7 +871,8 @@ public final class PowerManager { * {@link #FULL_WAKE_LOCK}, {@link #SCREEN_DIM_WAKE_LOCK} * and {@link #SCREEN_BRIGHT_WAKE_LOCK}. Exactly one wake lock level must be * specified as part of the {@code levelAndFlags} parameter. - * </p><p> + * </p> + * <p> * The wake lock flags are: {@link #ACQUIRE_CAUSES_WAKEUP} * and {@link #ON_AFTER_RELEASE}. Multiple flags can be combined as part of the * {@code levelAndFlags} parameters. diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java index a5188e7cd58d..fbd11ca62a0c 100644 --- a/core/java/android/os/SystemVibrator.java +++ b/core/java/android/os/SystemVibrator.java @@ -70,6 +70,20 @@ public class SystemVibrator extends Vibrator { } @Override + public boolean setAlwaysOnEffect(int id, VibrationEffect effect, AudioAttributes attributes) { + if (mService == null) { + Log.w(TAG, "Failed to set always-on effect; no vibrator service."); + return false; + } + try { + return mService.setAlwaysOnEffect(id, effect, attributes); + } catch (RemoteException e) { + Log.w(TAG, "Failed to set always-on effect.", e); + } + return false; + } + + @Override public void vibrate(int uid, String opPkg, VibrationEffect effect, String reason, AudioAttributes attributes) { if (mService == null) { diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java index 28909c88a734..6456d72a4a6f 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -17,6 +17,7 @@ package android.os; import android.annotation.IntDef; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.annotation.UnsupportedAppUsage; @@ -152,6 +153,24 @@ public abstract class Vibrator { public abstract boolean hasAmplitudeControl(); /** + * Configure an always-on haptics effect. + * + * @param id The board-specific always-on ID to configure. + * @param effect Vibration effect to assign to always-on id. Passing null will disable it. + * @param attributes {@link AudioAttributes} corresponding to the vibration. For example, + * specify {@link AudioAttributes#USAGE_ALARM} for alarm vibrations or + * {@link AudioAttributes#USAGE_NOTIFICATION_RINGTONE} for + * vibrations associated with incoming calls. May only be null when effect is null. + * @hide + */ + @RequiresPermission(android.Manifest.permission.VIBRATE_ALWAYS_ON) + public boolean setAlwaysOnEffect(int id, @Nullable VibrationEffect effect, + @Nullable AudioAttributes attributes) { + Log.w(TAG, "Always-on effects aren't supported"); + return false; + } + + /** * Vibrate constantly for the specified period of time. * * @param milliseconds The number of milliseconds to vibrate. diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 62603fee1137..2e9f27e74544 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -264,6 +264,8 @@ public class StorageManager { public static final int FLAG_REAL_STATE = 1 << 9; /** {@hide} */ public static final int FLAG_INCLUDE_INVISIBLE = 1 << 10; + /** {@hide} */ + public static final int FLAG_INCLUDE_RECENT = 1 << 11; /** {@hide} */ public static final int FSTRIM_FLAG_DEEP = IVold.FSTRIM_FLAG_DEEP_TRIM; @@ -1125,7 +1127,7 @@ public class StorageManager { * Return the {@link StorageVolume} that contains the given file, or * {@code null} if none. */ - public @Nullable StorageVolume getStorageVolume(File file) { + public @Nullable StorageVolume getStorageVolume(@NonNull File file) { return getStorageVolume(getVolumeList(), file); } @@ -1140,7 +1142,7 @@ public class StorageManager { return getPrimaryStorageVolume(); default: for (StorageVolume vol : getStorageVolumes()) { - if (Objects.equals(vol.getNormalizedUuid(), volumeName)) { + if (Objects.equals(vol.getMediaStoreVolumeName(), volumeName)) { return vol; } } @@ -1201,12 +1203,13 @@ public class StorageManager { } /** - * Return the list of shared/external storage volumes available to the - * current user. This includes both the primary shared storage device and - * any attached external volumes including SD cards and USB drives. - * - * @see Environment#getExternalStorageDirectory() - * @see StorageVolume#createAccessIntent(String) + * Return the list of shared/external storage volumes currently available to + * the calling user. + * <p> + * These storage volumes are actively attached to the device, but may be in + * any mount state, as returned by {@link StorageVolume#getState()}. Returns + * both the primary shared storage device and any attached external volumes, + * including SD cards and USB drives. */ public @NonNull List<StorageVolume> getStorageVolumes() { final ArrayList<StorageVolume> res = new ArrayList<>(); @@ -1216,6 +1219,22 @@ public class StorageManager { } /** + * Return the list of shared/external storage volumes both currently and + * recently available to the calling user. + * <p> + * Recently available storage volumes are likely to reappear in the future, + * so apps are encouraged to preserve any indexed metadata related to these + * volumes to optimize user experiences. + */ + public @NonNull List<StorageVolume> getRecentStorageVolumes() { + final ArrayList<StorageVolume> res = new ArrayList<>(); + Collections.addAll(res, + getVolumeList(mContext.getUserId(), + FLAG_REAL_STATE | FLAG_INCLUDE_INVISIBLE | FLAG_INCLUDE_RECENT)); + return res; + } + + /** * Return the primary shared/external storage volume available to the * current user. This volume is the same storage device returned by * {@link Environment#getExternalStorageDirectory()} and diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java index aefe8430f9de..560d6171d5ee 100644 --- a/core/java/android/os/storage/StorageVolume.java +++ b/core/java/android/os/storage/StorageVolume.java @@ -29,6 +29,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; import android.provider.DocumentsContract; +import android.provider.MediaStore; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.Preconditions; @@ -173,7 +174,7 @@ public final class StorageVolume implements Parcelable { * @return the mount path * @hide */ - @UnsupportedAppUsage + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "{@link StorageVolume#getDirectory()}") @TestApi public String getPath() { return mPath.toString(); @@ -190,12 +191,35 @@ public final class StorageVolume implements Parcelable { } /** {@hide} */ - @UnsupportedAppUsage + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "{@link StorageVolume#getDirectory()}") public File getPathFile() { return mPath; } /** + * Returns the directory where this volume is currently mounted. + * <p> + * Direct filesystem access via this path has significant emulation + * overhead, and apps are instead strongly encouraged to interact with media + * on storage volumes via the {@link MediaStore} APIs. + * <p> + * This directory does not give apps any additional access beyond what they + * already have via {@link MediaStore}. + * + * @return directory where this volume is mounted, or {@code null} if the + * volume is not currently mounted. + */ + public @Nullable File getDirectory() { + switch (mState) { + case Environment.MEDIA_MOUNTED: + case Environment.MEDIA_MOUNTED_READ_ONLY: + return mPath; + default: + return null; + } + } + + /** * Returns a user-visible description of the volume. * * @return the volume description @@ -265,6 +289,24 @@ public final class StorageVolume implements Parcelable { return mFsUuid; } + /** + * Return the volume name that can be used to interact with this storage + * device through {@link MediaStore}. + * + * @return opaque volume name, or {@code null} if this volume is not indexed + * by {@link MediaStore}. + * @see android.provider.MediaStore.Audio.Media#getContentUri(String) + * @see android.provider.MediaStore.Video.Media#getContentUri(String) + * @see android.provider.MediaStore.Images.Media#getContentUri(String) + */ + public @Nullable String getMediaStoreVolumeName() { + if (isPrimary()) { + return MediaStore.VOLUME_EXTERNAL_PRIMARY; + } else { + return getNormalizedUuid(); + } + } + /** {@hide} */ public static @Nullable String normalizeUuid(@Nullable String fsUuid) { return fsUuid != null ? fsUuid.toLowerCase(Locale.US) : null; diff --git a/core/java/android/os/storage/VolumeRecord.java b/core/java/android/os/storage/VolumeRecord.java index 1a794ebf2a59..99b45d60c319 100644 --- a/core/java/android/os/storage/VolumeRecord.java +++ b/core/java/android/os/storage/VolumeRecord.java @@ -17,14 +17,18 @@ package android.os.storage; import android.annotation.UnsupportedAppUsage; +import android.content.Context; +import android.os.Environment; import android.os.Parcel; import android.os.Parcelable; +import android.os.UserHandle; import android.util.DebugUtils; import android.util.TimeUtils; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.Preconditions; +import java.io.File; import java.util.Locale; import java.util.Objects; @@ -92,6 +96,27 @@ public class VolumeRecord implements Parcelable { return (userFlags & USER_FLAG_SNOOZED) != 0; } + public StorageVolume buildStorageVolume(Context context) { + final String id = "unknown:" + fsUuid; + final File userPath = new File("/dev/null"); + final File internalPath = new File("/dev/null"); + final boolean primary = false; + final boolean removable = true; + final boolean emulated = false; + final boolean allowMassStorage = false; + final long maxFileSize = 0; + final UserHandle user = new UserHandle(UserHandle.USER_NULL); + final String envState = Environment.MEDIA_UNKNOWN; + + String description = nickname; + if (description == null) { + description = context.getString(android.R.string.unknownName); + } + + return new StorageVolume(id, userPath, internalPath, description, primary, removable, + emulated, allowMassStorage, maxFileSize, user, fsUuid, envState); + } + public void dump(IndentingPrintWriter pw) { pw.println("VolumeRecord:"); pw.increaseIndent(); diff --git a/core/java/android/provider/BaseColumns.java b/core/java/android/provider/BaseColumns.java index 00c9e72df880..b216e2b7ed8d 100644 --- a/core/java/android/provider/BaseColumns.java +++ b/core/java/android/provider/BaseColumns.java @@ -16,13 +16,11 @@ package android.provider; -import android.database.Cursor; - public interface BaseColumns { /** * The unique ID for a row. */ - @Column(Cursor.FIELD_TYPE_INTEGER) + // @Column(Cursor.FIELD_TYPE_INTEGER) public static final String _ID = "_id"; /** diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index 2fa3386bccb8..63204d36f396 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -21,17 +21,16 @@ import android.annotation.CurrentTimeMillisLong; import android.annotation.CurrentTimeSecondsLong; import android.annotation.DurationMillisLong; import android.annotation.IntDef; -import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; import android.annotation.UnsupportedAppUsage; import android.app.Activity; -import android.app.AppGlobals; +import android.app.PendingIntent; import android.content.ClipData; import android.content.ContentProviderClient; import android.content.ContentResolver; @@ -44,39 +43,28 @@ import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.ImageDecoder; -import android.graphics.Point; import android.graphics.PostProcessor; import android.media.ExifInterface; -import android.media.MediaFile; import android.media.MediaFormat; import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Environment; -import android.os.FileUtils; import android.os.OperationCanceledException; -import android.os.ParcelFileDescriptor; import android.os.RemoteException; -import android.os.UserHandle; -import android.os.UserManager; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; -import android.os.storage.VolumeInfo; -import android.os.storage.VolumeRecord; -import android.service.media.CameraPrewarmService; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; - -import com.android.internal.annotations.GuardedBy; +import android.util.Size; import libcore.util.HexEncoding; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -86,6 +74,7 @@ import java.lang.annotation.RetentionPolicy; import java.text.Collator; import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -160,44 +149,34 @@ public final class MediaStore { /** {@hide} */ public static final String SCAN_VOLUME_CALL = "scan_volume"; /** {@hide} */ - public static final String SUICIDE_CALL = "suicide"; - - /** - * Extra used with {@link #SCAN_FILE_CALL} or {@link #SCAN_VOLUME_CALL} to indicate that - * the file path originated from shell. - * - * {@hide} - */ - public static final String EXTRA_ORIGINATED_FROM_SHELL = - "android.intent.extra.originated_from_shell"; - - /** - * The method name used by the media scanner and mtp to tell the media provider to - * rescan and reclassify that have become unhidden because of renaming folders or - * removing nomedia files - * @hide - */ - @Deprecated - public static final String UNHIDE_CALL = "unhide"; + public static final String CREATE_WRITE_REQUEST_CALL = "create_write_request"; + /** {@hide} */ + public static final String CREATE_TRASH_REQUEST_CALL = "create_trash_request"; + /** {@hide} */ + public static final String CREATE_FAVORITE_REQUEST_CALL = "create_favorite_request"; + /** {@hide} */ + public static final String CREATE_DELETE_REQUEST_CALL = "create_delete_request"; - /** - * The method name used by the media scanner service to reload all localized ringtone titles due - * to a locale change. - * @hide - */ - public static final String RETRANSLATE_CALL = "update_titles"; /** {@hide} */ public static final String GET_VERSION_CALL = "get_version"; + /** {@hide} */ public static final String GET_DOCUMENT_URI_CALL = "get_document_uri"; /** {@hide} */ public static final String GET_MEDIA_URI_CALL = "get_media_uri"; /** {@hide} */ - public static final String GET_CONTRIBUTED_MEDIA_CALL = "get_contributed_media"; + public static final String EXTRA_URI = "uri"; /** {@hide} */ - public static final String DELETE_CONTRIBUTED_MEDIA_CALL = "delete_contributed_media"; + public static final String EXTRA_URI_PERMISSIONS = "uriPermissions"; + + /** {@hide} */ + public static final String EXTRA_CLIP_DATA = "clip_data"; + /** {@hide} */ + public static final String EXTRA_CONTENT_VALUES = "content_values"; + /** {@hide} */ + public static final String EXTRA_RESULT = "result"; /** * This is for internal use by the media scanner only. @@ -373,10 +352,10 @@ public final class MediaStore { * service. * <p> * This meta-data should reference the fully qualified class name of the prewarm service - * extending {@link CameraPrewarmService}. + * extending {@code CameraPrewarmService}. * <p> * The prewarm service will get bound and receive a prewarm signal - * {@link CameraPrewarmService#onPrewarm()} when a camera launch intent fire might be imminent. + * {@code CameraPrewarmService#onPrewarm()} when a camera launch intent fire might be imminent. * An application implementing a prewarm service should do the absolute minimum amount of work * to initialize the camera in order to reduce startup time in likely case that shortly after a * camera launch intent would be sent. @@ -606,6 +585,10 @@ public final class MediaStore { * {@link ContentResolver#delete}. * <p> * By default, trashed items are filtered away from operations. + * + * @see MediaColumns#IS_TRASHED + * @see MediaStore#QUERY_ARG_MATCH_TRASHED + * @see MediaStore#createTrashRequest */ @Match public static final String QUERY_ARG_MATCH_TRASHED = "android:query-arg-match-trashed"; @@ -620,6 +603,10 @@ public final class MediaStore { * <p> * By default, favorite items are <em>not</em> filtered away from * operations. + * + * @see MediaColumns#IS_FAVORITE + * @see MediaStore#QUERY_ARG_MATCH_FAVORITE + * @see MediaStore#createFavoriteRequest */ @Match public static final String QUERY_ARG_MATCH_FAVORITE = "android:query-arg-match-favorite"; @@ -749,197 +736,6 @@ public final class MediaStore { } /** - * Create a new pending media item using the given parameters. Pending items - * are expected to have a short lifetime, and owners should either - * {@link PendingSession#publish()} or {@link PendingSession#abandon()} a - * pending item within a few hours after first creating it. - * - * @return token which can be passed to {@link #openPending(Context, Uri)} - * to work with this pending item. - * @see MediaColumns#IS_PENDING - * @see MediaStore#setIncludePending(Uri) - * @see MediaStore#createPending(Context, PendingParams) - * @removed - */ - @Deprecated - public static @NonNull Uri createPending(@NonNull Context context, - @NonNull PendingParams params) { - return context.getContentResolver().insert(params.insertUri, params.insertValues); - } - - /** - * Open a pending media item to make progress on it. You can open a pending - * item multiple times before finally calling either - * {@link PendingSession#publish()} or {@link PendingSession#abandon()}. - * - * @param uri token which was previously returned from - * {@link #createPending(Context, PendingParams)}. - * @removed - */ - @Deprecated - public static @NonNull PendingSession openPending(@NonNull Context context, @NonNull Uri uri) { - return new PendingSession(context, uri); - } - - /** - * Parameters that describe a pending media item. - * - * @removed - */ - @Deprecated - public static class PendingParams { - /** {@hide} */ - public final Uri insertUri; - /** {@hide} */ - public final ContentValues insertValues; - - /** - * Create parameters that describe a pending media item. - * - * @param insertUri the {@code content://} Uri where this pending item - * should be inserted when finally published. For example, to - * publish an image, use - * {@link MediaStore.Images.Media#getContentUri(String)}. - */ - public PendingParams(@NonNull Uri insertUri, @NonNull String displayName, - @NonNull String mimeType) { - this.insertUri = Objects.requireNonNull(insertUri); - final long now = System.currentTimeMillis() / 1000; - this.insertValues = new ContentValues(); - this.insertValues.put(MediaColumns.DISPLAY_NAME, Objects.requireNonNull(displayName)); - this.insertValues.put(MediaColumns.MIME_TYPE, Objects.requireNonNull(mimeType)); - this.insertValues.put(MediaColumns.DATE_ADDED, now); - this.insertValues.put(MediaColumns.DATE_MODIFIED, now); - this.insertValues.put(MediaColumns.IS_PENDING, 1); - this.insertValues.put(MediaColumns.DATE_EXPIRES, - (System.currentTimeMillis() + DateUtils.DAY_IN_MILLIS) / 1000); - } - - public void setRelativePath(@Nullable String relativePath) { - if (relativePath == null) { - this.insertValues.remove(MediaColumns.RELATIVE_PATH); - } else { - this.insertValues.put(MediaColumns.RELATIVE_PATH, relativePath); - } - } - - /** - * Optionally set the Uri from where the file has been downloaded. This is used - * for files being added to {@link Downloads} table. - * - * @see DownloadColumns#DOWNLOAD_URI - */ - public void setDownloadUri(@Nullable Uri downloadUri) { - if (downloadUri == null) { - this.insertValues.remove(DownloadColumns.DOWNLOAD_URI); - } else { - this.insertValues.put(DownloadColumns.DOWNLOAD_URI, downloadUri.toString()); - } - } - - /** - * Optionally set the Uri indicating HTTP referer of the file. This is used for - * files being added to {@link Downloads} table. - * - * @see DownloadColumns#REFERER_URI - */ - public void setRefererUri(@Nullable Uri refererUri) { - if (refererUri == null) { - this.insertValues.remove(DownloadColumns.REFERER_URI); - } else { - this.insertValues.put(DownloadColumns.REFERER_URI, refererUri.toString()); - } - } - } - - /** - * Session actively working on a pending media item. Pending items are - * expected to have a short lifetime, and owners should either - * {@link PendingSession#publish()} or {@link PendingSession#abandon()} a - * pending item within a few hours after first creating it. - * - * @removed - */ - @Deprecated - public static class PendingSession implements AutoCloseable { - /** {@hide} */ - private final Context mContext; - /** {@hide} */ - private final Uri mUri; - - /** {@hide} */ - public PendingSession(Context context, Uri uri) { - mContext = Objects.requireNonNull(context); - mUri = Objects.requireNonNull(uri); - } - - /** - * Open the underlying file representing this media item. When a media - * item is successfully completed, you should - * {@link ParcelFileDescriptor#close()} and then {@link #publish()} it. - * - * @see #notifyProgress(int) - */ - public @NonNull ParcelFileDescriptor open() throws FileNotFoundException { - return mContext.getContentResolver().openFileDescriptor(mUri, "rw"); - } - - /** - * Open the underlying file representing this media item. When a media - * item is successfully completed, you should - * {@link OutputStream#close()} and then {@link #publish()} it. - * - * @see #notifyProgress(int) - */ - public @NonNull OutputStream openOutputStream() throws FileNotFoundException { - return mContext.getContentResolver().openOutputStream(mUri); - } - - /** - * Notify of current progress on this pending media item. Gallery - * applications may choose to surface progress information of this - * pending item. - * - * @param progress a percentage between 0 and 100. - */ - public void notifyProgress(@IntRange(from = 0, to = 100) int progress) { - final Uri withProgress = mUri.buildUpon() - .appendQueryParameter(PARAM_PROGRESS, Integer.toString(progress)).build(); - mContext.getContentResolver().notifyChange(withProgress, null, 0); - } - - /** - * When this media item is successfully completed, call this method to - * publish and make the final item visible to the user. - * - * @return the final {@code content://} Uri representing the newly - * published media. - */ - public @NonNull Uri publish() { - final ContentValues values = new ContentValues(); - values.put(MediaColumns.IS_PENDING, 0); - values.putNull(MediaColumns.DATE_EXPIRES); - mContext.getContentResolver().update(mUri, values, null, null); - return mUri; - } - - /** - * When this media item has failed to be completed, call this method to - * destroy the pending item record and any data related to it. - */ - public void abandon() { - mContext.getContentResolver().delete(mUri, null, null); - } - - @Override - public void close() { - // No resources to close, but at least we can inform people that no - // progress is being actively made. - notifyProgress(-1); - } - } - - /** * Mark the given item as being "trashed", meaning it should be deleted at * some point in the future. This is a more gentle operation than simply * calling {@link ContentResolver#delete(Uri, String, String[])}, which @@ -952,7 +748,9 @@ public final class MediaStore { * @see MediaStore#setIncludeTrashed(Uri) * @see MediaStore#trash(Context, Uri) * @see MediaStore#untrash(Context, Uri) + * @removed */ + @Deprecated public static void trash(@NonNull Context context, @NonNull Uri uri) { trash(context, uri, 48 * DateUtils.HOUR_IN_MILLIS); } @@ -970,7 +768,9 @@ public final class MediaStore { * @see MediaStore#setIncludeTrashed(Uri) * @see MediaStore#trash(Context, Uri) * @see MediaStore#untrash(Context, Uri) + * @removed */ + @Deprecated public static void trash(@NonNull Context context, @NonNull Uri uri, @DurationMillisLong long timeoutMillis) { if (timeoutMillis < 0) { @@ -992,7 +792,9 @@ public final class MediaStore { * @see MediaStore#setIncludeTrashed(Uri) * @see MediaStore#trash(Context, Uri) * @see MediaStore#untrash(Context, Uri) + * @removed */ + @Deprecated public static void untrash(@NonNull Context context, @NonNull Uri uri) { final ContentValues values = new ContentValues(); values.put(MediaColumns.IS_TRASHED, 0); @@ -1010,6 +812,180 @@ public final class MediaStore { return uri.buildUpon().authority(MediaStore.AUTHORITY_LEGACY).build(); } + private static @NonNull PendingIntent createRequest(@NonNull ContentResolver resolver, + @NonNull String method, @NonNull Collection<Uri> uris, @Nullable ContentValues values) { + Objects.requireNonNull(resolver); + Objects.requireNonNull(uris); + + final Iterator<Uri> it = uris.iterator(); + final ClipData clipData = ClipData.newRawUri(null, it.next()); + while (it.hasNext()) { + clipData.addItem(new ClipData.Item(it.next())); + } + + final Bundle extras = new Bundle(); + extras.putParcelable(EXTRA_CLIP_DATA, clipData); + extras.putParcelable(EXTRA_CONTENT_VALUES, values); + return resolver.call(AUTHORITY, method, null, extras).getParcelable(EXTRA_RESULT); + } + + /** + * Create a {@link PendingIntent} that will prompt the user to grant your + * app write access for the requested media items. + * <p> + * This call only generates the request for a prompt; to display the prompt, + * call {@link Activity#startIntentSenderForResult} with + * {@link PendingIntent#getIntentSender()}. You can then determine if the + * user granted your request by testing for {@link Activity#RESULT_OK} in + * {@link Activity#onActivityResult}. + * <p> + * Permissions granted through this mechanism are tied to the lifecycle of + * the {@link Activity} that requests them. If you need to retain + * longer-term access for background actions, you can place items into a + * {@link ClipData} or {@link Intent} which can then be passed to + * {@link Context#startService} or + * {@link android.app.job.JobInfo.Builder#setClipData}. Be sure to include + * any relevant access modes you want to retain, such as + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. + * <p> + * The displayed prompt will reflect all the media items you're requesting, + * including those for which you already hold write access. If you want to + * determine if you already hold write access before requesting access, use + * {@code ContentResolver#checkUriPermission(Uri, int, int)} with + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. + * <p> + * For security and performance reasons this method does not support + * {@link Intent#FLAG_GRANT_PERSISTABLE_URI_PERMISSION} or + * {@link Intent#FLAG_GRANT_PREFIX_URI_PERMISSION}. + * + * @param resolver Used to connect with {@link MediaStore#AUTHORITY}. + * Typically this value is {@link Context#getContentResolver()}, + * but if you need more explicit lifecycle controls, you can + * obtain a {@link ContentProviderClient} and wrap it using + * {@link ContentResolver#wrap(ContentProviderClient)}. + * @param uris The set of media items to include in this request. Each item + * must be hosted by {@link MediaStore#AUTHORITY} and must + * reference a specific media item by {@link BaseColumns#_ID}. + */ + public static @NonNull PendingIntent createWriteRequest(@NonNull ContentResolver resolver, + @NonNull Collection<Uri> uris) { + return createRequest(resolver, CREATE_WRITE_REQUEST_CALL, uris, null); + } + + /** + * Create a {@link PendingIntent} that will prompt the user to trash the + * requested media items. When the user approves this request, + * {@link MediaColumns#IS_TRASHED} is set on these items. + * <p> + * This call only generates the request for a prompt; to display the prompt, + * call {@link Activity#startIntentSenderForResult} with + * {@link PendingIntent#getIntentSender()}. You can then determine if the + * user granted your request by testing for {@link Activity#RESULT_OK} in + * {@link Activity#onActivityResult}. + * <p> + * The displayed prompt will reflect all the media items you're requesting, + * including those for which you already hold write access. If you want to + * determine if you already hold write access before requesting access, use + * {@code ContentResolver#checkUriPermission(Uri, int, int)} with + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. + * + * @param resolver Used to connect with {@link MediaStore#AUTHORITY}. + * Typically this value is {@link Context#getContentResolver()}, + * but if you need more explicit lifecycle controls, you can + * obtain a {@link ContentProviderClient} and wrap it using + * {@link ContentResolver#wrap(ContentProviderClient)}. + * @param uris The set of media items to include in this request. Each item + * must be hosted by {@link MediaStore#AUTHORITY} and must + * reference a specific media item by {@link BaseColumns#_ID}. + * @param value The {@link MediaColumns#IS_TRASHED} value to apply. + * @see MediaColumns#IS_TRASHED + * @see MediaStore#QUERY_ARG_MATCH_TRASHED + */ + public static @NonNull PendingIntent createTrashRequest(@NonNull ContentResolver resolver, + @NonNull Collection<Uri> uris, boolean value) { + final ContentValues values = new ContentValues(); + if (value) { + values.put(MediaColumns.IS_TRASHED, 1); + values.put(MediaColumns.DATE_EXPIRES, + (System.currentTimeMillis() + DateUtils.WEEK_IN_MILLIS) / 1000); + } else { + values.put(MediaColumns.IS_TRASHED, 0); + values.putNull(MediaColumns.DATE_EXPIRES); + } + return createRequest(resolver, CREATE_TRASH_REQUEST_CALL, uris, values); + } + + /** + * Create a {@link PendingIntent} that will prompt the user to favorite the + * requested media items. When the user approves this request, + * {@link MediaColumns#IS_FAVORITE} is set on these items. + * <p> + * This call only generates the request for a prompt; to display the prompt, + * call {@link Activity#startIntentSenderForResult} with + * {@link PendingIntent#getIntentSender()}. You can then determine if the + * user granted your request by testing for {@link Activity#RESULT_OK} in + * {@link Activity#onActivityResult}. + * <p> + * The displayed prompt will reflect all the media items you're requesting, + * including those for which you already hold write access. If you want to + * determine if you already hold write access before requesting access, use + * {@code ContentResolver#checkUriPermission(Uri, int, int)} with + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. + * + * @param resolver Used to connect with {@link MediaStore#AUTHORITY}. + * Typically this value is {@link Context#getContentResolver()}, + * but if you need more explicit lifecycle controls, you can + * obtain a {@link ContentProviderClient} and wrap it using + * {@link ContentResolver#wrap(ContentProviderClient)}. + * @param uris The set of media items to include in this request. Each item + * must be hosted by {@link MediaStore#AUTHORITY} and must + * reference a specific media item by {@link BaseColumns#_ID}. + * @param value The {@link MediaColumns#IS_FAVORITE} value to apply. + * @see MediaColumns#IS_FAVORITE + * @see MediaStore#QUERY_ARG_MATCH_FAVORITE + */ + public static @NonNull PendingIntent createFavoriteRequest(@NonNull ContentResolver resolver, + @NonNull Collection<Uri> uris, boolean value) { + final ContentValues values = new ContentValues(); + if (value) { + values.put(MediaColumns.IS_FAVORITE, 1); + } else { + values.put(MediaColumns.IS_FAVORITE, 0); + } + return createRequest(resolver, CREATE_FAVORITE_REQUEST_CALL, uris, values); + } + + /** + * Create a {@link PendingIntent} that will prompt the user to permanently + * delete the requested media items. When the user approves this request, + * {@link ContentResolver#delete} will be called on these items. + * <p> + * This call only generates the request for a prompt; to display the prompt, + * call {@link Activity#startIntentSenderForResult} with + * {@link PendingIntent#getIntentSender()}. You can then determine if the + * user granted your request by testing for {@link Activity#RESULT_OK} in + * {@link Activity#onActivityResult}. + * <p> + * The displayed prompt will reflect all the media items you're requesting, + * including those for which you already hold write access. If you want to + * determine if you already hold write access before requesting access, use + * {@code ContentResolver#checkUriPermission(Uri, int, int)} with + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. + * + * @param resolver Used to connect with {@link MediaStore#AUTHORITY}. + * Typically this value is {@link Context#getContentResolver()}, + * but if you need more explicit lifecycle controls, you can + * obtain a {@link ContentProviderClient} and wrap it using + * {@link ContentResolver#wrap(ContentProviderClient)}. + * @param uris The set of media items to include in this request. Each item + * must be hosted by {@link MediaStore#AUTHORITY} and must + * reference a specific media item by {@link BaseColumns#_ID}. + */ + public static @NonNull PendingIntent createDeleteRequest(@NonNull ContentResolver resolver, + @NonNull Collection<Uri> uris) { + return createRequest(resolver, CREATE_DELETE_REQUEST_CALL, uris, null); + } + /** * Common media metadata columns. */ @@ -1127,9 +1103,9 @@ public final class MediaStore { * Trashed items are retained until they expire as defined by * {@link #DATE_EXPIRES}. * + * @see MediaColumns#IS_TRASHED * @see MediaStore#QUERY_ARG_MATCH_TRASHED - * @see MediaStore#trash(Context, Uri) - * @see MediaStore#untrash(Context, Uri) + * @see MediaStore#createTrashRequest */ @Column(Cursor.FIELD_TYPE_INTEGER) public static final String IS_TRASHED = "is_trashed"; @@ -1302,7 +1278,9 @@ public final class MediaStore { * Flag indicating if the media item has been marked as being a * "favorite" by the user. * + * @see MediaColumns#IS_FAVORITE * @see MediaStore#QUERY_ARG_MATCH_FAVORITE + * @see MediaStore#createFavoriteRequest */ @Column(Cursor.FIELD_TYPE_INTEGER) public static final String IS_FAVORITE = "is_favorite"; @@ -1493,34 +1471,22 @@ public final class MediaStore { return ContentUris.withAppendedId(getContentUri(volumeName), rowId); } - /** - * For use only by the MTP implementation. - * @hide - */ + /** {@hide} */ @UnsupportedAppUsage - public static Uri getMtpObjectsUri(String volumeName) { - return AUTHORITY_URI.buildUpon().appendPath(volumeName).appendPath("object").build(); + public static Uri getMtpObjectsUri(@NonNull String volumeName) { + return MediaStore.Files.getContentUri(volumeName); } - /** - * For use only by the MTP implementation. - * @hide - */ + /** {@hide} */ @UnsupportedAppUsage - public static final Uri getMtpObjectsUri(String volumeName, - long fileId) { - return ContentUris.withAppendedId(getMtpObjectsUri(volumeName), fileId); + public static final Uri getMtpObjectsUri(@NonNull String volumeName, long fileId) { + return MediaStore.Files.getContentUri(volumeName, fileId); } - /** - * Used to implement the MTP GetObjectReferences and SetObjectReferences commands. - * @hide - */ + /** {@hide} */ @UnsupportedAppUsage - public static final Uri getMtpReferencesUri(String volumeName, - long fileId) { - return getMtpObjectsUri(volumeName, fileId).buildUpon().appendPath("references") - .build(); + public static final Uri getMtpReferencesUri(@NonNull String volumeName, long fileId) { + return MediaStore.Files.getContentUri(volumeName, fileId); } /** @@ -1643,9 +1609,21 @@ public final class MediaStore { public static final int FULL_SCREEN_KIND = 2; public static final int MICRO_KIND = 3; - public static final Point MINI_SIZE = new Point(512, 384); - public static final Point FULL_SCREEN_SIZE = new Point(1024, 786); - public static final Point MICRO_SIZE = new Point(96, 96); + public static final Size MINI_SIZE = new Size(512, 384); + public static final Size FULL_SCREEN_SIZE = new Size(1024, 786); + public static final Size MICRO_SIZE = new Size(96, 96); + + public static @NonNull Size getKindSize(int kind) { + if (kind == ThumbnailConstants.MICRO_KIND) { + return ThumbnailConstants.MICRO_SIZE; + } else if (kind == ThumbnailConstants.FULL_SCREEN_KIND) { + return ThumbnailConstants.FULL_SCREEN_SIZE; + } else if (kind == ThumbnailConstants.MINI_KIND) { + return ThumbnailConstants.MINI_SIZE; + } else { + throw new IllegalArgumentException("Unsupported kind: " + kind); + } + } } /** @@ -1749,22 +1727,22 @@ public final class MediaStore { } } - /** {@hide} */ + /** + * @deprecated since this method doesn't have a {@link Context}, we can't + * find the actual {@link StorageVolume} for the given path, so + * only a vague guess is returned. Callers should use + * {@link StorageManager#getStorageVolume(File)} instead. + * @hide + */ + @Deprecated public static @NonNull String getVolumeName(@NonNull File path) { - if (FileUtils.contains(Environment.getStorageDirectory(), path)) { - final StorageManager sm = AppGlobals.getInitialApplication() - .getSystemService(StorageManager.class); - final StorageVolume sv = sm.getStorageVolume(path); - if (sv != null) { - if (sv.isPrimary()) { - return VOLUME_EXTERNAL_PRIMARY; - } else { - return checkArgumentVolumeName(sv.getNormalizedUuid()); - } - } - throw new IllegalStateException("Unknown volume at " + path); + // Ideally we'd find the relevant StorageVolume, but we don't have a + // Context to obtain it from, so the best we can do is assume + if (path.getAbsolutePath() + .startsWith(Environment.getStorageDirectory().getAbsolutePath())) { + return MediaStore.VOLUME_EXTERNAL; } else { - return VOLUME_INTERNAL; + return MediaStore.VOLUME_INTERNAL; } } @@ -1777,7 +1755,7 @@ public final class MediaStore { /** * Currently outstanding thumbnail requests that can be cancelled. */ - @GuardedBy("sPending") + // @GuardedBy("sPending") private static ArrayMap<Uri, CancellationSignal> sPending = new ArrayMap<>(); /** @@ -1789,16 +1767,7 @@ public final class MediaStore { @Deprecated static @Nullable Bitmap getThumbnail(@NonNull ContentResolver cr, @NonNull Uri uri, int kind, @Nullable BitmapFactory.Options opts) { - final Point size; - if (kind == ThumbnailConstants.MICRO_KIND) { - size = ThumbnailConstants.MICRO_SIZE; - } else if (kind == ThumbnailConstants.FULL_SCREEN_KIND) { - size = ThumbnailConstants.FULL_SCREEN_SIZE; - } else if (kind == ThumbnailConstants.MINI_KIND) { - size = ThumbnailConstants.MINI_SIZE; - } else { - throw new IllegalArgumentException("Unsupported kind: " + kind); - } + final Size size = ThumbnailConstants.getKindSize(kind); CancellationSignal signal = null; synchronized (sPending) { @@ -1810,7 +1779,7 @@ public final class MediaStore { } try { - return cr.loadThumbnail(uri, Point.convert(size), signal); + return cr.loadThumbnail(uri, size, signal); } catch (IOException e) { Log.w(TAG, "Failed to obtain thumbnail for " + uri, e); return null; @@ -2007,26 +1976,14 @@ public final class MediaStore { @Deprecated public static final String insertImage(ContentResolver cr, String imagePath, String name, String description) throws FileNotFoundException { - final File file = new File(imagePath); - final String mimeType = MediaFile.getMimeTypeForFile(imagePath); - - if (TextUtils.isEmpty(name)) name = "Image"; - final PendingParams params = new PendingParams( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, name, mimeType); - - final Context context = AppGlobals.getInitialApplication(); - final Uri pendingUri = createPending(context, params); - try (PendingSession session = openPending(context, pendingUri)) { - try (InputStream in = new FileInputStream(file); - OutputStream out = session.openOutputStream()) { - FileUtils.copy(in, out); - } - return session.publish().toString(); - } catch (Exception e) { - Log.w(TAG, "Failed to insert image", e); - context.getContentResolver().delete(pendingUri, null, null); - return null; + final Bitmap source; + try { + source = ImageDecoder + .decodeBitmap(ImageDecoder.createSource(new File(imagePath))); + } catch (IOException e) { + throw new FileNotFoundException(e.getMessage()); } + return insertImage(cr, source, name, description); } /** @@ -2043,22 +2000,34 @@ public final class MediaStore { * control over lifecycle. */ @Deprecated - public static final String insertImage(ContentResolver cr, Bitmap source, - String title, String description) { + public static final String insertImage(ContentResolver cr, Bitmap source, String title, + String description) { if (TextUtils.isEmpty(title)) title = "Image"; - final PendingParams params = new PendingParams( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, title, "image/jpeg"); - final Context context = AppGlobals.getInitialApplication(); - final Uri pendingUri = createPending(context, params); - try (PendingSession session = openPending(context, pendingUri)) { - try (OutputStream out = session.openOutputStream()) { + final long now = System.currentTimeMillis(); + final ContentValues values = new ContentValues(); + values.put(MediaColumns.DISPLAY_NAME, title); + values.put(MediaColumns.MIME_TYPE, "image/jpeg"); + values.put(MediaColumns.DATE_ADDED, now / 1000); + values.put(MediaColumns.DATE_MODIFIED, now / 1000); + values.put(MediaColumns.DATE_EXPIRES, (now + DateUtils.DAY_IN_MILLIS) / 1000); + values.put(MediaColumns.IS_PENDING, 1); + + final Uri uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + try { + try (OutputStream out = cr.openOutputStream(uri)) { source.compress(Bitmap.CompressFormat.JPEG, 90, out); } - return session.publish().toString(); + + // Everything went well above, publish it! + values.clear(); + values.put(MediaColumns.IS_PENDING, 0); + values.putNull(MediaColumns.DATE_EXPIRES); + cr.update(uri, values, null, null); + return uri.toString(); } catch (Exception e) { Log.w(TAG, "Failed to insert image", e); - context.getContentResolver().delete(pendingUri, null, null); + cr.delete(uri, null, null); return null; } } @@ -2318,6 +2287,14 @@ public final class MediaStore { public static final int MICRO_KIND = ThumbnailConstants.MICRO_KIND; /** + * Return the typical {@link Size} (in pixels) used internally when + * the given thumbnail kind is requested. + */ + public static @NonNull Size getKindSize(int kind) { + return ThumbnailConstants.getKindSize(kind); + } + + /** * The blob raw data of thumbnail * * @deprecated this column never existed internally, and could never @@ -3572,6 +3549,14 @@ public final class MediaStore { public static final int MICRO_KIND = ThumbnailConstants.MICRO_KIND; /** + * Return the typical {@link Size} (in pixels) used internally when + * the given thumbnail kind is requested. + */ + public static @NonNull Size getKindSize(int kind) { + return ThumbnailConstants.getKindSize(kind); + } + + /** * The width of the thumbnal */ @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true) @@ -3603,54 +3588,44 @@ public final class MediaStore { */ public static @NonNull Set<String> getExternalVolumeNames(@NonNull Context context) { final StorageManager sm = context.getSystemService(StorageManager.class); - final Set<String> volumeNames = new ArraySet<>(); - for (VolumeInfo vi : sm.getVolumes()) { - if (vi.isVisibleForUser(UserHandle.myUserId()) && vi.isMountedReadable()) { - if (vi.isPrimary()) { - volumeNames.add(VOLUME_EXTERNAL_PRIMARY); - } else { - volumeNames.add(vi.getNormalizedFsUuid()); + final Set<String> res = new ArraySet<>(); + for (StorageVolume sv : sm.getStorageVolumes()) { + switch (sv.getState()) { + case Environment.MEDIA_MOUNTED: + case Environment.MEDIA_MOUNTED_READ_ONLY: { + final String volumeName = sv.getMediaStoreVolumeName(); + if (volumeName != null) { + res.add(volumeName); + } + break; } } } - return volumeNames; + return res; } /** - * Return list of all specific volume names that have recently been part of + * Return list of all recent volume names that have been part of * {@link #VOLUME_EXTERNAL}. * <p> - * This includes both currently mounted volumes <em>and</em> recently - * mounted (but currently unmounted) volumes. Any indexed metadata for these - * volumes is preserved to optimize the speed of remounting at a later time. - * - * @hide + * These volume names are not currently mounted, but they're likely to + * reappear in the future, so apps are encouraged to preserve any indexed + * metadata related to these volumes to optimize user experiences. + * <p> + * Each specific volume name can be passed to APIs like + * {@link MediaStore.Images.Media#getContentUri(String)} to interact with + * media on that storage device. */ - @SystemApi - @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) public static @NonNull Set<String> getRecentExternalVolumeNames(@NonNull Context context) { final StorageManager sm = context.getSystemService(StorageManager.class); - - // We always have primary storage - final Set<String> volumeNames = new ArraySet<>(); - volumeNames.add(VOLUME_EXTERNAL_PRIMARY); - - final long lastWeek = System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS; - for (VolumeRecord rec : sm.getVolumeRecords()) { - // Skip volumes without valid UUIDs - if (TextUtils.isEmpty(rec.fsUuid)) continue; - - final VolumeInfo vi = sm.findVolumeByUuid(rec.fsUuid); - if (vi != null && vi.isVisibleForUser(UserHandle.myUserId()) - && vi.isMountedReadable()) { - // We're mounted right now - volumeNames.add(rec.getNormalizedFsUuid()); - } else if (rec.lastSeenMillis > 0 && rec.lastSeenMillis < lastWeek) { - // We're not mounted right now, but we've been seen recently - volumeNames.add(rec.getNormalizedFsUuid()); + final Set<String> res = new ArraySet<>(); + for (StorageVolume sv : sm.getRecentStorageVolumes()) { + final String volumeName = sv.getMediaStoreVolumeName(); + if (volumeName != null) { + res.add(volumeName); } } - return volumeNames; + return res; } /** @@ -3703,97 +3678,6 @@ public final class MediaStore { } /** - * Return path where the given specific volume is mounted. Not valid for - * {@link #VOLUME_INTERNAL} or {@link #VOLUME_EXTERNAL}, since those are - * broad collections that cover many paths. - * - * @hide - */ - @TestApi - public static @NonNull File getVolumePath(@NonNull String volumeName) - throws FileNotFoundException { - final StorageManager sm = AppGlobals.getInitialApplication() - .getSystemService(StorageManager.class); - return getVolumePath(sm.getVolumes(), volumeName); - } - - /** {@hide} */ - public static @NonNull File getVolumePath(@NonNull List<VolumeInfo> volumes, - @NonNull String volumeName) throws FileNotFoundException { - if (TextUtils.isEmpty(volumeName)) { - throw new IllegalArgumentException(); - } - - switch (volumeName) { - case VOLUME_INTERNAL: - case VOLUME_EXTERNAL: - throw new FileNotFoundException(volumeName + " has no associated path"); - } - - final boolean wantPrimary = VOLUME_EXTERNAL_PRIMARY.equals(volumeName); - for (VolumeInfo volume : volumes) { - final boolean matchPrimary = wantPrimary - && volume.isPrimary(); - final boolean matchSecondary = !wantPrimary - && Objects.equals(volume.getNormalizedFsUuid(), volumeName); - if (matchPrimary || matchSecondary) { - final File path = volume.getPathForUser(UserHandle.myUserId()); - if (path != null) { - return path; - } - } - } - throw new FileNotFoundException("Failed to find path for " + volumeName); - } - - /** - * Return paths that should be scanned for the given volume. - * - * @hide - */ - @TestApi - @SystemApi - @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) - public static @NonNull Collection<File> getVolumeScanPaths(@NonNull String volumeName) - throws FileNotFoundException { - if (TextUtils.isEmpty(volumeName)) { - throw new IllegalArgumentException(); - } - - final Context context = AppGlobals.getInitialApplication(); - final UserManager um = context.getSystemService(UserManager.class); - - final ArrayList<File> res = new ArrayList<>(); - if (VOLUME_INTERNAL.equals(volumeName)) { - addCanonicalFile(res, new File(Environment.getRootDirectory(), "media")); - addCanonicalFile(res, new File(Environment.getOemDirectory(), "media")); - addCanonicalFile(res, new File(Environment.getProductDirectory(), "media")); - } else if (VOLUME_EXTERNAL.equals(volumeName)) { - for (String exactVolume : getExternalVolumeNames(context)) { - addCanonicalFile(res, getVolumePath(exactVolume)); - } - if (um.isDemoUser()) { - addCanonicalFile(res, Environment.getDataPreloadsMediaDirectory()); - } - } else { - addCanonicalFile(res, getVolumePath(volumeName)); - if (VOLUME_EXTERNAL_PRIMARY.equals(volumeName) && um.isDemoUser()) { - addCanonicalFile(res, Environment.getDataPreloadsMediaDirectory()); - } - } - return res; - } - - private static void addCanonicalFile(List<File> list, File file) { - try { - list.add(file.getCanonicalFile()); - } catch (IOException e) { - Log.w(TAG, "Failed to resolve " + file + ": " + e); - list.add(file); - } - } - - /** * Uri for querying the state of the media scanner. */ public static Uri getMediaScannerUri() { @@ -3876,10 +3760,10 @@ public final class MediaStore { try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) { final Bundle in = new Bundle(); - in.putParcelable(DocumentsContract.EXTRA_URI, mediaUri); - in.putParcelableList(DocumentsContract.EXTRA_URI_PERMISSIONS, uriPermissions); + in.putParcelable(EXTRA_URI, mediaUri); + in.putParcelableArrayList(EXTRA_URI_PERMISSIONS, new ArrayList<>(uriPermissions)); final Bundle out = client.call(GET_DOCUMENT_URI_CALL, null, in); - return out.getParcelable(DocumentsContract.EXTRA_URI); + return out.getParcelable(EXTRA_URI); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } @@ -3906,134 +3790,43 @@ public final class MediaStore { try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) { final Bundle in = new Bundle(); - in.putParcelable(DocumentsContract.EXTRA_URI, documentUri); - in.putParcelableList(DocumentsContract.EXTRA_URI_PERMISSIONS, uriPermissions); + in.putParcelable(EXTRA_URI, documentUri); + in.putParcelableArrayList(EXTRA_URI_PERMISSIONS, new ArrayList<>(uriPermissions)); final Bundle out = client.call(GET_MEDIA_URI_CALL, null, in); - return out.getParcelable(DocumentsContract.EXTRA_URI); + return out.getParcelable(EXTRA_URI); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } + /** @hide */ + @TestApi + public static void waitForIdle(@NonNull ContentResolver resolver) { + resolver.call(AUTHORITY, WAIT_FOR_IDLE_CALL, null, null); + } + /** - * Calculate size of media contributed by given package under the calling - * user. The meaning of "contributed" means it won't automatically be - * deleted when the app is uninstalled. + * Perform a blocking scan of the given {@link File}, returning the + * {@link Uri} of the scanned file. * * @hide */ + @SystemApi @TestApi - @RequiresPermission(android.Manifest.permission.CLEAR_APP_USER_DATA) - public static @BytesLong long getContributedMediaSize(Context context, String packageName, - UserHandle user) throws IOException { - final UserManager um = context.getSystemService(UserManager.class); - if (um.isUserUnlocked(user) && um.isUserRunning(user)) { - try { - final ContentResolver resolver = context - .createPackageContextAsUser(packageName, 0, user).getContentResolver(); - final Bundle in = new Bundle(); - in.putString(Intent.EXTRA_PACKAGE_NAME, packageName); - final Bundle out = resolver.call(AUTHORITY, GET_CONTRIBUTED_MEDIA_CALL, null, in); - return out.getLong(Intent.EXTRA_INDEX); - } catch (Exception e) { - throw new IOException(e); - } - } else { - throw new IOException("User " + user + " must be unlocked and running"); - } + @SuppressLint("StreamFiles") + public static @NonNull Uri scanFile(@NonNull ContentResolver resolver, @NonNull File file) { + final Bundle out = resolver.call(AUTHORITY, SCAN_FILE_CALL, file.getAbsolutePath(), null); + return out.getParcelable(Intent.EXTRA_STREAM); } /** - * Delete all media contributed by given package under the calling user. The - * meaning of "contributed" means it won't automatically be deleted when the - * app is uninstalled. + * Perform a blocking scan of the given storage volume. * * @hide */ + @SystemApi @TestApi - @RequiresPermission(android.Manifest.permission.CLEAR_APP_USER_DATA) - public static void deleteContributedMedia(Context context, String packageName, - UserHandle user) throws IOException { - final UserManager um = context.getSystemService(UserManager.class); - if (um.isUserUnlocked(user) && um.isUserRunning(user)) { - try { - final ContentResolver resolver = context - .createPackageContextAsUser(packageName, 0, user).getContentResolver(); - final Bundle in = new Bundle(); - in.putString(Intent.EXTRA_PACKAGE_NAME, packageName); - resolver.call(AUTHORITY, DELETE_CONTRIBUTED_MEDIA_CALL, null, in); - } catch (Exception e) { - throw new IOException(e); - } - } else { - throw new IOException("User " + user + " must be unlocked and running"); - } - } - - /** @hide */ - @TestApi - public static void waitForIdle(Context context) { - final ContentResolver resolver = context.getContentResolver(); - try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) { - client.call(WAIT_FOR_IDLE_CALL, null, null); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } - } - - /** @hide */ - public static void suicide(Context context) { - final ContentResolver resolver = context.getContentResolver(); - try (ContentProviderClient client = resolver - .acquireUnstableContentProviderClient(AUTHORITY)) { - client.call(SUICIDE_CALL, null, null); - } catch (Exception ignored) { - } - } - - /** @hide */ - @TestApi - public static Uri scanFile(Context context, File file) { - return scan(context, SCAN_FILE_CALL, file, false); - } - - /** @hide */ - @TestApi - public static Uri scanFileFromShell(Context context, File file) { - return scan(context, SCAN_FILE_CALL, file, true); - } - - /** @hide */ - @TestApi - public static void scanVolume(Context context, File file) { - scan(context, SCAN_VOLUME_CALL, file, false); - } - - /** @hide */ - public static Uri scanFile(ContentProviderClient client, File file) { - return scan(client, SCAN_FILE_CALL, file, false); - } - - /** @hide */ - private static Uri scan(Context context, String method, File file, - boolean originatedFromShell) { - final ContentResolver resolver = context.getContentResolver(); - try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) { - return scan(client, method, file, originatedFromShell); - } - } - - /** @hide */ - private static Uri scan(ContentProviderClient client, String method, File file, - boolean originatedFromShell) { - try { - final Bundle in = new Bundle(); - in.putParcelable(Intent.EXTRA_STREAM, Uri.fromFile(file)); - in.putBoolean(EXTRA_ORIGINATED_FROM_SHELL, originatedFromShell); - final Bundle out = client.call(method, null, in); - return out.getParcelable(Intent.EXTRA_STREAM); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } + public static void scanVolume(@NonNull ContentResolver resolver, @NonNull String volumeName) { + resolver.call(AUTHORITY, SCAN_VOLUME_CALL, volumeName, null); } } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ee9aa097d706..f4e2329797b2 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -8392,6 +8392,20 @@ public final class Settings { "navigation_mode"; /** + * Scale factor for the back gesture inset size on the left side of the screen. + * @hide + */ + public static final String BACK_GESTURE_INSET_SCALE_LEFT = + "back_gesture_inset_scale_left"; + + /** + * Scale factor for the back gesture inset size on the right side of the screen. + * @hide + */ + public static final String BACK_GESTURE_INSET_SCALE_RIGHT = + "back_gesture_inset_scale_right"; + + /** * Controls whether aware is enabled. * @hide */ @@ -8416,6 +8430,12 @@ public final class Settings { public static final String PEOPLE_STRIP = "people_strip"; /** + * Controls if window magnification is enabled. + * @hide + */ + public static final String WINDOW_MAGNIFICATION = "window_magnification"; + + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. * @@ -13633,13 +13653,6 @@ public final class Settings { public static final String KERNEL_CPU_THREAD_READER = "kernel_cpu_thread_reader"; /** - * Default user id to boot into. They map to user ids, for example, 10, 11, 12. - * - * @hide - */ - public static final String DEFAULT_USER_ID_TO_BOOT_INTO = "default_boot_into_user_id"; - - /** * Persistent user id that is last logged in to. * * They map to user ids, for example, 10, 11, 12. diff --git a/core/java/android/service/autofill/FillRequest.java b/core/java/android/service/autofill/FillRequest.java index e53ebada55fb..72e9ad047ed7 100644 --- a/core/java/android/service/autofill/FillRequest.java +++ b/core/java/android/service/autofill/FillRequest.java @@ -23,6 +23,7 @@ import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.view.View; +import android.view.inputmethod.InlineSuggestionsRequest; import com.android.internal.util.DataClass; import com.android.internal.util.Preconditions; @@ -116,20 +117,34 @@ public final class FillRequest implements Parcelable { */ private final @RequestFlags int mFlags; + /** + * Gets the {@link android.view.inputmethod.InlineSuggestionsRequest} associated + * with this request. + * + * TODO(b/137800469): Add more doc describing how to handle the inline suggestions request. + * + * @return the suggestionspec + */ + private final @Nullable InlineSuggestionsRequest mInlineSuggestionsRequest; + private void onConstructed() { Preconditions.checkCollectionElementsNotNull(mFillContexts, "contexts"); } - // Code below generated by codegen v1.0.0. + // Code below generated by codegen v1.0.14. // // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code // // To regenerate run: // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/autofill/FillRequest.java // - // CHECKSTYLE:OFF Generated code + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + /** @hide */ @IntDef(flag = true, prefix = "FLAG_", value = { @@ -184,6 +199,11 @@ public final class FillRequest implements Parcelable { * * @return any combination of {@link #FLAG_MANUAL_REQUEST} and * {@link #FLAG_COMPATIBILITY_MODE_REQUEST}. + * @param inlineSuggestionsRequest + * Gets the {@link android.view.inputmethod.InlineSuggestionsRequest} associated + * with this request. + * + * TODO(b/137800469): Add more doc describing how to handle the inline suggestions request. * @hide */ @DataClass.Generated.Member @@ -191,7 +211,8 @@ public final class FillRequest implements Parcelable { int id, @NonNull List<FillContext> fillContexts, @Nullable Bundle clientState, - @RequestFlags int flags) { + @RequestFlags int flags, + @Nullable InlineSuggestionsRequest inlineSuggestionsRequest) { this.mId = id; this.mFillContexts = fillContexts; com.android.internal.util.AnnotationValidations.validate( @@ -203,6 +224,7 @@ public final class FillRequest implements Parcelable { mFlags, FLAG_MANUAL_REQUEST | FLAG_COMPATIBILITY_MODE_REQUEST); + this.mInlineSuggestionsRequest = inlineSuggestionsRequest; onConstructed(); } @@ -256,6 +278,19 @@ public final class FillRequest implements Parcelable { return mFlags; } + /** + * Gets the {@link android.view.inputmethod.InlineSuggestionsRequest} associated + * with this request. + * + * TODO(b/137800469): Add more doc describing how to handle the inline suggestions request. + * + * @return the suggestionspec + */ + @DataClass.Generated.Member + public @Nullable InlineSuggestionsRequest getInlineSuggestionsRequest() { + return mInlineSuggestionsRequest; + } + @Override @DataClass.Generated.Member public String toString() { @@ -266,29 +301,63 @@ public final class FillRequest implements Parcelable { "id = " + mId + ", " + "fillContexts = " + mFillContexts + ", " + "clientState = " + mClientState + ", " + - "flags = " + requestFlagsToString(mFlags) + + "flags = " + requestFlagsToString(mFlags) + ", " + + "inlineSuggestionsRequest = " + mInlineSuggestionsRequest + " }"; } @Override @DataClass.Generated.Member - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(@NonNull Parcel dest, int flags) { // You can override field parcelling by defining methods like: // void parcelFieldName(Parcel dest, int flags) { ... } byte flg = 0; if (mClientState != null) flg |= 0x4; + if (mInlineSuggestionsRequest != null) flg |= 0x10; dest.writeByte(flg); dest.writeInt(mId); dest.writeParcelableList(mFillContexts, flags); if (mClientState != null) dest.writeBundle(mClientState); dest.writeInt(mFlags); + if (mInlineSuggestionsRequest != null) dest.writeTypedObject(mInlineSuggestionsRequest, flags); } @Override @DataClass.Generated.Member public int describeContents() { return 0; } + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ FillRequest(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + byte flg = in.readByte(); + int id = in.readInt(); + List<FillContext> fillContexts = new ArrayList<>(); + in.readParcelableList(fillContexts, FillContext.class.getClassLoader()); + Bundle clientState = (flg & 0x4) == 0 ? null : in.readBundle(); + int flags = in.readInt(); + InlineSuggestionsRequest inlineSuggestionsRequest = (flg & 0x10) == 0 ? null : (InlineSuggestionsRequest) in.readTypedObject(InlineSuggestionsRequest.CREATOR); + + this.mId = id; + this.mFillContexts = fillContexts; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mFillContexts); + this.mClientState = clientState; + this.mFlags = flags; + + Preconditions.checkFlagsArgument( + mFlags, + FLAG_MANUAL_REQUEST + | FLAG_COMPATIBILITY_MODE_REQUEST); + this.mInlineSuggestionsRequest = inlineSuggestionsRequest; + + onConstructed(); + } + @DataClass.Generated.Member public static final @NonNull Parcelable.Creator<FillRequest> CREATOR = new Parcelable.Creator<FillRequest>() { @@ -298,31 +367,21 @@ public final class FillRequest implements Parcelable { } @Override - @SuppressWarnings({"unchecked", "RedundantCast"}) - public FillRequest createFromParcel(Parcel in) { - // You can override field unparcelling by defining methods like: - // static FieldType unparcelFieldName(Parcel in) { ... } - - byte flg = in.readByte(); - int id = in.readInt(); - List<FillContext> fillContexts = new ArrayList<>(); - in.readParcelableList(fillContexts, FillContext.class.getClassLoader()); - Bundle clientState = (flg & 0x4) == 0 ? null : in.readBundle(); - int flags = in.readInt(); - return new FillRequest( - id, - fillContexts, - clientState, - flags); + public FillRequest createFromParcel(@NonNull Parcel in) { + return new FillRequest(in); } }; @DataClass.Generated( - time = 1565152134349L, - codegenVersion = "1.0.0", + time = 1575928271155L, + codegenVersion = "1.0.14", sourceFile = "frameworks/base/core/java/android/service/autofill/FillRequest.java", - inputSignatures = "public static final @android.service.autofill.FillRequest.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final int INVALID_REQUEST_ID\nprivate final int mId\nprivate final @android.annotation.NonNull java.util.List<android.service.autofill.FillContext> mFillContexts\nprivate final @android.annotation.Nullable android.os.Bundle mClientState\nprivate final @android.service.autofill.FillRequest.RequestFlags int mFlags\nprivate void onConstructed()\nclass FillRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)") + inputSignatures = "public static final @android.service.autofill.FillRequest.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final int INVALID_REQUEST_ID\nprivate final int mId\nprivate final @android.annotation.NonNull java.util.List<android.service.autofill.FillContext> mFillContexts\nprivate final @android.annotation.Nullable android.os.Bundle mClientState\nprivate final @android.service.autofill.FillRequest.RequestFlags int mFlags\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\nprivate void onConstructed()\nclass FillRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)") @Deprecated private void __metadata() {} + + //@formatter:on + // End of generated code + } diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java index c99fe6149aab..02a6390a08bb 100644 --- a/core/java/android/service/autofill/FillResponse.java +++ b/core/java/android/service/autofill/FillResponse.java @@ -25,6 +25,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.app.Activity; +import android.app.slice.Slice; import android.content.IntentSender; import android.content.pm.ParceledListSlice; import android.os.Bundle; @@ -86,6 +87,7 @@ public final class FillResponse implements Parcelable { private int mRequestId; private final @Nullable UserData mUserData; private final @Nullable int[] mCancelIds; + private final @Nullable ParceledListSlice<Slice> mInlineSuggestionSlices; private FillResponse(@NonNull Builder builder) { mDatasets = (builder.mDatasets != null) ? new ParceledListSlice<>(builder.mDatasets) : null; @@ -103,6 +105,8 @@ public final class FillResponse implements Parcelable { mRequestId = INVALID_REQUEST_ID; mUserData = builder.mUserData; mCancelIds = builder.mCancelIds; + mInlineSuggestionSlices = (builder.mInlineSuggestionSlices != null) + ? new ParceledListSlice<>(builder.mInlineSuggestionSlices) : null; } /** @hide */ @@ -195,6 +199,11 @@ public final class FillResponse implements Parcelable { return mCancelIds; } + /** @hide */ + public List<Slice> getInlineSuggestionSlices() { + return (mInlineSuggestionSlices != null) ? mInlineSuggestionSlices.getList() : null; + } + /** * Builder for {@link FillResponse} objects. You must to provide at least * one dataset or set an authentication intent with a presentation view. @@ -215,6 +224,7 @@ public final class FillResponse implements Parcelable { private boolean mDestroyed; private UserData mUserData; private int[] mCancelIds; + private ArrayList<Slice> mInlineSuggestionSlices; /** * Triggers a custom UI before before autofilling the screen with any data set in this @@ -570,6 +580,20 @@ public final class FillResponse implements Parcelable { } /** + * TODO(b/137800469): add javadoc + */ + @NonNull + public Builder addInlineSuggestionSlice(@NonNull Slice inlineSuggestionSlice) { + throwIfDestroyed(); + throwIfAuthenticationCalled(); + if (mInlineSuggestionSlices == null) { + mInlineSuggestionSlices = new ArrayList<>(); + } + mInlineSuggestionSlices.add(inlineSuggestionSlice); + return this; + } + + /** * Builds a new {@link FillResponse} instance. * * @throws IllegalStateException if any of the following conditions occur: @@ -670,7 +694,9 @@ public final class FillResponse implements Parcelable { if (mCancelIds != null) { builder.append(", mCancelIds=").append(mCancelIds.length); } - + if (mInlineSuggestionSlices != null) { + builder.append(", inlinedSuggestions=").append(mInlineSuggestionSlices.getList()); + } return builder.append("]").toString(); } @@ -699,7 +725,7 @@ public final class FillResponse implements Parcelable { parcel.writeParcelableArray(mFieldClassificationIds, flags); parcel.writeInt(mFlags); parcel.writeIntArray(mCancelIds); - + parcel.writeParcelable(mInlineSuggestionSlices, flags); parcel.writeInt(mRequestId); } @@ -755,6 +781,16 @@ public final class FillResponse implements Parcelable { final int[] cancelIds = parcel.createIntArray(); builder.setCancelTargetIds(cancelIds); + final ParceledListSlice<Slice> parceledInlineSuggestionSlices = + parcel.readParcelable(null); + if (parceledInlineSuggestionSlices != null) { + final List<Slice> inlineSuggestionSlices = parceledInlineSuggestionSlices.getList(); + final int size = inlineSuggestionSlices.size(); + for (int i = 0; i < size; i++) { + builder.addInlineSuggestionSlice(inlineSuggestionSlices.get(i)); + } + } + final FillResponse response = builder.build(); response.setRequestId(parcel.readInt()); diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java index 8ab687f0d001..c84fbc7287bb 100644 --- a/core/java/android/service/notification/Adjustment.java +++ b/core/java/android/service/notification/Adjustment.java @@ -124,6 +124,13 @@ public final class Adjustment implements Parcelable { public static final String KEY_IMPORTANCE = "key_importance"; /** + * Data type: float, a ranking score from 0 (lowest) to 1 (highest). + * Used to rank notifications inside that fall under the same classification (i.e. alerting, + * silenced). + */ + public static final String KEY_RANKING_SCORE = "key_ranking_score"; + + /** * Create a notification adjustment. * * @param pkg The package of the notification. diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index 85f13d552fcf..c04ac5929e1f 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -1895,7 +1895,8 @@ public abstract class NotificationListenerService extends Service { && ((mSmartActions == null ? 0 : mSmartActions.size()) == (other.mSmartActions == null ? 0 : other.mSmartActions.size())) && Objects.equals(mSmartReplies, other.mSmartReplies) - && Objects.equals(mCanBubble, other.mCanBubble); + && Objects.equals(mCanBubble, other.mCanBubble) + && Objects.equals(mVisuallyInterruptive, other.mVisuallyInterruptive); } } diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java index 716a5225ea91..51a9c864ee29 100644 --- a/core/java/android/telephony/PhoneStateListener.java +++ b/core/java/android/telephony/PhoneStateListener.java @@ -35,8 +35,8 @@ import android.telephony.Annotation.SrvccState; import android.telephony.emergency.EmergencyNumber; import android.telephony.ims.ImsReasonInfo; -import com.android.internal.telephony.IPhoneStateListener; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.telephony.IPhoneStateListener; import dalvik.system.VMRuntime; @@ -169,14 +169,6 @@ public class PhoneStateListener { public static final int LISTEN_SIGNAL_STRENGTHS = 0x00000100; /** - * Listen for changes to OTASP mode. - * - * @see #onOtaspChanged - * @hide - */ - public static final int LISTEN_OTASP_CHANGED = 0x00000200; - - /** * Listen for changes to observed cell info. * * @see #onCellInfoChanged @@ -196,12 +188,13 @@ public class PhoneStateListener { /** * Listen for {@link PreciseDataConnectionState} on the data connection (cellular). * - * @see #onPreciseDataConnectionStateChanged + * <p>Requires permission {@link android.Manifest.permission#MODIFY_PHONE_STATE} + * or the calling app has carrier privileges + * (see {@link TelephonyManager#hasCarrierPrivileges}). * - * @hide + * @see #onPreciseDataConnectionStateChanged */ - @RequiresPermission((android.Manifest.permission.READ_PRECISE_PHONE_STATE)) - @SystemApi + @RequiresPermission((android.Manifest.permission.MODIFY_PHONE_STATE)) public static final int LISTEN_PRECISE_DATA_CONNECTION_STATE = 0x00001000; /** @@ -624,29 +617,6 @@ public class PhoneStateListener { // default implementation empty } - - /** - * The Over The Air Service Provisioning (OTASP) has changed on the registered subscription. - * Note, the registration subId comes from {@link TelephonyManager} object which registers - * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. - * If this TelephonyManager object was created with - * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the - * subId. Otherwise, this callback applies to - * {@link SubscriptionManager#getDefaultSubscriptionId()}. - * - * Requires the READ_PHONE_STATE permission. - * @param otaspMode is integer <code>OTASP_UNKNOWN=1<code> - * means the value is currently unknown and the system should wait until - * <code>OTASP_NEEDED=2<code> or <code>OTASP_NOT_NEEDED=3<code> is received before - * making the decision to perform OTASP or not. - * - * @hide - */ - @UnsupportedAppUsage - public void onOtaspChanged(int otaspMode) { - // default implementation empty - } - /** * Callback invoked when a observed cell info has changed or new cells have been added * or removed on the registered subscription. @@ -719,8 +689,9 @@ public class PhoneStateListener { } /** - * Callback invoked when data connection state changes with precise information - * on the registered subscription. + * Callback providing update about the default/internet data connection on the registered + * subscription. + * * Note, the registration subId comes from {@link TelephonyManager} object which registers * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. * If this TelephonyManager object was created with @@ -728,12 +699,13 @@ public class PhoneStateListener { * subId. Otherwise, this callback applies to * {@link SubscriptionManager#getDefaultSubscriptionId()}. * - * @param dataConnectionState {@link PreciseDataConnectionState} + * <p>Requires permission {@link android.Manifest.permission#MODIFY_PHONE_STATE} + * or the calling app has carrier privileges + * (see {@link TelephonyManager#hasCarrierPrivileges}). * - * @hide + * @param dataConnectionState {@link PreciseDataConnectionState} */ - @RequiresPermission((android.Manifest.permission.READ_PRECISE_PHONE_STATE)) - @SystemApi + @RequiresPermission((android.Manifest.permission.MODIFY_PHONE_STATE)) public void onPreciseDataConnectionStateChanged( @NonNull PreciseDataConnectionState dataConnectionState) { // default implementation empty @@ -1042,11 +1014,21 @@ public class PhoneStateListener { PhoneStateListener psl = mPhoneStateListenerWeakRef.get(); if (psl == null) return; - Binder.withCleanCallingIdentity(() -> mExecutor.execute( - () -> { - psl.onDataConnectionStateChanged(state, networkType); - psl.onDataConnectionStateChanged(state); - })); + if (state == TelephonyManager.DATA_DISCONNECTING + && VMRuntime.getRuntime().getTargetSdkVersion() < Build.VERSION_CODES.R) { + Binder.withCleanCallingIdentity(() -> mExecutor.execute( + () -> { + psl.onDataConnectionStateChanged( + TelephonyManager.DATA_CONNECTED, networkType); + psl.onDataConnectionStateChanged(TelephonyManager.DATA_CONNECTED); + })); + } else { + Binder.withCleanCallingIdentity(() -> mExecutor.execute( + () -> { + psl.onDataConnectionStateChanged(state, networkType); + psl.onDataConnectionStateChanged(state); + })); + } } public void onDataActivity(int direction) { @@ -1065,14 +1047,6 @@ public class PhoneStateListener { () -> mExecutor.execute(() -> psl.onSignalStrengthsChanged(signalStrength))); } - public void onOtaspChanged(int otaspMode) { - PhoneStateListener psl = mPhoneStateListenerWeakRef.get(); - if (psl == null) return; - - Binder.withCleanCallingIdentity( - () -> mExecutor.execute(() -> psl.onOtaspChanged(otaspMode))); - } - public void onCellInfoChanged(List<CellInfo> cellInfo) { PhoneStateListener psl = mPhoneStateListenerWeakRef.get(); if (psl == null) return; diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java index 9d7b57ba250d..f574160e5303 100644 --- a/core/java/android/telephony/TelephonyRegistryManager.java +++ b/core/java/android/telephony/TelephonyRegistryManager.java @@ -21,17 +21,13 @@ import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; import android.content.Context; -import android.net.LinkProperties; -import android.net.NetworkCapabilities; import android.os.Binder; import android.os.Bundle; import android.os.RemoteException; import android.os.ServiceManager; -import android.telephony.Annotation.ApnType; import android.telephony.Annotation.CallState; import android.telephony.Annotation.DataActivityType; import android.telephony.Annotation.DataFailureCause; -import android.telephony.Annotation.DataState; import android.telephony.Annotation.NetworkType; import android.telephony.Annotation.PreciseCallStates; import android.telephony.Annotation.RadioPowerState; @@ -357,27 +353,18 @@ public class TelephonyRegistryManager { * @param subId for which data connection state changed. * @param slotIndex for which data connections state changed. Can be derived from subId except * when subId is invalid. - * @param state latest data connection state, e.g, - * @param isDataConnectivityPossible indicates if data is allowed - * @param apn the APN {@link ApnSetting#getApnName()} of this data connection. - * @param apnType the apnType, "ims" for IMS APN, "emergency" for EMERGENCY APN. - * @param linkProperties {@link LinkProperties} associated with this data connection. - * @param networkCapabilities {@link NetworkCapabilities} associated with this data connection. - * @param networkType associated with this data connection. - * @param roaming {@code true} indicates in roaming, {@false} otherwise. - * @see TelephonyManager#DATA_DISCONNECTED - * @see TelephonyManager#isDataConnectivityPossible() + * @param apnType the APN type that triggered this update + * @param preciseState the PreciseDataConnectionState * + * @see android.telephony.PreciseDataConnection + * @see TelephonyManager#DATA_DISCONNECTED * @hide */ - public void notifyDataConnectionForSubscriber(int slotIndex, int subId, @DataState int state, - boolean isDataConnectivityPossible, - @ApnType String apn, String apnType, LinkProperties linkProperties, - NetworkCapabilities networkCapabilities, int networkType, boolean roaming) { + public void notifyDataConnectionForSubscriber(int slotIndex, int subId, + String apnType, PreciseDataConnectionState preciseState) { try { - sRegistry.notifyDataConnectionForSubscriber(slotIndex, subId, state, - isDataConnectivityPossible, - apn, apnType, linkProperties, networkCapabilities, networkType, roaming); + sRegistry.notifyDataConnectionForSubscriber( + slotIndex, subId, apnType, preciseState); } catch (RemoteException ex) { // system process is dead } @@ -600,22 +587,6 @@ public class TelephonyRegistryManager { } /** - * Notify over the air sim provisioning(OTASP) mode changed on certain subscription. - * - * @param subId for which otasp mode changed. - * @param otaspMode latest mode for OTASP e.g, OTASP needed. - * - * @hide - */ - public void notifyOtaspChanged(int subId, int otaspMode) { - try { - sRegistry.notifyOtaspChanged(subId, otaspMode); - } catch (RemoteException ex) { - // system process is dead - } - } - - /** * Notify precise call state changed on certain subscription, including foreground, background * and ringcall states. * @@ -662,25 +633,6 @@ public class TelephonyRegistryManager { } /** - * Notify data connection failed on certain subscription. - * - * @param subId for which data connection failed. - * @param slotIndex for which data conenction faled. Can be derived from subId except when subId - * is invalid. - * @param apnType the apnType, "ims" for IMS APN, "emergency" for EMERGENCY APN. Note each data - * connection can support multiple anyTypes. - * - * @hide - */ - public void notifyDataConnectionFailed(int subId, int slotIndex, String apnType) { - try { - sRegistry.notifyDataConnectionFailedForSubscriber(slotIndex, subId, apnType); - } catch (RemoteException ex) { - // system process is dead - } - } - - /** * TODO change from bundle to CellLocation? * @hide */ diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index 28eb79ae1f2a..71ac578e2c68 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -16,13 +16,13 @@ package android.view; -import static android.view.DisplayEventReceiver.CONFIG_CHANGED_EVENT_SUPPRESS; import static android.view.DisplayEventReceiver.VSYNC_SOURCE_APP; import static android.view.DisplayEventReceiver.VSYNC_SOURCE_SURFACE_FLINGER; import android.annotation.TestApi; import android.annotation.UnsupportedAppUsage; import android.graphics.FrameInfo; +import android.graphics.Insets; import android.hardware.display.DisplayManagerGlobal; import android.os.Build; import android.os.Handler; @@ -219,9 +219,10 @@ public final class Choreographer { /** * Callback type: Animation callback to handle inset updates. This is separate from * {@link #CALLBACK_ANIMATION} as we need to "gather" all inset animation updates via - * {@link WindowInsetsAnimationController#changeInsets} for multiple ongoing animations but then - * update the whole view system with a single callback to {@link View#dispatchWindowInsetsAnimationProgress} - * that contains all the combined updated insets. + * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)} for multiple + * ongoing animations but then update the whole view system with a single callback to + * {@link View#dispatchWindowInsetsAnimationProgress} that contains all the combined updated + * insets. * <p> * Both input and animation may change insets, so we need to run this after these callbacks, but * before traversals. diff --git a/core/java/android/view/DisplayCutout.java b/core/java/android/view/DisplayCutout.java index 411508faa84b..615dab0f04e4 100644 --- a/core/java/android/view/DisplayCutout.java +++ b/core/java/android/view/DisplayCutout.java @@ -66,6 +66,7 @@ public final class DisplayCutout { private static final String BOTTOM_MARKER = "@bottom"; private static final String DP_MARKER = "@dp"; private static final String RIGHT_MARKER = "@right"; + private static final String LEFT_MARKER = "@left"; /** * Category for overlays that allow emulating a display cutout on devices that don't have @@ -647,6 +648,9 @@ public final class DisplayCutout { if (spec.endsWith(RIGHT_MARKER)) { offsetX = displayWidth; spec = spec.substring(0, spec.length() - RIGHT_MARKER.length()).trim(); + } else if (spec.endsWith(LEFT_MARKER)) { + offsetX = 0; + spec = spec.substring(0, spec.length() - LEFT_MARKER.length()).trim(); } else { offsetX = displayWidth / 2f; } diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java index 3d139cd73518..cdfd397a732a 100644 --- a/core/java/android/view/InsetsAnimationControlImpl.java +++ b/core/java/android/view/InsetsAnimationControlImpl.java @@ -21,7 +21,6 @@ import static android.view.InsetsState.ISIDE_FLOATING; import static android.view.InsetsState.ISIDE_LEFT; import static android.view.InsetsState.ISIDE_RIGHT; import static android.view.InsetsState.ISIDE_TOP; -import static android.view.InsetsState.toPublicType; import android.annotation.Nullable; import android.graphics.Insets; @@ -34,7 +33,8 @@ import android.util.SparseSetArray; import android.view.InsetsState.InternalInsetsSide; import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; import android.view.WindowInsets.Type.InsetsType; -import android.view.WindowInsetsAnimationListener.InsetsAnimation; +import android.view.WindowInsetsAnimationCallback.AnimationBounds; +import android.view.WindowInsetsAnimationCallback.InsetsAnimation; import android.view.WindowManager.LayoutParams; import com.android.internal.annotations.VisibleForTesting; @@ -66,23 +66,28 @@ public class InsetsAnimationControlImpl implements WindowInsetsAnimationControll private final @InsetsType int mTypes; private final Supplier<SyncRtSurfaceTransactionApplier> mTransactionApplierSupplier; private final InsetsController mController; - private final WindowInsetsAnimationListener.InsetsAnimation mAnimation; + private final WindowInsetsAnimationCallback.InsetsAnimation mAnimation; private final Rect mFrame; + private final boolean mFade; private Insets mCurrentInsets; private Insets mPendingInsets; + private float mPendingFraction; private boolean mFinished; private boolean mCancelled; - private int mFinishedShownTypes; + private boolean mShownOnFinish; + private float mCurrentAlpha; + private float mPendingAlpha; @VisibleForTesting public InsetsAnimationControlImpl(SparseArray<InsetsSourceConsumer> consumers, Rect frame, InsetsState state, WindowInsetsAnimationControlListener listener, @InsetsType int types, Supplier<SyncRtSurfaceTransactionApplier> transactionApplierSupplier, - InsetsController controller) { + InsetsController controller, long durationMs, boolean fade) { mConsumers = consumers; mListener = listener; mTypes = types; + mFade = fade; mTransactionApplierSupplier = transactionApplierSupplier; mController = controller; mInitialInsetsState = new InsetsState(state, true /* copySources */); @@ -97,9 +102,11 @@ public class InsetsAnimationControlImpl implements WindowInsetsAnimationControll // TODO: Check for controllability first and wait for IME if needed. listener.onReady(this, types); - mAnimation = new WindowInsetsAnimationListener.InsetsAnimation(mTypes, mHiddenInsets, - mShownInsets); - mController.dispatchAnimationStarted(mAnimation); + mAnimation = new WindowInsetsAnimationCallback.InsetsAnimation(mTypes, + InsetsController.INTERPOLATOR, durationMs); + mAnimation.setAlpha(getCurrentAlpha()); + mController.dispatchAnimationStarted(mAnimation, + new AnimationBounds(mHiddenInsets, mShownInsets)); } @Override @@ -118,12 +125,17 @@ public class InsetsAnimationControlImpl implements WindowInsetsAnimationControll } @Override + public float getCurrentAlpha() { + return mCurrentAlpha; + } + + @Override @InsetsType public int getTypes() { return mTypes; } @Override - public void changeInsets(Insets insets) { + public void setInsetsAndAlpha(Insets insets, float alpha, float fraction) { if (mFinished) { throw new IllegalStateException( "Can't change insets on an animation that is finished."); @@ -132,7 +144,9 @@ public class InsetsAnimationControlImpl implements WindowInsetsAnimationControll throw new IllegalStateException( "Can't change insets on an animation that is cancelled."); } + mPendingFraction = sanitize(fraction); mPendingInsets = sanitize(insets); + mPendingAlpha = 1 - sanitize(alpha); mController.scheduleApplyChangeInsets(); } @@ -145,40 +159,52 @@ public class InsetsAnimationControlImpl implements WindowInsetsAnimationControll return false; } final Insets offset = Insets.subtract(mShownInsets, mPendingInsets); + final Float alphaOffset = 1 - mPendingAlpha; ArrayList<SurfaceParams> params = new ArrayList<>(); - updateLeashesForSide(ISIDE_LEFT, offset.left, mPendingInsets.left, params, state); - updateLeashesForSide(ISIDE_TOP, offset.top, mPendingInsets.top, params, state); - updateLeashesForSide(ISIDE_RIGHT, offset.right, mPendingInsets.right, params, state); - updateLeashesForSide(ISIDE_BOTTOM, offset.bottom, mPendingInsets.bottom, params, state); - updateLeashesForSide(ISIDE_FLOATING, 0 /* offset */, 0 /* inset */, params, state); + updateLeashesForSide(ISIDE_LEFT, offset.left, mShownInsets.left, mPendingInsets.left, + params, state, alphaOffset); + updateLeashesForSide(ISIDE_TOP, offset.top, mShownInsets.top, mPendingInsets.top, params, + state, alphaOffset); + updateLeashesForSide(ISIDE_RIGHT, offset.right, mShownInsets.right, mPendingInsets.right, + params, state, alphaOffset); + updateLeashesForSide(ISIDE_BOTTOM, offset.bottom, mShownInsets.bottom, + mPendingInsets.bottom, params, state, alphaOffset); + updateLeashesForSide(ISIDE_FLOATING, 0 /* offset */, 0 /* inset */, 0 /* maxInset */, + params, state, alphaOffset); SyncRtSurfaceTransactionApplier applier = mTransactionApplierSupplier.get(); applier.scheduleApply(params.toArray(new SurfaceParams[params.size()])); mCurrentInsets = mPendingInsets; + mAnimation.setFraction(mPendingFraction); + mCurrentAlpha = 1 - alphaOffset; if (mFinished) { - mController.notifyFinished(this, mFinishedShownTypes); + mController.notifyFinished(this, mShownOnFinish); } return mFinished; } @Override - public void finish(int shownTypes) { + public void finish(boolean shown) { if (mCancelled) { return; } InsetsState state = new InsetsState(mController.getState()); for (int i = mConsumers.size() - 1; i >= 0; i--) { InsetsSourceConsumer consumer = mConsumers.valueAt(i); - boolean visible = (shownTypes & toPublicType(consumer.getType())) != 0; - state.getSource(consumer.getType()).setVisible(visible); + state.getSource(consumer.getType()).setVisible(shown); } Insets insets = getInsetsFromState(state, mFrame, null /* typeSideMap */); - changeInsets(insets); + setInsetsAndAlpha(insets, 1f /* alpha */, shown ? 1f : 0f /* fraction */); mFinished = true; - mFinishedShownTypes = shownTypes; + mShownOnFinish = shown; } + @Override @VisibleForTesting + public float getCurrentFraction() { + return mAnimation.getFraction(); + } + public void onCancelled() { if (mFinished) { return; @@ -191,6 +217,10 @@ public class InsetsAnimationControlImpl implements WindowInsetsAnimationControll return mAnimation; } + WindowInsetsAnimationControlListener getListener() { + return mListener; + } + private Insets calculateInsets(InsetsState state, Rect frame, SparseArray<InsetsSourceConsumer> consumers, boolean shown, @Nullable @InternalInsetsSide SparseIntArray typeSideMap) { @@ -210,11 +240,18 @@ public class InsetsAnimationControlImpl implements WindowInsetsAnimationControll } private Insets sanitize(Insets insets) { + if (insets == null) { + insets = getCurrentInsets(); + } return Insets.max(Insets.min(insets, mShownInsets), mHiddenInsets); } + private static float sanitize(float alpha) { + return alpha >= 1 ? 1 : (alpha <= 0 ? 0 : alpha); + } + private void updateLeashesForSide(@InternalInsetsSide int side, int offset, int inset, - ArrayList<SurfaceParams> surfaceParams, InsetsState state) { + int maxInset, ArrayList<SurfaceParams> surfaceParams, InsetsState state, Float alpha) { ArraySet<InsetsSourceConsumer> items = mSideSourceMap.get(side); if (items == null) { return; @@ -238,7 +275,9 @@ public class InsetsAnimationControlImpl implements WindowInsetsAnimationControll // If the system is controlling the insets source, the leash can be null. if (leash != null) { - surfaceParams.add(new SurfaceParams(leash, 1f /* alpha */, mTmpMatrix, + // TODO: use a better interpolation for fade. + alpha = mFade ? ((float) maxInset / inset * 0.3f + 0.7f) : alpha; + surfaceParams.add(new SurfaceParams(leash, alpha, mTmpMatrix, null /* windowCrop */, 0 /* layer */, 0f /* cornerRadius*/, side == ISIDE_FLOATING ? consumer.isVisible() : inset != 0 /* visible */)); } diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 43fec8227f3f..5563d629f25e 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -39,6 +39,8 @@ import android.view.InsetsState.InternalInsetsType; import android.view.SurfaceControl.Transaction; import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; +import android.view.WindowInsetsAnimationCallback.AnimationBounds; +import android.view.WindowInsetsAnimationCallback.InsetsAnimation; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; @@ -55,11 +57,12 @@ public class InsetsController implements WindowInsetsController { private static final int ANIMATION_DURATION_SHOW_MS = 275; private static final int ANIMATION_DURATION_HIDE_MS = 340; - private static final Interpolator INTERPOLATOR = new PathInterpolator(0.4f, 0f, 0.2f, 1f); private static final int DIRECTION_NONE = 0; private static final int DIRECTION_SHOW = 1; private static final int DIRECTION_HIDE = 2; + static final Interpolator INTERPOLATOR = new PathInterpolator(0.4f, 0f, 0.2f, 1f); + @IntDef ({DIRECTION_NONE, DIRECTION_SHOW, DIRECTION_HIDE}) private @interface AnimationDirection{} @@ -85,8 +88,75 @@ public class InsetsController implements WindowInsetsController { return object.getCurrentInsets(); } @Override - public void set(WindowInsetsAnimationController object, Insets value) { - object.changeInsets(value); + public void set(WindowInsetsAnimationController controller, Insets value) { + controller.setInsetsAndAlpha( + value, 1f /* alpha */, (((DefaultAnimationControlListener) + ((InsetsAnimationControlImpl) controller).getListener()) + .getRawProgress())); + } + } + + private class DefaultAnimationControlListener implements WindowInsetsAnimationControlListener { + + private WindowInsetsAnimationController mController; + private ObjectAnimator mAnimator; + private boolean mShow; + + DefaultAnimationControlListener(boolean show) { + mShow = show; + } + + @Override + public void onReady(WindowInsetsAnimationController controller, int types) { + mController = controller; + if (mShow) { + showDirectly(types); + } else { + hideDirectly(types); + } + mAnimationDirection = mShow ? DIRECTION_SHOW : DIRECTION_HIDE; + mAnimator = ObjectAnimator.ofObject( + controller, + new InsetsProperty(), + sEvaluator, + mShow ? controller.getHiddenStateInsets() : controller.getShownStateInsets(), + mShow ? controller.getShownStateInsets() : controller.getHiddenStateInsets() + ); + mAnimator.setDuration(getDurationMs()); + mAnimator.setInterpolator(INTERPOLATOR); + mAnimator.addListener(new AnimatorListenerAdapter() { + + @Override + public void onAnimationEnd(Animator animation) { + onAnimationFinish(); + } + }); + mAnimator.start(); + } + + @Override + public void onCancelled() { + // Animator can be null when it is cancelled before onReady() completes. + if (mAnimator != null) { + mAnimator.cancel(); + } + } + + private void onAnimationFinish() { + mAnimationDirection = DIRECTION_NONE; + mController.finish(mShow); + } + + private float getRawProgress() { + float fraction = (float) mAnimator.getCurrentPlayTime() / mAnimator.getDuration(); + return mShow ? fraction : 1 - fraction; + } + + private long getDurationMs() { + if (mAnimator != null) { + return mAnimator.getDuration(); + } + return mShow ? ANIMATION_DURATION_SHOW_MS : ANIMATION_DURATION_HIDE_MS; } } @@ -278,24 +348,25 @@ public class InsetsController implements WindowInsetsController { } @Override - public void controlWindowInsetsAnimation(@InsetsType int types, + public void controlWindowInsetsAnimation(@InsetsType int types, long durationMs, WindowInsetsAnimationControlListener listener) { - controlWindowInsetsAnimation(types, listener, false /* fromIme */); + controlWindowInsetsAnimation(types, listener, false /* fromIme */, durationMs); } private void controlWindowInsetsAnimation(@InsetsType int types, - WindowInsetsAnimationControlListener listener, boolean fromIme) { + WindowInsetsAnimationControlListener listener, boolean fromIme, long durationMs) { // If the frame of our window doesn't span the entire display, the control API makes very // little sense, as we don't deal with negative insets. So just cancel immediately. if (!mState.getDisplayFrame().equals(mFrame)) { listener.onCancelled(); return; } - controlAnimationUnchecked(types, listener, mFrame, fromIme); + controlAnimationUnchecked(types, listener, mFrame, fromIme, durationMs, false /* fade */); } private void controlAnimationUnchecked(@InsetsType int types, - WindowInsetsAnimationControlListener listener, Rect frame, boolean fromIme) { + WindowInsetsAnimationControlListener listener, Rect frame, boolean fromIme, + long durationMs, boolean fade) { if (types == 0) { // nothing to animate. return; @@ -326,7 +397,7 @@ public class InsetsController implements WindowInsetsController { final InsetsAnimationControlImpl controller = new InsetsAnimationControlImpl(consumers, frame, mState, listener, typesReady, - () -> new SyncRtSurfaceTransactionApplier(mViewRoot.mView), this); + () -> new SyncRtSurfaceTransactionApplier(mViewRoot.mView), this, durationMs, fade); mAnimationControls.add(controller); } @@ -397,10 +468,13 @@ public class InsetsController implements WindowInsetsController { } @VisibleForTesting - public void notifyFinished(InsetsAnimationControlImpl controller, int shownTypes) { + public void notifyFinished(InsetsAnimationControlImpl controller, boolean shown) { mAnimationControls.remove(controller); - hideDirectly(controller.getTypes() & ~shownTypes); - showDirectly(controller.getTypes() & shownTypes); + if (shown) { + showDirectly(controller.getTypes()); + } else { + hideDirectly(controller.getTypes()); + } } void notifyControlRevoked(InsetsSourceConsumer consumer) { @@ -510,58 +584,12 @@ public class InsetsController implements WindowInsetsController { return; } - WindowInsetsAnimationControlListener listener = new WindowInsetsAnimationControlListener() { - - private WindowInsetsAnimationController mController; - private ObjectAnimator mAnimator; - - @Override - public void onReady(WindowInsetsAnimationController controller, int types) { - mController = controller; - if (show) { - showDirectly(types); - } else { - hideDirectly(types); - } - mAnimationDirection = show ? DIRECTION_SHOW : DIRECTION_HIDE; - mAnimator = ObjectAnimator.ofObject( - controller, - new InsetsProperty(), - sEvaluator, - show ? controller.getHiddenStateInsets() : controller.getShownStateInsets(), - show ? controller.getShownStateInsets() : controller.getHiddenStateInsets() - ); - mAnimator.setDuration(show - ? ANIMATION_DURATION_SHOW_MS - : ANIMATION_DURATION_HIDE_MS); - mAnimator.setInterpolator(INTERPOLATOR); - mAnimator.addListener(new AnimatorListenerAdapter() { - - @Override - public void onAnimationEnd(Animator animation) { - onAnimationFinish(); - } - }); - mAnimator.start(); - } - - @Override - public void onCancelled() { - // Animator can be null when it is cancelled before onReady() completes. - if (mAnimator != null) { - mAnimator.cancel(); - } - } - - private void onAnimationFinish() { - mAnimationDirection = DIRECTION_NONE; - mController.finish(show ? types : 0); - } - }; - + final DefaultAnimationControlListener listener = new DefaultAnimationControlListener(show); // Show/hide animations always need to be relative to the display frame, in order that shown // and hidden state insets are correct. - controlAnimationUnchecked(types, listener, mState.getDisplayFrame(), fromIme); + controlAnimationUnchecked( + types, listener, mState.getDisplayFrame(), fromIme, listener.getDurationMs(), + true /* fade */); } private void hideDirectly(@InsetsType int types) { @@ -592,12 +620,12 @@ public class InsetsController implements WindowInsetsController { } @VisibleForTesting - public void dispatchAnimationStarted(WindowInsetsAnimationListener.InsetsAnimation animation) { - mViewRoot.mView.dispatchWindowInsetsAnimationStarted(animation); + public void dispatchAnimationStarted(InsetsAnimation animation, AnimationBounds bounds) { + mViewRoot.mView.dispatchWindowInsetsAnimationStarted(animation, bounds); } @VisibleForTesting - public void dispatchAnimationFinished(WindowInsetsAnimationListener.InsetsAnimation animation) { + public void dispatchAnimationFinished(InsetsAnimation animation) { mViewRoot.mView.dispatchWindowInsetsAnimationFinished(animation); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 9d4f387895bb..44f211ac8ede 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -109,7 +109,8 @@ import android.view.AccessibilityIterators.ParagraphTextSegmentIterator; import android.view.AccessibilityIterators.TextSegmentIterator; import android.view.AccessibilityIterators.WordTextSegmentIterator; import android.view.ContextMenu.ContextMenuInfo; -import android.view.WindowInsetsAnimationListener.InsetsAnimation; +import android.view.WindowInsetsAnimationCallback.AnimationBounds; +import android.view.WindowInsetsAnimationCallback.InsetsAnimation; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityEventSource; import android.view.accessibility.AccessibilityManager; @@ -4626,7 +4627,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private ArrayList<OnUnhandledKeyEventListener> mUnhandledKeyListeners; - private WindowInsetsAnimationListener mWindowInsetsAnimationListener; + private WindowInsetsAnimationCallback mWindowInsetsAnimationCallback; /** * This lives here since it's only valid for interactive views. @@ -11091,33 +11092,55 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Sets a {@link WindowInsetsAnimationListener} to be notified about animations of windows that + * Sets a {@link WindowInsetsAnimationCallback} to be notified about animations of windows that * cause insets. * * @param listener The listener to set. - * @hide pending unhide */ - public void setWindowInsetsAnimationListener(WindowInsetsAnimationListener listener) { - getListenerInfo().mWindowInsetsAnimationListener = listener; + public void setWindowInsetsAnimationCallback(@Nullable WindowInsetsAnimationCallback listener) { + getListenerInfo().mWindowInsetsAnimationCallback = listener; } - void dispatchWindowInsetsAnimationStarted(InsetsAnimation animation) { - if (mListenerInfo != null && mListenerInfo.mWindowInsetsAnimationListener != null) { - mListenerInfo.mWindowInsetsAnimationListener.onStarted(animation); + /** + * Dispatches {@link WindowInsetsAnimationCallback#onStarted(InsetsAnimation, AnimationBounds)} + * when Window Insets animation is started. + * @param animation current animation + * @param bounds the upper and lower {@link AnimationBounds} that provides range of + * {@link InsetsAnimation}. + * @return the upper and lower {@link AnimationBounds}. + */ + @NonNull + public AnimationBounds dispatchWindowInsetsAnimationStarted( + @NonNull InsetsAnimation animation, @NonNull AnimationBounds bounds) { + if (mListenerInfo != null && mListenerInfo.mWindowInsetsAnimationCallback != null) { + return mListenerInfo.mWindowInsetsAnimationCallback.onStarted(animation, bounds); } + return bounds; } - WindowInsets dispatchWindowInsetsAnimationProgress(WindowInsets insets) { - if (mListenerInfo != null && mListenerInfo.mWindowInsetsAnimationListener != null) { - return mListenerInfo.mWindowInsetsAnimationListener.onProgress(insets); + /** + * Dispatches {@link WindowInsetsAnimationCallback#onProgress(WindowInsets)} + * when Window Insets animation makes progress. + * @param insets The current {@link WindowInsets}. + * @return current {@link WindowInsets}. + */ + @NonNull + public WindowInsets dispatchWindowInsetsAnimationProgress(@NonNull WindowInsets insets) { + if (mListenerInfo != null && mListenerInfo.mWindowInsetsAnimationCallback != null) { + return mListenerInfo.mWindowInsetsAnimationCallback.onProgress(insets); } else { return insets; } } - void dispatchWindowInsetsAnimationFinished(InsetsAnimation animation) { - if (mListenerInfo != null && mListenerInfo.mWindowInsetsAnimationListener != null) { - mListenerInfo.mWindowInsetsAnimationListener.onFinished(animation); + /** + * Dispatches {@link WindowInsetsAnimationCallback#onFinished(InsetsAnimation)} + * when Window Insets animation finishes. + * @param animation The current ongoing {@link InsetsAnimation}. + */ + public void dispatchWindowInsetsAnimationFinished(@NonNull InsetsAnimation animation) { + if (mListenerInfo != null && mListenerInfo.mWindowInsetsAnimationCallback != null) { + mListenerInfo.mWindowInsetsAnimationCallback.onFinished(animation); } } @@ -11253,7 +11276,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return The {@link WindowInsetsController} or {@code null} if the view isn't attached to a * a window. * @see Window#getInsetsController() - * @hide pending unhide */ public @Nullable WindowInsetsController getWindowInsetsController() { if (mAttachInfo != null) { @@ -16163,7 +16185,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * by the most recent call to {@link #measure(int, int)}. This result is a bit mask * as defined by {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}. * This should be used during measurement and layout calculations only. Use - * {@link #getHeight()} to see how wide a view is after layout. + * {@link #getHeight()} to see how high a view is after layout. * * @return The measured height of this view as a bit mask. */ diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 853a30226982..4334bb510176 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -51,7 +51,8 @@ import android.util.Pools; import android.util.Pools.SynchronizedPool; import android.util.SparseArray; import android.util.SparseBooleanArray; -import android.view.WindowInsetsAnimationListener.InsetsAnimation; +import android.view.WindowInsetsAnimationCallback.AnimationBounds; +import android.view.WindowInsetsAnimationCallback.InsetsAnimation; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; @@ -7198,16 +7199,20 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } @Override - void dispatchWindowInsetsAnimationStarted(InsetsAnimation animation) { - super.dispatchWindowInsetsAnimationStarted(animation); + @NonNull + public AnimationBounds dispatchWindowInsetsAnimationStarted( + @NonNull InsetsAnimation animation, @NonNull AnimationBounds bounds) { + super.dispatchWindowInsetsAnimationStarted(animation, bounds); final int count = getChildCount(); for (int i = 0; i < count; i++) { - getChildAt(i).dispatchWindowInsetsAnimationStarted(animation); + getChildAt(i).dispatchWindowInsetsAnimationStarted(animation, bounds); } + return bounds; } @Override - WindowInsets dispatchWindowInsetsAnimationProgress(WindowInsets insets) { + @NonNull + public WindowInsets dispatchWindowInsetsAnimationProgress(@NonNull WindowInsets insets) { insets = super.dispatchWindowInsetsAnimationProgress(insets); final int count = getChildCount(); for (int i = 0; i < count; i++) { @@ -7217,7 +7222,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } @Override - void dispatchWindowInsetsAnimationFinished(InsetsAnimation animation) { + public void dispatchWindowInsetsAnimationFinished(@NonNull InsetsAnimation animation) { super.dispatchWindowInsetsAnimationFinished(animation); final int count = getChildCount(); for (int i = 0; i < count; i++) { diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index af1882b6cb5c..ff31115a37f0 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -2573,7 +2573,8 @@ public abstract class Window { /** * @return The {@link WindowInsetsController} associated with this window * @see View#getWindowInsetsController() - * @hide pending unhide */ - public abstract @NonNull WindowInsetsController getInsetsController(); + public @Nullable WindowInsetsController getInsetsController() { + return null; + } } diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java index b16a4ca6e916..2404c84457bb 100644 --- a/core/java/android/view/WindowInsets.java +++ b/core/java/android/view/WindowInsets.java @@ -177,7 +177,7 @@ public final class WindowInsets { * @return The insets that include system bars indicated by {@code typeMask}, taken from * {@code typeInsetsMap}. */ - private static Insets getInsets(Insets[] typeInsetsMap, @InsetsType int typeMask) { + static Insets getInsets(Insets[] typeInsetsMap, @InsetsType int typeMask) { Insets result = null; for (int i = FIRST; i <= LAST; i = i << 1) { if ((typeMask & i) == 0) { @@ -289,9 +289,8 @@ public final class WindowInsets { * * @param typeMask Bit mask of {@link InsetsType}s to query the insets for. * @return The insets. - * - * @hide pending unhide */ + @NonNull public Insets getInsets(@InsetsType int typeMask) { return getInsets(mTypeInsetsMap, typeMask); } @@ -313,8 +312,8 @@ public final class WindowInsets { * insets are not available for this type as the height of the * IME is dynamic depending on the {@link EditorInfo} of the * currently focused view, as well as the UI state of the IME. - * @hide pending unhide */ + @NonNull public Insets getMaxInsets(@InsetsType int typeMask) throws IllegalArgumentException { if ((typeMask & IME) != 0) { throw new IllegalArgumentException("Unable to query the maximum insets for IME"); @@ -329,7 +328,6 @@ public final class WindowInsets { * @param typeMask Bit mask of {@link Type.InsetsType}s to query visibility status. * @return {@code true} if and only if all windows included in {@code typeMask} are currently * visible on screen. - * @hide pending unhide */ public boolean isVisible(@InsetsType int typeMask) { for (int i = FIRST; i <= LAST; i = i << 1) { @@ -874,7 +872,7 @@ public final class WindowInsets { return typeInsetsMap; } - private static Insets insetInsets(Insets insets, int left, int top, int right, int bottom) { + static Insets insetInsets(Insets insets, int left, int top, int right, int bottom) { int newLeft = Math.max(0, insets.left - left); int newTop = Math.max(0, insets.top - top); int newRight = Math.max(0, insets.right - right); @@ -1015,7 +1013,6 @@ public final class WindowInsets { * @param insets The insets to set. * * @return itself - * @hide pending unhide */ @NonNull public Builder setInsets(@InsetsType int typeMask, @NonNull Insets insets) { @@ -1046,7 +1043,6 @@ public final class WindowInsets { * the IME is dynamic depending on the {@link EditorInfo} * of the currently focused view, as well as the UI * state of the IME. - * @hide pending unhide */ @NonNull public Builder setMaxInsets(@InsetsType int typeMask, @NonNull Insets insets) @@ -1070,7 +1066,6 @@ public final class WindowInsets { * @param visible Whether to mark the windows as visible or not. * * @return itself - * @hide pending unhide */ @NonNull public Builder setVisible(@InsetsType int typeMask, boolean visible) { @@ -1145,7 +1140,6 @@ public final class WindowInsets { /** * Class that defines different types of sources causing window insets. - * @hide pending unhide */ public static final class Type { diff --git a/core/java/android/view/WindowInsetsAnimationCallback.java b/core/java/android/view/WindowInsetsAnimationCallback.java new file mode 100644 index 000000000000..5e71f271f1d4 --- /dev/null +++ b/core/java/android/view/WindowInsetsAnimationCallback.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2019 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; + +import android.annotation.FloatRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Insets; +import android.view.WindowInsets.Type.InsetsType; +import android.view.animation.Interpolator; + +/** + * Interface that allows the application to listen to animation events for windows that cause + * insets. + */ +public interface WindowInsetsAnimationCallback { + + /** + * Called when an inset animation gets started. + * <p> + * Note that, like {@link #onProgress}, dispatch of the animation start event is hierarchical: + * It will starts at the root of the view hierarchy and then traverse it and invoke the callback + * of the specific {@link View} that is being traversed. The method my return a modified + * instance of the bounds by calling {@link AnimationBounds#inset} to indicate that a part of + * the insets have been used to offset or clip its children, and the children shouldn't worry + * about that part anymore. + * + * @param animation The animation that is about to start. + * @param bounds The bounds in which animation happens. + * @return The animation representing the part of the insets that should be dispatched to the + * subtree of the hierarchy. + */ + @NonNull + default AnimationBounds onStarted( + @NonNull InsetsAnimation animation, @NonNull AnimationBounds bounds) { + return bounds; + } + + /** + * Called when the insets change as part of running an animation. Note that even if multiple + * animations for different types are running, there will only be one progress callback per + * frame. The {@code insets} passed as an argument represents the overall state and will include + * all types, regardless of whether they are animating or not. + * <p> + * Note that insets dispatch is hierarchical: It will start at the root of the view hierarchy, + * and then traverse it and invoke the callback of the specific {@link View} being traversed. + * The method may return a modified instance by calling + * {@link WindowInsets#inset(int, int, int, int)} to indicate that a part of the insets have + * been used to offset or clip its children, and the children shouldn't worry about that part + * anymore. + * TODO: Introduce a way to map (type -> InsetAnimation) so app developer can query animation + * for a given type e.g. callback.getAnimation(type) OR controller.getAnimation(type). + * Or on the controller directly? + * @param insets The current insets. + * @return The insets to dispatch to the subtree of the hierarchy. + */ + @NonNull + WindowInsets onProgress(@NonNull WindowInsets insets); + + /** + * Called when an inset animation has finished. + * + * @param animation The animation that has finished running. This will be the same instance as + * passed into {@link #onStarted} + */ + default void onFinished(@NonNull InsetsAnimation animation) { + } + + /** + * Class representing an animation of a set of windows that cause insets. + */ + final class InsetsAnimation { + + private final @InsetsType int mTypeMask; + private float mFraction; + @Nullable private final Interpolator mInterpolator; + private long mDurationMs; + private float mAlpha; + + public InsetsAnimation( + @InsetsType int typeMask, @Nullable Interpolator interpolator, long durationMs) { + mTypeMask = typeMask; + mInterpolator = interpolator; + mDurationMs = durationMs; + } + + /** + * @return The bitmask of {@link WindowInsets.Type.InsetsType}s that are animating. + */ + public @InsetsType int getTypeMask() { + return mTypeMask; + } + + /** + * Returns the raw fractional progress of this animation between + * {@link AnimationBounds#getLowerBound()} and {@link AnimationBounds#getUpperBound()}. Note + * that this progress is the global progress of the animation, whereas + * {@link WindowInsetsAnimationCallback#onProgress} will only dispatch the insets that may + * be inset with {@link WindowInsets#inset} by parents of views in the hierarchy. + * Progress per insets animation is global for the entire animation. One animation animates + * all things together (in, out, ...). If they don't animate together, we'd have + * multiple animations. + * + * @return The current progress of this animation. + */ + @FloatRange(from = 0f, to = 1f) + public float getFraction() { + return mFraction; + } + + /** + * Returns the interpolated fractional progress of this animation between + * {@link AnimationBounds#getLowerBound()} and {@link AnimationBounds#getUpperBound()}. Note + * that this progress is the global progress of the animation, whereas + * {@link WindowInsetsAnimationCallback#onProgress} will only dispatch the insets that may + * be inset with {@link WindowInsets#inset} by parents of views in the hierarchy. + * Progress per insets animation is global for the entire animation. One animation animates + * all things together (in, out, ...). If they don't animate together, we'd have + * multiple animations. + * @see #getFraction() for raw fraction. + * @return The current interpolated progress of this animation. -1 if interpolator isn't + * specified. + */ + public float getInterpolatedFraction() { + if (mInterpolator != null) { + return mInterpolator.getInterpolation(mFraction); + } + return -1; + } + + @Nullable + public Interpolator getInterpolator() { + return mInterpolator; + } + + /** + * @return duration of animation in {@link java.util.concurrent.TimeUnit#MILLISECONDS}. + */ + public long getDurationMillis() { + return mDurationMs; + } + + /** + * Set fraction of the progress if {@link WindowInsets.Type.InsetsType} animation is + * controlled by the app {@see #getCurrentFraction}. + * <p>Note: If app didn't create {@link InsetsAnimation}, it shouldn't set progress either. + * Progress would be set by system with the system-default animation. + * </p> + * @param fraction fractional progress between 0 and 1 where 0 represents hidden and + * zero progress and 1 represent fully shown final state. + */ + public void setFraction(@FloatRange(from = 0f, to = 1f) float fraction) { + mFraction = fraction; + } + + /** + * Set duration of the animation if {@link WindowInsets.Type.InsetsType} animation is + * controlled by the app. + * <p>Note: If app didn't create {@link InsetsAnimation}, it shouldn't set duration either. + * Duration would be set by system with the system-default animation. + * </p> + * @param durationMs in {@link java.util.concurrent.TimeUnit#MILLISECONDS} + */ + public void setDuration(long durationMs) { + mDurationMs = durationMs; + } + + /** + * @return alpha of {@link WindowInsets.Type.InsetsType}. + */ + @FloatRange(from = 0f, to = 1f) + public float getAlpha() { + return mAlpha; + } + + void setAlpha(@FloatRange(from = 0f, to = 1f) float alpha) { + mAlpha = alpha; + } + } + + /** + * Class representing the range of an {@link InsetsAnimation} + */ + final class AnimationBounds { + private final Insets mLowerBound; + private final Insets mUpperBound; + + public AnimationBounds(@NonNull Insets lowerBound, @NonNull Insets upperBound) { + mLowerBound = lowerBound; + mUpperBound = upperBound; + } + + /** + * Queries the lower inset bound of the animation. If the animation is about showing or + * hiding a window that cause insets, the lower bound is {@link Insets#NONE} and the upper + * bound is the same as {@link WindowInsets#getInsets(int)} for the fully shown state. This + * is the same as {@link WindowInsetsAnimationController#getHiddenStateInsets} and + * {@link WindowInsetsAnimationController#getShownStateInsets} in case the listener gets + * invoked because of an animation that originates from + * {@link WindowInsetsAnimationController}. + * <p> + * However, if the size of a window that causes insets is changing, these are the + * lower/upper bounds of that size animation. + * </p> + * There are no overlapping animations for a specific type, but there may be multiple + * animations running at the same time for different inset types. + * + * @see #getUpperBound() + * @see WindowInsetsAnimationController#getHiddenStateInsets + */ + @NonNull + public Insets getLowerBound() { + return mLowerBound; + } + + /** + * Queries the upper inset bound of the animation. If the animation is about showing or + * hiding a window that cause insets, the lower bound is {@link Insets#NONE} + * nd the upper bound is the same as {@link WindowInsets#getInsets(int)} for the fully + * shown state. This is the same as + * {@link WindowInsetsAnimationController#getHiddenStateInsets} and + * {@link WindowInsetsAnimationController#getShownStateInsets} in case the listener gets + * invoked because of an animation that originates from + * {@link WindowInsetsAnimationController}. + * <p> + * However, if the size of a window that causes insets is changing, these are the + * lower/upper bounds of that size animation. + * <p> + * There are no overlapping animations for a specific type, but there may be multiple + * animations running at the same time for different inset types. + * + * @see #getLowerBound() + * @see WindowInsetsAnimationController#getShownStateInsets + */ + @NonNull + public Insets getUpperBound() { + return mUpperBound; + } + + /** + * Insets both the lower and upper bound by the specified insets. This is to be used in + * {@link WindowInsetsAnimationCallback#onStarted} to indicate that a part of the insets has + * been used to offset or clip its children, and the children shouldn't worry about that + * part anymore. + * + * @param insets The amount to inset. + * @return A copy of this instance inset in the given directions. + * @see WindowInsets#inset + * @see WindowInsetsAnimationCallback#onStarted + */ + @NonNull + public AnimationBounds inset(@NonNull Insets insets) { + return new AnimationBounds( + // TODO: refactor so that WindowInsets.insetInsets() is in a more appropriate + // place eventually. + WindowInsets.insetInsets( + mLowerBound, insets.left, insets.top, insets.right, insets.bottom), + WindowInsets.insetInsets( + mUpperBound, insets.left, insets.top, insets.right, insets.bottom)); + } + } +} diff --git a/core/java/android/view/WindowInsetsAnimationControlListener.java b/core/java/android/view/WindowInsetsAnimationControlListener.java index 33fb327121c0..8a226c1bbe23 100644 --- a/core/java/android/view/WindowInsetsAnimationControlListener.java +++ b/core/java/android/view/WindowInsetsAnimationControlListener.java @@ -22,19 +22,17 @@ import android.view.inputmethod.EditorInfo; /** * Interface that informs the client about {@link WindowInsetsAnimationController} state changes. - * @hide pending unhide */ public interface WindowInsetsAnimationControlListener { /** - * Gets called as soon as the animation is ready to be controlled. This may be - * delayed when the IME needs to redraw because of an {@link EditorInfo} change, or when the - * window is starting up. + * Called when the animation is ready to be controlled. This may be delayed when the IME needs + * to redraw because of an {@link EditorInfo} change, or when the window is starting up. * * @param controller The controller to control the inset animation. * @param types The {@link InsetsType}s it was able to gain control over. Note that this may be * different than the types passed into - * {@link WindowInsetsController#controlWindowInsetsAnimation} in case the window + * {@link WindowInsetsController#controlInputMethodAnimation} in case the window * wasn't able to gain the controls because it wasn't the IME target or not * currently the window that's controlling the system bars. */ diff --git a/core/java/android/view/WindowInsetsAnimationController.java b/core/java/android/view/WindowInsetsAnimationController.java index 5cbf3b852018..2bf0d277268d 100644 --- a/core/java/android/view/WindowInsetsAnimationController.java +++ b/core/java/android/view/WindowInsetsAnimationController.java @@ -16,83 +16,133 @@ package android.view; +import android.annotation.FloatRange; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.graphics.Insets; import android.view.WindowInsets.Type.InsetsType; -import android.view.WindowInsetsAnimationListener.InsetsAnimation; +import android.view.WindowInsetsAnimationCallback.AnimationBounds; /** - * Interface to control a window inset animation frame-by-frame. - * @hide pending unhide + * Controller for app-driven animation of system windows. + * <p> + * {@code WindowInsetsAnimationController} lets apps animate system windows such as + * the {@link android.inputmethodservice.InputMethodService IME}. The animation is + * synchronized, such that changes the system windows and the app's current frame + * are rendered at the same time. + * <p> + * Control is obtained through {@link WindowInsetsController#controlInputMethodAnimation}. */ +@SuppressLint("NotClosable") public interface WindowInsetsAnimationController { /** * Retrieves the {@link Insets} when the windows this animation is controlling are fully hidden. * <p> + * Note that these insets are always relative to the window, which is the same as being relative + * to {@link View#getRootView} + * <p> * If there are any animation listeners registered, this value is the same as - * {@link InsetsAnimation#getLowerBound()} that will be passed into the callbacks. + * {@link AnimationBounds#getLowerBound()} that is being be passed into the root view of the + * hierarchy. * * @return Insets when the windows this animation is controlling are fully hidden. * - * @see InsetsAnimation#getLowerBound() + * @see AnimationBounds#getLowerBound() */ @NonNull Insets getHiddenStateInsets(); /** * Retrieves the {@link Insets} when the windows this animation is controlling are fully shown. * <p> - * In case the size of a window causing insets is changing in the middle of the animation, we - * execute that height change after this animation has finished. + * Note that these insets are always relative to the window, which is the same as being relative + * to {@link View#getRootView} * <p> * If there are any animation listeners registered, this value is the same as - * {@link InsetsAnimation#getUpperBound()} that will be passed into the callbacks. + * {@link AnimationBounds#getUpperBound()} that is being passed into the root view of hierarchy. * * @return Insets when the windows this animation is controlling are fully shown. * - * @see InsetsAnimation#getUpperBound() + * @see AnimationBounds#getUpperBound() */ @NonNull Insets getShownStateInsets(); /** - * @return The current insets on the window. These will follow any animation changes. + * Retrieves the current insets. + * <p> + * Note that these insets are always relative to the window, which is the same as + * being relative + * to {@link View#getRootView} + * @return The current insets on the currently showing frame. These insets will change as the + * animation progresses to reflect the current insets provided by the controlled window. */ @NonNull Insets getCurrentInsets(); /** + * Returns the progress as previously set by {@code fraction} in {@link #setInsetsAndAlpha} + * + * @return the progress of the animation, where {@code 0} is fully hidden and {@code 1} is + * fully shown. + * <p> + * Note: this value represents raw overall progress of the animation + * i.e. the combined progress of insets and alpha. + * <p> + */ + @FloatRange(from = 0f, to = 1f) + float getCurrentFraction(); + + /** + * Current alpha value of the window. + * @return float value between 0 and 1. + */ + float getCurrentAlpha(); + + /** * @return The {@link InsetsType}s this object is currently controlling. */ @InsetsType int getTypes(); /** - * Modifies the insets by indirectly moving the windows around in the system that are causing - * window insets. + * Modifies the insets for the frame being drawn by indirectly moving the windows around in the + * system that are causing window insets. + * <p> + * Note that these insets are always relative to the window, which is the same as being relative + * to {@link View#getRootView} * <p> - * Note that this will <b>not</b> inform the view system of a full inset change via + * Also note that this will <b>not</b> inform the view system of a full inset change via * {@link View#dispatchApplyWindowInsets} in order to avoid a full layout pass during the * animation. If you'd like to animate views during a window inset animation, register a - * {@link WindowInsetsAnimationListener} by calling - * {@link View#setWindowInsetsAnimationListener(WindowInsetsAnimationListener)} that will be - * notified about any insets change via {@link WindowInsetsAnimationListener#onProgress} during + * {@link WindowInsetsAnimationCallback} by calling + * {@link View#setWindowInsetsAnimationCallback(WindowInsetsAnimationCallback)} that will be + * notified about any insets change via {@link WindowInsetsAnimationCallback#onProgress} during * the animation. * <p> * {@link View#dispatchApplyWindowInsets} will instead be called once the animation has * finished, i.e. once {@link #finish} has been called. + * Note: If there are no insets, alpha animation is still applied. * * @param insets The new insets to apply. Based on the requested insets, the system will * calculate the positions of the windows in the system causing insets such that * the resulting insets of that configuration will match the passed in parameter. * Note that these insets are being clamped to the range from - * {@link #getHiddenStateInsets} to {@link #getShownStateInsets} + * {@link #getHiddenStateInsets} to {@link #getShownStateInsets}. + * If you intend on changing alpha only, pass null or {@link #getCurrentInsets()}. + * @param alpha The new alpha to apply to the inset side. + * @param fraction instantaneous animation progress. This value is dispatched to + * {@link WindowInsetsAnimationCallback}. * - * @see WindowInsetsAnimationListener - * @see View#setWindowInsetsAnimationListener(WindowInsetsAnimationListener) + * @see WindowInsetsAnimationCallback + * @see View#setWindowInsetsAnimationCallback(WindowInsetsAnimationCallback) */ - void changeInsets(@NonNull Insets insets); + void setInsetsAndAlpha(@Nullable Insets insets, @FloatRange(from = 0f, to = 1f) float alpha, + @FloatRange(from = 0f, to = 1f) float fraction); /** - * @param shownTypes The list of windows causing insets that should remain shown after finishing - * the animation. + * Finishes the animation, and leaves the windows shown or hidden. After invoking + * {@link #finish(boolean)}, this instance is no longer valid. + * @param shown if {@code true}, the windows will be shown after finishing the + * animation. Otherwise they will be hidden. */ - void finish(@InsetsType int shownTypes); + void finish(boolean shown); } diff --git a/core/java/android/view/WindowInsetsAnimationListener.java b/core/java/android/view/WindowInsetsAnimationListener.java deleted file mode 100644 index f734b4b7cb0f..000000000000 --- a/core/java/android/view/WindowInsetsAnimationListener.java +++ /dev/null @@ -1,122 +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.view; - -import android.graphics.Insets; - -/** - * Interface that allows the application to listen to animation events for windows that cause - * insets. - * @hide pending unhide - */ -public interface WindowInsetsAnimationListener { - - /** - * Called when an inset animation gets started. - * - * @param animation The animation that is about to start. - */ - void onStarted(InsetsAnimation animation); - - /** - * Called when the insets change as part of running an animation. Note that even if multiple - * animations for different types are running, there will only be one progress callback per - * frame. The {@code insets} passed as an argument represents the overall state and will include - * all types, regardless of whether they are animating or not. - * <p> - * Note that insets dispatch is hierarchical: It will start at the root of the view hierarchy, - * and then traverse it and invoke the callback of the specific {@link View} being traversed. - * The callback may return a modified instance by calling {@link WindowInsets#inset(int, int, int, int)} - * to indicate that a part of the insets have been used to offset or clip its children, and the - * children shouldn't worry about that part anymore. - * - * @param insets The current insets. - * @return The insets to dispatch to the subtree of the hierarchy. - */ - WindowInsets onProgress(WindowInsets insets); - - /** - * Called when an inset animation has finished. - * - * @param animation The animation that has finished running. - */ - void onFinished(InsetsAnimation animation); - - /** - * Class representing an animation of a set of windows that cause insets. - */ - class InsetsAnimation { - - private final @WindowInsets.Type.InsetsType int mTypeMask; - private final Insets mLowerBound; - private final Insets mUpperBound; - - /** - * @hide - */ - InsetsAnimation(int typeMask, Insets lowerBound, Insets upperBound) { - mTypeMask = typeMask; - mLowerBound = lowerBound; - mUpperBound = upperBound; - } - - /** - * @return The bitmask of {@link WindowInsets.Type.InsetsType}s that are animating. - */ - public @WindowInsets.Type.InsetsType int getTypeMask() { - return mTypeMask; - } - - /** - * Queries the lower inset bound of the animation. If the animation is about showing or - * hiding a window that cause insets, the lower bound is {@link Insets#NONE} and the upper - * bound is the same as {@link WindowInsets#getInsets(int)} for the fully shown state. This - * is the same as {@link WindowInsetsAnimationController#getHiddenStateInsets} and - * {@link WindowInsetsAnimationController#getShownStateInsets} in case the listener gets - * invoked because of an animation that originates from - * {@link WindowInsetsAnimationController}. - * <p> - * However, if the size of a window that causes insets is changing, these are the - * lower/upper bounds of that size animation. - * <p> - * There are no overlapping animations for a specific type, but there may be two animations - * running at the same time for different inset types. - * - * @see #getUpperBound() - * @see WindowInsetsAnimationController#getHiddenStateInsets - * TODO: It's a bit weird that these are global per window but onProgress is hierarchical. - * TODO: If multiple types are animating, querying the bound per type isn't possible. Should - * we: - * 1. Offer bounds by type here? - * 2. Restrict one animation to one single type only? - * Returning WindowInsets here isn't feasible in case of overlapping animations: We can't - * fill in the insets for the types from the other animation into the WindowInsets object - * as it's changing as well. - */ - public Insets getLowerBound() { - return mLowerBound; - } - - /** - * @see #getLowerBound() - * @see WindowInsetsAnimationController#getShownStateInsets - */ - public Insets getUpperBound() { - return mUpperBound; - } - } -} diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java index a045a6a9d511..6de56be2f3c5 100644 --- a/core/java/android/view/WindowInsetsController.java +++ b/core/java/android/view/WindowInsetsController.java @@ -30,7 +30,6 @@ import java.lang.annotation.RetentionPolicy; * Interface to control windows that generate insets. * * TODO Needs more information and examples once the API is more baked. - * @hide pending unhide */ public interface WindowInsetsController { @@ -64,7 +63,10 @@ public interface WindowInsetsController { */ int APPEARANCE_LIGHT_NAVIGATION_BARS = 1 << 4; - /** Determines the appearance of system bars. */ + /** + * Determines the appearance of system bars. + * @hide + */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {APPEARANCE_OPAQUE_STATUS_BARS, APPEARANCE_OPAQUE_NAVIGATION_BARS, APPEARANCE_LOW_PROFILE_BARS, APPEARANCE_LIGHT_STATUS_BARS, @@ -75,33 +77,40 @@ public interface WindowInsetsController { /** * The default option for {@link #setSystemBarsBehavior(int)}. System bars will be forcibly * shown on any user interaction on the corresponding display if navigation bars are hidden by - * {@link #hide(int)} or {@link WindowInsetsAnimationController#changeInsets(Insets)}. + * {@link #hide(int)} or + * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)}. + * @hide */ int BEHAVIOR_SHOW_BARS_BY_TOUCH = 0; /** * Option for {@link #setSystemBarsBehavior(int)}: Window would like to remain interactive when * hiding navigation bars by calling {@link #hide(int)} or - * {@link WindowInsetsAnimationController#changeInsets(Insets)}. + * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)}. * * <p>When system bars are hidden in this mode, they can be revealed with system gestures, such * as swiping from the edge of the screen where the bar is hidden from.</p> + * @hide */ int BEHAVIOR_SHOW_BARS_BY_SWIPE = 1; /** * Option for {@link #setSystemBarsBehavior(int)}: Window would like to remain interactive when * hiding navigation bars by calling {@link #hide(int)} or - * {@link WindowInsetsAnimationController#changeInsets(Insets)}. + * {@link WindowInsetsAnimationController#setInsetsAndAlpha(Insets, float, float)}. * * <p>When system bars are hidden in this mode, they can be revealed temporarily with system * gestures, such as swiping from the edge of the screen where the bar is hidden from. These * transient system bars will overlay app’s content, may have some degree of transparency, and * will automatically hide after a short timeout.</p> + * @hide */ int BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2; - /** Determines the behavior of system bars when hiding them by calling {@link #hide}. */ + /** + * Determines the behavior of system bars when hiding them by calling {@link #hide}. + * @hide + */ @Retention(RetentionPolicy.SOURCE) @IntDef(value = {BEHAVIOR_SHOW_BARS_BY_TOUCH, BEHAVIOR_SHOW_BARS_BY_SWIPE, BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE}) @@ -139,23 +148,27 @@ public interface WindowInsetsController { * the position of the windows in the system causing insets directly. * * @param types The {@link InsetsType}s the application has requested to control. + * @param durationMillis duration of animation in + * {@link java.util.concurrent.TimeUnit#MILLISECONDS} * @param listener The {@link WindowInsetsAnimationControlListener} that gets called when the * windows are ready to be controlled, among other callbacks. * @hide */ - void controlWindowInsetsAnimation(@InsetsType int types, + void controlWindowInsetsAnimation(@InsetsType int types, long durationMillis, @NonNull WindowInsetsAnimationControlListener listener); /** * Lets the application control the animation for showing the IME in a frame-by-frame manner by * modifying the position of the IME when it's causing insets. * + * @param durationMillis duration of the animation in + * {@link java.util.concurrent.TimeUnit#MILLISECONDS} * @param listener The {@link WindowInsetsAnimationControlListener} that gets called when the * IME are ready to be controlled, among other callbacks. */ - default void controlInputMethodAnimation( + default void controlInputMethodAnimation(long durationMillis, @NonNull WindowInsetsAnimationControlListener listener) { - controlWindowInsetsAnimation(ime(), listener); + controlWindowInsetsAnimation(ime(), durationMillis, listener); } /** @@ -166,7 +179,7 @@ public interface WindowInsetsController { * the event by observing {@link View#onApplyWindowInsets} and checking visibility with * {@link WindowInsets#isVisible}. * - * @see #controlInputMethodAnimation(WindowInsetsAnimationControlListener) + * @see #controlInputMethodAnimation(long, WindowInsetsAnimationControlListener) * @see #hideInputMethod() */ default void showInputMethod() { @@ -181,7 +194,7 @@ public interface WindowInsetsController { * the event by observing {@link View#onApplyWindowInsets} and checking visibility with * {@link WindowInsets#isVisible}. * - * @see #controlInputMethodAnimation(WindowInsetsAnimationControlListener) + * @see #controlInputMethodAnimation(long, WindowInsetsAnimationControlListener) * @see #showInputMethod() */ default void hideInputMethod() { diff --git a/core/java/android/view/inline/InlineContentView.java b/core/java/android/view/inline/InlineContentView.java index 4bc2176e8279..b143fad778ec 100644 --- a/core/java/android/view/inline/InlineContentView.java +++ b/core/java/android/view/inline/InlineContentView.java @@ -50,7 +50,10 @@ public class InlineContentView extends SurfaceView { @Override public void surfaceDestroyed(SurfaceHolder holder) { - // TODO(b/137800469): implement this. + new SurfaceControl.Transaction() + .setVisibility(surfaceControl, false) + .reparent(surfaceControl, null) + .apply(); } }); } diff --git a/core/java/android/view/inputmethod/InlineSuggestion.java b/core/java/android/view/inputmethod/InlineSuggestion.java index 25e34d36aa9a..c10144e6ee25 100644 --- a/core/java/android/view/inputmethod/InlineSuggestion.java +++ b/core/java/android/view/inputmethod/InlineSuggestion.java @@ -271,7 +271,7 @@ public final class InlineSuggestion implements Parcelable { }; @DataClass.Generated( - time = 1574446220398L, + time = 1575933636929L, codegenVersion = "1.0.14", sourceFile = "frameworks/base/core/java/android/view/inputmethod/InlineSuggestion.java", inputSignatures = "private static final java.lang.String TAG\nprivate final @android.annotation.NonNull android.view.inputmethod.InlineSuggestionInfo mInfo\nprivate final @android.annotation.Nullable com.android.internal.view.inline.IInlineContentProvider mContentProvider\npublic void inflate(android.content.Context,android.util.Size,java.util.concurrent.Executor,java.util.function.Consumer<android.view.View>)\nclass InlineSuggestion extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genHiddenConstDefs=true, genHiddenConstructor=true)") diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java index 112653aa34e3..1e5a3b027609 100644 --- a/core/java/android/view/inputmethod/InputMethod.java +++ b/core/java/android/view/inputmethod/InputMethod.java @@ -21,11 +21,16 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.content.ComponentName; import android.inputmethodservice.InputMethodService; import android.os.IBinder; +import android.os.RemoteException; import android.os.ResultReceiver; +import android.util.Log; +import android.view.autofill.AutofillId; import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; +import com.android.internal.view.IInlineSuggestionsRequestCallback; /** * The InputMethod interface represents an input method which can generate key @@ -104,6 +109,20 @@ public interface InputMethod { } /** + * Called to notify the IME that Autofill Frameworks requested an inline suggestions request. + * + * @hide + */ + default void onCreateInlineSuggestionsRequest(ComponentName componentName, + AutofillId autofillId, IInlineSuggestionsRequestCallback cb) { + try { + cb.onInlineSuggestionsUnsupported(); + } catch (RemoteException e) { + Log.w("InputMethod", "RemoteException calling onInlineSuggestionsUnsupported: " + e); + } + } + + /** * Called first thing after an input method is created, this supplies a * unique token for the session it has with the system service. It is * needed to identify itself with the service to validate its operations. diff --git a/core/java/android/widget/RadioButton.java b/core/java/android/widget/RadioButton.java index d44fbd7c6f8e..3e26f6369747 100644 --- a/core/java/android/widget/RadioButton.java +++ b/core/java/android/widget/RadioButton.java @@ -18,6 +18,7 @@ package android.widget; import android.content.Context; import android.util.AttributeSet; +import android.view.accessibility.AccessibilityNodeInfo; /** @@ -38,19 +39,19 @@ import android.util.AttributeSet; * guide.</p> * * <p><strong>XML attributes</strong></p> - * <p> - * See {@link android.R.styleable#CompoundButton CompoundButton Attributes}, - * {@link android.R.styleable#Button Button Attributes}, - * {@link android.R.styleable#TextView TextView Attributes}, + * <p> + * See {@link android.R.styleable#CompoundButton CompoundButton Attributes}, + * {@link android.R.styleable#Button Button Attributes}, + * {@link android.R.styleable#TextView TextView Attributes}, * {@link android.R.styleable#View View Attributes} * </p> */ public class RadioButton extends CompoundButton { - + public RadioButton(Context context) { this(context, null); } - + public RadioButton(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.radioButtonStyle); } @@ -81,4 +82,20 @@ public class RadioButton extends CompoundButton { public CharSequence getAccessibilityClassName() { return RadioButton.class.getName(); } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + if (getParent() instanceof RadioGroup) { + RadioGroup radioGroup = (RadioGroup) getParent(); + if (radioGroup.getOrientation() == LinearLayout.HORIZONTAL) { + info.setCollectionItemInfo(AccessibilityNodeInfo.CollectionItemInfo.obtain(0, 1, + radioGroup.getIndexWithinVisibleButtons(this), 1, false, isChecked())); + } else { + info.setCollectionItemInfo(AccessibilityNodeInfo.CollectionItemInfo.obtain( + radioGroup.getIndexWithinVisibleButtons(this), 1, 0, 1, + false, isChecked())); + } + } + } } diff --git a/core/java/android/widget/RadioGroup.java b/core/java/android/widget/RadioGroup.java index c62c16c7e719..90bc0a3ee7b2 100644 --- a/core/java/android/widget/RadioGroup.java +++ b/core/java/android/widget/RadioGroup.java @@ -18,6 +18,7 @@ package android.widget; import android.annotation.IdRes; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.res.TypedArray; @@ -26,6 +27,7 @@ import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.ViewStructure; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.autofill.AutofillManager; import android.view.autofill.AutofillValue; @@ -93,6 +95,7 @@ public class RadioGroup extends LinearLayout { if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) { setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES); } + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); // retrieve selected radio button as requested by the user in the // XML layout file @@ -475,4 +478,50 @@ public class RadioGroup extends LinearLayout { } return null; } -} + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + if (this.getOrientation() == HORIZONTAL) { + info.setCollectionInfo(AccessibilityNodeInfo.CollectionInfo.obtain(1, + getVisibleChildCount(), false, + AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_SINGLE)); + } else { + info.setCollectionInfo( + AccessibilityNodeInfo.CollectionInfo.obtain(getVisibleChildCount(), + 1, false, + AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_SINGLE)); + } + } + + private int getVisibleChildCount() { + int count = 0; + for (int i = 0; i < getChildCount(); i++) { + if (this.getChildAt(i) instanceof RadioButton) { + if (((RadioButton) this.getChildAt(i)).getVisibility() == VISIBLE) { + count++; + } + } + } + return count; + } + + int getIndexWithinVisibleButtons(@Nullable View child) { + if (!(child instanceof RadioButton)) { + return -1; + } + int index = 0; + for (int i = 0; i < getChildCount(); i++) { + if (this.getChildAt(i) instanceof RadioButton) { + RadioButton radioButton = (RadioButton) this.getChildAt(i); + if (radioButton == child) { + return index; + } + if (radioButton.getVisibility() == VISIBLE) { + index++; + } + } + } + return -1; + } +}
\ No newline at end of file diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index c571737cec8f..a6304b1a4f87 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -3473,18 +3473,10 @@ public class RemoteViews implements Parcelable, Filter { return applyAsync(context, parent, executor, listener, null); } - private CancellationSignal startTaskOnExecutor(AsyncApplyTask task, Executor executor) { - CancellationSignal cancelSignal = new CancellationSignal(); - cancelSignal.setOnCancelListener(task); - - task.executeOnExecutor(executor == null ? AsyncTask.THREAD_POOL_EXECUTOR : executor); - return cancelSignal; - } - /** @hide */ public CancellationSignal applyAsync(Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener, OnClickHandler handler) { - return startTaskOnExecutor(getAsyncApplyTask(context, parent, listener, handler), executor); + return getAsyncApplyTask(context, parent, listener, handler).startTaskOnExecutor(executor); } private AsyncApplyTask getAsyncApplyTask(Context context, ViewGroup parent, @@ -3495,6 +3487,7 @@ public class RemoteViews implements Parcelable, Filter { private class AsyncApplyTask extends AsyncTask<Void, Void, ViewTree> implements CancellationSignal.OnCancelListener { + final CancellationSignal mCancelSignal = new CancellationSignal(); final RemoteViews mRV; final ViewGroup mParent; final Context mContext; @@ -3545,6 +3538,7 @@ public class RemoteViews implements Parcelable, Filter { @Override protected void onPostExecute(ViewTree viewTree) { + mCancelSignal.setOnCancelListener(null); if (mError == null) { if (mListener != null) { mListener.onViewInflated(viewTree.mRoot); @@ -3582,6 +3576,12 @@ public class RemoteViews implements Parcelable, Filter { public void onCancel() { cancel(true); } + + private CancellationSignal startTaskOnExecutor(Executor executor) { + mCancelSignal.setOnCancelListener(this); + executeOnExecutor(executor == null ? AsyncTask.THREAD_POOL_EXECUTOR : executor); + return mCancelSignal; + } } /** @@ -3646,8 +3646,8 @@ public class RemoteViews implements Parcelable, Filter { } } - return startTaskOnExecutor(new AsyncApplyTask(rvToApply, (ViewGroup) v.getParent(), - context, listener, handler, v), executor); + return new AsyncApplyTask(rvToApply, (ViewGroup) v.getParent(), + context, listener, handler, v).startTaskOnExecutor(executor); } private void performApply(View v, ViewGroup parent, OnClickHandler handler) { diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 8856f99272b6..f361784e81d7 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -111,6 +111,7 @@ import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGett import com.android.internal.app.ResolverListAdapter.ViewHolder; import com.android.internal.app.chooser.ChooserTargetInfo; import com.android.internal.app.chooser.DisplayResolveInfo; +import com.android.internal.app.chooser.MultiDisplayResolveInfo; import com.android.internal.app.chooser.NotSelectableTargetInfo; import com.android.internal.app.chooser.SelectableTargetInfo; import com.android.internal.app.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator; @@ -1352,17 +1353,31 @@ public class ChooserActivity extends ResolverActivity implements return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true); } - @Override - public void showTargetDetails(ResolveInfo ri) { - if (ri == null) { + void showTargetDetails(TargetInfo ti) { + if (ti == null) { return; } - - ComponentName name = ri.activityInfo.getComponentName(); + ComponentName name = ti.getResolveInfo().activityInfo.getComponentName(); boolean pinned = mPinnedSharedPrefs.getBoolean(name.flattenToString(), false); - ResolverTargetActionsDialogFragment f = - new ResolverTargetActionsDialogFragment(ri.loadLabel(getPackageManager()), - name, pinned); + + ResolverTargetActionsDialogFragment f; + + // For multiple targets, include info on all targets + if (ti instanceof MultiDisplayResolveInfo) { + MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) ti; + List<CharSequence> labels = new ArrayList<>(); + + for (TargetInfo innerInfo : mti.getTargets()) { + labels.add(innerInfo.getResolveInfo().loadLabel(getPackageManager())); + } + f = new ResolverTargetActionsDialogFragment( + mti.getResolveInfo().loadLabel(getPackageManager()), name, mti.getTargets(), + labels); + } else { + f = new ResolverTargetActionsDialogFragment( + ti.getResolveInfo().loadLabel(getPackageManager()), name, pinned); + } + f.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG); } @@ -1416,8 +1431,26 @@ public class ChooserActivity extends ResolverActivity implements } final long selectionCost = System.currentTimeMillis() - mChooserShownTime; + + // Stacked apps get a disambiguation first + if (targetInfo instanceof MultiDisplayResolveInfo) { + MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo; + CharSequence[] labels = new CharSequence[mti.getTargets().size()]; + int i = 0; + for (TargetInfo ti : mti.getTargets()) { + labels[i++] = ti.getResolveInfo().loadLabel(getPackageManager()); + } + ChooserStackedAppDialogFragment f = new ChooserStackedAppDialogFragment( + targetInfo.getDisplayLabel(), + ((MultiDisplayResolveInfo) targetInfo).getTargets(), labels); + + f.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG); + return; + } + super.startSelected(which, always, filtered); + if (currentListAdapter.getCount() > 0) { // Log the index of which type of target the user picked. // Lower values mean the ranking was better. @@ -2363,7 +2396,7 @@ public class ChooserActivity extends ResolverActivity implements itemView.setOnLongClickListener(v -> { showTargetDetails( mChooserMultiProfilePagerAdapter.getActiveListAdapter() - .resolveInfoForPosition(mListPosition, /* filtered */ true)); + .targetInfoForPosition(mListPosition, /* filtered */ true)); return true; }); } @@ -2615,7 +2648,7 @@ public class ChooserActivity extends ResolverActivity implements @Override public boolean onLongClick(View v) { showTargetDetails( - mChooserListAdapter.resolveInfoForPosition( + mChooserListAdapter.targetInfoForPosition( holder.getItemIndex(column), true)); return true; } diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java index 4eccf21677d5..a8a676d03971 100644 --- a/core/java/com/android/internal/app/ChooserListAdapter.java +++ b/core/java/com/android/internal/app/ChooserListAdapter.java @@ -38,17 +38,22 @@ import com.android.internal.R; import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; import com.android.internal.app.chooser.ChooserTargetInfo; import com.android.internal.app.chooser.DisplayResolveInfo; +import com.android.internal.app.chooser.MultiDisplayResolveInfo; import com.android.internal.app.chooser.SelectableTargetInfo; import com.android.internal.app.chooser.TargetInfo; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class ChooserListAdapter extends ResolverListAdapter { private static final String TAG = "ChooserListAdapter"; private static final boolean DEBUG = false; + private boolean mEnableStackedApps = true; + public static final int NO_POSITION = -1; public static final int TARGET_BAD = -1; public static final int TARGET_CALLER = 0; @@ -218,7 +223,25 @@ public class ChooserListAdapter extends ResolverListAdapter { void updateAlphabeticalList() { mSortedList.clear(); - mSortedList.addAll(mDisplayList); + if (mEnableStackedApps) { + // Consolidate multiple targets from same app. + Map<String, DisplayResolveInfo> consolidated = new HashMap<>(); + for (DisplayResolveInfo info : mDisplayList) { + String packageName = info.getResolvedComponentName().getPackageName(); + if (consolidated.get(packageName) != null) { + // create consolidated target + MultiDisplayResolveInfo multiDisplayResolveInfo = + new MultiDisplayResolveInfo(packageName, info); + multiDisplayResolveInfo.addTarget(consolidated.get(packageName)); + consolidated.put(packageName, multiDisplayResolveInfo); + } else { + consolidated.put(packageName, info); + } + } + mSortedList.addAll(consolidated.values()); + } else { + mSortedList.addAll(mDisplayList); + } Collections.sort(mSortedList, new ChooserActivity.AzInfoComparator(mContext)); } @@ -270,7 +293,7 @@ public class ChooserListAdapter extends ResolverListAdapter { } int getAlphaTargetCount() { - int standardCount = super.getCount(); + int standardCount = mSortedList.size(); return standardCount > mChooserListCommunicator.getMaxRankedTargets() ? standardCount : 0; } diff --git a/core/java/com/android/internal/app/ChooserStackedAppDialogFragment.java b/core/java/com/android/internal/app/ChooserStackedAppDialogFragment.java new file mode 100644 index 000000000000..ff6582d10535 --- /dev/null +++ b/core/java/com/android/internal/app/ChooserStackedAppDialogFragment.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.internal.app; + +import android.app.AlertDialog.Builder; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.content.res.Configuration; +import android.os.Bundle; + +import com.android.internal.app.chooser.DisplayResolveInfo; + +import java.util.ArrayList; +import java.util.List; + +/** + * Shows individual actions for a "stacked" app target - such as an app with multiple posting + * streams represented in the Sharesheet. + */ +public class ChooserStackedAppDialogFragment extends DialogFragment + implements DialogInterface.OnClickListener { + private static final String TITLE_KEY = "title"; + private static final String PINNED_KEY = "pinned"; + + private List<DisplayResolveInfo> mTargetInfos = new ArrayList<>(); + private CharSequence[] mLabels; + + public ChooserStackedAppDialogFragment() { + } + + public ChooserStackedAppDialogFragment(CharSequence title) { + Bundle args = new Bundle(); + args.putCharSequence(TITLE_KEY, title); + setArguments(args); + } + + public ChooserStackedAppDialogFragment(CharSequence title, + List<DisplayResolveInfo> targets, CharSequence[] labels) { + Bundle args = new Bundle(); + args.putCharSequence(TITLE_KEY, title); + mTargetInfos = targets; + mLabels = labels; + setArguments(args); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Bundle args = getArguments(); + return new Builder(getContext()) + .setCancelable(true) + .setItems(mLabels, this) + .setTitle(args.getCharSequence(TITLE_KEY)) + .create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + final Bundle args = getArguments(); + mTargetInfos.get(which).start(getActivity(), null); + dismiss(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + // Dismiss on config changed (eg: rotation) + // TODO: Maintain state on config change + super.onConfigurationChanged(newConfig); + dismiss(); + } +} diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index cb7f2e424e7b..8dc3a072b47e 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -1160,7 +1160,7 @@ public class ResolverActivity extends Activity implements return !target.isSuspended(); } - public void showTargetDetails(ResolveInfo ri) { + void showTargetDetails(ResolveInfo ri) { Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) .setData(Uri.fromParts("package", ri.activityInfo.packageName, null)) .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); diff --git a/core/java/com/android/internal/app/ResolverTargetActionsDialogFragment.java b/core/java/com/android/internal/app/ResolverTargetActionsDialogFragment.java index df91c4a1f88d..bdbe2109cf05 100644 --- a/core/java/com/android/internal/app/ResolverTargetActionsDialogFragment.java +++ b/core/java/com/android/internal/app/ResolverTargetActionsDialogFragment.java @@ -24,14 +24,19 @@ import android.content.ComponentName; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.provider.Settings; import com.android.internal.R; +import com.android.internal.app.chooser.DisplayResolveInfo; + +import java.util.ArrayList; +import java.util.List; /** - * Shows a dialog with actions to take on a chooser target + * Shows a dialog with actions to take on a chooser target. */ public class ResolverTargetActionsDialogFragment extends DialogFragment implements DialogInterface.OnClickListener { @@ -43,6 +48,10 @@ public class ResolverTargetActionsDialogFragment extends DialogFragment private static final int TOGGLE_PIN_INDEX = 0; private static final int APP_INFO_INDEX = 1; + private List<DisplayResolveInfo> mTargetInfos = new ArrayList<>(); + private List<CharSequence> mLabels = new ArrayList<>(); + private boolean[] mPinned; + public ResolverTargetActionsDialogFragment() { } @@ -55,15 +64,43 @@ public class ResolverTargetActionsDialogFragment extends DialogFragment setArguments(args); } + public ResolverTargetActionsDialogFragment(CharSequence title, ComponentName name, + List<DisplayResolveInfo> targets, List<CharSequence> labels) { + Bundle args = new Bundle(); + args.putCharSequence(TITLE_KEY, title); + args.putParcelable(NAME_KEY, name); + mTargetInfos = targets; + mLabels = labels; + setArguments(args); + } + @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final Bundle args = getArguments(); final int itemRes = args.getBoolean(PINNED_KEY, false) ? R.array.resolver_target_actions_unpin : R.array.resolver_target_actions_pin; + String[] defaultActions = getResources().getStringArray(itemRes); + CharSequence[] items; + + if (mTargetInfos == null || mTargetInfos.size() < 2) { + items = defaultActions; + } else { + // Pin item for each sub-item + items = new CharSequence[mTargetInfos.size() + 1]; + for (int i = 0; i < mTargetInfos.size(); i++) { + items[i] = mTargetInfos.get(i).isPinned() + ? getResources().getString(R.string.unpin_specific_target, mLabels.get(i)) + : getResources().getString(R.string.pin_specific_target, mLabels.get(i)); + } + // "App info" + items[mTargetInfos.size()] = defaultActions[1]; + } + + return new Builder(getContext()) .setCancelable(true) - .setItems(itemRes, this) + .setItems(items, this) .setTitle(args.getCharSequence(TITLE_KEY)) .create(); } @@ -72,27 +109,41 @@ public class ResolverTargetActionsDialogFragment extends DialogFragment public void onClick(DialogInterface dialog, int which) { final Bundle args = getArguments(); ComponentName name = args.getParcelable(NAME_KEY); - switch (which) { - case TOGGLE_PIN_INDEX: - SharedPreferences sp = ChooserActivity.getPinnedSharedPrefs(getContext()); - final String key = name.flattenToString(); - boolean currentVal = sp.getBoolean(name.flattenToString(), false); - if (currentVal) { - sp.edit().remove(key).apply(); - } else { - sp.edit().putBoolean(key, true).apply(); - } - - // Force the chooser to requery and resort things - getActivity().recreate(); - break; - case APP_INFO_INDEX: - Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - .setData(Uri.fromParts("package", name.getPackageName(), null)) - .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); - startActivity(in); - break; + if (which == 0 || (mTargetInfos.size() > 0 && which < mTargetInfos.size())) { + if (mTargetInfos == null || mTargetInfos.size() == 0) { + pinComponent(name); + } else { + pinComponent(mTargetInfos.get(which).getResolvedComponentName()); + } + // Force the chooser to requery and resort things + getActivity().recreate(); + } else { + // Last item in dialog is App Info + Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", name.getPackageName(), null)) + .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); + startActivity(in); + } + dismiss(); + } + + private void pinComponent(ComponentName name) { + SharedPreferences sp = ChooserActivity.getPinnedSharedPrefs(getContext()); + final String key = name.flattenToString(); + boolean currentVal = sp.getBoolean(name.flattenToString(), false); + if (currentVal) { + sp.edit().remove(key).apply(); + } else { + sp.edit().putBoolean(key, true).apply(); } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + // Dismiss on config changed (eg: rotation) + // TODO: Maintain state on config change + super.onConfigurationChanged(newConfig); dismiss(); } + } diff --git a/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java b/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java index f92637c1bf01..86a9af3db196 100644 --- a/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java +++ b/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java @@ -91,6 +91,16 @@ public class DisplayResolveInfo implements TargetInfo { mResolveInfoPresentationGetter = resolveInfoPresentationGetter; } + DisplayResolveInfo(DisplayResolveInfo other) { + mSourceIntents.addAll(other.getAllSourceIntents()); + mResolveInfo = other.mResolveInfo; + mDisplayLabel = other.mDisplayLabel; + mDisplayIcon = other.mDisplayIcon; + mExtendedInfo = other.mExtendedInfo; + mResolvedIntent = other.mResolvedIntent; + mResolveInfoPresentationGetter = other.mResolveInfoPresentationGetter; + } + public ResolveInfo getResolveInfo() { return mResolveInfo; } @@ -189,4 +199,5 @@ public class DisplayResolveInfo implements TargetInfo { public void setPinned(boolean pinned) { mPinned = pinned; } + } diff --git a/core/java/com/android/internal/app/chooser/MultiDisplayResolveInfo.java b/core/java/com/android/internal/app/chooser/MultiDisplayResolveInfo.java new file mode 100644 index 000000000000..4c52411d6376 --- /dev/null +++ b/core/java/com/android/internal/app/chooser/MultiDisplayResolveInfo.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app.chooser; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a "stack" of chooser targets for various activities within the same component. + */ +public class MultiDisplayResolveInfo extends DisplayResolveInfo { + + List<DisplayResolveInfo> mTargetInfos = new ArrayList<>(); + String mPackageName; + // We'll use this DRI for basic presentation info - eg icon, name. + final DisplayResolveInfo mBaseInfo; + + /** + * @param firstInfo A representative DRI to use for the main icon, title, etc for this Info. + */ + public MultiDisplayResolveInfo(String packageName, DisplayResolveInfo firstInfo) { + super(firstInfo); + mBaseInfo = firstInfo; + mTargetInfos.add(firstInfo); + } + + /** + * Add another DisplayResolveInfo to the list included for this target. + */ + public void addTarget(DisplayResolveInfo target) { + mTargetInfos.add(target); + } + + /** + * List of all DisplayResolveInfos included in this target. + */ + public List<DisplayResolveInfo> getTargets() { + return mTargetInfos; + } + +} diff --git a/core/java/com/android/internal/content/FileSystemProvider.java b/core/java/com/android/internal/content/FileSystemProvider.java index f5708a5c89af..dec9ae701fb2 100644 --- a/core/java/com/android/internal/content/FileSystemProvider.java +++ b/core/java/com/android/internal/content/FileSystemProvider.java @@ -259,7 +259,7 @@ public abstract class FileSystemProvider extends DocumentsProvider { throw new IllegalStateException("Failed to touch " + file + ": " + e); } } - MediaStore.scanFile(getContext(), file); + MediaStore.scanFile(getContext().getContentResolver(), file); return childId; } @@ -316,10 +316,10 @@ public abstract class FileSystemProvider extends DocumentsProvider { private void moveInMediaStore(@Nullable File oldVisibleFile, @Nullable File newVisibleFile) { if (oldVisibleFile != null) { - MediaStore.scanFile(getContext(), oldVisibleFile); + MediaStore.scanFile(getContext().getContentResolver(), oldVisibleFile); } if (newVisibleFile != null) { - MediaStore.scanFile(getContext(), newVisibleFile); + MediaStore.scanFile(getContext().getContentResolver(), newVisibleFile); } } diff --git a/core/java/com/android/internal/infra/AndroidFuture.java b/core/java/com/android/internal/infra/AndroidFuture.java index d78bd73b080b..b250578c8454 100644 --- a/core/java/com/android/internal/infra/AndroidFuture.java +++ b/core/java/com/android/internal/infra/AndroidFuture.java @@ -531,6 +531,12 @@ public class AndroidFuture<T> extends CompletableFuture<T> implements Parcelable try { AndroidFuture.this.complete((T) resultContainer.get()); } catch (Throwable t) { + // If resultContainer was completed exceptionally, get() wraps the + // underlying exception in an ExecutionException. Unwrap it now to avoid + // double-layering ExecutionExceptions. + if (t instanceof ExecutionException && t.getCause() != null) { + t = t.getCause(); + } completeExceptionally(t); } } diff --git a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl index 01f57438a02c..cb67309ce74f 100644 --- a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl +++ b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl @@ -43,7 +43,6 @@ oneway interface IPhoneStateListener { void onDataConnectionStateChanged(int state, int networkType); void onDataActivity(int direction); void onSignalStrengthsChanged(in SignalStrength signalStrength); - void onOtaspChanged(in int otaspMode); void onCellInfoChanged(in List<CellInfo> cellInfo); void onPreciseCallStateChanged(in PreciseCallState callState); void onPreciseDataConnectionStateChanged(in PreciseDataConnectionState dataConnectionState); diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl index d8e8d67c806d..f954679ebf4d 100644 --- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl +++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl @@ -24,6 +24,8 @@ import android.telephony.CallQuality; import android.telephony.CellInfo; import android.telephony.ims.ImsReasonInfo; import android.telephony.PhoneCapability; +import android.telephony.PhysicalChannelConfig; +import android.telephony.PreciseDataConnectionState; import android.telephony.ServiceState; import android.telephony.SignalStrength; import android.telephony.emergency.EmergencyNumber; @@ -60,16 +62,13 @@ interface ITelephonyRegistry { @UnsupportedAppUsage(maxTargetSdk = 28) void notifyDataActivity(int state); void notifyDataActivityForSubscriber(in int subId, int state); - void notifyDataConnectionForSubscriber(int phoneId, int subId, int state, - boolean isDataConnectivityPossible, - String apn, String apnType, in LinkProperties linkProperties, - in NetworkCapabilities networkCapabilities, int networkType, boolean roaming); - void notifyDataConnectionFailedForSubscriber(int phoneId, int subId, String apnType); + void notifyDataConnectionForSubscriber( + int phoneId, int subId, String apnType, in PreciseDataConnectionState preciseState); + @UnsupportedAppUsage + void notifyDataConnectionFailed(String apnType); @UnsupportedAppUsage(maxTargetSdk = 28) void notifyCellLocation(in Bundle cellLocation); void notifyCellLocationForSubscriber(in int subId, in Bundle cellLocation); - @UnsupportedAppUsage(maxTargetSdk = 28) - void notifyOtaspChanged(in int subId, in int otaspMode); @UnsupportedAppUsage void notifyCellInfo(in List<CellInfo> cellInfo); void notifyPreciseCallState(int phoneId, int subId, int ringingCallState, diff --git a/core/java/com/android/internal/util/CollectionUtils.java b/core/java/com/android/internal/util/CollectionUtils.java index 4165f202998c..4dac5427095b 100644 --- a/core/java/com/android/internal/util/CollectionUtils.java +++ b/core/java/com/android/internal/util/CollectionUtils.java @@ -308,6 +308,17 @@ public class CollectionUtils { } /** + * @see #add(List, Object) + */ + public static @NonNull <K, V> Map<K, V> add(@Nullable Map<K, V> map, K key, V value) { + if (map == null || map == Collections.emptyMap()) { + map = new ArrayMap<>(); + } + map.put(key, value); + return map; + } + + /** * Similar to {@link List#remove}, but with support for list values of {@code null} and * {@link Collections#emptyList} */ diff --git a/core/java/com/android/internal/view/IInputMethod.aidl b/core/java/com/android/internal/view/IInputMethod.aidl index 2ee902ab6468..58aaa80b51be 100644 --- a/core/java/com/android/internal/view/IInputMethod.aidl +++ b/core/java/com/android/internal/view/IInputMethod.aidl @@ -16,13 +16,16 @@ package com.android.internal.view; +import android.content.ComponentName; import android.os.IBinder; import android.os.ResultReceiver; +import android.view.autofill.AutofillId; import android.view.InputChannel; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputBinding; import android.view.inputmethod.InputMethodSubtype; import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; +import com.android.internal.view.IInlineSuggestionsRequestCallback; import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethodSession; import com.android.internal.view.IInputSessionCallback; @@ -35,6 +38,9 @@ import com.android.internal.view.IInputSessionCallback; oneway interface IInputMethod { void initializeInternal(IBinder token, int displayId, IInputMethodPrivilegedOperations privOps); + void onCreateInlineSuggestionsRequest(in ComponentName componentName, in AutofillId autofillId, + in IInlineSuggestionsRequestCallback cb); + void bindInput(in InputBinding binding); void unbindInput(); diff --git a/core/java/com/android/server/SystemConfig.java b/core/java/com/android/server/SystemConfig.java index 7562bad80a54..8a59c998dacb 100644 --- a/core/java/com/android/server/SystemConfig.java +++ b/core/java/com/android/server/SystemConfig.java @@ -25,6 +25,7 @@ import android.content.pm.FeatureInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Environment; +import android.os.FileUtils; import android.os.Process; import android.os.SystemProperties; import android.os.Trace; @@ -228,7 +229,7 @@ public class SystemConfig { * Map of system pre-defined, uniquely named actors; keys are namespace, * value maps actor name to package name. */ - private ArrayMap<String, ArrayMap<String, String>> mNamedActors = null; + private Map<String, Map<String, String>> mNamedActors = null; public static SystemConfig getInstance() { if (!isSystemProcess()) { @@ -412,7 +413,7 @@ public class SystemConfig { } @NonNull - public Map<String, ? extends Map<String, String>> getNamedActors() { + public Map<String, Map<String, String>> getNamedActors() { return mNamedActors != null ? mNamedActors : Collections.emptyMap(); } @@ -498,6 +499,19 @@ public class SystemConfig { Environment.getSystemExtDirectory(), "etc", "sysconfig"), ALLOW_ALL); readPermissions(Environment.buildPath( Environment.getSystemExtDirectory(), "etc", "permissions"), ALLOW_ALL); + + // Skip loading configuration from apex if it is not a system process. + if (!isSystemProcess()) { + return; + } + // Read configuration of libs from apex module. + // TODO: Use a solid way to filter apex module folders? + for (File f: FileUtils.listFilesOrEmpty(Environment.getApexDirectory())) { + if (f.isFile() || f.getPath().contains("@")) { + continue; + } + readPermissions(Environment.buildPath(f, "etc", "permissions"), ALLOW_LIBS); + } } @VisibleForTesting @@ -1069,7 +1083,7 @@ public class SystemConfig { mNamedActors = new ArrayMap<>(); } - ArrayMap<String, String> nameToPkgMap = mNamedActors.get(namespace); + Map<String, String> nameToPkgMap = mNamedActors.get(namespace); if (nameToPkgMap == null) { nameToPkgMap = new ArrayMap<>(); mNamedActors.put(namespace, nameToPkgMap); diff --git a/core/jni/Android.bp b/core/jni/Android.bp index b91d3595fe6a..a2f6a62157ed 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -177,7 +177,6 @@ cc_library_shared { "android_hardware_HardwareBuffer.cpp", "android_hardware_SensorManager.cpp", "android_hardware_SerialPort.cpp", - "android_hardware_SoundTrigger.cpp", "android_hardware_UsbDevice.cpp", "android_hardware_UsbDeviceConnection.cpp", "android_hardware_UsbRequest.cpp", @@ -259,7 +258,6 @@ cc_library_shared { "libpdfium", "libimg_utils", "libnetd_client", - "libsoundtrigger", "libprocessgroup", "libnativebridge_lazy", "libnativeloader_lazy", diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index b8fd3ad43ce5..3cde887ba465 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -80,7 +80,6 @@ extern int register_android_hardware_camera2_DngCreator(JNIEnv *env); extern int register_android_hardware_HardwareBuffer(JNIEnv *env); extern int register_android_hardware_SensorManager(JNIEnv *env); extern int register_android_hardware_SerialPort(JNIEnv *env); -extern int register_android_hardware_SoundTrigger(JNIEnv *env); extern int register_android_hardware_UsbDevice(JNIEnv *env); extern int register_android_hardware_UsbDeviceConnection(JNIEnv *env); extern int register_android_hardware_UsbRequest(JNIEnv *env); @@ -130,6 +129,7 @@ extern int register_android_database_CursorWindow(JNIEnv* env); extern int register_android_database_SQLiteConnection(JNIEnv* env); extern int register_android_database_SQLiteGlobal(JNIEnv* env); extern int register_android_database_SQLiteDebug(JNIEnv* env); +extern int register_android_media_MediaMetrics(JNIEnv *env); extern int register_android_os_Debug(JNIEnv* env); extern int register_android_os_GraphicsEnvironment(JNIEnv* env); extern int register_android_os_HidlSupport(JNIEnv* env); @@ -1508,7 +1508,6 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_hardware_HardwareBuffer), REG_JNI(register_android_hardware_SensorManager), REG_JNI(register_android_hardware_SerialPort), - REG_JNI(register_android_hardware_SoundTrigger), REG_JNI(register_android_hardware_UsbDevice), REG_JNI(register_android_hardware_UsbDeviceConnection), REG_JNI(register_android_hardware_UsbRequest), @@ -1522,6 +1521,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_media_AudioProductStrategies), REG_JNI(register_android_media_AudioVolumeGroups), REG_JNI(register_android_media_AudioVolumeGroupChangeHandler), + REG_JNI(register_android_media_MediaMetrics), REG_JNI(register_android_media_MicrophoneInfo), REG_JNI(register_android_media_RemoteDisplay), REG_JNI(register_android_media_ToneGenerator), diff --git a/core/jni/android_hardware_SoundTrigger.cpp b/core/jni/android_hardware_SoundTrigger.cpp deleted file mode 100644 index 4376b0b4ac42..000000000000 --- a/core/jni/android_hardware_SoundTrigger.cpp +++ /dev/null @@ -1,1071 +0,0 @@ -/* -** -** Copyright 2014, 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. -*/ - -//#define LOG_NDEBUG 0 -#define LOG_TAG "SoundTrigger-JNI" -#include <utils/Log.h> - -#include "jni.h" -#include <nativehelper/JNIHelp.h> -#include <nativehelper/ScopedUtfChars.h> -#include "core_jni_helpers.h" -#include <system/sound_trigger.h> -#include <soundtrigger/SoundTriggerCallback.h> -#include <soundtrigger/SoundTrigger.h> -#include <utils/RefBase.h> -#include <utils/Vector.h> -#include <binder/IMemory.h> -#include <binder/MemoryDealer.h> -#include "android_media_AudioFormat.h" - -using namespace android; - -static jclass gArrayListClass; -static struct { - jmethodID add; -} gArrayListMethods; - -static jclass gUUIDClass; -static struct { - jmethodID toString; -} gUUIDMethods; - -static const char* const kUnsupportedOperationExceptionClassPathName = - "java/lang/UnsupportedOperationException"; -static jclass gUnsupportedOperationExceptionClass; -static const char* const kIllegalArgumentExceptionClassPathName = - "java/lang/IllegalArgumentException"; -static jclass gIllegalArgumentExceptionClass; - -static const char* const kSoundTriggerClassPathName = "android/hardware/soundtrigger/SoundTrigger"; -static jclass gSoundTriggerClass; - -static const char* const kModuleClassPathName = "android/hardware/soundtrigger/SoundTriggerModule"; -static jclass gModuleClass; -static struct { - jfieldID mNativeContext; - jfieldID mId; -} gModuleFields; -static jmethodID gPostEventFromNative; - -static const char* const kModulePropertiesClassPathName = - "android/hardware/soundtrigger/SoundTrigger$ModuleProperties"; -static jclass gModulePropertiesClass; -static jmethodID gModulePropertiesCstor; - -static const char* const kSoundModelClassPathName = - "android/hardware/soundtrigger/SoundTrigger$SoundModel"; -static jclass gSoundModelClass; -static struct { - jfieldID uuid; - jfieldID vendorUuid; - jfieldID data; -} gSoundModelFields; - -static const char* const kGenericSoundModelClassPathName = - "android/hardware/soundtrigger/SoundTrigger$GenericSoundModel"; -static jclass gGenericSoundModelClass; - -static const char* const kKeyphraseClassPathName = - "android/hardware/soundtrigger/SoundTrigger$Keyphrase"; -static jclass gKeyphraseClass; -static struct { - jfieldID id; - jfieldID recognitionModes; - jfieldID locale; - jfieldID text; - jfieldID users; -} gKeyphraseFields; - -static const char* const kKeyphraseSoundModelClassPathName = - "android/hardware/soundtrigger/SoundTrigger$KeyphraseSoundModel"; -static jclass gKeyphraseSoundModelClass; -static struct { - jfieldID keyphrases; -} gKeyphraseSoundModelFields; - -static const char* const kModelParamRangeClassPathName = - "android/hardware/soundtrigger/SoundTrigger$ModelParamRange"; -static jclass gModelParamRangeClass; -static jmethodID gModelParamRangeCstor; - -static const char* const kRecognitionConfigClassPathName = - "android/hardware/soundtrigger/SoundTrigger$RecognitionConfig"; -static jclass gRecognitionConfigClass; -static struct { - jfieldID captureRequested; - jfieldID keyphrases; - jfieldID data; -} gRecognitionConfigFields; - -static const char* const kRecognitionEventClassPathName = - "android/hardware/soundtrigger/SoundTrigger$RecognitionEvent"; -static jclass gRecognitionEventClass; -static jmethodID gRecognitionEventCstor; - -static const char* const kKeyphraseRecognitionEventClassPathName = - "android/hardware/soundtrigger/SoundTrigger$KeyphraseRecognitionEvent"; -static jclass gKeyphraseRecognitionEventClass; -static jmethodID gKeyphraseRecognitionEventCstor; - -static const char* const kGenericRecognitionEventClassPathName = - "android/hardware/soundtrigger/SoundTrigger$GenericRecognitionEvent"; -static jclass gGenericRecognitionEventClass; -static jmethodID gGenericRecognitionEventCstor; - -static const char* const kKeyphraseRecognitionExtraClassPathName = - "android/hardware/soundtrigger/SoundTrigger$KeyphraseRecognitionExtra"; -static jclass gKeyphraseRecognitionExtraClass; -static jmethodID gKeyphraseRecognitionExtraCstor; -static struct { - jfieldID id; - jfieldID recognitionModes; - jfieldID coarseConfidenceLevel; - jfieldID confidenceLevels; -} gKeyphraseRecognitionExtraFields; - -static const char* const kConfidenceLevelClassPathName = - "android/hardware/soundtrigger/SoundTrigger$ConfidenceLevel"; -static jclass gConfidenceLevelClass; -static jmethodID gConfidenceLevelCstor; -static struct { - jfieldID userId; - jfieldID confidenceLevel; -} gConfidenceLevelFields; - -static const char* const kAudioFormatClassPathName = - "android/media/AudioFormat"; -static jclass gAudioFormatClass; -static jmethodID gAudioFormatCstor; - -static const char* const kSoundModelEventClassPathName = - "android/hardware/soundtrigger/SoundTrigger$SoundModelEvent"; -static jclass gSoundModelEventClass; -static jmethodID gSoundModelEventCstor; - -static Mutex gLock; - -enum { - SOUNDTRIGGER_STATUS_OK = 0, - SOUNDTRIGGER_STATUS_ERROR = INT_MIN, - SOUNDTRIGGER_PERMISSION_DENIED = -1, - SOUNDTRIGGER_STATUS_NO_INIT = -19, - SOUNDTRIGGER_STATUS_BAD_VALUE = -22, - SOUNDTRIGGER_STATUS_DEAD_OBJECT = -32, - SOUNDTRIGGER_INVALID_OPERATION = -38, -}; - -enum { - SOUNDTRIGGER_EVENT_RECOGNITION = 1, - SOUNDTRIGGER_EVENT_SERVICE_DIED = 2, - SOUNDTRIGGER_EVENT_SOUNDMODEL = 3, - SOUNDTRIGGER_EVENT_SERVICE_STATE_CHANGE = 4, -}; - -static jint throwUnsupportedOperationException(JNIEnv *env) -{ - return env->ThrowNew(gUnsupportedOperationExceptionClass, nullptr); -} - -static jint throwIllegalArgumentException(JNIEnv *env) -{ - return env->ThrowNew(gIllegalArgumentExceptionClass, nullptr); -} - -// ---------------------------------------------------------------------------- -// ref-counted object for callbacks -class JNISoundTriggerCallback: public SoundTriggerCallback -{ -public: - JNISoundTriggerCallback(JNIEnv* env, jobject thiz, jobject weak_thiz); - ~JNISoundTriggerCallback(); - - virtual void onRecognitionEvent(struct sound_trigger_recognition_event *event); - virtual void onSoundModelEvent(struct sound_trigger_model_event *event); - virtual void onServiceStateChange(sound_trigger_service_state_t state); - virtual void onServiceDied(); - -private: - jclass mClass; // Reference to SoundTrigger class - jobject mObject; // Weak ref to SoundTrigger Java object to call on -}; - -JNISoundTriggerCallback::JNISoundTriggerCallback(JNIEnv* env, jobject thiz, jobject weak_thiz) -{ - - // Hold onto the SoundTriggerModule class for use in calling the static method - // that posts events to the application thread. - jclass clazz = env->GetObjectClass(thiz); - if (clazz == NULL) { - ALOGE("Can't find class %s", kModuleClassPathName); - return; - } - mClass = (jclass)env->NewGlobalRef(clazz); - - // We use a weak reference so the SoundTriggerModule object can be garbage collected. - // The reference is only used as a proxy for callbacks. - mObject = env->NewGlobalRef(weak_thiz); -} - -JNISoundTriggerCallback::~JNISoundTriggerCallback() -{ - // remove global references - JNIEnv *env = AndroidRuntime::getJNIEnv(); - env->DeleteGlobalRef(mObject); - env->DeleteGlobalRef(mClass); -} - -void JNISoundTriggerCallback::onRecognitionEvent(struct sound_trigger_recognition_event *event) -{ - JNIEnv *env = AndroidRuntime::getJNIEnv(); - jobject jEvent = NULL; - jbyteArray jData = NULL; - - if (event->data_size) { - jData = env->NewByteArray(event->data_size); - jbyte *nData = env->GetByteArrayElements(jData, NULL); - memcpy(nData, (char *)event + event->data_offset, event->data_size); - env->ReleaseByteArrayElements(jData, nData, 0); - } - - jobject jAudioFormat = NULL; - if (event->trigger_in_data || event->capture_available) { - jint channelMask = (jint)audio_channel_mask_get_bits(event->audio_config.channel_mask); - jint channelIndexMask = (jint)AUDIO_CHANNEL_NONE; - - switch (audio_channel_mask_get_representation(event->audio_config.channel_mask)) { - case AUDIO_CHANNEL_REPRESENTATION_INDEX: - channelIndexMask = channelMask; - channelMask = (jint)AUDIO_CHANNEL_NONE; - break; - default: - break; - } - jAudioFormat = env->NewObject(gAudioFormatClass, - gAudioFormatCstor, - audioFormatFromNative(event->audio_config.format), - event->audio_config.sample_rate, - channelMask, - channelIndexMask); - - } - if (event->type == SOUND_MODEL_TYPE_KEYPHRASE) { - struct sound_trigger_phrase_recognition_event *phraseEvent = - (struct sound_trigger_phrase_recognition_event *)event; - - jobjectArray jExtras = env->NewObjectArray(phraseEvent->num_phrases, - gKeyphraseRecognitionExtraClass, NULL); - if (jExtras == NULL) { - return; - } - - for (size_t i = 0; i < phraseEvent->num_phrases; i++) { - jobjectArray jConfidenceLevels = env->NewObjectArray( - phraseEvent->phrase_extras[i].num_levels, - gConfidenceLevelClass, NULL); - - if (jConfidenceLevels == NULL) { - return; - } - for (size_t j = 0; j < phraseEvent->phrase_extras[i].num_levels; j++) { - jobject jConfidenceLevel = env->NewObject(gConfidenceLevelClass, - gConfidenceLevelCstor, - phraseEvent->phrase_extras[i].levels[j].user_id, - phraseEvent->phrase_extras[i].levels[j].level); - env->SetObjectArrayElement(jConfidenceLevels, j, jConfidenceLevel); - env->DeleteLocalRef(jConfidenceLevel); - } - - jobject jNewExtra = env->NewObject(gKeyphraseRecognitionExtraClass, - gKeyphraseRecognitionExtraCstor, - phraseEvent->phrase_extras[i].id, - phraseEvent->phrase_extras[i].recognition_modes, - phraseEvent->phrase_extras[i].confidence_level, - jConfidenceLevels); - - if (jNewExtra == NULL) { - return; - } - env->SetObjectArrayElement(jExtras, i, jNewExtra); - env->DeleteLocalRef(jNewExtra); - env->DeleteLocalRef(jConfidenceLevels); - } - jEvent = env->NewObject(gKeyphraseRecognitionEventClass, gKeyphraseRecognitionEventCstor, - event->status, event->model, event->capture_available, - event->capture_session, event->capture_delay_ms, - event->capture_preamble_ms, event->trigger_in_data, - jAudioFormat, jData, jExtras); - env->DeleteLocalRef(jExtras); - } else if (event->type == SOUND_MODEL_TYPE_GENERIC) { - jEvent = env->NewObject(gGenericRecognitionEventClass, gGenericRecognitionEventCstor, - event->status, event->model, event->capture_available, - event->capture_session, event->capture_delay_ms, - event->capture_preamble_ms, event->trigger_in_data, - jAudioFormat, jData); - } else { - jEvent = env->NewObject(gRecognitionEventClass, gRecognitionEventCstor, - event->status, event->model, event->capture_available, - event->capture_session, event->capture_delay_ms, - event->capture_preamble_ms, event->trigger_in_data, - jAudioFormat, jData); - } - - if (jAudioFormat != NULL) { - env->DeleteLocalRef(jAudioFormat); - } - if (jData != NULL) { - env->DeleteLocalRef(jData); - } - - env->CallStaticVoidMethod(mClass, gPostEventFromNative, mObject, - SOUNDTRIGGER_EVENT_RECOGNITION, 0, 0, jEvent); - - env->DeleteLocalRef(jEvent); - if (env->ExceptionCheck()) { - ALOGW("An exception occurred while notifying an event."); - env->ExceptionClear(); - } -} - -void JNISoundTriggerCallback::onSoundModelEvent(struct sound_trigger_model_event *event) -{ - JNIEnv *env = AndroidRuntime::getJNIEnv(); - jobject jEvent = NULL; - jbyteArray jData = NULL; - - if (event->data_size) { - jData = env->NewByteArray(event->data_size); - jbyte *nData = env->GetByteArrayElements(jData, NULL); - memcpy(nData, (char *)event + event->data_offset, event->data_size); - env->ReleaseByteArrayElements(jData, nData, 0); - } - - jEvent = env->NewObject(gSoundModelEventClass, gSoundModelEventCstor, - event->status, event->model, jData); - - env->DeleteLocalRef(jData); - env->CallStaticVoidMethod(mClass, gPostEventFromNative, mObject, - SOUNDTRIGGER_EVENT_SOUNDMODEL, 0, 0, jEvent); - env->DeleteLocalRef(jEvent); - if (env->ExceptionCheck()) { - ALOGW("An exception occurred while notifying an event."); - env->ExceptionClear(); - } -} - -void JNISoundTriggerCallback::onServiceStateChange(sound_trigger_service_state_t state) -{ - JNIEnv *env = AndroidRuntime::getJNIEnv(); - env->CallStaticVoidMethod(mClass, gPostEventFromNative, mObject, - SOUNDTRIGGER_EVENT_SERVICE_STATE_CHANGE, state, 0, NULL); - if (env->ExceptionCheck()) { - ALOGW("An exception occurred while notifying an event."); - env->ExceptionClear(); - } -} - -void JNISoundTriggerCallback::onServiceDied() -{ - JNIEnv *env = AndroidRuntime::getJNIEnv(); - - env->CallStaticVoidMethod(mClass, gPostEventFromNative, mObject, - SOUNDTRIGGER_EVENT_SERVICE_DIED, 0, 0, NULL); - if (env->ExceptionCheck()) { - ALOGW("An exception occurred while notifying an event."); - env->ExceptionClear(); - } -} - -// ---------------------------------------------------------------------------- - -static sp<SoundTrigger> getSoundTrigger(JNIEnv* env, jobject thiz) -{ - Mutex::Autolock l(gLock); - SoundTrigger* const st = (SoundTrigger*)env->GetLongField(thiz, - gModuleFields.mNativeContext); - return sp<SoundTrigger>(st); -} - -static sp<SoundTrigger> setSoundTrigger(JNIEnv* env, jobject thiz, const sp<SoundTrigger>& module) -{ - Mutex::Autolock l(gLock); - sp<SoundTrigger> old = (SoundTrigger*)env->GetLongField(thiz, - gModuleFields.mNativeContext); - if (module.get()) { - module->incStrong((void*)setSoundTrigger); - } - if (old != 0) { - old->decStrong((void*)setSoundTrigger); - } - env->SetLongField(thiz, gModuleFields.mNativeContext, (jlong)module.get()); - return old; -} - - -static jint -android_hardware_SoundTrigger_listModules(JNIEnv *env, jobject clazz, - jstring opPackageName, jobject jModules) -{ - ALOGV("listModules"); - - if (jModules == NULL) { - ALOGE("listModules NULL AudioPatch ArrayList"); - return SOUNDTRIGGER_STATUS_BAD_VALUE; - } - if (!env->IsInstanceOf(jModules, gArrayListClass)) { - ALOGE("listModules not an arraylist"); - return SOUNDTRIGGER_STATUS_BAD_VALUE; - } - - unsigned int numModules = 0; - struct sound_trigger_module_descriptor *nModules = NULL; - - ScopedUtfChars opPackageNameStr(env, opPackageName); - const String16 opPackageNameString16 = String16(opPackageNameStr.c_str()); - - status_t status = SoundTrigger::listModules(opPackageNameString16, nModules, &numModules); - if (status != NO_ERROR || numModules == 0) { - return (jint)status; - } - - nModules = (struct sound_trigger_module_descriptor *) - calloc(numModules, sizeof(struct sound_trigger_module_descriptor)); - - status = SoundTrigger::listModules(opPackageNameString16, nModules, &numModules); - ALOGV("listModules SoundTrigger::listModules status %d numModules %d", status, numModules); - - if (status != NO_ERROR) { - numModules = 0; - } - - for (size_t i = 0; i < numModules; i++) { - char str[SOUND_TRIGGER_MAX_STRING_LEN]; - - jstring implementor = env->NewStringUTF(nModules[i].properties.implementor); - jstring description = env->NewStringUTF(nModules[i].properties.description); - SoundTrigger::guidToString(&nModules[i].properties.uuid, - str, - SOUND_TRIGGER_MAX_STRING_LEN); - jstring uuid = env->NewStringUTF(str); - - ALOGV("listModules module %zu id %d description %s maxSoundModels %d", - i, nModules[i].handle, nModules[i].properties.description, - nModules[i].properties.max_sound_models); - - jobject newModuleDesc = env->NewObject(gModulePropertiesClass, gModulePropertiesCstor, - nModules[i].handle, - implementor, description, uuid, - nModules[i].properties.version, - nModules[i].properties.max_sound_models, - nModules[i].properties.max_key_phrases, - nModules[i].properties.max_users, - nModules[i].properties.recognition_modes, - nModules[i].properties.capture_transition, - nModules[i].properties.max_buffer_ms, - nModules[i].properties.concurrent_capture, - nModules[i].properties.power_consumption_mw, - nModules[i].properties.trigger_in_event); - - env->DeleteLocalRef(implementor); - env->DeleteLocalRef(description); - env->DeleteLocalRef(uuid); - if (newModuleDesc == NULL) { - status = SOUNDTRIGGER_STATUS_ERROR; - goto exit; - } - env->CallBooleanMethod(jModules, gArrayListMethods.add, newModuleDesc); - } - -exit: - free(nModules); - return (jint) status; -} - -static void -android_hardware_SoundTrigger_setup(JNIEnv *env, jobject thiz, - jstring opPackageName, jobject weak_this) -{ - ALOGV("setup"); - - ScopedUtfChars opPackageNameStr(env, opPackageName); - const String16 opPackageNameString16 = String16(opPackageNameStr.c_str()); - - sp<JNISoundTriggerCallback> callback = new JNISoundTriggerCallback(env, thiz, weak_this); - - sound_trigger_module_handle_t handle = - (sound_trigger_module_handle_t)env->GetIntField(thiz, gModuleFields.mId); - - sp<SoundTrigger> module = SoundTrigger::attach(opPackageNameString16, handle, callback); - if (module == 0) { - return; - } - - setSoundTrigger(env, thiz, module); -} - -static void -android_hardware_SoundTrigger_detach(JNIEnv *env, jobject thiz) -{ - ALOGV("detach"); - sp<SoundTrigger> module = setSoundTrigger(env, thiz, 0); - ALOGV("detach module %p", module.get()); - if (module != 0) { - ALOGV("detach module->detach()"); - module->detach(); - } -} - -static void -android_hardware_SoundTrigger_finalize(JNIEnv *env, jobject thiz) -{ - ALOGV("finalize"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module != 0) { - ALOGW("SoundTrigger finalized without being detached"); - } - android_hardware_SoundTrigger_detach(env, thiz); -} - -static jint -android_hardware_SoundTrigger_loadSoundModel(JNIEnv *env, jobject thiz, - jobject jSoundModel, jintArray jHandle) -{ - jint status = SOUNDTRIGGER_STATUS_OK; - jbyte *nData = NULL; - struct sound_trigger_sound_model *nSoundModel; - jbyteArray jData; - sp<MemoryDealer> memoryDealer; - sp<IMemory> memory; - size_t size; - sound_model_handle_t handle = 0; - jobject jUuid; - jstring jUuidString; - const char *nUuidString; - - ALOGV("loadSoundModel"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - if (jHandle == NULL) { - return SOUNDTRIGGER_STATUS_BAD_VALUE; - } - jsize jHandleLen = env->GetArrayLength(jHandle); - if (jHandleLen == 0) { - return SOUNDTRIGGER_STATUS_BAD_VALUE; - } - jint *nHandle = env->GetIntArrayElements(jHandle, NULL); - if (nHandle == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - if (!env->IsInstanceOf(jSoundModel, gSoundModelClass)) { - status = SOUNDTRIGGER_STATUS_BAD_VALUE; - goto exit; - } - size_t offset; - sound_trigger_sound_model_type_t type; - if (env->IsInstanceOf(jSoundModel, gKeyphraseSoundModelClass)) { - offset = sizeof(struct sound_trigger_phrase_sound_model); - type = SOUND_MODEL_TYPE_KEYPHRASE; - } else if (env->IsInstanceOf(jSoundModel, gGenericSoundModelClass)) { - offset = sizeof(struct sound_trigger_generic_sound_model); - type = SOUND_MODEL_TYPE_GENERIC; - } else { - offset = sizeof(struct sound_trigger_sound_model); - type = SOUND_MODEL_TYPE_UNKNOWN; - } - - jUuid = env->GetObjectField(jSoundModel, gSoundModelFields.uuid); - jUuidString = (jstring)env->CallObjectMethod(jUuid, gUUIDMethods.toString); - nUuidString = env->GetStringUTFChars(jUuidString, NULL); - sound_trigger_uuid_t nUuid; - SoundTrigger::stringToGuid(nUuidString, &nUuid); - env->ReleaseStringUTFChars(jUuidString, nUuidString); - env->DeleteLocalRef(jUuidString); - - sound_trigger_uuid_t nVendorUuid; - jUuid = env->GetObjectField(jSoundModel, gSoundModelFields.vendorUuid); - if (jUuid != NULL) { - jUuidString = (jstring)env->CallObjectMethod(jUuid, gUUIDMethods.toString); - nUuidString = env->GetStringUTFChars(jUuidString, NULL); - SoundTrigger::stringToGuid(nUuidString, &nVendorUuid); - env->ReleaseStringUTFChars(jUuidString, nUuidString); - env->DeleteLocalRef(jUuidString); - } else { - SoundTrigger::stringToGuid("00000000-0000-0000-0000-000000000000", &nVendorUuid); - } - - jData = (jbyteArray)env->GetObjectField(jSoundModel, gSoundModelFields.data); - if (jData == NULL) { - status = SOUNDTRIGGER_STATUS_BAD_VALUE; - goto exit; - } - size = env->GetArrayLength(jData); - - nData = env->GetByteArrayElements(jData, NULL); - if (jData == NULL) { - status = SOUNDTRIGGER_STATUS_ERROR; - goto exit; - } - - memoryDealer = new MemoryDealer(offset + size, "SoundTrigge-JNI::LoadModel"); - if (memoryDealer == 0) { - status = SOUNDTRIGGER_STATUS_ERROR; - goto exit; - } - memory = memoryDealer->allocate(offset + size); - if (memory == 0 || memory->unsecurePointer() == NULL) { - status = SOUNDTRIGGER_STATUS_ERROR; - goto exit; - } - - nSoundModel = (struct sound_trigger_sound_model *)memory->unsecurePointer(); - - nSoundModel->type = type; - nSoundModel->uuid = nUuid; - nSoundModel->vendor_uuid = nVendorUuid; - nSoundModel->data_size = size; - nSoundModel->data_offset = offset; - memcpy((char *)nSoundModel + offset, nData, size); - if (type == SOUND_MODEL_TYPE_KEYPHRASE) { - struct sound_trigger_phrase_sound_model *phraseModel = - (struct sound_trigger_phrase_sound_model *)nSoundModel; - - jobjectArray jPhrases = - (jobjectArray)env->GetObjectField(jSoundModel, gKeyphraseSoundModelFields.keyphrases); - if (jPhrases == NULL) { - status = SOUNDTRIGGER_STATUS_BAD_VALUE; - goto exit; - } - - size_t numPhrases = env->GetArrayLength(jPhrases); - phraseModel->num_phrases = numPhrases; - ALOGV("loadSoundModel numPhrases %zu", numPhrases); - for (size_t i = 0; i < numPhrases; i++) { - jobject jPhrase = env->GetObjectArrayElement(jPhrases, i); - phraseModel->phrases[i].id = - env->GetIntField(jPhrase,gKeyphraseFields.id); - phraseModel->phrases[i].recognition_mode = - env->GetIntField(jPhrase,gKeyphraseFields.recognitionModes); - - jintArray jUsers = (jintArray)env->GetObjectField(jPhrase, gKeyphraseFields.users); - phraseModel->phrases[i].num_users = env->GetArrayLength(jUsers); - jint *nUsers = env->GetIntArrayElements(jUsers, NULL); - memcpy(phraseModel->phrases[i].users, - nUsers, - phraseModel->phrases[i].num_users * sizeof(int)); - env->ReleaseIntArrayElements(jUsers, nUsers, 0); - env->DeleteLocalRef(jUsers); - - jstring jLocale = (jstring)env->GetObjectField(jPhrase, gKeyphraseFields.locale); - const char *nLocale = env->GetStringUTFChars(jLocale, NULL); - strncpy(phraseModel->phrases[i].locale, - nLocale, - SOUND_TRIGGER_MAX_LOCALE_LEN); - jstring jText = (jstring)env->GetObjectField(jPhrase, gKeyphraseFields.text); - const char *nText = env->GetStringUTFChars(jText, NULL); - strncpy(phraseModel->phrases[i].text, - nText, - SOUND_TRIGGER_MAX_STRING_LEN); - - env->ReleaseStringUTFChars(jLocale, nLocale); - env->DeleteLocalRef(jLocale); - env->ReleaseStringUTFChars(jText, nText); - env->DeleteLocalRef(jText); - ALOGV("loadSoundModel phrases %zu text %s locale %s", - i, phraseModel->phrases[i].text, phraseModel->phrases[i].locale); - env->DeleteLocalRef(jPhrase); - } - env->DeleteLocalRef(jPhrases); - } else if (type == SOUND_MODEL_TYPE_GENERIC) { - /* No initialization needed */ - } - status = module->loadSoundModel(memory, &handle); - ALOGV("loadSoundModel status %d handle %d", status, handle); - -exit: - if (nHandle != NULL) { - nHandle[0] = (jint)handle; - env->ReleaseIntArrayElements(jHandle, nHandle, NULL); - } - if (nData != NULL) { - env->ReleaseByteArrayElements(jData, nData, NULL); - } - return status; -} - -static jint -android_hardware_SoundTrigger_unloadSoundModel(JNIEnv *env, jobject thiz, - jint jHandle) -{ - jint status = SOUNDTRIGGER_STATUS_OK; - ALOGV("unloadSoundModel"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - status = module->unloadSoundModel((sound_model_handle_t)jHandle); - - return status; -} - -static jint -android_hardware_SoundTrigger_startRecognition(JNIEnv *env, jobject thiz, - jint jHandle, jobject jConfig) -{ - jint status = SOUNDTRIGGER_STATUS_OK; - ALOGV("startRecognition"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - - if (!env->IsInstanceOf(jConfig, gRecognitionConfigClass)) { - return SOUNDTRIGGER_STATUS_BAD_VALUE; - } - - jbyteArray jData = (jbyteArray)env->GetObjectField(jConfig, gRecognitionConfigFields.data); - jsize dataSize = 0; - jbyte *nData = NULL; - if (jData != NULL) { - dataSize = env->GetArrayLength(jData); - if (dataSize == 0) { - return SOUNDTRIGGER_STATUS_BAD_VALUE; - } - nData = env->GetByteArrayElements(jData, NULL); - if (nData == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - } - - size_t totalSize = sizeof(struct sound_trigger_recognition_config) + dataSize; - sp<MemoryDealer> memoryDealer = - new MemoryDealer(totalSize, "SoundTrigge-JNI::StartRecognition"); - if (memoryDealer == 0) { - return SOUNDTRIGGER_STATUS_ERROR; - } - sp<IMemory> memory = memoryDealer->allocate(totalSize); - if (memory == 0 || memory->unsecurePointer() == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - if (dataSize != 0) { - memcpy((char *)memory->unsecurePointer() + sizeof(struct sound_trigger_recognition_config), - nData, - dataSize); - env->ReleaseByteArrayElements(jData, nData, 0); - } - env->DeleteLocalRef(jData); - struct sound_trigger_recognition_config *config = - (struct sound_trigger_recognition_config *)memory->unsecurePointer(); - config->data_size = dataSize; - config->data_offset = sizeof(struct sound_trigger_recognition_config); - config->capture_requested = env->GetBooleanField(jConfig, - gRecognitionConfigFields.captureRequested); - - config->num_phrases = 0; - jobjectArray jPhrases = - (jobjectArray)env->GetObjectField(jConfig, gRecognitionConfigFields.keyphrases); - if (jPhrases != NULL) { - config->num_phrases = env->GetArrayLength(jPhrases); - } - ALOGV("startRecognition num phrases %d", config->num_phrases); - for (size_t i = 0; i < config->num_phrases; i++) { - jobject jPhrase = env->GetObjectArrayElement(jPhrases, i); - config->phrases[i].id = env->GetIntField(jPhrase, - gKeyphraseRecognitionExtraFields.id); - config->phrases[i].recognition_modes = env->GetIntField(jPhrase, - gKeyphraseRecognitionExtraFields.recognitionModes); - config->phrases[i].confidence_level = env->GetIntField(jPhrase, - gKeyphraseRecognitionExtraFields.coarseConfidenceLevel); - config->phrases[i].num_levels = 0; - jobjectArray jConfidenceLevels = (jobjectArray)env->GetObjectField(jPhrase, - gKeyphraseRecognitionExtraFields.confidenceLevels); - if (jConfidenceLevels != NULL) { - config->phrases[i].num_levels = env->GetArrayLength(jConfidenceLevels); - } - ALOGV("startRecognition phrase %zu num_levels %d", i, config->phrases[i].num_levels); - for (size_t j = 0; j < config->phrases[i].num_levels; j++) { - jobject jConfidenceLevel = env->GetObjectArrayElement(jConfidenceLevels, j); - config->phrases[i].levels[j].user_id = env->GetIntField(jConfidenceLevel, - gConfidenceLevelFields.userId); - config->phrases[i].levels[j].level = env->GetIntField(jConfidenceLevel, - gConfidenceLevelFields.confidenceLevel); - env->DeleteLocalRef(jConfidenceLevel); - } - ALOGV("startRecognition phrases %zu", i); - env->DeleteLocalRef(jConfidenceLevels); - env->DeleteLocalRef(jPhrase); - } - env->DeleteLocalRef(jPhrases); - - status = module->startRecognition(jHandle, memory); - return status; -} - -static jint -android_hardware_SoundTrigger_stopRecognition(JNIEnv *env, jobject thiz, - jint jHandle) -{ - jint status = SOUNDTRIGGER_STATUS_OK; - ALOGV("stopRecognition"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - status = module->stopRecognition(jHandle); - return status; -} - -static jint -android_hardware_SoundTrigger_getModelState(JNIEnv *env, jobject thiz, - jint jHandle) -{ - jint status = SOUNDTRIGGER_STATUS_OK; - ALOGV("getModelState"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == NULL) { - return SOUNDTRIGGER_STATUS_ERROR; - } - status = module->getModelState(jHandle); - return status; -} - -static jint -android_hardware_SoundTrigger_setParameter(JNIEnv *env, jobject thiz, - jint jHandle, jint jModelParam, jint jValue) -{ - ALOGV("setParameter"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == NULL) { - return SOUNDTRIGGER_STATUS_NO_INIT; - } - return module->setParameter(jHandle, (sound_trigger_model_parameter_t) jModelParam, jValue); -} - -static jint -android_hardware_SoundTrigger_getParameter(JNIEnv *env, jobject thiz, - jint jHandle, jint jModelParam) -{ - ALOGV("getParameter"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == NULL) { - throwUnsupportedOperationException(env); - return -1; - } - - jint nValue; - jint status = module->getParameter(jHandle, - (sound_trigger_model_parameter_t) jModelParam, &nValue); - - switch (status) { - case 0: - return nValue; - case -EINVAL: - throwIllegalArgumentException(env); - break; - default: - throwUnsupportedOperationException(env); - break; - } - - return -1; -} - -static jobject -android_hardware_SoundTrigger_queryParameter(JNIEnv *env, jobject thiz, - jint jHandle, jint jModelParam) -{ - ALOGV("queryParameter"); - sp<SoundTrigger> module = getSoundTrigger(env, thiz); - if (module == nullptr) { - return nullptr; - } - - sound_trigger_model_parameter_range_t nRange; - jint nValue = module->queryParameter(jHandle, - (sound_trigger_model_parameter_t) jModelParam, &nRange); - - if (nValue != 0) { - ALOGE("failed to query parameter error code: %d", nValue); - return nullptr; - } - - return env->NewObject(gModelParamRangeClass, gModelParamRangeCstor, nRange.start, nRange.end); -} - -static const JNINativeMethod gMethods[] = { - {"listModules", - "(Ljava/lang/String;Ljava/util/ArrayList;)I", - (void *)android_hardware_SoundTrigger_listModules}, -}; - - -static const JNINativeMethod gModuleMethods[] = { - {"native_setup", - "(Ljava/lang/String;Ljava/lang/Object;)V", - (void *)android_hardware_SoundTrigger_setup}, - {"native_finalize", - "()V", - (void *)android_hardware_SoundTrigger_finalize}, - {"detach", - "()V", - (void *)android_hardware_SoundTrigger_detach}, - {"loadSoundModel", - "(Landroid/hardware/soundtrigger/SoundTrigger$SoundModel;[I)I", - (void *)android_hardware_SoundTrigger_loadSoundModel}, - {"unloadSoundModel", - "(I)I", - (void *)android_hardware_SoundTrigger_unloadSoundModel}, - {"startRecognition", - "(ILandroid/hardware/soundtrigger/SoundTrigger$RecognitionConfig;)I", - (void *)android_hardware_SoundTrigger_startRecognition}, - {"stopRecognition", - "(I)I", - (void *)android_hardware_SoundTrigger_stopRecognition}, - {"getModelState", - "(I)I", - (void *)android_hardware_SoundTrigger_getModelState}, - {"setParameter", - "(III)I", - (void *)android_hardware_SoundTrigger_setParameter}, - {"getParameter", - "(II)I", - (void *)android_hardware_SoundTrigger_getParameter}, - {"queryParameter", - "(II)Landroid/hardware/soundtrigger/SoundTrigger$ModelParamRange;", - (void *)android_hardware_SoundTrigger_queryParameter} -}; - -int register_android_hardware_SoundTrigger(JNIEnv *env) -{ - jclass arrayListClass = FindClassOrDie(env, "java/util/ArrayList"); - gArrayListClass = MakeGlobalRefOrDie(env, arrayListClass); - gArrayListMethods.add = GetMethodIDOrDie(env, arrayListClass, "add", "(Ljava/lang/Object;)Z"); - - jclass uuidClass = FindClassOrDie(env, "java/util/UUID"); - gUUIDClass = MakeGlobalRefOrDie(env, uuidClass); - gUUIDMethods.toString = GetMethodIDOrDie(env, uuidClass, "toString", "()Ljava/lang/String;"); - - jclass exUClass = FindClassOrDie(env, kUnsupportedOperationExceptionClassPathName); - gUnsupportedOperationExceptionClass = MakeGlobalRefOrDie(env, exUClass); - - jclass exIClass = FindClassOrDie(env, kIllegalArgumentExceptionClassPathName); - gIllegalArgumentExceptionClass = MakeGlobalRefOrDie(env, exIClass); - - jclass lClass = FindClassOrDie(env, kSoundTriggerClassPathName); - gSoundTriggerClass = MakeGlobalRefOrDie(env, lClass); - - jclass moduleClass = FindClassOrDie(env, kModuleClassPathName); - gModuleClass = MakeGlobalRefOrDie(env, moduleClass); - gPostEventFromNative = GetStaticMethodIDOrDie(env, moduleClass, "postEventFromNative", - "(Ljava/lang/Object;IIILjava/lang/Object;)V"); - gModuleFields.mNativeContext = GetFieldIDOrDie(env, moduleClass, "mNativeContext", "J"); - gModuleFields.mId = GetFieldIDOrDie(env, moduleClass, "mId", "I"); - - jclass modulePropertiesClass = FindClassOrDie(env, kModulePropertiesClassPathName); - gModulePropertiesClass = MakeGlobalRefOrDie(env, modulePropertiesClass); - gModulePropertiesCstor = GetMethodIDOrDie(env, modulePropertiesClass, "<init>", - "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;IIIIIZIZIZ)V"); - - jclass soundModelClass = FindClassOrDie(env, kSoundModelClassPathName); - gSoundModelClass = MakeGlobalRefOrDie(env, soundModelClass); - gSoundModelFields.uuid = GetFieldIDOrDie(env, soundModelClass, "uuid", "Ljava/util/UUID;"); - gSoundModelFields.vendorUuid = GetFieldIDOrDie(env, soundModelClass, "vendorUuid", - "Ljava/util/UUID;"); - gSoundModelFields.data = GetFieldIDOrDie(env, soundModelClass, "data", "[B"); - - jclass genericSoundModelClass = FindClassOrDie(env, kGenericSoundModelClassPathName); - gGenericSoundModelClass = MakeGlobalRefOrDie(env, genericSoundModelClass); - - jclass keyphraseClass = FindClassOrDie(env, kKeyphraseClassPathName); - gKeyphraseClass = MakeGlobalRefOrDie(env, keyphraseClass); - gKeyphraseFields.id = GetFieldIDOrDie(env, keyphraseClass, "id", "I"); - gKeyphraseFields.recognitionModes = GetFieldIDOrDie(env, keyphraseClass, "recognitionModes", - "I"); - gKeyphraseFields.locale = GetFieldIDOrDie(env, keyphraseClass, "locale", "Ljava/lang/String;"); - gKeyphraseFields.text = GetFieldIDOrDie(env, keyphraseClass, "text", "Ljava/lang/String;"); - gKeyphraseFields.users = GetFieldIDOrDie(env, keyphraseClass, "users", "[I"); - - jclass keyphraseSoundModelClass = FindClassOrDie(env, kKeyphraseSoundModelClassPathName); - gKeyphraseSoundModelClass = MakeGlobalRefOrDie(env, keyphraseSoundModelClass); - gKeyphraseSoundModelFields.keyphrases = GetFieldIDOrDie(env, keyphraseSoundModelClass, - "keyphrases", - "[Landroid/hardware/soundtrigger/SoundTrigger$Keyphrase;"); - - jclass modelParamRangeClass = FindClassOrDie(env, kModelParamRangeClassPathName); - gModelParamRangeClass = MakeGlobalRefOrDie(env, modelParamRangeClass); - gModelParamRangeCstor = GetMethodIDOrDie(env, modelParamRangeClass, "<init>", "(II)V"); - - jclass recognitionEventClass = FindClassOrDie(env, kRecognitionEventClassPathName); - gRecognitionEventClass = MakeGlobalRefOrDie(env, recognitionEventClass); - gRecognitionEventCstor = GetMethodIDOrDie(env, recognitionEventClass, "<init>", - "(IIZIIIZLandroid/media/AudioFormat;[B)V"); - - jclass keyphraseRecognitionEventClass = FindClassOrDie(env, - kKeyphraseRecognitionEventClassPathName); - gKeyphraseRecognitionEventClass = MakeGlobalRefOrDie(env, keyphraseRecognitionEventClass); - gKeyphraseRecognitionEventCstor = GetMethodIDOrDie(env, keyphraseRecognitionEventClass, "<init>", - "(IIZIIIZLandroid/media/AudioFormat;[B[Landroid/hardware/soundtrigger/SoundTrigger$KeyphraseRecognitionExtra;)V"); - - jclass genericRecognitionEventClass = FindClassOrDie(env, - kGenericRecognitionEventClassPathName); - gGenericRecognitionEventClass = MakeGlobalRefOrDie(env, genericRecognitionEventClass); - gGenericRecognitionEventCstor = GetMethodIDOrDie(env, genericRecognitionEventClass, "<init>", - "(IIZIIIZLandroid/media/AudioFormat;[B)V"); - - jclass keyRecognitionConfigClass = FindClassOrDie(env, kRecognitionConfigClassPathName); - gRecognitionConfigClass = MakeGlobalRefOrDie(env, keyRecognitionConfigClass); - gRecognitionConfigFields.captureRequested = GetFieldIDOrDie(env, keyRecognitionConfigClass, - "captureRequested", "Z"); - gRecognitionConfigFields.keyphrases = GetFieldIDOrDie(env, keyRecognitionConfigClass, - "keyphrases", "[Landroid/hardware/soundtrigger/SoundTrigger$KeyphraseRecognitionExtra;"); - gRecognitionConfigFields.data = GetFieldIDOrDie(env, keyRecognitionConfigClass, "data", "[B"); - - jclass keyphraseRecognitionExtraClass = FindClassOrDie(env, - kKeyphraseRecognitionExtraClassPathName); - gKeyphraseRecognitionExtraClass = MakeGlobalRefOrDie(env, keyphraseRecognitionExtraClass); - gKeyphraseRecognitionExtraCstor = GetMethodIDOrDie(env, keyphraseRecognitionExtraClass, - "<init>", "(III[Landroid/hardware/soundtrigger/SoundTrigger$ConfidenceLevel;)V"); - gKeyphraseRecognitionExtraFields.id = GetFieldIDOrDie(env, gKeyphraseRecognitionExtraClass, - "id", "I"); - gKeyphraseRecognitionExtraFields.recognitionModes = GetFieldIDOrDie(env, - gKeyphraseRecognitionExtraClass, "recognitionModes", "I"); - gKeyphraseRecognitionExtraFields.coarseConfidenceLevel = GetFieldIDOrDie(env, - gKeyphraseRecognitionExtraClass, "coarseConfidenceLevel", "I"); - gKeyphraseRecognitionExtraFields.confidenceLevels = GetFieldIDOrDie(env, - gKeyphraseRecognitionExtraClass, "confidenceLevels", - "[Landroid/hardware/soundtrigger/SoundTrigger$ConfidenceLevel;"); - - jclass confidenceLevelClass = FindClassOrDie(env, kConfidenceLevelClassPathName); - gConfidenceLevelClass = MakeGlobalRefOrDie(env, confidenceLevelClass); - gConfidenceLevelCstor = GetMethodIDOrDie(env, confidenceLevelClass, "<init>", "(II)V"); - gConfidenceLevelFields.userId = GetFieldIDOrDie(env, confidenceLevelClass, "userId", "I"); - gConfidenceLevelFields.confidenceLevel = GetFieldIDOrDie(env, confidenceLevelClass, - "confidenceLevel", "I"); - - jclass audioFormatClass = FindClassOrDie(env, kAudioFormatClassPathName); - gAudioFormatClass = MakeGlobalRefOrDie(env, audioFormatClass); - gAudioFormatCstor = GetMethodIDOrDie(env, audioFormatClass, "<init>", "(IIII)V"); - - jclass soundModelEventClass = FindClassOrDie(env, kSoundModelEventClassPathName); - gSoundModelEventClass = MakeGlobalRefOrDie(env, soundModelEventClass); - gSoundModelEventCstor = GetMethodIDOrDie(env, soundModelEventClass, "<init>", "(II[B)V"); - - - RegisterMethodsOrDie(env, kSoundTriggerClassPathName, gMethods, NELEM(gMethods)); - return RegisterMethodsOrDie(env, kModuleClassPathName, gModuleMethods, NELEM(gModuleMethods)); -} diff --git a/core/jni/android_media_AudioRecord.cpp b/core/jni/android_media_AudioRecord.cpp index 342aba024fc5..6cbc5878db61 100644 --- a/core/jni/android_media_AudioRecord.cpp +++ b/core/jni/android_media_AudioRecord.cpp @@ -763,7 +763,7 @@ android_media_AudioRecord_native_getMetrics(JNIEnv *env, jobject thiz) } // get what we have for the metrics from the record session - MediaAnalyticsItem *item = NULL; + mediametrics::Item *item = NULL; status_t err = lpRecord->getMetrics(item); if (err != OK) { diff --git a/core/jni/android_media_AudioTrack.cpp b/core/jni/android_media_AudioTrack.cpp index c5049ecd3784..c979133d2493 100644 --- a/core/jni/android_media_AudioTrack.cpp +++ b/core/jni/android_media_AudioTrack.cpp @@ -1032,7 +1032,7 @@ android_media_AudioTrack_native_getMetrics(JNIEnv *env, jobject thiz) } // get what we have for the metrics from the track - MediaAnalyticsItem *item = NULL; + mediametrics::Item *item = NULL; status_t err = lpTrack->getMetrics(item); if (err != OK) { diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index c4ac89acd0db..0ca0dc857545 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -715,8 +715,16 @@ static void MountEmulatedStorage(uid_t uid, jint mount_mode, CreateDir(user_source, 0751, AID_ROOT, AID_ROOT, fail_fn); if (isFuse) { - BindMount(mount_mode == MOUNT_EXTERNAL_PASS_THROUGH ? pass_through_source : user_source, - "/storage", fail_fn); + if (mount_mode == MOUNT_EXTERNAL_PASS_THROUGH || mount_mode == + MOUNT_EXTERNAL_INSTALLER || mount_mode == MOUNT_EXTERNAL_FULL) { + // For now, MediaProvider, installers and "full" get the pass_through mount + // view, which is currently identical to the sdcardfs write view. + // + // TODO(b/146189163): scope down MOUNT_EXTERNAL_INSTALLER + BindMount(pass_through_source, "/storage", fail_fn); + } else { + BindMount(user_source, "/storage", fail_fn); + } } else { const std::string& storage_source = ExternalStorageViews[mount_mode]; BindMount(storage_source, "/storage", fail_fn); diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp index bef0ee48afc0..8c1ecae67401 100644 --- a/core/jni/fd_utils.cpp +++ b/core/jni/fd_utils.cpp @@ -33,6 +33,7 @@ // Static whitelist of open paths that the zygote is allowed to keep open. static const char* kPathWhitelist[] = { + "/apex/com.android.appsearch/javalib/framework-appsearch.jar", "/apex/com.android.conscrypt/javalib/conscrypt.jar", "/apex/com.android.ipsec/javalib/ike.jar", "/apex/com.android.media/javalib/updatable-media.jar", diff --git a/core/proto/android/app/settings_enums.proto b/core/proto/android/app/settings_enums.proto index 0c2129ffa625..44581af10b95 100644 --- a/core/proto/android/app/settings_enums.proto +++ b/core/proto/android/app/settings_enums.proto @@ -2472,4 +2472,36 @@ enum PageId { // CATEGORY: SETTINGS // OS: R SETTINGS_BUGREPORT_HANDLER = 1808; + + // Panel for adding Wi-Fi networks + // CATEGORY: SETTINGS + // OS: R + PANEL_ADD_WIFI_NETWORKS = 1809; + + // OPEN: Settings > Accessibility > Enable accessibility service > Show tutorial dialog + // CATEGORY: SETTINGS + // OS: R + DIALOG_TOGGLE_SCREEN_ACCESSIBILITY_BUTTON = 1810; + + // OPEN: Settings > Accessibility > Enable accessibility service > Show tutorial dialog in + // gesture mode + // CATEGORY: SETTINGS + // OS: R + DIALOG_TOGGLE_SCREEN_GESTURE_NAVIGATION = 1811; + + // OPEN: Settings > Accessibility > Edit shortcut dialog + // CATEGORY: SETTINGS + // OS: R + DIALOG_ACCESSIBILITY_SERVICE_EDIT_SHORTCUT = 1812; + + // OPEN: Settings > Accessibility > Magnification > Edit shortcut dialog + // CATEGORY: SETTINGS + // OS: R + DIALOG_MAGNIFICATION_EDIT_SHORTCUT = 1813; + + // OPEN: Settings > Accessibility > Color correction > Edit shortcut dialog + // CATEGORY: SETTINGS + // OS: R + DIALOG_DALTONIZER_EDIT_SHORTCUT = 1814; + } diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 4ea574dadc73..6a1ec6c849c8 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -219,6 +219,12 @@ message SecureSettingsProto { } optional Gesture gesture = 74; + message GestureNavigation { + optional SettingProto back_gesture_inset_scale_left = 1 [(android.privacy).dest = DEST_AUTOMATIC]; + optional SettingProto back_gesture_inset_scale_right = 2 [(android.privacy).dest = DEST_AUTOMATIC]; + } + optional GestureNavigation gesture_navigation = 77; + optional SettingProto immersive_mode_confirmations = 24 [ (android.privacy).dest = DEST_AUTOMATIC ]; message Incall { @@ -316,6 +322,7 @@ message SecureSettingsProto { optional SettingProto multi_press_timeout = 38 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto navigation_mode = 76 [ (android.privacy).dest = DEST_AUTOMATIC ]; + message NfcPayment { option (android.msg_privacy).dest = DEST_EXPLICIT; @@ -564,5 +571,5 @@ message SecureSettingsProto { // Please insert fields in alphabetical order and group them into messages // if possible (to avoid reaching the method limit). - // Next tag = 77; + // Next tag = 78; } diff --git a/core/proto/android/stats/docsui/docsui_enums.proto b/core/proto/android/stats/docsui/docsui_enums.proto index f648912d36eb..5963f6a7f938 100644 --- a/core/proto/android/stats/docsui/docsui_enums.proto +++ b/core/proto/android/stats/docsui/docsui_enums.proto @@ -56,6 +56,7 @@ enum Root { ROOT_VIDEOS = 9; ROOT_MTP = 10; ROOT_THIRD_PARTY_APP = 11; + ROOT_DOCUMENTS = 12; } enum ContextScope { diff --git a/core/proto/android/util/quotatracker.proto b/core/proto/android/util/quotatracker.proto new file mode 100644 index 000000000000..0dea853b0986 --- /dev/null +++ b/core/proto/android/util/quotatracker.proto @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2019 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. + */ + +syntax = "proto2"; + +package android.util.quota; + +option java_multiple_files = true; + +import "frameworks/base/core/proto/android/privacy.proto"; + +// A com.android.util.quota.QuotaTracker object. +message QuotaTrackerProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + optional bool is_enabled = 1; + + // If quota is free for everything in the tracker. + optional bool is_global_quota_free = 2; + + // Current elapsed realtime. + optional int64 elapsed_realtime = 3; + + message AlarmListener { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + // Whether the listener is waiting for an alarm or not. + optional bool is_waiting = 1; + // The time at which the alarm should go off, in the elapsed realtime timebase. Only + // valid if is_waiting is true. + optional int64 trigger_time_elapsed = 2; + } + + // Next tag: 4 +} + +// A com.android.util.quota.Category object. +message CategoryProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + // Name of the category set by the system service. + optional string name = 1; +} + +// A com.android.util.quota.Uptc object. +message UptcProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + // UserHandle value. Should be 0, 10, 11, 12, etc. where 0 is the owner. + optional int32 user_id = 1; + // Package name + optional string name = 2; + // Tag set by the system service to differentiate calls. + optional string tag = 3; +} + +// A com.android.util.quota.CountQuotaTracker object. +message CountQuotaTrackerProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + optional QuotaTrackerProto base_quota_data = 1; + + message CountLimit { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + optional CategoryProto category = 1; + optional int32 limit = 2; + optional int64 window_size_ms = 3; + } + repeated CountLimit count_limit = 2; + + message Event { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + // The time the event occurred, in the elapsed realtime timebase. + optional int64 timestamp_elapsed = 1; + } + + message ExecutionStats { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + // The time after which this record should be considered invalid (out of date), in the + // elapsed realtime timebase. + optional int64 expiration_time_elapsed = 1; + + optional int64 window_size_ms = 2; + optional int32 count_limit = 3; + + // The total number of events that occurred in the window. + optional int32 count_in_window = 4; + + // The time after which the app will be under the bucket quota. This is only valid if + // count_in_window >= count_limit. + optional int64 in_quota_time_elapsed = 5; + } + + message UptcStats { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + optional UptcProto uptc = 1; + + // True if the UPTC has been given free quota. + optional bool is_quota_free = 2; + + repeated Event events = 3; + + repeated ExecutionStats execution_stats = 4; + + optional QuotaTrackerProto.AlarmListener in_quota_alarm_listener = 5; + } + repeated UptcStats uptc_stats = 3; + + // Next tag: 4 +} + +// A com.android.util.quota.DurationQuotaTracker object. +message DurationQuotaTrackerProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + optional QuotaTrackerProto base_quota_data = 1; + + message DurationLimit { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + optional CategoryProto category = 1; + optional int64 limit_ms = 2; + optional int64 window_size_ms = 3; + } + repeated DurationLimit duration_limit = 2; + + message ExecutionStats { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + // The time after which this record should be considered invalid (out of date), in the + // elapsed realtime timebase. + optional int64 expiration_time_elapsed = 1; + + optional int32 window_size_ms = 2; + optional int64 duration_limit_ms = 3; + + // The overall session duration in the window. + optional int64 session_duration_in_window_ms = 4; + // The number of individual long-running events in the window. + optional int32 event_count_in_window = 5; + + // The time after which the app will be under the bucket quota. This is only valid if + // session_duration_in_window_ms >= duration_limit_ms. + optional int64 in_quota_time_elapsed = 6; + } + + message Timer { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + // True if the Timer is actively tracking long-running events. + optional bool is_active = 1; + // The time this timer last became active. Only valid if is_active is true. + optional int64 start_time_elapsed = 2; + // How many long-running events are currently running. Valid only if is_active is true. + optional int32 event_count = 3; + } + + message TimingSession { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + optional int64 start_time_elapsed = 1; + optional int64 end_time_elapsed = 2; + // How many events started during this session. This only count long-running events, not + // instantaneous events. + optional int32 event_count = 3; + } + + message UptcStats { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + optional UptcProto uptc = 1; + + // True if the UPTC has been given free quota. + optional bool is_quota_free = 2; + + optional Timer timer = 3; + + repeated TimingSession saved_sessions = 4; + + repeated ExecutionStats execution_stats = 5; + + optional QuotaTrackerProto.AlarmListener in_quota_alarm_listener = 6; + } + repeated UptcStats uptc_stats = 3; + + message ReachedQuotaAlarmListener { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + optional int64 trigger_time_elapsed = 1; + + message UptcTimes { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + optional UptcProto uptc = 1; + optional int64 out_of_quota_time_elapsed = 2; + } + repeated UptcTimes uptc_times = 2; + } + optional ReachedQuotaAlarmListener reached_quota_alarm_listener = 4; + + // Next tag: 5 +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index c925744fea3b..ba2f64d6db5f 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1850,6 +1850,13 @@ android:description="@string/permdesc_vibrate" android:protectionLevel="normal|instant" /> + <!-- Allows access to the vibrator always-on settings. + <p>Protection level: signature + @hide + --> + <permission android:name="android.permission.VIBRATE_ALWAYS_ON" + android:protectionLevel="signature" /> + <!-- Allows using PowerManager WakeLocks to keep processor from sleeping or screen from dimming. <p>Protection level: normal @@ -4410,6 +4417,12 @@ <permission android:name="android.permission.MANAGE_SOUND_TRIGGER" android:protectionLevel="signature|privileged" /> + <!-- Allows preempting sound trigger recognitions for the sake of capturing audio on + implementations which do not support running both concurrently. + @hide --> + <permission android:name="android.permission.PREEMPT_SOUND_TRIGGER" + android:protectionLevel="signature|privileged" /> + <!-- Must be required by system/priv apps implementing sound trigger detection services @hide @SystemApi --> diff --git a/core/res/res/layout/chooser_grid_preview_text.xml b/core/res/res/layout/chooser_grid_preview_text.xml index 9c725b93eec6..4d7846dfb9cc 100644 --- a/core/res/res/layout/chooser_grid_preview_text.xml +++ b/core/res/res/layout/chooser_grid_preview_text.xml @@ -45,7 +45,8 @@ android:ellipsize="end" android:fontFamily="@android:string/config_headlineFontFamily" android:textColor="?android:attr/textColorPrimary" - android:maxLines="2"/> + android:maxLines="2" + android:focusable="true"/> <LinearLayout android:id="@+id/copy_button" diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index d6d5c573fbb9..03f8ebd96884 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2907,10 +2907,6 @@ <!-- Whether to use voip audio mode for ims call --> <bool name="config_use_voip_mode_for_ims">false</bool> - <!-- ImsService package name to bind to by default. If none is specified in an overlay, an - empty string is passed in --> - <string name="config_ims_package"/> - <!-- String array containing numbers that shouldn't be logged. Country-specific. --> <string-array name="unloggable_phone_numbers" /> @@ -3126,7 +3122,9 @@ The path is assumed to be specified in display coordinates with pixel units and in the display's native orientation, with the origin of the coordinate system at the - center top of the display. + center top of the display. Optionally, you can append either `@left` or `@right` to the + end of the path string, in order to change the path origin to either the top left, + or top right of the display. To facilitate writing device-independent emulation overlays, the marker `@dp` can be appended after the path string to interpret coordinates in dp instead of px units. diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index de1b5baec4b2..ab10738327b3 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -4946,8 +4946,12 @@ <!-- Resolver target actions strings --> <!-- Pin this app to the top of the Sharesheet app list. [CHAR LIMIT=60]--> <string name="pin_target">Pin</string> + <!-- Pin this app to the top of the Sharesheet app list. [CHAR LIMIT=60]--> + <string name="pin_specific_target">Pin <xliff:g id="label" example="Tweet">%1$s</xliff:g></string> + <!-- Un-pin this app in the Sharesheet, so that it is sorted normally. [CHAR LIMIT=60]--> + <string name="unpin_target">Unpin </string> <!-- Un-pin this app in the Sharesheet, so that it is sorted normally. [CHAR LIMIT=60]--> - <string name="unpin_target">Unpin</string> + <string name="unpin_specific_target">Unpin <xliff:g id="label" example="Tweet">%1$s</xliff:g></string> <!-- View application info for a target. --> <string name="app_info">App info</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 011dc3961c4d..a8d30c120377 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -295,7 +295,6 @@ <java-symbol type="bool" name="config_enableBurnInProtection" /> <java-symbol type="bool" name="config_hotswapCapable" /> <java-symbol type="bool" name="config_mms_content_disposition_support" /> - <java-symbol type="string" name="config_ims_package" /> <java-symbol type="string" name="config_wwan_network_service_package" /> <java-symbol type="string" name="config_wlan_network_service_package" /> <java-symbol type="string" name="config_wwan_network_service_class" /> @@ -2958,6 +2957,8 @@ <!-- Resolver target actions --> <java-symbol type="array" name="resolver_target_actions_pin" /> <java-symbol type="array" name="resolver_target_actions_unpin" /> + <java-symbol type="string" name="pin_specific_target" /> + <java-symbol type="string" name="unpin_specific_target" /> <java-symbol type="array" name="non_removable_euicc_slots" /> diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java index 1a482604d782..68d95cd38497 100644 --- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java +++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java @@ -19,11 +19,9 @@ package android.view; import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.view.ViewRootImpl.NEW_INSETS_MODE_FULL; -import static android.view.WindowInsets.Type.navigationBars; import static android.view.WindowInsets.Type.systemBars; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.eq; @@ -118,7 +116,8 @@ public class InsetsAnimationControlImplTest { consumers.put(ITYPE_NAVIGATION_BAR, navConsumer); mController = new InsetsAnimationControlImpl(consumers, new Rect(0, 0, 500, 500), mInsetsState, mMockListener, systemBars(), - () -> mMockTransactionApplier, mMockController); + () -> mMockTransactionApplier, mMockController, 10 /* durationMs */, + false /* fade */); } @Test @@ -131,9 +130,11 @@ public class InsetsAnimationControlImplTest { @Test public void testChangeInsets() { - mController.changeInsets(Insets.of(0, 30, 40, 0)); + mController.setInsetsAndAlpha(Insets.of(0, 30, 40, 0), 1f /* alpha */, + 0f /* fraction */); mController.applyChangeInsets(new InsetsState()); assertEquals(Insets.of(0, 30, 40, 0), mController.getCurrentInsets()); + assertEquals(1f, mController.getCurrentAlpha(), 1f - mController.getCurrentAlpha()); ArgumentCaptor<SurfaceParams> captor = ArgumentCaptor.forClass(SurfaceParams.class); verify(mMockTransactionApplier).scheduleApply(captor.capture()); @@ -148,26 +149,43 @@ public class InsetsAnimationControlImplTest { } @Test + public void testChangeAlphaNoInsets() { + Insets initialInsets = mController.getCurrentInsets(); + mController.setInsetsAndAlpha(null, 0.5f, 0f /* fraction*/); + mController.applyChangeInsets(new InsetsState()); + assertEquals(0.5f, mController.getCurrentAlpha(), 0.5f - mController.getCurrentAlpha()); + assertEquals(initialInsets, mController.getCurrentInsets()); + } + + @Test + public void testChangeInsetsAndAlpha() { + mController.setInsetsAndAlpha(Insets.of(0, 30, 40, 0), 0.5f, 1f); + mController.applyChangeInsets(new InsetsState()); + assertEquals(0.5f, mController.getCurrentAlpha(), 0.5f - mController.getCurrentAlpha()); + assertEquals(Insets.of(0, 30, 40, 0), mController.getCurrentInsets()); + } + + @Test public void testFinishing() { when(mMockController.getState()).thenReturn(mInsetsState); - mController.finish(navigationBars()); + mController.finish(true /* shown */); mController.applyChangeInsets(mInsetsState); - assertFalse(mInsetsState.getSource(ITYPE_STATUS_BAR).isVisible()); + assertTrue(mInsetsState.getSource(ITYPE_STATUS_BAR).isVisible()); assertTrue(mInsetsState.getSource(ITYPE_NAVIGATION_BAR).isVisible()); - assertEquals(Insets.of(0, 0, 100, 0), mController.getCurrentInsets()); - verify(mMockController).notifyFinished(eq(mController), eq(navigationBars())); + assertEquals(Insets.of(0, 100, 100, 0), mController.getCurrentInsets()); + verify(mMockController).notifyFinished(eq(mController), eq(true /* shown */)); } @Test public void testCancelled() { mController.onCancelled(); try { - mController.changeInsets(Insets.NONE); + mController.setInsetsAndAlpha(Insets.NONE, 1f /*alpha */, 0f /* fraction */); fail("Expected exception to be thrown"); } catch (IllegalStateException ignored) { } verify(mMockListener).onCancelled(); - mController.finish(navigationBars()); + mController.finish(true /* shown */); } private void assertPosition(Matrix m, Rect original, Rect transformed) { diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index e4d8279a52cd..a89fc1e6315f 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -123,7 +123,7 @@ public class InsetsControllerTest { WindowInsetsAnimationControlListener mockListener = mock(WindowInsetsAnimationControlListener.class); - mController.controlWindowInsetsAnimation(statusBars(), mockListener); + mController.controlWindowInsetsAnimation(statusBars(), 10 /* durationMs */, mockListener); verify(mockListener).onReady(any(), anyInt()); mController.onControlsChanged(new InsetsSourceControl[0]); verify(mockListener).onCancelled(); @@ -135,7 +135,7 @@ public class InsetsControllerTest { mController.getState().setDisplayFrame(new Rect(0, 0, 200, 200)); WindowInsetsAnimationControlListener controlListener = mock(WindowInsetsAnimationControlListener.class); - mController.controlWindowInsetsAnimation(0, controlListener); + mController.controlWindowInsetsAnimation(0, 0 /* durationMs */, controlListener); verify(controlListener).onCancelled(); verify(controlListener, never()).onReady(any(), anyInt()); } @@ -331,12 +331,13 @@ public class InsetsControllerTest { InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { WindowInsetsAnimationControlListener mockListener = mock(WindowInsetsAnimationControlListener.class); - mController.controlWindowInsetsAnimation(statusBars(), mockListener); + mController.controlWindowInsetsAnimation(statusBars(), 0 /* durationMs */, + mockListener); ArgumentCaptor<WindowInsetsAnimationController> controllerCaptor = ArgumentCaptor.forClass(WindowInsetsAnimationController.class); verify(mockListener).onReady(controllerCaptor.capture(), anyInt()); - controllerCaptor.getValue().finish(0 /* shownTypes */); + controllerCaptor.getValue().finish(false /* shown */); }); waitUntilNextFrame(); InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { diff --git a/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java b/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java index 9002c2c6b184..ffc925ff82cd 100644 --- a/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java +++ b/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java @@ -111,7 +111,7 @@ public class AndroidFutureTest { } @Test - public void testWriteToParcel_Exceptionally() throws Exception { + public void testWriteToParcel_Exception() throws Exception { Parcel parcel = Parcel.obtain(); AndroidFuture<Integer> future1 = new AndroidFuture<>(); future1.completeExceptionally(new UnsupportedOperationException()); @@ -123,4 +123,30 @@ public class AndroidFutureTest { expectThrows(ExecutionException.class, future2::get); assertThat(executionException.getCause()).isInstanceOf(UnsupportedOperationException.class); } + + @Test + public void testWriteToParcel_Incomplete() throws Exception { + Parcel parcel = Parcel.obtain(); + AndroidFuture<Integer> future1 = new AndroidFuture<>(); + future1.writeToParcel(parcel, 0); + + parcel.setDataPosition(0); + AndroidFuture future2 = AndroidFuture.CREATOR.createFromParcel(parcel); + future2.complete(5); + assertThat(future1.get()).isEqualTo(5); + } + + @Test + public void testWriteToParcel_Incomplete_Exception() throws Exception { + Parcel parcel = Parcel.obtain(); + AndroidFuture<Integer> future1 = new AndroidFuture<>(); + future1.writeToParcel(parcel, 0); + + parcel.setDataPosition(0); + AndroidFuture future2 = AndroidFuture.CREATOR.createFromParcel(parcel); + future2.completeExceptionally(new UnsupportedOperationException()); + ExecutionException executionException = + expectThrows(ExecutionException.class, future1::get); + assertThat(executionException.getCause()).isInstanceOf(UnsupportedOperationException.class); + } } diff --git a/data/etc/platform.xml b/data/etc/platform.xml index 80098c5a81f5..0574775712a6 100644 --- a/data/etc/platform.xml +++ b/data/etc/platform.xml @@ -157,6 +157,7 @@ <assign-permission name="android.permission.UPDATE_DEVICE_STATS" uid="audioserver" /> <assign-permission name="android.permission.UPDATE_APP_OPS_STATS" uid="audioserver" /> <assign-permission name="android.permission.PACKAGE_USAGE_STATS" uid="audioserver" /> + <assign-permission name="android.permission.PREEMPT_SOUND_TRIGGER" uid="audioserver" /> <assign-permission name="android.permission.MODIFY_AUDIO_SETTINGS" uid="cameraserver" /> <assign-permission name="android.permission.ACCESS_SURFACE_FLINGER" uid="cameraserver" /> diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 322cbd798203..624fc50030a0 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -145,7 +145,7 @@ applications that come with the platform <permission name="android.permission.CHANGE_COMPONENT_ENABLED_STATE"/> <permission name="android.permission.CHANGE_CONFIGURATION"/> <permission name="android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST"/> - <permission name="android.permission.CONNECTIVITY_INTERNAL"/> + <permission name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/> <permission name="android.permission.CONTROL_INCALL_EXPERIENCE"/> <permission name="android.permission.DUMP"/> <permission name="android.permission.INTERACT_ACROSS_USERS"/> @@ -240,6 +240,13 @@ applications that come with the platform <permission name="android.permission.WRITE_SECURE_SETTINGS"/> </privapp-permissions> + <privapp-permissions package="com.android.tethering"> + <permission name="android.permission.MANAGE_USB"/> + <permission name="android.permission.MODIFY_PHONE_STATE"/> + <permission name="android.permission.READ_NETWORK_USAGE_HISTORY"/> + <permission name="android.permission.UPDATE_APP_OPS_STATS"/> + </privapp-permissions> + <privapp-permissions package="com.android.server.telecom"> <permission name="android.permission.BIND_CONNECTION_SERVICE"/> <permission name="android.permission.BIND_INCALL_SERVICE"/> @@ -343,6 +350,8 @@ applications that come with the platform <permission name="android.permission.ENTER_CAR_MODE_PRIORITIZED"/> <!-- Permission required for Telecom car mode CTS tests. --> <permission name="android.permission.CONTROL_INCALL_EXPERIENCE"/> + <!-- Permission required for Tethering CTS tests. --> + <permission name="android.permission.TETHER_PRIVILEGED"/> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java index 8ebac667aca3..aecef8ee3413 100644 --- a/graphics/java/android/graphics/ImageDecoder.java +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -270,7 +270,7 @@ public final class ImageDecoder implements AutoCloseable { public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { AssetFileDescriptor assetFd = null; try { - if (mUri.getScheme() == ContentResolver.SCHEME_CONTENT) { + if (mUri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { assetFd = mResolver.openTypedAssetFileDescriptor(mUri, "image/*", null); } else { diff --git a/keystore/java/android/security/Credentials.java b/keystore/java/android/security/Credentials.java index 572fa8c529df..048dee604a37 100644 --- a/keystore/java/android/security/Credentials.java +++ b/keystore/java/android/security/Credentials.java @@ -65,6 +65,9 @@ public class Credentials { /** Key prefix for VPN. */ public static final String VPN = "VPN_"; + /** Key prefix for platform VPNs. */ + public static final String PLATFORM_VPN = "PLATFORM_VPN_"; + /** Key prefix for WIFI. */ public static final String WIFI = "WIFI_"; diff --git a/libs/androidfw/LocaleDataTables.cpp b/libs/androidfw/LocaleDataTables.cpp index c276a238e2e1..c462eb7f2401 100644 --- a/libs/androidfw/LocaleDataTables.cpp +++ b/libs/androidfw/LocaleDataTables.cpp @@ -10,1439 +10,1466 @@ const char SCRIPT_CODES[][4] = { /* 6 */ {'B', 'a', 's', 's'}, /* 7 */ {'B', 'e', 'n', 'g'}, /* 8 */ {'B', 'r', 'a', 'h'}, - /* 9 */ {'C', 'a', 'n', 's'}, - /* 10 */ {'C', 'a', 'r', 'i'}, - /* 11 */ {'C', 'h', 'a', 'm'}, - /* 12 */ {'C', 'h', 'e', 'r'}, - /* 13 */ {'C', 'o', 'p', 't'}, - /* 14 */ {'C', 'p', 'r', 't'}, - /* 15 */ {'C', 'y', 'r', 'l'}, - /* 16 */ {'D', 'e', 'v', 'a'}, - /* 17 */ {'E', 'g', 'y', 'p'}, - /* 18 */ {'E', 't', 'h', 'i'}, - /* 19 */ {'G', 'e', 'o', 'r'}, - /* 20 */ {'G', 'o', 't', 'h'}, - /* 21 */ {'G', 'r', 'e', 'k'}, - /* 22 */ {'G', 'u', 'j', 'r'}, - /* 23 */ {'G', 'u', 'r', 'u'}, - /* 24 */ {'H', 'a', 'n', 's'}, - /* 25 */ {'H', 'a', 'n', 't'}, - /* 26 */ {'H', 'a', 't', 'r'}, - /* 27 */ {'H', 'e', 'b', 'r'}, - /* 28 */ {'H', 'l', 'u', 'w'}, - /* 29 */ {'H', 'm', 'n', 'g'}, - /* 30 */ {'I', 't', 'a', 'l'}, - /* 31 */ {'J', 'p', 'a', 'n'}, - /* 32 */ {'K', 'a', 'l', 'i'}, - /* 33 */ {'K', 'a', 'n', 'a'}, - /* 34 */ {'K', 'h', 'a', 'r'}, - /* 35 */ {'K', 'h', 'm', 'r'}, - /* 36 */ {'K', 'n', 'd', 'a'}, - /* 37 */ {'K', 'o', 'r', 'e'}, - /* 38 */ {'L', 'a', 'n', 'a'}, - /* 39 */ {'L', 'a', 'o', 'o'}, - /* 40 */ {'L', 'a', 't', 'n'}, - /* 41 */ {'L', 'e', 'p', 'c'}, - /* 42 */ {'L', 'i', 'n', 'a'}, - /* 43 */ {'L', 'i', 's', 'u'}, - /* 44 */ {'L', 'y', 'c', 'i'}, - /* 45 */ {'L', 'y', 'd', 'i'}, - /* 46 */ {'M', 'a', 'n', 'd'}, - /* 47 */ {'M', 'a', 'n', 'i'}, - /* 48 */ {'M', 'e', 'r', 'c'}, - /* 49 */ {'M', 'l', 'y', 'm'}, - /* 50 */ {'M', 'o', 'n', 'g'}, - /* 51 */ {'M', 'r', 'o', 'o'}, - /* 52 */ {'M', 'y', 'm', 'r'}, - /* 53 */ {'N', 'a', 'r', 'b'}, - /* 54 */ {'N', 'k', 'o', 'o'}, - /* 55 */ {'O', 'g', 'a', 'm'}, - /* 56 */ {'O', 'r', 'k', 'h'}, - /* 57 */ {'O', 'r', 'y', 'a'}, - /* 58 */ {'O', 's', 'g', 'e'}, - /* 59 */ {'P', 'a', 'u', 'c'}, - /* 60 */ {'P', 'h', 'l', 'i'}, - /* 61 */ {'P', 'h', 'n', 'x'}, - /* 62 */ {'P', 'l', 'r', 'd'}, - /* 63 */ {'P', 'r', 't', 'i'}, - /* 64 */ {'R', 'u', 'n', 'r'}, - /* 65 */ {'S', 'a', 'm', 'r'}, - /* 66 */ {'S', 'a', 'r', 'b'}, - /* 67 */ {'S', 'a', 'u', 'r'}, - /* 68 */ {'S', 'g', 'n', 'w'}, - /* 69 */ {'S', 'i', 'n', 'h'}, - /* 70 */ {'S', 'o', 'r', 'a'}, - /* 71 */ {'S', 'y', 'r', 'c'}, - /* 72 */ {'T', 'a', 'l', 'e'}, - /* 73 */ {'T', 'a', 'l', 'u'}, - /* 74 */ {'T', 'a', 'm', 'l'}, - /* 75 */ {'T', 'a', 'n', 'g'}, - /* 76 */ {'T', 'a', 'v', 't'}, - /* 77 */ {'T', 'e', 'l', 'u'}, - /* 78 */ {'T', 'f', 'n', 'g'}, - /* 79 */ {'T', 'h', 'a', 'a'}, - /* 80 */ {'T', 'h', 'a', 'i'}, - /* 81 */ {'T', 'i', 'b', 't'}, - /* 82 */ {'U', 'g', 'a', 'r'}, - /* 83 */ {'V', 'a', 'i', 'i'}, - /* 84 */ {'X', 'p', 'e', 'o'}, - /* 85 */ {'X', 's', 'u', 'x'}, - /* 86 */ {'Y', 'i', 'i', 'i'}, - /* 87 */ {'~', '~', '~', 'A'}, - /* 88 */ {'~', '~', '~', 'B'}, + /* 9 */ {'C', 'a', 'k', 'm'}, + /* 10 */ {'C', 'a', 'n', 's'}, + /* 11 */ {'C', 'a', 'r', 'i'}, + /* 12 */ {'C', 'h', 'a', 'm'}, + /* 13 */ {'C', 'h', 'e', 'r'}, + /* 14 */ {'C', 'o', 'p', 't'}, + /* 15 */ {'C', 'p', 'r', 't'}, + /* 16 */ {'C', 'y', 'r', 'l'}, + /* 17 */ {'D', 'e', 'v', 'a'}, + /* 18 */ {'E', 'g', 'y', 'p'}, + /* 19 */ {'E', 't', 'h', 'i'}, + /* 20 */ {'G', 'e', 'o', 'r'}, + /* 21 */ {'G', 'o', 'n', 'g'}, + /* 22 */ {'G', 'o', 'n', 'm'}, + /* 23 */ {'G', 'o', 't', 'h'}, + /* 24 */ {'G', 'r', 'e', 'k'}, + /* 25 */ {'G', 'u', 'j', 'r'}, + /* 26 */ {'G', 'u', 'r', 'u'}, + /* 27 */ {'H', 'a', 'n', 's'}, + /* 28 */ {'H', 'a', 'n', 't'}, + /* 29 */ {'H', 'a', 't', 'r'}, + /* 30 */ {'H', 'e', 'b', 'r'}, + /* 31 */ {'H', 'l', 'u', 'w'}, + /* 32 */ {'H', 'm', 'n', 'g'}, + /* 33 */ {'H', 'm', 'n', 'p'}, + /* 34 */ {'I', 't', 'a', 'l'}, + /* 35 */ {'J', 'p', 'a', 'n'}, + /* 36 */ {'K', 'a', 'l', 'i'}, + /* 37 */ {'K', 'a', 'n', 'a'}, + /* 38 */ {'K', 'h', 'a', 'r'}, + /* 39 */ {'K', 'h', 'm', 'r'}, + /* 40 */ {'K', 'n', 'd', 'a'}, + /* 41 */ {'K', 'o', 'r', 'e'}, + /* 42 */ {'L', 'a', 'n', 'a'}, + /* 43 */ {'L', 'a', 'o', 'o'}, + /* 44 */ {'L', 'a', 't', 'n'}, + /* 45 */ {'L', 'e', 'p', 'c'}, + /* 46 */ {'L', 'i', 'n', 'a'}, + /* 47 */ {'L', 'i', 's', 'u'}, + /* 48 */ {'L', 'y', 'c', 'i'}, + /* 49 */ {'L', 'y', 'd', 'i'}, + /* 50 */ {'M', 'a', 'n', 'd'}, + /* 51 */ {'M', 'a', 'n', 'i'}, + /* 52 */ {'M', 'e', 'r', 'c'}, + /* 53 */ {'M', 'l', 'y', 'm'}, + /* 54 */ {'M', 'o', 'n', 'g'}, + /* 55 */ {'M', 'r', 'o', 'o'}, + /* 56 */ {'M', 'y', 'm', 'r'}, + /* 57 */ {'N', 'a', 'r', 'b'}, + /* 58 */ {'N', 'k', 'o', 'o'}, + /* 59 */ {'N', 's', 'h', 'u'}, + /* 60 */ {'O', 'g', 'a', 'm'}, + /* 61 */ {'O', 'r', 'k', 'h'}, + /* 62 */ {'O', 'r', 'y', 'a'}, + /* 63 */ {'O', 's', 'g', 'e'}, + /* 64 */ {'P', 'a', 'u', 'c'}, + /* 65 */ {'P', 'h', 'l', 'i'}, + /* 66 */ {'P', 'h', 'n', 'x'}, + /* 67 */ {'P', 'l', 'r', 'd'}, + /* 68 */ {'P', 'r', 't', 'i'}, + /* 69 */ {'R', 'u', 'n', 'r'}, + /* 70 */ {'S', 'a', 'm', 'r'}, + /* 71 */ {'S', 'a', 'r', 'b'}, + /* 72 */ {'S', 'a', 'u', 'r'}, + /* 73 */ {'S', 'g', 'n', 'w'}, + /* 74 */ {'S', 'i', 'n', 'h'}, + /* 75 */ {'S', 'o', 'g', 'd'}, + /* 76 */ {'S', 'o', 'r', 'a'}, + /* 77 */ {'S', 'o', 'y', 'o'}, + /* 78 */ {'S', 'y', 'r', 'c'}, + /* 79 */ {'T', 'a', 'l', 'e'}, + /* 80 */ {'T', 'a', 'l', 'u'}, + /* 81 */ {'T', 'a', 'm', 'l'}, + /* 82 */ {'T', 'a', 'n', 'g'}, + /* 83 */ {'T', 'a', 'v', 't'}, + /* 84 */ {'T', 'e', 'l', 'u'}, + /* 85 */ {'T', 'f', 'n', 'g'}, + /* 86 */ {'T', 'h', 'a', 'a'}, + /* 87 */ {'T', 'h', 'a', 'i'}, + /* 88 */ {'T', 'i', 'b', 't'}, + /* 89 */ {'U', 'g', 'a', 'r'}, + /* 90 */ {'V', 'a', 'i', 'i'}, + /* 91 */ {'W', 'c', 'h', 'o'}, + /* 92 */ {'X', 'p', 'e', 'o'}, + /* 93 */ {'X', 's', 'u', 'x'}, + /* 94 */ {'Y', 'i', 'i', 'i'}, + /* 95 */ {'~', '~', '~', 'A'}, + /* 96 */ {'~', '~', '~', 'B'}, }; const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({ - {0x61610000u, 40u}, // aa -> Latn - {0xA0000000u, 40u}, // aai -> Latn - {0xA8000000u, 40u}, // aak -> Latn - {0xD0000000u, 40u}, // aau -> Latn - {0x61620000u, 15u}, // ab -> Cyrl - {0xA0200000u, 40u}, // abi -> Latn - {0xC4200000u, 40u}, // abr -> Latn - {0xCC200000u, 40u}, // abt -> Latn - {0xE0200000u, 40u}, // aby -> Latn - {0x8C400000u, 40u}, // acd -> Latn - {0x90400000u, 40u}, // ace -> Latn - {0x9C400000u, 40u}, // ach -> Latn - {0x80600000u, 40u}, // ada -> Latn - {0x90600000u, 40u}, // ade -> Latn - {0xA4600000u, 40u}, // adj -> Latn - {0xE0600000u, 15u}, // ady -> Cyrl - {0xE4600000u, 40u}, // adz -> Latn + {0x61610000u, 44u}, // aa -> Latn + {0xA0000000u, 44u}, // aai -> Latn + {0xA8000000u, 44u}, // aak -> Latn + {0xD0000000u, 44u}, // aau -> Latn + {0x61620000u, 16u}, // ab -> Cyrl + {0xA0200000u, 44u}, // abi -> Latn + {0xC0200000u, 16u}, // abq -> Cyrl + {0xC4200000u, 44u}, // abr -> Latn + {0xCC200000u, 44u}, // abt -> Latn + {0xE0200000u, 44u}, // aby -> Latn + {0x8C400000u, 44u}, // acd -> Latn + {0x90400000u, 44u}, // ace -> Latn + {0x9C400000u, 44u}, // ach -> Latn + {0x80600000u, 44u}, // ada -> Latn + {0x90600000u, 44u}, // ade -> Latn + {0xA4600000u, 44u}, // adj -> Latn + {0xE0600000u, 16u}, // ady -> Cyrl + {0xE4600000u, 44u}, // adz -> Latn {0x61650000u, 4u}, // ae -> Avst {0x84800000u, 1u}, // aeb -> Arab - {0xE0800000u, 40u}, // aey -> Latn - {0x61660000u, 40u}, // af -> Latn - {0x88C00000u, 40u}, // agc -> Latn - {0x8CC00000u, 40u}, // agd -> Latn - {0x98C00000u, 40u}, // agg -> Latn - {0xB0C00000u, 40u}, // agm -> Latn - {0xB8C00000u, 40u}, // ago -> Latn - {0xC0C00000u, 40u}, // agq -> Latn - {0x80E00000u, 40u}, // aha -> Latn - {0xACE00000u, 40u}, // ahl -> Latn + {0xE0800000u, 44u}, // aey -> Latn + {0x61660000u, 44u}, // af -> Latn + {0x88C00000u, 44u}, // agc -> Latn + {0x8CC00000u, 44u}, // agd -> Latn + {0x98C00000u, 44u}, // agg -> Latn + {0xB0C00000u, 44u}, // agm -> Latn + {0xB8C00000u, 44u}, // ago -> Latn + {0xC0C00000u, 44u}, // agq -> Latn + {0x80E00000u, 44u}, // aha -> Latn + {0xACE00000u, 44u}, // ahl -> Latn {0xB8E00000u, 0u}, // aho -> Ahom - {0x99200000u, 40u}, // ajg -> Latn - {0x616B0000u, 40u}, // ak -> Latn - {0xA9400000u, 85u}, // akk -> Xsux - {0x81600000u, 40u}, // ala -> Latn - {0xA1600000u, 40u}, // ali -> Latn - {0xB5600000u, 40u}, // aln -> Latn - {0xCD600000u, 15u}, // alt -> Cyrl - {0x616D0000u, 18u}, // am -> Ethi - {0xB1800000u, 40u}, // amm -> Latn - {0xB5800000u, 40u}, // amn -> Latn - {0xB9800000u, 40u}, // amo -> Latn - {0xBD800000u, 40u}, // amp -> Latn - {0x89A00000u, 40u}, // anc -> Latn - {0xA9A00000u, 40u}, // ank -> Latn - {0xB5A00000u, 40u}, // ann -> Latn - {0xE1A00000u, 40u}, // any -> Latn - {0xA5C00000u, 40u}, // aoj -> Latn - {0xB1C00000u, 40u}, // aom -> Latn - {0xE5C00000u, 40u}, // aoz -> Latn + {0x99200000u, 44u}, // ajg -> Latn + {0x616B0000u, 44u}, // ak -> Latn + {0xA9400000u, 93u}, // akk -> Xsux + {0x81600000u, 44u}, // ala -> Latn + {0xA1600000u, 44u}, // ali -> Latn + {0xB5600000u, 44u}, // aln -> Latn + {0xCD600000u, 16u}, // alt -> Cyrl + {0x616D0000u, 19u}, // am -> Ethi + {0xB1800000u, 44u}, // amm -> Latn + {0xB5800000u, 44u}, // amn -> Latn + {0xB9800000u, 44u}, // amo -> Latn + {0xBD800000u, 44u}, // amp -> Latn + {0x89A00000u, 44u}, // anc -> Latn + {0xA9A00000u, 44u}, // ank -> Latn + {0xB5A00000u, 44u}, // ann -> Latn + {0xE1A00000u, 44u}, // any -> Latn + {0xA5C00000u, 44u}, // aoj -> Latn + {0xB1C00000u, 44u}, // aom -> Latn + {0xE5C00000u, 44u}, // aoz -> Latn {0x89E00000u, 1u}, // apc -> Arab {0x8DE00000u, 1u}, // apd -> Arab - {0x91E00000u, 40u}, // ape -> Latn - {0xC5E00000u, 40u}, // apr -> Latn - {0xC9E00000u, 40u}, // aps -> Latn - {0xE5E00000u, 40u}, // apz -> Latn + {0x91E00000u, 44u}, // ape -> Latn + {0xC5E00000u, 44u}, // apr -> Latn + {0xC9E00000u, 44u}, // aps -> Latn + {0xE5E00000u, 44u}, // apz -> Latn {0x61720000u, 1u}, // ar -> Arab - {0x61725842u, 88u}, // ar-XB -> ~~~B + {0x61725842u, 96u}, // ar-XB -> ~~~B {0x8A200000u, 2u}, // arc -> Armi - {0x9E200000u, 40u}, // arh -> Latn - {0xB6200000u, 40u}, // arn -> Latn - {0xBA200000u, 40u}, // aro -> Latn + {0x9E200000u, 44u}, // arh -> Latn + {0xB6200000u, 44u}, // arn -> Latn + {0xBA200000u, 44u}, // aro -> Latn {0xC2200000u, 1u}, // arq -> Arab {0xE2200000u, 1u}, // ary -> Arab {0xE6200000u, 1u}, // arz -> Arab {0x61730000u, 7u}, // as -> Beng - {0x82400000u, 40u}, // asa -> Latn - {0x92400000u, 68u}, // ase -> Sgnw - {0x9A400000u, 40u}, // asg -> Latn - {0xBA400000u, 40u}, // aso -> Latn - {0xCE400000u, 40u}, // ast -> Latn - {0x82600000u, 40u}, // ata -> Latn - {0x9A600000u, 40u}, // atg -> Latn - {0xA6600000u, 40u}, // atj -> Latn - {0xE2800000u, 40u}, // auy -> Latn - {0x61760000u, 15u}, // av -> Cyrl + {0x82400000u, 44u}, // asa -> Latn + {0x92400000u, 73u}, // ase -> Sgnw + {0x9A400000u, 44u}, // asg -> Latn + {0xBA400000u, 44u}, // aso -> Latn + {0xCE400000u, 44u}, // ast -> Latn + {0x82600000u, 44u}, // ata -> Latn + {0x9A600000u, 44u}, // atg -> Latn + {0xA6600000u, 44u}, // atj -> Latn + {0xE2800000u, 44u}, // auy -> Latn + {0x61760000u, 16u}, // av -> Cyrl {0xAEA00000u, 1u}, // avl -> Arab - {0xB6A00000u, 40u}, // avn -> Latn - {0xCEA00000u, 40u}, // avt -> Latn - {0xD2A00000u, 40u}, // avu -> Latn - {0x82C00000u, 16u}, // awa -> Deva - {0x86C00000u, 40u}, // awb -> Latn - {0xBAC00000u, 40u}, // awo -> Latn - {0xDEC00000u, 40u}, // awx -> Latn - {0x61790000u, 40u}, // ay -> Latn - {0x87000000u, 40u}, // ayb -> Latn - {0x617A0000u, 40u}, // az -> Latn + {0xB6A00000u, 44u}, // avn -> Latn + {0xCEA00000u, 44u}, // avt -> Latn + {0xD2A00000u, 44u}, // avu -> Latn + {0x82C00000u, 17u}, // awa -> Deva + {0x86C00000u, 44u}, // awb -> Latn + {0xBAC00000u, 44u}, // awo -> Latn + {0xDEC00000u, 44u}, // awx -> Latn + {0x61790000u, 44u}, // ay -> Latn + {0x87000000u, 44u}, // ayb -> Latn + {0x617A0000u, 44u}, // az -> Latn {0x617A4951u, 1u}, // az-IQ -> Arab {0x617A4952u, 1u}, // az-IR -> Arab - {0x617A5255u, 15u}, // az-RU -> Cyrl - {0x62610000u, 15u}, // ba -> Cyrl + {0x617A5255u, 16u}, // az-RU -> Cyrl + {0x62610000u, 16u}, // ba -> Cyrl {0xAC010000u, 1u}, // bal -> Arab - {0xB4010000u, 40u}, // ban -> Latn - {0xBC010000u, 16u}, // bap -> Deva - {0xC4010000u, 40u}, // bar -> Latn - {0xC8010000u, 40u}, // bas -> Latn - {0xD4010000u, 40u}, // bav -> Latn + {0xB4010000u, 44u}, // ban -> Latn + {0xBC010000u, 17u}, // bap -> Deva + {0xC4010000u, 44u}, // bar -> Latn + {0xC8010000u, 44u}, // bas -> Latn + {0xD4010000u, 44u}, // bav -> Latn {0xDC010000u, 5u}, // bax -> Bamu - {0x80210000u, 40u}, // bba -> Latn - {0x84210000u, 40u}, // bbb -> Latn - {0x88210000u, 40u}, // bbc -> Latn - {0x8C210000u, 40u}, // bbd -> Latn - {0xA4210000u, 40u}, // bbj -> Latn - {0xBC210000u, 40u}, // bbp -> Latn - {0xC4210000u, 40u}, // bbr -> Latn - {0x94410000u, 40u}, // bcf -> Latn - {0x9C410000u, 40u}, // bch -> Latn - {0xA0410000u, 40u}, // bci -> Latn - {0xB0410000u, 40u}, // bcm -> Latn - {0xB4410000u, 40u}, // bcn -> Latn - {0xB8410000u, 40u}, // bco -> Latn - {0xC0410000u, 18u}, // bcq -> Ethi - {0xD0410000u, 40u}, // bcu -> Latn - {0x8C610000u, 40u}, // bdd -> Latn - {0x62650000u, 15u}, // be -> Cyrl - {0x94810000u, 40u}, // bef -> Latn - {0x9C810000u, 40u}, // beh -> Latn + {0x80210000u, 44u}, // bba -> Latn + {0x84210000u, 44u}, // bbb -> Latn + {0x88210000u, 44u}, // bbc -> Latn + {0x8C210000u, 44u}, // bbd -> Latn + {0xA4210000u, 44u}, // bbj -> Latn + {0xBC210000u, 44u}, // bbp -> Latn + {0xC4210000u, 44u}, // bbr -> Latn + {0x94410000u, 44u}, // bcf -> Latn + {0x9C410000u, 44u}, // bch -> Latn + {0xA0410000u, 44u}, // bci -> Latn + {0xB0410000u, 44u}, // bcm -> Latn + {0xB4410000u, 44u}, // bcn -> Latn + {0xB8410000u, 44u}, // bco -> Latn + {0xC0410000u, 19u}, // bcq -> Ethi + {0xD0410000u, 44u}, // bcu -> Latn + {0x8C610000u, 44u}, // bdd -> Latn + {0x62650000u, 16u}, // be -> Cyrl + {0x94810000u, 44u}, // bef -> Latn + {0x9C810000u, 44u}, // beh -> Latn {0xA4810000u, 1u}, // bej -> Arab - {0xB0810000u, 40u}, // bem -> Latn - {0xCC810000u, 40u}, // bet -> Latn - {0xD8810000u, 40u}, // bew -> Latn - {0xDC810000u, 40u}, // bex -> Latn - {0xE4810000u, 40u}, // bez -> Latn - {0x8CA10000u, 40u}, // bfd -> Latn - {0xC0A10000u, 74u}, // bfq -> Taml + {0xB0810000u, 44u}, // bem -> Latn + {0xCC810000u, 44u}, // bet -> Latn + {0xD8810000u, 44u}, // bew -> Latn + {0xDC810000u, 44u}, // bex -> Latn + {0xE4810000u, 44u}, // bez -> Latn + {0x8CA10000u, 44u}, // bfd -> Latn + {0xC0A10000u, 81u}, // bfq -> Taml {0xCCA10000u, 1u}, // bft -> Arab - {0xE0A10000u, 16u}, // bfy -> Deva - {0x62670000u, 15u}, // bg -> Cyrl - {0x88C10000u, 16u}, // bgc -> Deva + {0xE0A10000u, 17u}, // bfy -> Deva + {0x62670000u, 16u}, // bg -> Cyrl + {0x88C10000u, 17u}, // bgc -> Deva {0xB4C10000u, 1u}, // bgn -> Arab - {0xDCC10000u, 21u}, // bgx -> Grek - {0x84E10000u, 16u}, // bhb -> Deva - {0x98E10000u, 40u}, // bhg -> Latn - {0xA0E10000u, 16u}, // bhi -> Deva - {0xA8E10000u, 40u}, // bhk -> Latn - {0xACE10000u, 40u}, // bhl -> Latn - {0xB8E10000u, 16u}, // bho -> Deva - {0xE0E10000u, 40u}, // bhy -> Latn - {0x62690000u, 40u}, // bi -> Latn - {0x85010000u, 40u}, // bib -> Latn - {0x99010000u, 40u}, // big -> Latn - {0xA9010000u, 40u}, // bik -> Latn - {0xB1010000u, 40u}, // bim -> Latn - {0xB5010000u, 40u}, // bin -> Latn - {0xB9010000u, 40u}, // bio -> Latn - {0xC1010000u, 40u}, // biq -> Latn - {0x9D210000u, 40u}, // bjh -> Latn - {0xA1210000u, 18u}, // bji -> Ethi - {0xA5210000u, 16u}, // bjj -> Deva - {0xB5210000u, 40u}, // bjn -> Latn - {0xB9210000u, 40u}, // bjo -> Latn - {0xC5210000u, 40u}, // bjr -> Latn - {0xE5210000u, 40u}, // bjz -> Latn - {0x89410000u, 40u}, // bkc -> Latn - {0xB1410000u, 40u}, // bkm -> Latn - {0xC1410000u, 40u}, // bkq -> Latn - {0xD1410000u, 40u}, // bku -> Latn - {0xD5410000u, 40u}, // bkv -> Latn - {0xCD610000u, 76u}, // blt -> Tavt - {0x626D0000u, 40u}, // bm -> Latn - {0x9D810000u, 40u}, // bmh -> Latn - {0xA9810000u, 40u}, // bmk -> Latn - {0xC1810000u, 40u}, // bmq -> Latn - {0xD1810000u, 40u}, // bmu -> Latn + {0xDCC10000u, 24u}, // bgx -> Grek + {0x84E10000u, 17u}, // bhb -> Deva + {0x98E10000u, 44u}, // bhg -> Latn + {0xA0E10000u, 17u}, // bhi -> Deva + {0xA8E10000u, 44u}, // bhk -> Latn + {0xACE10000u, 44u}, // bhl -> Latn + {0xB8E10000u, 17u}, // bho -> Deva + {0xE0E10000u, 44u}, // bhy -> Latn + {0x62690000u, 44u}, // bi -> Latn + {0x85010000u, 44u}, // bib -> Latn + {0x99010000u, 44u}, // big -> Latn + {0xA9010000u, 44u}, // bik -> Latn + {0xB1010000u, 44u}, // bim -> Latn + {0xB5010000u, 44u}, // bin -> Latn + {0xB9010000u, 44u}, // bio -> Latn + {0xC1010000u, 44u}, // biq -> Latn + {0x9D210000u, 44u}, // bjh -> Latn + {0xA1210000u, 19u}, // bji -> Ethi + {0xA5210000u, 17u}, // bjj -> Deva + {0xB5210000u, 44u}, // bjn -> Latn + {0xB9210000u, 44u}, // bjo -> Latn + {0xC5210000u, 44u}, // bjr -> Latn + {0xCD210000u, 44u}, // bjt -> Latn + {0xE5210000u, 44u}, // bjz -> Latn + {0x89410000u, 44u}, // bkc -> Latn + {0xB1410000u, 44u}, // bkm -> Latn + {0xC1410000u, 44u}, // bkq -> Latn + {0xD1410000u, 44u}, // bku -> Latn + {0xD5410000u, 44u}, // bkv -> Latn + {0xCD610000u, 83u}, // blt -> Tavt + {0x626D0000u, 44u}, // bm -> Latn + {0x9D810000u, 44u}, // bmh -> Latn + {0xA9810000u, 44u}, // bmk -> Latn + {0xC1810000u, 44u}, // bmq -> Latn + {0xD1810000u, 44u}, // bmu -> Latn {0x626E0000u, 7u}, // bn -> Beng - {0x99A10000u, 40u}, // bng -> Latn - {0xB1A10000u, 40u}, // bnm -> Latn - {0xBDA10000u, 40u}, // bnp -> Latn - {0x626F0000u, 81u}, // bo -> Tibt - {0xA5C10000u, 40u}, // boj -> Latn - {0xB1C10000u, 40u}, // bom -> Latn - {0xB5C10000u, 40u}, // bon -> Latn + {0x99A10000u, 44u}, // bng -> Latn + {0xB1A10000u, 44u}, // bnm -> Latn + {0xBDA10000u, 44u}, // bnp -> Latn + {0x626F0000u, 88u}, // bo -> Tibt + {0xA5C10000u, 44u}, // boj -> Latn + {0xB1C10000u, 44u}, // bom -> Latn + {0xB5C10000u, 44u}, // bon -> Latn {0xE1E10000u, 7u}, // bpy -> Beng - {0x8A010000u, 40u}, // bqc -> Latn + {0x8A010000u, 44u}, // bqc -> Latn {0xA2010000u, 1u}, // bqi -> Arab - {0xBE010000u, 40u}, // bqp -> Latn - {0xD6010000u, 40u}, // bqv -> Latn - {0x62720000u, 40u}, // br -> Latn - {0x82210000u, 16u}, // bra -> Deva + {0xBE010000u, 44u}, // bqp -> Latn + {0xD6010000u, 44u}, // bqv -> Latn + {0x62720000u, 44u}, // br -> Latn + {0x82210000u, 17u}, // bra -> Deva {0x9E210000u, 1u}, // brh -> Arab - {0xDE210000u, 16u}, // brx -> Deva - {0xE6210000u, 40u}, // brz -> Latn - {0x62730000u, 40u}, // bs -> Latn - {0xA6410000u, 40u}, // bsj -> Latn + {0xDE210000u, 17u}, // brx -> Deva + {0xE6210000u, 44u}, // brz -> Latn + {0x62730000u, 44u}, // bs -> Latn + {0xA6410000u, 44u}, // bsj -> Latn {0xC2410000u, 6u}, // bsq -> Bass - {0xCA410000u, 40u}, // bss -> Latn - {0xCE410000u, 18u}, // bst -> Ethi - {0xBA610000u, 40u}, // bto -> Latn - {0xCE610000u, 40u}, // btt -> Latn - {0xD6610000u, 16u}, // btv -> Deva - {0x82810000u, 15u}, // bua -> Cyrl - {0x8A810000u, 40u}, // buc -> Latn - {0x8E810000u, 40u}, // bud -> Latn - {0x9A810000u, 40u}, // bug -> Latn - {0xAA810000u, 40u}, // buk -> Latn - {0xB2810000u, 40u}, // bum -> Latn - {0xBA810000u, 40u}, // buo -> Latn - {0xCA810000u, 40u}, // bus -> Latn - {0xD2810000u, 40u}, // buu -> Latn - {0x86A10000u, 40u}, // bvb -> Latn - {0x8EC10000u, 40u}, // bwd -> Latn - {0xC6C10000u, 40u}, // bwr -> Latn - {0x9EE10000u, 40u}, // bxh -> Latn - {0x93010000u, 40u}, // bye -> Latn - {0xB7010000u, 18u}, // byn -> Ethi - {0xC7010000u, 40u}, // byr -> Latn - {0xCB010000u, 40u}, // bys -> Latn - {0xD7010000u, 40u}, // byv -> Latn - {0xDF010000u, 40u}, // byx -> Latn - {0x83210000u, 40u}, // bza -> Latn - {0x93210000u, 40u}, // bze -> Latn - {0x97210000u, 40u}, // bzf -> Latn - {0x9F210000u, 40u}, // bzh -> Latn - {0xDB210000u, 40u}, // bzw -> Latn - {0x63610000u, 40u}, // ca -> Latn - {0xB4020000u, 40u}, // can -> Latn - {0xA4220000u, 40u}, // cbj -> Latn - {0x9C420000u, 40u}, // cch -> Latn - {0xBC420000u, 7u}, // ccp -> Beng - {0x63650000u, 15u}, // ce -> Cyrl - {0x84820000u, 40u}, // ceb -> Latn - {0x80A20000u, 40u}, // cfa -> Latn - {0x98C20000u, 40u}, // cgg -> Latn - {0x63680000u, 40u}, // ch -> Latn - {0xA8E20000u, 40u}, // chk -> Latn - {0xB0E20000u, 15u}, // chm -> Cyrl - {0xB8E20000u, 40u}, // cho -> Latn - {0xBCE20000u, 40u}, // chp -> Latn - {0xC4E20000u, 12u}, // chr -> Cher + {0xCA410000u, 44u}, // bss -> Latn + {0xCE410000u, 19u}, // bst -> Ethi + {0xBA610000u, 44u}, // bto -> Latn + {0xCE610000u, 44u}, // btt -> Latn + {0xD6610000u, 17u}, // btv -> Deva + {0x82810000u, 16u}, // bua -> Cyrl + {0x8A810000u, 44u}, // buc -> Latn + {0x8E810000u, 44u}, // bud -> Latn + {0x9A810000u, 44u}, // bug -> Latn + {0xAA810000u, 44u}, // buk -> Latn + {0xB2810000u, 44u}, // bum -> Latn + {0xBA810000u, 44u}, // buo -> Latn + {0xCA810000u, 44u}, // bus -> Latn + {0xD2810000u, 44u}, // buu -> Latn + {0x86A10000u, 44u}, // bvb -> Latn + {0x8EC10000u, 44u}, // bwd -> Latn + {0xC6C10000u, 44u}, // bwr -> Latn + {0x9EE10000u, 44u}, // bxh -> Latn + {0x93010000u, 44u}, // bye -> Latn + {0xB7010000u, 19u}, // byn -> Ethi + {0xC7010000u, 44u}, // byr -> Latn + {0xCB010000u, 44u}, // bys -> Latn + {0xD7010000u, 44u}, // byv -> Latn + {0xDF010000u, 44u}, // byx -> Latn + {0x83210000u, 44u}, // bza -> Latn + {0x93210000u, 44u}, // bze -> Latn + {0x97210000u, 44u}, // bzf -> Latn + {0x9F210000u, 44u}, // bzh -> Latn + {0xDB210000u, 44u}, // bzw -> Latn + {0x63610000u, 44u}, // ca -> Latn + {0xB4020000u, 44u}, // can -> Latn + {0xA4220000u, 44u}, // cbj -> Latn + {0x9C420000u, 44u}, // cch -> Latn + {0xBC420000u, 9u}, // ccp -> Cakm + {0x63650000u, 16u}, // ce -> Cyrl + {0x84820000u, 44u}, // ceb -> Latn + {0x80A20000u, 44u}, // cfa -> Latn + {0x98C20000u, 44u}, // cgg -> Latn + {0x63680000u, 44u}, // ch -> Latn + {0xA8E20000u, 44u}, // chk -> Latn + {0xB0E20000u, 16u}, // chm -> Cyrl + {0xB8E20000u, 44u}, // cho -> Latn + {0xBCE20000u, 44u}, // chp -> Latn + {0xC4E20000u, 13u}, // chr -> Cher {0x81220000u, 1u}, // cja -> Arab - {0xB1220000u, 11u}, // cjm -> Cham - {0xD5220000u, 40u}, // cjv -> Latn + {0xB1220000u, 12u}, // cjm -> Cham + {0xD5220000u, 44u}, // cjv -> Latn {0x85420000u, 1u}, // ckb -> Arab - {0xAD420000u, 40u}, // ckl -> Latn - {0xB9420000u, 40u}, // cko -> Latn - {0xE1420000u, 40u}, // cky -> Latn - {0x81620000u, 40u}, // cla -> Latn - {0x91820000u, 40u}, // cme -> Latn - {0x636F0000u, 40u}, // co -> Latn - {0xBDC20000u, 13u}, // cop -> Copt - {0xC9E20000u, 40u}, // cps -> Latn - {0x63720000u, 9u}, // cr -> Cans - {0xA6220000u, 9u}, // crj -> Cans - {0xAA220000u, 9u}, // crk -> Cans - {0xAE220000u, 9u}, // crl -> Cans - {0xB2220000u, 9u}, // crm -> Cans - {0xCA220000u, 40u}, // crs -> Latn - {0x63730000u, 40u}, // cs -> Latn - {0x86420000u, 40u}, // csb -> Latn - {0xDA420000u, 9u}, // csw -> Cans - {0x8E620000u, 59u}, // ctd -> Pauc - {0x63750000u, 15u}, // cu -> Cyrl - {0x63760000u, 15u}, // cv -> Cyrl - {0x63790000u, 40u}, // cy -> Latn - {0x64610000u, 40u}, // da -> Latn - {0x8C030000u, 40u}, // dad -> Latn - {0x94030000u, 40u}, // daf -> Latn - {0x98030000u, 40u}, // dag -> Latn - {0x9C030000u, 40u}, // dah -> Latn - {0xA8030000u, 40u}, // dak -> Latn - {0xC4030000u, 15u}, // dar -> Cyrl - {0xD4030000u, 40u}, // dav -> Latn - {0x8C230000u, 40u}, // dbd -> Latn - {0xC0230000u, 40u}, // dbq -> Latn + {0xAD420000u, 44u}, // ckl -> Latn + {0xB9420000u, 44u}, // cko -> Latn + {0xE1420000u, 44u}, // cky -> Latn + {0x81620000u, 44u}, // cla -> Latn + {0x91820000u, 44u}, // cme -> Latn + {0x99820000u, 77u}, // cmg -> Soyo + {0x636F0000u, 44u}, // co -> Latn + {0xBDC20000u, 14u}, // cop -> Copt + {0xC9E20000u, 44u}, // cps -> Latn + {0x63720000u, 10u}, // cr -> Cans + {0x9E220000u, 16u}, // crh -> Cyrl + {0xA6220000u, 10u}, // crj -> Cans + {0xAA220000u, 10u}, // crk -> Cans + {0xAE220000u, 10u}, // crl -> Cans + {0xB2220000u, 10u}, // crm -> Cans + {0xCA220000u, 44u}, // crs -> Latn + {0x63730000u, 44u}, // cs -> Latn + {0x86420000u, 44u}, // csb -> Latn + {0xDA420000u, 10u}, // csw -> Cans + {0x8E620000u, 64u}, // ctd -> Pauc + {0x63750000u, 16u}, // cu -> Cyrl + {0x63760000u, 16u}, // cv -> Cyrl + {0x63790000u, 44u}, // cy -> Latn + {0x64610000u, 44u}, // da -> Latn + {0x8C030000u, 44u}, // dad -> Latn + {0x94030000u, 44u}, // daf -> Latn + {0x98030000u, 44u}, // dag -> Latn + {0x9C030000u, 44u}, // dah -> Latn + {0xA8030000u, 44u}, // dak -> Latn + {0xC4030000u, 16u}, // dar -> Cyrl + {0xD4030000u, 44u}, // dav -> Latn + {0x8C230000u, 44u}, // dbd -> Latn + {0xC0230000u, 44u}, // dbq -> Latn {0x88430000u, 1u}, // dcc -> Arab - {0xB4630000u, 40u}, // ddn -> Latn - {0x64650000u, 40u}, // de -> Latn - {0x8C830000u, 40u}, // ded -> Latn - {0xB4830000u, 40u}, // den -> Latn - {0x80C30000u, 40u}, // dga -> Latn - {0x9CC30000u, 40u}, // dgh -> Latn - {0xA0C30000u, 40u}, // dgi -> Latn + {0xB4630000u, 44u}, // ddn -> Latn + {0x64650000u, 44u}, // de -> Latn + {0x8C830000u, 44u}, // ded -> Latn + {0xB4830000u, 44u}, // den -> Latn + {0x80C30000u, 44u}, // dga -> Latn + {0x9CC30000u, 44u}, // dgh -> Latn + {0xA0C30000u, 44u}, // dgi -> Latn {0xACC30000u, 1u}, // dgl -> Arab - {0xC4C30000u, 40u}, // dgr -> Latn - {0xE4C30000u, 40u}, // dgz -> Latn - {0x81030000u, 40u}, // dia -> Latn - {0x91230000u, 40u}, // dje -> Latn - {0xA5A30000u, 40u}, // dnj -> Latn - {0x85C30000u, 40u}, // dob -> Latn + {0xC4C30000u, 44u}, // dgr -> Latn + {0xE4C30000u, 44u}, // dgz -> Latn + {0x81030000u, 44u}, // dia -> Latn + {0x91230000u, 44u}, // dje -> Latn + {0xA5A30000u, 44u}, // dnj -> Latn + {0x85C30000u, 44u}, // dob -> Latn {0xA1C30000u, 1u}, // doi -> Arab - {0xBDC30000u, 40u}, // dop -> Latn - {0xD9C30000u, 40u}, // dow -> Latn - {0xA2230000u, 40u}, // dri -> Latn - {0xCA230000u, 18u}, // drs -> Ethi - {0x86430000u, 40u}, // dsb -> Latn - {0xB2630000u, 40u}, // dtm -> Latn - {0xBE630000u, 40u}, // dtp -> Latn - {0xCA630000u, 40u}, // dts -> Latn - {0xE2630000u, 16u}, // dty -> Deva - {0x82830000u, 40u}, // dua -> Latn - {0x8A830000u, 40u}, // duc -> Latn - {0x8E830000u, 40u}, // dud -> Latn - {0x9A830000u, 40u}, // dug -> Latn - {0x64760000u, 79u}, // dv -> Thaa - {0x82A30000u, 40u}, // dva -> Latn - {0xDAC30000u, 40u}, // dww -> Latn - {0xBB030000u, 40u}, // dyo -> Latn - {0xD3030000u, 40u}, // dyu -> Latn - {0x647A0000u, 81u}, // dz -> Tibt - {0x9B230000u, 40u}, // dzg -> Latn - {0xD0240000u, 40u}, // ebu -> Latn - {0x65650000u, 40u}, // ee -> Latn - {0xA0A40000u, 40u}, // efi -> Latn - {0xACC40000u, 40u}, // egl -> Latn - {0xE0C40000u, 17u}, // egy -> Egyp - {0xE1440000u, 32u}, // eky -> Kali - {0x656C0000u, 21u}, // el -> Grek - {0x81840000u, 40u}, // ema -> Latn - {0xA1840000u, 40u}, // emi -> Latn - {0x656E0000u, 40u}, // en -> Latn - {0x656E5841u, 87u}, // en-XA -> ~~~A - {0xB5A40000u, 40u}, // enn -> Latn - {0xC1A40000u, 40u}, // enq -> Latn - {0x656F0000u, 40u}, // eo -> Latn - {0xA2240000u, 40u}, // eri -> Latn - {0x65730000u, 40u}, // es -> Latn - {0xD2440000u, 40u}, // esu -> Latn - {0x65740000u, 40u}, // et -> Latn - {0xC6640000u, 40u}, // etr -> Latn - {0xCE640000u, 30u}, // ett -> Ital - {0xD2640000u, 40u}, // etu -> Latn - {0xDE640000u, 40u}, // etx -> Latn - {0x65750000u, 40u}, // eu -> Latn - {0xBAC40000u, 40u}, // ewo -> Latn - {0xCEE40000u, 40u}, // ext -> Latn + {0xBDC30000u, 44u}, // dop -> Latn + {0xD9C30000u, 44u}, // dow -> Latn + {0xA2230000u, 44u}, // dri -> Latn + {0xCA230000u, 19u}, // drs -> Ethi + {0x86430000u, 44u}, // dsb -> Latn + {0xB2630000u, 44u}, // dtm -> Latn + {0xBE630000u, 44u}, // dtp -> Latn + {0xCA630000u, 44u}, // dts -> Latn + {0xE2630000u, 17u}, // dty -> Deva + {0x82830000u, 44u}, // dua -> Latn + {0x8A830000u, 44u}, // duc -> Latn + {0x8E830000u, 44u}, // dud -> Latn + {0x9A830000u, 44u}, // dug -> Latn + {0x64760000u, 86u}, // dv -> Thaa + {0x82A30000u, 44u}, // dva -> Latn + {0xDAC30000u, 44u}, // dww -> Latn + {0xBB030000u, 44u}, // dyo -> Latn + {0xD3030000u, 44u}, // dyu -> Latn + {0x647A0000u, 88u}, // dz -> Tibt + {0x9B230000u, 44u}, // dzg -> Latn + {0xD0240000u, 44u}, // ebu -> Latn + {0x65650000u, 44u}, // ee -> Latn + {0xA0A40000u, 44u}, // efi -> Latn + {0xACC40000u, 44u}, // egl -> Latn + {0xE0C40000u, 18u}, // egy -> Egyp + {0x81440000u, 44u}, // eka -> Latn + {0xE1440000u, 36u}, // eky -> Kali + {0x656C0000u, 24u}, // el -> Grek + {0x81840000u, 44u}, // ema -> Latn + {0xA1840000u, 44u}, // emi -> Latn + {0x656E0000u, 44u}, // en -> Latn + {0x656E5841u, 95u}, // en-XA -> ~~~A + {0xB5A40000u, 44u}, // enn -> Latn + {0xC1A40000u, 44u}, // enq -> Latn + {0x656F0000u, 44u}, // eo -> Latn + {0xA2240000u, 44u}, // eri -> Latn + {0x65730000u, 44u}, // es -> Latn + {0x9A440000u, 22u}, // esg -> Gonm + {0xD2440000u, 44u}, // esu -> Latn + {0x65740000u, 44u}, // et -> Latn + {0xC6640000u, 44u}, // etr -> Latn + {0xCE640000u, 34u}, // ett -> Ital + {0xD2640000u, 44u}, // etu -> Latn + {0xDE640000u, 44u}, // etx -> Latn + {0x65750000u, 44u}, // eu -> Latn + {0xBAC40000u, 44u}, // ewo -> Latn + {0xCEE40000u, 44u}, // ext -> Latn {0x66610000u, 1u}, // fa -> Arab - {0x80050000u, 40u}, // faa -> Latn - {0x84050000u, 40u}, // fab -> Latn - {0x98050000u, 40u}, // fag -> Latn - {0xA0050000u, 40u}, // fai -> Latn - {0xB4050000u, 40u}, // fan -> Latn - {0x66660000u, 40u}, // ff -> Latn - {0xA0A50000u, 40u}, // ffi -> Latn - {0xB0A50000u, 40u}, // ffm -> Latn - {0x66690000u, 40u}, // fi -> Latn + {0x80050000u, 44u}, // faa -> Latn + {0x84050000u, 44u}, // fab -> Latn + {0x98050000u, 44u}, // fag -> Latn + {0xA0050000u, 44u}, // fai -> Latn + {0xB4050000u, 44u}, // fan -> Latn + {0x66660000u, 44u}, // ff -> Latn + {0xA0A50000u, 44u}, // ffi -> Latn + {0xB0A50000u, 44u}, // ffm -> Latn + {0x66690000u, 44u}, // fi -> Latn {0x81050000u, 1u}, // fia -> Arab - {0xAD050000u, 40u}, // fil -> Latn - {0xCD050000u, 40u}, // fit -> Latn - {0x666A0000u, 40u}, // fj -> Latn - {0xC5650000u, 40u}, // flr -> Latn - {0xBD850000u, 40u}, // fmp -> Latn - {0x666F0000u, 40u}, // fo -> Latn - {0x8DC50000u, 40u}, // fod -> Latn - {0xB5C50000u, 40u}, // fon -> Latn - {0xC5C50000u, 40u}, // for -> Latn - {0x91E50000u, 40u}, // fpe -> Latn - {0xCA050000u, 40u}, // fqs -> Latn - {0x66720000u, 40u}, // fr -> Latn - {0x8A250000u, 40u}, // frc -> Latn - {0xBE250000u, 40u}, // frp -> Latn - {0xC6250000u, 40u}, // frr -> Latn - {0xCA250000u, 40u}, // frs -> Latn + {0xAD050000u, 44u}, // fil -> Latn + {0xCD050000u, 44u}, // fit -> Latn + {0x666A0000u, 44u}, // fj -> Latn + {0xC5650000u, 44u}, // flr -> Latn + {0xBD850000u, 44u}, // fmp -> Latn + {0x666F0000u, 44u}, // fo -> Latn + {0x8DC50000u, 44u}, // fod -> Latn + {0xB5C50000u, 44u}, // fon -> Latn + {0xC5C50000u, 44u}, // for -> Latn + {0x91E50000u, 44u}, // fpe -> Latn + {0xCA050000u, 44u}, // fqs -> Latn + {0x66720000u, 44u}, // fr -> Latn + {0x8A250000u, 44u}, // frc -> Latn + {0xBE250000u, 44u}, // frp -> Latn + {0xC6250000u, 44u}, // frr -> Latn + {0xCA250000u, 44u}, // frs -> Latn {0x86850000u, 1u}, // fub -> Arab - {0x8E850000u, 40u}, // fud -> Latn - {0x92850000u, 40u}, // fue -> Latn - {0x96850000u, 40u}, // fuf -> Latn - {0x9E850000u, 40u}, // fuh -> Latn - {0xC2850000u, 40u}, // fuq -> Latn - {0xC6850000u, 40u}, // fur -> Latn - {0xD6850000u, 40u}, // fuv -> Latn - {0xE2850000u, 40u}, // fuy -> Latn - {0xC6A50000u, 40u}, // fvr -> Latn - {0x66790000u, 40u}, // fy -> Latn - {0x67610000u, 40u}, // ga -> Latn - {0x80060000u, 40u}, // gaa -> Latn - {0x94060000u, 40u}, // gaf -> Latn - {0x98060000u, 40u}, // gag -> Latn - {0x9C060000u, 40u}, // gah -> Latn - {0xA4060000u, 40u}, // gaj -> Latn - {0xB0060000u, 40u}, // gam -> Latn - {0xB4060000u, 24u}, // gan -> Hans - {0xD8060000u, 40u}, // gaw -> Latn - {0xE0060000u, 40u}, // gay -> Latn - {0x94260000u, 40u}, // gbf -> Latn - {0xB0260000u, 16u}, // gbm -> Deva - {0xE0260000u, 40u}, // gby -> Latn + {0x8E850000u, 44u}, // fud -> Latn + {0x92850000u, 44u}, // fue -> Latn + {0x96850000u, 44u}, // fuf -> Latn + {0x9E850000u, 44u}, // fuh -> Latn + {0xC2850000u, 44u}, // fuq -> Latn + {0xC6850000u, 44u}, // fur -> Latn + {0xD6850000u, 44u}, // fuv -> Latn + {0xE2850000u, 44u}, // fuy -> Latn + {0xC6A50000u, 44u}, // fvr -> Latn + {0x66790000u, 44u}, // fy -> Latn + {0x67610000u, 44u}, // ga -> Latn + {0x80060000u, 44u}, // gaa -> Latn + {0x94060000u, 44u}, // gaf -> Latn + {0x98060000u, 44u}, // gag -> Latn + {0x9C060000u, 44u}, // gah -> Latn + {0xA4060000u, 44u}, // gaj -> Latn + {0xB0060000u, 44u}, // gam -> Latn + {0xB4060000u, 27u}, // gan -> Hans + {0xD8060000u, 44u}, // gaw -> Latn + {0xE0060000u, 44u}, // gay -> Latn + {0x80260000u, 44u}, // gba -> Latn + {0x94260000u, 44u}, // gbf -> Latn + {0xB0260000u, 17u}, // gbm -> Deva + {0xE0260000u, 44u}, // gby -> Latn {0xE4260000u, 1u}, // gbz -> Arab - {0xC4460000u, 40u}, // gcr -> Latn - {0x67640000u, 40u}, // gd -> Latn - {0x90660000u, 40u}, // gde -> Latn - {0xB4660000u, 40u}, // gdn -> Latn - {0xC4660000u, 40u}, // gdr -> Latn - {0x84860000u, 40u}, // geb -> Latn - {0xA4860000u, 40u}, // gej -> Latn - {0xAC860000u, 40u}, // gel -> Latn - {0xE4860000u, 18u}, // gez -> Ethi - {0xA8A60000u, 40u}, // gfk -> Latn - {0xB4C60000u, 16u}, // ggn -> Deva - {0xC8E60000u, 40u}, // ghs -> Latn - {0xAD060000u, 40u}, // gil -> Latn - {0xB1060000u, 40u}, // gim -> Latn + {0xC4460000u, 44u}, // gcr -> Latn + {0x67640000u, 44u}, // gd -> Latn + {0x90660000u, 44u}, // gde -> Latn + {0xB4660000u, 44u}, // gdn -> Latn + {0xC4660000u, 44u}, // gdr -> Latn + {0x84860000u, 44u}, // geb -> Latn + {0xA4860000u, 44u}, // gej -> Latn + {0xAC860000u, 44u}, // gel -> Latn + {0xE4860000u, 19u}, // gez -> Ethi + {0xA8A60000u, 44u}, // gfk -> Latn + {0xB4C60000u, 17u}, // ggn -> Deva + {0xC8E60000u, 44u}, // ghs -> Latn + {0xAD060000u, 44u}, // gil -> Latn + {0xB1060000u, 44u}, // gim -> Latn {0xA9260000u, 1u}, // gjk -> Arab - {0xB5260000u, 40u}, // gjn -> Latn + {0xB5260000u, 44u}, // gjn -> Latn {0xD1260000u, 1u}, // gju -> Arab - {0xB5460000u, 40u}, // gkn -> Latn - {0xBD460000u, 40u}, // gkp -> Latn - {0x676C0000u, 40u}, // gl -> Latn + {0xB5460000u, 44u}, // gkn -> Latn + {0xBD460000u, 44u}, // gkp -> Latn + {0x676C0000u, 44u}, // gl -> Latn {0xA9660000u, 1u}, // glk -> Arab - {0xB1860000u, 40u}, // gmm -> Latn - {0xD5860000u, 18u}, // gmv -> Ethi - {0x676E0000u, 40u}, // gn -> Latn - {0x8DA60000u, 40u}, // gnd -> Latn - {0x99A60000u, 40u}, // gng -> Latn - {0x8DC60000u, 40u}, // god -> Latn - {0x95C60000u, 18u}, // gof -> Ethi - {0xA1C60000u, 40u}, // goi -> Latn - {0xB1C60000u, 16u}, // gom -> Deva - {0xB5C60000u, 77u}, // gon -> Telu - {0xC5C60000u, 40u}, // gor -> Latn - {0xC9C60000u, 40u}, // gos -> Latn - {0xCDC60000u, 20u}, // got -> Goth - {0x8A260000u, 14u}, // grc -> Cprt + {0xB1860000u, 44u}, // gmm -> Latn + {0xD5860000u, 19u}, // gmv -> Ethi + {0x676E0000u, 44u}, // gn -> Latn + {0x8DA60000u, 44u}, // gnd -> Latn + {0x99A60000u, 44u}, // gng -> Latn + {0x8DC60000u, 44u}, // god -> Latn + {0x95C60000u, 19u}, // gof -> Ethi + {0xA1C60000u, 44u}, // goi -> Latn + {0xB1C60000u, 17u}, // gom -> Deva + {0xB5C60000u, 84u}, // gon -> Telu + {0xC5C60000u, 44u}, // gor -> Latn + {0xC9C60000u, 44u}, // gos -> Latn + {0xCDC60000u, 23u}, // got -> Goth + {0x86260000u, 44u}, // grb -> Latn + {0x8A260000u, 15u}, // grc -> Cprt {0xCE260000u, 7u}, // grt -> Beng - {0xDA260000u, 40u}, // grw -> Latn - {0xDA460000u, 40u}, // gsw -> Latn - {0x67750000u, 22u}, // gu -> Gujr - {0x86860000u, 40u}, // gub -> Latn - {0x8A860000u, 40u}, // guc -> Latn - {0x8E860000u, 40u}, // gud -> Latn - {0xC6860000u, 40u}, // gur -> Latn - {0xDA860000u, 40u}, // guw -> Latn - {0xDE860000u, 40u}, // gux -> Latn - {0xE6860000u, 40u}, // guz -> Latn - {0x67760000u, 40u}, // gv -> Latn - {0x96A60000u, 40u}, // gvf -> Latn - {0xC6A60000u, 16u}, // gvr -> Deva - {0xCAA60000u, 40u}, // gvs -> Latn + {0xDA260000u, 44u}, // grw -> Latn + {0xDA460000u, 44u}, // gsw -> Latn + {0x67750000u, 25u}, // gu -> Gujr + {0x86860000u, 44u}, // gub -> Latn + {0x8A860000u, 44u}, // guc -> Latn + {0x8E860000u, 44u}, // gud -> Latn + {0xC6860000u, 44u}, // gur -> Latn + {0xDA860000u, 44u}, // guw -> Latn + {0xDE860000u, 44u}, // gux -> Latn + {0xE6860000u, 44u}, // guz -> Latn + {0x67760000u, 44u}, // gv -> Latn + {0x96A60000u, 44u}, // gvf -> Latn + {0xC6A60000u, 17u}, // gvr -> Deva + {0xCAA60000u, 44u}, // gvs -> Latn {0x8AC60000u, 1u}, // gwc -> Arab - {0xA2C60000u, 40u}, // gwi -> Latn + {0xA2C60000u, 44u}, // gwi -> Latn {0xCEC60000u, 1u}, // gwt -> Arab - {0xA3060000u, 40u}, // gyi -> Latn - {0x68610000u, 40u}, // ha -> Latn + {0xA3060000u, 44u}, // gyi -> Latn + {0x68610000u, 44u}, // ha -> Latn {0x6861434Du, 1u}, // ha-CM -> Arab {0x68615344u, 1u}, // ha-SD -> Arab - {0x98070000u, 40u}, // hag -> Latn - {0xA8070000u, 24u}, // hak -> Hans - {0xB0070000u, 40u}, // ham -> Latn - {0xD8070000u, 40u}, // haw -> Latn + {0x98070000u, 44u}, // hag -> Latn + {0xA8070000u, 27u}, // hak -> Hans + {0xB0070000u, 44u}, // ham -> Latn + {0xD8070000u, 44u}, // haw -> Latn {0xE4070000u, 1u}, // haz -> Arab - {0x84270000u, 40u}, // hbb -> Latn - {0xE0670000u, 18u}, // hdy -> Ethi - {0x68650000u, 27u}, // he -> Hebr - {0xE0E70000u, 40u}, // hhy -> Latn - {0x68690000u, 16u}, // hi -> Deva - {0x81070000u, 40u}, // hia -> Latn - {0x95070000u, 40u}, // hif -> Latn - {0x99070000u, 40u}, // hig -> Latn - {0x9D070000u, 40u}, // hih -> Latn - {0xAD070000u, 40u}, // hil -> Latn - {0x81670000u, 40u}, // hla -> Latn - {0xD1670000u, 28u}, // hlu -> Hluw - {0x8D870000u, 62u}, // hmd -> Plrd - {0xCD870000u, 40u}, // hmt -> Latn + {0x84270000u, 44u}, // hbb -> Latn + {0xE0670000u, 19u}, // hdy -> Ethi + {0x68650000u, 30u}, // he -> Hebr + {0xE0E70000u, 44u}, // hhy -> Latn + {0x68690000u, 17u}, // hi -> Deva + {0x81070000u, 44u}, // hia -> Latn + {0x95070000u, 44u}, // hif -> Latn + {0x99070000u, 44u}, // hig -> Latn + {0x9D070000u, 44u}, // hih -> Latn + {0xAD070000u, 44u}, // hil -> Latn + {0x81670000u, 44u}, // hla -> Latn + {0xD1670000u, 31u}, // hlu -> Hluw + {0x8D870000u, 67u}, // hmd -> Plrd + {0xCD870000u, 44u}, // hmt -> Latn {0x8DA70000u, 1u}, // hnd -> Arab - {0x91A70000u, 16u}, // hne -> Deva - {0xA5A70000u, 29u}, // hnj -> Hmng - {0xB5A70000u, 40u}, // hnn -> Latn + {0x91A70000u, 17u}, // hne -> Deva + {0xA5A70000u, 32u}, // hnj -> Hmng + {0xB5A70000u, 44u}, // hnn -> Latn {0xB9A70000u, 1u}, // hno -> Arab - {0x686F0000u, 40u}, // ho -> Latn - {0x89C70000u, 16u}, // hoc -> Deva - {0xA5C70000u, 16u}, // hoj -> Deva - {0xCDC70000u, 40u}, // hot -> Latn - {0x68720000u, 40u}, // hr -> Latn - {0x86470000u, 40u}, // hsb -> Latn - {0xB6470000u, 24u}, // hsn -> Hans - {0x68740000u, 40u}, // ht -> Latn - {0x68750000u, 40u}, // hu -> Latn - {0xA2870000u, 40u}, // hui -> Latn + {0x686F0000u, 44u}, // ho -> Latn + {0x89C70000u, 17u}, // hoc -> Deva + {0xA5C70000u, 17u}, // hoj -> Deva + {0xCDC70000u, 44u}, // hot -> Latn + {0x68720000u, 44u}, // hr -> Latn + {0x86470000u, 44u}, // hsb -> Latn + {0xB6470000u, 27u}, // hsn -> Hans + {0x68740000u, 44u}, // ht -> Latn + {0x68750000u, 44u}, // hu -> Latn + {0xA2870000u, 44u}, // hui -> Latn {0x68790000u, 3u}, // hy -> Armn - {0x687A0000u, 40u}, // hz -> Latn - {0x69610000u, 40u}, // ia -> Latn - {0xB4080000u, 40u}, // ian -> Latn - {0xC4080000u, 40u}, // iar -> Latn - {0x80280000u, 40u}, // iba -> Latn - {0x84280000u, 40u}, // ibb -> Latn - {0xE0280000u, 40u}, // iby -> Latn - {0x80480000u, 40u}, // ica -> Latn - {0x9C480000u, 40u}, // ich -> Latn - {0x69640000u, 40u}, // id -> Latn - {0x8C680000u, 40u}, // idd -> Latn - {0xA0680000u, 40u}, // idi -> Latn - {0xD0680000u, 40u}, // idu -> Latn - {0x69670000u, 40u}, // ig -> Latn - {0x84C80000u, 40u}, // igb -> Latn - {0x90C80000u, 40u}, // ige -> Latn - {0x69690000u, 86u}, // ii -> Yiii - {0xA5280000u, 40u}, // ijj -> Latn - {0x696B0000u, 40u}, // ik -> Latn - {0xA9480000u, 40u}, // ikk -> Latn - {0xCD480000u, 40u}, // ikt -> Latn - {0xD9480000u, 40u}, // ikw -> Latn - {0xDD480000u, 40u}, // ikx -> Latn - {0xB9680000u, 40u}, // ilo -> Latn - {0xB9880000u, 40u}, // imo -> Latn - {0x696E0000u, 40u}, // in -> Latn - {0x9DA80000u, 15u}, // inh -> Cyrl - {0xD1C80000u, 40u}, // iou -> Latn - {0xA2280000u, 40u}, // iri -> Latn - {0x69730000u, 40u}, // is -> Latn - {0x69740000u, 40u}, // it -> Latn - {0x69750000u, 9u}, // iu -> Cans - {0x69770000u, 27u}, // iw -> Hebr - {0xB2C80000u, 40u}, // iwm -> Latn - {0xCAC80000u, 40u}, // iws -> Latn - {0x9F280000u, 40u}, // izh -> Latn - {0xA3280000u, 40u}, // izi -> Latn - {0x6A610000u, 31u}, // ja -> Jpan - {0x84090000u, 40u}, // jab -> Latn - {0xB0090000u, 40u}, // jam -> Latn - {0xD0290000u, 40u}, // jbu -> Latn - {0xB4890000u, 40u}, // jen -> Latn - {0xA8C90000u, 40u}, // jgk -> Latn - {0xB8C90000u, 40u}, // jgo -> Latn - {0x6A690000u, 27u}, // ji -> Hebr - {0x85090000u, 40u}, // jib -> Latn - {0x89890000u, 40u}, // jmc -> Latn - {0xAD890000u, 16u}, // jml -> Deva - {0x82290000u, 40u}, // jra -> Latn - {0xCE890000u, 40u}, // jut -> Latn - {0x6A760000u, 40u}, // jv -> Latn - {0x6A770000u, 40u}, // jw -> Latn - {0x6B610000u, 19u}, // ka -> Geor - {0x800A0000u, 15u}, // kaa -> Cyrl - {0x840A0000u, 40u}, // kab -> Latn - {0x880A0000u, 40u}, // kac -> Latn - {0x8C0A0000u, 40u}, // kad -> Latn - {0xA00A0000u, 40u}, // kai -> Latn - {0xA40A0000u, 40u}, // kaj -> Latn - {0xB00A0000u, 40u}, // kam -> Latn - {0xB80A0000u, 40u}, // kao -> Latn - {0x8C2A0000u, 15u}, // kbd -> Cyrl - {0xB02A0000u, 40u}, // kbm -> Latn - {0xBC2A0000u, 40u}, // kbp -> Latn - {0xC02A0000u, 40u}, // kbq -> Latn - {0xDC2A0000u, 40u}, // kbx -> Latn + {0x687A0000u, 44u}, // hz -> Latn + {0x69610000u, 44u}, // ia -> Latn + {0xB4080000u, 44u}, // ian -> Latn + {0xC4080000u, 44u}, // iar -> Latn + {0x80280000u, 44u}, // iba -> Latn + {0x84280000u, 44u}, // ibb -> Latn + {0xE0280000u, 44u}, // iby -> Latn + {0x80480000u, 44u}, // ica -> Latn + {0x9C480000u, 44u}, // ich -> Latn + {0x69640000u, 44u}, // id -> Latn + {0x8C680000u, 44u}, // idd -> Latn + {0xA0680000u, 44u}, // idi -> Latn + {0xD0680000u, 44u}, // idu -> Latn + {0x90A80000u, 44u}, // ife -> Latn + {0x69670000u, 44u}, // ig -> Latn + {0x84C80000u, 44u}, // igb -> Latn + {0x90C80000u, 44u}, // ige -> Latn + {0x69690000u, 94u}, // ii -> Yiii + {0xA5280000u, 44u}, // ijj -> Latn + {0x696B0000u, 44u}, // ik -> Latn + {0xA9480000u, 44u}, // ikk -> Latn + {0xCD480000u, 44u}, // ikt -> Latn + {0xD9480000u, 44u}, // ikw -> Latn + {0xDD480000u, 44u}, // ikx -> Latn + {0xB9680000u, 44u}, // ilo -> Latn + {0xB9880000u, 44u}, // imo -> Latn + {0x696E0000u, 44u}, // in -> Latn + {0x9DA80000u, 16u}, // inh -> Cyrl + {0x696F0000u, 44u}, // io -> Latn + {0xD1C80000u, 44u}, // iou -> Latn + {0xA2280000u, 44u}, // iri -> Latn + {0x69730000u, 44u}, // is -> Latn + {0x69740000u, 44u}, // it -> Latn + {0x69750000u, 10u}, // iu -> Cans + {0x69770000u, 30u}, // iw -> Hebr + {0xB2C80000u, 44u}, // iwm -> Latn + {0xCAC80000u, 44u}, // iws -> Latn + {0x9F280000u, 44u}, // izh -> Latn + {0xA3280000u, 44u}, // izi -> Latn + {0x6A610000u, 35u}, // ja -> Jpan + {0x84090000u, 44u}, // jab -> Latn + {0xB0090000u, 44u}, // jam -> Latn + {0xB8290000u, 44u}, // jbo -> Latn + {0xD0290000u, 44u}, // jbu -> Latn + {0xB4890000u, 44u}, // jen -> Latn + {0xA8C90000u, 44u}, // jgk -> Latn + {0xB8C90000u, 44u}, // jgo -> Latn + {0x6A690000u, 30u}, // ji -> Hebr + {0x85090000u, 44u}, // jib -> Latn + {0x89890000u, 44u}, // jmc -> Latn + {0xAD890000u, 17u}, // jml -> Deva + {0x82290000u, 44u}, // jra -> Latn + {0xCE890000u, 44u}, // jut -> Latn + {0x6A760000u, 44u}, // jv -> Latn + {0x6A770000u, 44u}, // jw -> Latn + {0x6B610000u, 20u}, // ka -> Geor + {0x800A0000u, 16u}, // kaa -> Cyrl + {0x840A0000u, 44u}, // kab -> Latn + {0x880A0000u, 44u}, // kac -> Latn + {0x8C0A0000u, 44u}, // kad -> Latn + {0xA00A0000u, 44u}, // kai -> Latn + {0xA40A0000u, 44u}, // kaj -> Latn + {0xB00A0000u, 44u}, // kam -> Latn + {0xB80A0000u, 44u}, // kao -> Latn + {0x8C2A0000u, 16u}, // kbd -> Cyrl + {0xB02A0000u, 44u}, // kbm -> Latn + {0xBC2A0000u, 44u}, // kbp -> Latn + {0xC02A0000u, 44u}, // kbq -> Latn + {0xDC2A0000u, 44u}, // kbx -> Latn {0xE02A0000u, 1u}, // kby -> Arab - {0x984A0000u, 40u}, // kcg -> Latn - {0xA84A0000u, 40u}, // kck -> Latn - {0xAC4A0000u, 40u}, // kcl -> Latn - {0xCC4A0000u, 40u}, // kct -> Latn - {0x906A0000u, 40u}, // kde -> Latn + {0x984A0000u, 44u}, // kcg -> Latn + {0xA84A0000u, 44u}, // kck -> Latn + {0xAC4A0000u, 44u}, // kcl -> Latn + {0xCC4A0000u, 44u}, // kct -> Latn + {0x906A0000u, 44u}, // kde -> Latn {0x9C6A0000u, 1u}, // kdh -> Arab - {0xAC6A0000u, 40u}, // kdl -> Latn - {0xCC6A0000u, 80u}, // kdt -> Thai - {0x808A0000u, 40u}, // kea -> Latn - {0xB48A0000u, 40u}, // ken -> Latn - {0xE48A0000u, 40u}, // kez -> Latn - {0xB8AA0000u, 40u}, // kfo -> Latn - {0xC4AA0000u, 16u}, // kfr -> Deva - {0xE0AA0000u, 16u}, // kfy -> Deva - {0x6B670000u, 40u}, // kg -> Latn - {0x90CA0000u, 40u}, // kge -> Latn - {0x94CA0000u, 40u}, // kgf -> Latn - {0xBCCA0000u, 40u}, // kgp -> Latn - {0x80EA0000u, 40u}, // kha -> Latn - {0x84EA0000u, 73u}, // khb -> Talu - {0xB4EA0000u, 16u}, // khn -> Deva - {0xC0EA0000u, 40u}, // khq -> Latn - {0xC8EA0000u, 40u}, // khs -> Latn - {0xCCEA0000u, 52u}, // kht -> Mymr + {0xAC6A0000u, 44u}, // kdl -> Latn + {0xCC6A0000u, 87u}, // kdt -> Thai + {0x808A0000u, 44u}, // kea -> Latn + {0xB48A0000u, 44u}, // ken -> Latn + {0xE48A0000u, 44u}, // kez -> Latn + {0xB8AA0000u, 44u}, // kfo -> Latn + {0xC4AA0000u, 17u}, // kfr -> Deva + {0xE0AA0000u, 17u}, // kfy -> Deva + {0x6B670000u, 44u}, // kg -> Latn + {0x90CA0000u, 44u}, // kge -> Latn + {0x94CA0000u, 44u}, // kgf -> Latn + {0xBCCA0000u, 44u}, // kgp -> Latn + {0x80EA0000u, 44u}, // kha -> Latn + {0x84EA0000u, 80u}, // khb -> Talu + {0xB4EA0000u, 17u}, // khn -> Deva + {0xC0EA0000u, 44u}, // khq -> Latn + {0xC8EA0000u, 44u}, // khs -> Latn + {0xCCEA0000u, 56u}, // kht -> Mymr {0xD8EA0000u, 1u}, // khw -> Arab - {0xE4EA0000u, 40u}, // khz -> Latn - {0x6B690000u, 40u}, // ki -> Latn - {0xA50A0000u, 40u}, // kij -> Latn - {0xD10A0000u, 40u}, // kiu -> Latn - {0xD90A0000u, 40u}, // kiw -> Latn - {0x6B6A0000u, 40u}, // kj -> Latn - {0x8D2A0000u, 40u}, // kjd -> Latn - {0x992A0000u, 39u}, // kjg -> Laoo - {0xC92A0000u, 40u}, // kjs -> Latn - {0xE12A0000u, 40u}, // kjy -> Latn - {0x6B6B0000u, 15u}, // kk -> Cyrl + {0xE4EA0000u, 44u}, // khz -> Latn + {0x6B690000u, 44u}, // ki -> Latn + {0xA50A0000u, 44u}, // kij -> Latn + {0xD10A0000u, 44u}, // kiu -> Latn + {0xD90A0000u, 44u}, // kiw -> Latn + {0x6B6A0000u, 44u}, // kj -> Latn + {0x8D2A0000u, 44u}, // kjd -> Latn + {0x992A0000u, 43u}, // kjg -> Laoo + {0xC92A0000u, 44u}, // kjs -> Latn + {0xE12A0000u, 44u}, // kjy -> Latn + {0x6B6B0000u, 16u}, // kk -> Cyrl {0x6B6B4146u, 1u}, // kk-AF -> Arab {0x6B6B434Eu, 1u}, // kk-CN -> Arab {0x6B6B4952u, 1u}, // kk-IR -> Arab {0x6B6B4D4Eu, 1u}, // kk-MN -> Arab - {0x894A0000u, 40u}, // kkc -> Latn - {0xA54A0000u, 40u}, // kkj -> Latn - {0x6B6C0000u, 40u}, // kl -> Latn - {0xB56A0000u, 40u}, // kln -> Latn - {0xC16A0000u, 40u}, // klq -> Latn - {0xCD6A0000u, 40u}, // klt -> Latn - {0xDD6A0000u, 40u}, // klx -> Latn - {0x6B6D0000u, 35u}, // km -> Khmr - {0x858A0000u, 40u}, // kmb -> Latn - {0x9D8A0000u, 40u}, // kmh -> Latn - {0xB98A0000u, 40u}, // kmo -> Latn - {0xC98A0000u, 40u}, // kms -> Latn - {0xD18A0000u, 40u}, // kmu -> Latn - {0xD98A0000u, 40u}, // kmw -> Latn - {0x6B6E0000u, 36u}, // kn -> Knda - {0xBDAA0000u, 40u}, // knp -> Latn - {0x6B6F0000u, 37u}, // ko -> Kore - {0xA1CA0000u, 15u}, // koi -> Cyrl - {0xA9CA0000u, 16u}, // kok -> Deva - {0xADCA0000u, 40u}, // kol -> Latn - {0xC9CA0000u, 40u}, // kos -> Latn - {0xE5CA0000u, 40u}, // koz -> Latn - {0x91EA0000u, 40u}, // kpe -> Latn - {0x95EA0000u, 40u}, // kpf -> Latn - {0xB9EA0000u, 40u}, // kpo -> Latn - {0xC5EA0000u, 40u}, // kpr -> Latn - {0xDDEA0000u, 40u}, // kpx -> Latn - {0x860A0000u, 40u}, // kqb -> Latn - {0x960A0000u, 40u}, // kqf -> Latn - {0xCA0A0000u, 40u}, // kqs -> Latn - {0xE20A0000u, 18u}, // kqy -> Ethi - {0x8A2A0000u, 15u}, // krc -> Cyrl - {0xA22A0000u, 40u}, // kri -> Latn - {0xA62A0000u, 40u}, // krj -> Latn - {0xAE2A0000u, 40u}, // krl -> Latn - {0xCA2A0000u, 40u}, // krs -> Latn - {0xD22A0000u, 16u}, // kru -> Deva + {0x894A0000u, 44u}, // kkc -> Latn + {0xA54A0000u, 44u}, // kkj -> Latn + {0x6B6C0000u, 44u}, // kl -> Latn + {0xB56A0000u, 44u}, // kln -> Latn + {0xC16A0000u, 44u}, // klq -> Latn + {0xCD6A0000u, 44u}, // klt -> Latn + {0xDD6A0000u, 44u}, // klx -> Latn + {0x6B6D0000u, 39u}, // km -> Khmr + {0x858A0000u, 44u}, // kmb -> Latn + {0x9D8A0000u, 44u}, // kmh -> Latn + {0xB98A0000u, 44u}, // kmo -> Latn + {0xC98A0000u, 44u}, // kms -> Latn + {0xD18A0000u, 44u}, // kmu -> Latn + {0xD98A0000u, 44u}, // kmw -> Latn + {0x6B6E0000u, 40u}, // kn -> Knda + {0x95AA0000u, 44u}, // knf -> Latn + {0xBDAA0000u, 44u}, // knp -> Latn + {0x6B6F0000u, 41u}, // ko -> Kore + {0xA1CA0000u, 16u}, // koi -> Cyrl + {0xA9CA0000u, 17u}, // kok -> Deva + {0xADCA0000u, 44u}, // kol -> Latn + {0xC9CA0000u, 44u}, // kos -> Latn + {0xE5CA0000u, 44u}, // koz -> Latn + {0x91EA0000u, 44u}, // kpe -> Latn + {0x95EA0000u, 44u}, // kpf -> Latn + {0xB9EA0000u, 44u}, // kpo -> Latn + {0xC5EA0000u, 44u}, // kpr -> Latn + {0xDDEA0000u, 44u}, // kpx -> Latn + {0x860A0000u, 44u}, // kqb -> Latn + {0x960A0000u, 44u}, // kqf -> Latn + {0xCA0A0000u, 44u}, // kqs -> Latn + {0xE20A0000u, 19u}, // kqy -> Ethi + {0x6B720000u, 44u}, // kr -> Latn + {0x8A2A0000u, 16u}, // krc -> Cyrl + {0xA22A0000u, 44u}, // kri -> Latn + {0xA62A0000u, 44u}, // krj -> Latn + {0xAE2A0000u, 44u}, // krl -> Latn + {0xCA2A0000u, 44u}, // krs -> Latn + {0xD22A0000u, 17u}, // kru -> Deva {0x6B730000u, 1u}, // ks -> Arab - {0x864A0000u, 40u}, // ksb -> Latn - {0x8E4A0000u, 40u}, // ksd -> Latn - {0x964A0000u, 40u}, // ksf -> Latn - {0x9E4A0000u, 40u}, // ksh -> Latn - {0xA64A0000u, 40u}, // ksj -> Latn - {0xC64A0000u, 40u}, // ksr -> Latn - {0x866A0000u, 18u}, // ktb -> Ethi - {0xB26A0000u, 40u}, // ktm -> Latn - {0xBA6A0000u, 40u}, // kto -> Latn - {0x6B750000u, 40u}, // ku -> Latn + {0x864A0000u, 44u}, // ksb -> Latn + {0x8E4A0000u, 44u}, // ksd -> Latn + {0x964A0000u, 44u}, // ksf -> Latn + {0x9E4A0000u, 44u}, // ksh -> Latn + {0xA64A0000u, 44u}, // ksj -> Latn + {0xC64A0000u, 44u}, // ksr -> Latn + {0x866A0000u, 19u}, // ktb -> Ethi + {0xB26A0000u, 44u}, // ktm -> Latn + {0xBA6A0000u, 44u}, // kto -> Latn + {0x6B750000u, 44u}, // ku -> Latn {0x6B754952u, 1u}, // ku-IR -> Arab {0x6B754C42u, 1u}, // ku-LB -> Arab - {0x868A0000u, 40u}, // kub -> Latn - {0x8E8A0000u, 40u}, // kud -> Latn - {0x928A0000u, 40u}, // kue -> Latn - {0xA68A0000u, 40u}, // kuj -> Latn - {0xB28A0000u, 15u}, // kum -> Cyrl - {0xB68A0000u, 40u}, // kun -> Latn - {0xBE8A0000u, 40u}, // kup -> Latn - {0xCA8A0000u, 40u}, // kus -> Latn - {0x6B760000u, 15u}, // kv -> Cyrl - {0x9AAA0000u, 40u}, // kvg -> Latn - {0xC6AA0000u, 40u}, // kvr -> Latn + {0x868A0000u, 44u}, // kub -> Latn + {0x8E8A0000u, 44u}, // kud -> Latn + {0x928A0000u, 44u}, // kue -> Latn + {0xA68A0000u, 44u}, // kuj -> Latn + {0xB28A0000u, 16u}, // kum -> Cyrl + {0xB68A0000u, 44u}, // kun -> Latn + {0xBE8A0000u, 44u}, // kup -> Latn + {0xCA8A0000u, 44u}, // kus -> Latn + {0x6B760000u, 16u}, // kv -> Cyrl + {0x9AAA0000u, 44u}, // kvg -> Latn + {0xC6AA0000u, 44u}, // kvr -> Latn {0xDEAA0000u, 1u}, // kvx -> Arab - {0x6B770000u, 40u}, // kw -> Latn - {0xA6CA0000u, 40u}, // kwj -> Latn - {0xBACA0000u, 40u}, // kwo -> Latn - {0x82EA0000u, 40u}, // kxa -> Latn - {0x8AEA0000u, 18u}, // kxc -> Ethi - {0xB2EA0000u, 80u}, // kxm -> Thai + {0x6B770000u, 44u}, // kw -> Latn + {0xA6CA0000u, 44u}, // kwj -> Latn + {0xBACA0000u, 44u}, // kwo -> Latn + {0x82EA0000u, 44u}, // kxa -> Latn + {0x8AEA0000u, 19u}, // kxc -> Ethi + {0xB2EA0000u, 87u}, // kxm -> Thai {0xBEEA0000u, 1u}, // kxp -> Arab - {0xDAEA0000u, 40u}, // kxw -> Latn - {0xE6EA0000u, 40u}, // kxz -> Latn - {0x6B790000u, 15u}, // ky -> Cyrl + {0xDAEA0000u, 44u}, // kxw -> Latn + {0xE6EA0000u, 44u}, // kxz -> Latn + {0x6B790000u, 16u}, // ky -> Cyrl {0x6B79434Eu, 1u}, // ky-CN -> Arab - {0x6B795452u, 40u}, // ky-TR -> Latn - {0x930A0000u, 40u}, // kye -> Latn - {0xDF0A0000u, 40u}, // kyx -> Latn - {0xC72A0000u, 40u}, // kzr -> Latn - {0x6C610000u, 40u}, // la -> Latn - {0x840B0000u, 42u}, // lab -> Lina - {0x8C0B0000u, 27u}, // lad -> Hebr - {0x980B0000u, 40u}, // lag -> Latn + {0x6B795452u, 44u}, // ky-TR -> Latn + {0x930A0000u, 44u}, // kye -> Latn + {0xDF0A0000u, 44u}, // kyx -> Latn + {0xC72A0000u, 44u}, // kzr -> Latn + {0x6C610000u, 44u}, // la -> Latn + {0x840B0000u, 46u}, // lab -> Lina + {0x8C0B0000u, 30u}, // lad -> Hebr + {0x980B0000u, 44u}, // lag -> Latn {0x9C0B0000u, 1u}, // lah -> Arab - {0xA40B0000u, 40u}, // laj -> Latn - {0xC80B0000u, 40u}, // las -> Latn - {0x6C620000u, 40u}, // lb -> Latn - {0x902B0000u, 15u}, // lbe -> Cyrl - {0xD02B0000u, 40u}, // lbu -> Latn - {0xD82B0000u, 40u}, // lbw -> Latn - {0xB04B0000u, 40u}, // lcm -> Latn - {0xBC4B0000u, 80u}, // lcp -> Thai - {0x846B0000u, 40u}, // ldb -> Latn - {0x8C8B0000u, 40u}, // led -> Latn - {0x908B0000u, 40u}, // lee -> Latn - {0xB08B0000u, 40u}, // lem -> Latn - {0xBC8B0000u, 41u}, // lep -> Lepc - {0xC08B0000u, 40u}, // leq -> Latn - {0xD08B0000u, 40u}, // leu -> Latn - {0xE48B0000u, 15u}, // lez -> Cyrl - {0x6C670000u, 40u}, // lg -> Latn - {0x98CB0000u, 40u}, // lgg -> Latn - {0x6C690000u, 40u}, // li -> Latn - {0x810B0000u, 40u}, // lia -> Latn - {0x8D0B0000u, 40u}, // lid -> Latn - {0x950B0000u, 16u}, // lif -> Deva - {0x990B0000u, 40u}, // lig -> Latn - {0x9D0B0000u, 40u}, // lih -> Latn - {0xA50B0000u, 40u}, // lij -> Latn - {0xC90B0000u, 43u}, // lis -> Lisu - {0xBD2B0000u, 40u}, // ljp -> Latn + {0xA40B0000u, 44u}, // laj -> Latn + {0xC80B0000u, 44u}, // las -> Latn + {0x6C620000u, 44u}, // lb -> Latn + {0x902B0000u, 16u}, // lbe -> Cyrl + {0xD02B0000u, 44u}, // lbu -> Latn + {0xD82B0000u, 44u}, // lbw -> Latn + {0xB04B0000u, 44u}, // lcm -> Latn + {0xBC4B0000u, 87u}, // lcp -> Thai + {0x846B0000u, 44u}, // ldb -> Latn + {0x8C8B0000u, 44u}, // led -> Latn + {0x908B0000u, 44u}, // lee -> Latn + {0xB08B0000u, 44u}, // lem -> Latn + {0xBC8B0000u, 45u}, // lep -> Lepc + {0xC08B0000u, 44u}, // leq -> Latn + {0xD08B0000u, 44u}, // leu -> Latn + {0xE48B0000u, 16u}, // lez -> Cyrl + {0x6C670000u, 44u}, // lg -> Latn + {0x98CB0000u, 44u}, // lgg -> Latn + {0x6C690000u, 44u}, // li -> Latn + {0x810B0000u, 44u}, // lia -> Latn + {0x8D0B0000u, 44u}, // lid -> Latn + {0x950B0000u, 17u}, // lif -> Deva + {0x990B0000u, 44u}, // lig -> Latn + {0x9D0B0000u, 44u}, // lih -> Latn + {0xA50B0000u, 44u}, // lij -> Latn + {0xC90B0000u, 47u}, // lis -> Lisu + {0xBD2B0000u, 44u}, // ljp -> Latn {0xA14B0000u, 1u}, // lki -> Arab - {0xCD4B0000u, 40u}, // lkt -> Latn - {0x916B0000u, 40u}, // lle -> Latn - {0xB56B0000u, 40u}, // lln -> Latn - {0xB58B0000u, 77u}, // lmn -> Telu - {0xB98B0000u, 40u}, // lmo -> Latn - {0xBD8B0000u, 40u}, // lmp -> Latn - {0x6C6E0000u, 40u}, // ln -> Latn - {0xC9AB0000u, 40u}, // lns -> Latn - {0xD1AB0000u, 40u}, // lnu -> Latn - {0x6C6F0000u, 39u}, // lo -> Laoo - {0xA5CB0000u, 40u}, // loj -> Latn - {0xA9CB0000u, 40u}, // lok -> Latn - {0xADCB0000u, 40u}, // lol -> Latn - {0xC5CB0000u, 40u}, // lor -> Latn - {0xC9CB0000u, 40u}, // los -> Latn - {0xE5CB0000u, 40u}, // loz -> Latn + {0xCD4B0000u, 44u}, // lkt -> Latn + {0x916B0000u, 44u}, // lle -> Latn + {0xB56B0000u, 44u}, // lln -> Latn + {0xB58B0000u, 84u}, // lmn -> Telu + {0xB98B0000u, 44u}, // lmo -> Latn + {0xBD8B0000u, 44u}, // lmp -> Latn + {0x6C6E0000u, 44u}, // ln -> Latn + {0xC9AB0000u, 44u}, // lns -> Latn + {0xD1AB0000u, 44u}, // lnu -> Latn + {0x6C6F0000u, 43u}, // lo -> Laoo + {0xA5CB0000u, 44u}, // loj -> Latn + {0xA9CB0000u, 44u}, // lok -> Latn + {0xADCB0000u, 44u}, // lol -> Latn + {0xC5CB0000u, 44u}, // lor -> Latn + {0xC9CB0000u, 44u}, // los -> Latn + {0xE5CB0000u, 44u}, // loz -> Latn {0x8A2B0000u, 1u}, // lrc -> Arab - {0x6C740000u, 40u}, // lt -> Latn - {0x9A6B0000u, 40u}, // ltg -> Latn - {0x6C750000u, 40u}, // lu -> Latn - {0x828B0000u, 40u}, // lua -> Latn - {0xBA8B0000u, 40u}, // luo -> Latn - {0xE28B0000u, 40u}, // luy -> Latn + {0x6C740000u, 44u}, // lt -> Latn + {0x9A6B0000u, 44u}, // ltg -> Latn + {0x6C750000u, 44u}, // lu -> Latn + {0x828B0000u, 44u}, // lua -> Latn + {0xBA8B0000u, 44u}, // luo -> Latn + {0xE28B0000u, 44u}, // luy -> Latn {0xE68B0000u, 1u}, // luz -> Arab - {0x6C760000u, 40u}, // lv -> Latn - {0xAECB0000u, 80u}, // lwl -> Thai - {0x9F2B0000u, 24u}, // lzh -> Hans - {0xE72B0000u, 40u}, // lzz -> Latn - {0x8C0C0000u, 40u}, // mad -> Latn - {0x940C0000u, 40u}, // maf -> Latn - {0x980C0000u, 16u}, // mag -> Deva - {0xA00C0000u, 16u}, // mai -> Deva - {0xA80C0000u, 40u}, // mak -> Latn - {0xB40C0000u, 40u}, // man -> Latn - {0xB40C474Eu, 54u}, // man-GN -> Nkoo - {0xC80C0000u, 40u}, // mas -> Latn - {0xD80C0000u, 40u}, // maw -> Latn - {0xE40C0000u, 40u}, // maz -> Latn - {0x9C2C0000u, 40u}, // mbh -> Latn - {0xB82C0000u, 40u}, // mbo -> Latn - {0xC02C0000u, 40u}, // mbq -> Latn - {0xD02C0000u, 40u}, // mbu -> Latn - {0xD82C0000u, 40u}, // mbw -> Latn - {0xA04C0000u, 40u}, // mci -> Latn - {0xBC4C0000u, 40u}, // mcp -> Latn - {0xC04C0000u, 40u}, // mcq -> Latn - {0xC44C0000u, 40u}, // mcr -> Latn - {0xD04C0000u, 40u}, // mcu -> Latn - {0x806C0000u, 40u}, // mda -> Latn + {0x6C760000u, 44u}, // lv -> Latn + {0xAECB0000u, 87u}, // lwl -> Thai + {0x9F2B0000u, 27u}, // lzh -> Hans + {0xE72B0000u, 44u}, // lzz -> Latn + {0x8C0C0000u, 44u}, // mad -> Latn + {0x940C0000u, 44u}, // maf -> Latn + {0x980C0000u, 17u}, // mag -> Deva + {0xA00C0000u, 17u}, // mai -> Deva + {0xA80C0000u, 44u}, // mak -> Latn + {0xB40C0000u, 44u}, // man -> Latn + {0xB40C474Eu, 58u}, // man-GN -> Nkoo + {0xC80C0000u, 44u}, // mas -> Latn + {0xD80C0000u, 44u}, // maw -> Latn + {0xE40C0000u, 44u}, // maz -> Latn + {0x9C2C0000u, 44u}, // mbh -> Latn + {0xB82C0000u, 44u}, // mbo -> Latn + {0xC02C0000u, 44u}, // mbq -> Latn + {0xD02C0000u, 44u}, // mbu -> Latn + {0xD82C0000u, 44u}, // mbw -> Latn + {0xA04C0000u, 44u}, // mci -> Latn + {0xBC4C0000u, 44u}, // mcp -> Latn + {0xC04C0000u, 44u}, // mcq -> Latn + {0xC44C0000u, 44u}, // mcr -> Latn + {0xD04C0000u, 44u}, // mcu -> Latn + {0x806C0000u, 44u}, // mda -> Latn {0x906C0000u, 1u}, // mde -> Arab - {0x946C0000u, 15u}, // mdf -> Cyrl - {0x9C6C0000u, 40u}, // mdh -> Latn - {0xA46C0000u, 40u}, // mdj -> Latn - {0xC46C0000u, 40u}, // mdr -> Latn - {0xDC6C0000u, 18u}, // mdx -> Ethi - {0x8C8C0000u, 40u}, // med -> Latn - {0x908C0000u, 40u}, // mee -> Latn - {0xA88C0000u, 40u}, // mek -> Latn - {0xB48C0000u, 40u}, // men -> Latn - {0xC48C0000u, 40u}, // mer -> Latn - {0xCC8C0000u, 40u}, // met -> Latn - {0xD08C0000u, 40u}, // meu -> Latn + {0x946C0000u, 16u}, // mdf -> Cyrl + {0x9C6C0000u, 44u}, // mdh -> Latn + {0xA46C0000u, 44u}, // mdj -> Latn + {0xC46C0000u, 44u}, // mdr -> Latn + {0xDC6C0000u, 19u}, // mdx -> Ethi + {0x8C8C0000u, 44u}, // med -> Latn + {0x908C0000u, 44u}, // mee -> Latn + {0xA88C0000u, 44u}, // mek -> Latn + {0xB48C0000u, 44u}, // men -> Latn + {0xC48C0000u, 44u}, // mer -> Latn + {0xCC8C0000u, 44u}, // met -> Latn + {0xD08C0000u, 44u}, // meu -> Latn {0x80AC0000u, 1u}, // mfa -> Arab - {0x90AC0000u, 40u}, // mfe -> Latn - {0xB4AC0000u, 40u}, // mfn -> Latn - {0xB8AC0000u, 40u}, // mfo -> Latn - {0xC0AC0000u, 40u}, // mfq -> Latn - {0x6D670000u, 40u}, // mg -> Latn - {0x9CCC0000u, 40u}, // mgh -> Latn - {0xACCC0000u, 40u}, // mgl -> Latn - {0xB8CC0000u, 40u}, // mgo -> Latn - {0xBCCC0000u, 16u}, // mgp -> Deva - {0xE0CC0000u, 40u}, // mgy -> Latn - {0x6D680000u, 40u}, // mh -> Latn - {0xA0EC0000u, 40u}, // mhi -> Latn - {0xACEC0000u, 40u}, // mhl -> Latn - {0x6D690000u, 40u}, // mi -> Latn - {0x950C0000u, 40u}, // mif -> Latn - {0xB50C0000u, 40u}, // min -> Latn - {0xC90C0000u, 26u}, // mis -> Hatr - {0xD90C0000u, 40u}, // miw -> Latn - {0x6D6B0000u, 15u}, // mk -> Cyrl + {0x90AC0000u, 44u}, // mfe -> Latn + {0xB4AC0000u, 44u}, // mfn -> Latn + {0xB8AC0000u, 44u}, // mfo -> Latn + {0xC0AC0000u, 44u}, // mfq -> Latn + {0x6D670000u, 44u}, // mg -> Latn + {0x9CCC0000u, 44u}, // mgh -> Latn + {0xACCC0000u, 44u}, // mgl -> Latn + {0xB8CC0000u, 44u}, // mgo -> Latn + {0xBCCC0000u, 17u}, // mgp -> Deva + {0xE0CC0000u, 44u}, // mgy -> Latn + {0x6D680000u, 44u}, // mh -> Latn + {0xA0EC0000u, 44u}, // mhi -> Latn + {0xACEC0000u, 44u}, // mhl -> Latn + {0x6D690000u, 44u}, // mi -> Latn + {0x950C0000u, 44u}, // mif -> Latn + {0xB50C0000u, 44u}, // min -> Latn + {0xC90C0000u, 29u}, // mis -> Hatr + {0xD90C0000u, 44u}, // miw -> Latn + {0x6D6B0000u, 16u}, // mk -> Cyrl {0xA14C0000u, 1u}, // mki -> Arab - {0xAD4C0000u, 40u}, // mkl -> Latn - {0xBD4C0000u, 40u}, // mkp -> Latn - {0xD94C0000u, 40u}, // mkw -> Latn - {0x6D6C0000u, 49u}, // ml -> Mlym - {0x916C0000u, 40u}, // mle -> Latn - {0xBD6C0000u, 40u}, // mlp -> Latn - {0xC96C0000u, 40u}, // mls -> Latn - {0xB98C0000u, 40u}, // mmo -> Latn - {0xD18C0000u, 40u}, // mmu -> Latn - {0xDD8C0000u, 40u}, // mmx -> Latn - {0x6D6E0000u, 15u}, // mn -> Cyrl - {0x6D6E434Eu, 50u}, // mn-CN -> Mong - {0x81AC0000u, 40u}, // mna -> Latn - {0x95AC0000u, 40u}, // mnf -> Latn + {0xAD4C0000u, 44u}, // mkl -> Latn + {0xBD4C0000u, 44u}, // mkp -> Latn + {0xD94C0000u, 44u}, // mkw -> Latn + {0x6D6C0000u, 53u}, // ml -> Mlym + {0x916C0000u, 44u}, // mle -> Latn + {0xBD6C0000u, 44u}, // mlp -> Latn + {0xC96C0000u, 44u}, // mls -> Latn + {0xB98C0000u, 44u}, // mmo -> Latn + {0xD18C0000u, 44u}, // mmu -> Latn + {0xDD8C0000u, 44u}, // mmx -> Latn + {0x6D6E0000u, 16u}, // mn -> Cyrl + {0x6D6E434Eu, 54u}, // mn-CN -> Mong + {0x81AC0000u, 44u}, // mna -> Latn + {0x95AC0000u, 44u}, // mnf -> Latn {0xA1AC0000u, 7u}, // mni -> Beng - {0xD9AC0000u, 52u}, // mnw -> Mymr - {0x81CC0000u, 40u}, // moa -> Latn - {0x91CC0000u, 40u}, // moe -> Latn - {0x9DCC0000u, 40u}, // moh -> Latn - {0xC9CC0000u, 40u}, // mos -> Latn - {0xDDCC0000u, 40u}, // mox -> Latn - {0xBDEC0000u, 40u}, // mpp -> Latn - {0xC9EC0000u, 40u}, // mps -> Latn - {0xCDEC0000u, 40u}, // mpt -> Latn - {0xDDEC0000u, 40u}, // mpx -> Latn - {0xAE0C0000u, 40u}, // mql -> Latn - {0x6D720000u, 16u}, // mr -> Deva - {0x8E2C0000u, 16u}, // mrd -> Deva - {0xA62C0000u, 15u}, // mrj -> Cyrl - {0xBA2C0000u, 51u}, // mro -> Mroo - {0x6D730000u, 40u}, // ms -> Latn + {0xD9AC0000u, 56u}, // mnw -> Mymr + {0x81CC0000u, 44u}, // moa -> Latn + {0x91CC0000u, 44u}, // moe -> Latn + {0x9DCC0000u, 44u}, // moh -> Latn + {0xC9CC0000u, 44u}, // mos -> Latn + {0xDDCC0000u, 44u}, // mox -> Latn + {0xBDEC0000u, 44u}, // mpp -> Latn + {0xC9EC0000u, 44u}, // mps -> Latn + {0xCDEC0000u, 44u}, // mpt -> Latn + {0xDDEC0000u, 44u}, // mpx -> Latn + {0xAE0C0000u, 44u}, // mql -> Latn + {0x6D720000u, 17u}, // mr -> Deva + {0x8E2C0000u, 17u}, // mrd -> Deva + {0xA62C0000u, 16u}, // mrj -> Cyrl + {0xBA2C0000u, 55u}, // mro -> Mroo + {0x6D730000u, 44u}, // ms -> Latn {0x6D734343u, 1u}, // ms-CC -> Arab {0x6D734944u, 1u}, // ms-ID -> Arab - {0x6D740000u, 40u}, // mt -> Latn - {0x8A6C0000u, 40u}, // mtc -> Latn - {0x966C0000u, 40u}, // mtf -> Latn - {0xA26C0000u, 40u}, // mti -> Latn - {0xC66C0000u, 16u}, // mtr -> Deva - {0x828C0000u, 40u}, // mua -> Latn - {0xC68C0000u, 40u}, // mur -> Latn - {0xCA8C0000u, 40u}, // mus -> Latn - {0x82AC0000u, 40u}, // mva -> Latn - {0xB6AC0000u, 40u}, // mvn -> Latn + {0x6D740000u, 44u}, // mt -> Latn + {0x8A6C0000u, 44u}, // mtc -> Latn + {0x966C0000u, 44u}, // mtf -> Latn + {0xA26C0000u, 44u}, // mti -> Latn + {0xC66C0000u, 17u}, // mtr -> Deva + {0x828C0000u, 44u}, // mua -> Latn + {0xC68C0000u, 44u}, // mur -> Latn + {0xCA8C0000u, 44u}, // mus -> Latn + {0x82AC0000u, 44u}, // mva -> Latn + {0xB6AC0000u, 44u}, // mvn -> Latn {0xE2AC0000u, 1u}, // mvy -> Arab - {0xAACC0000u, 40u}, // mwk -> Latn - {0xC6CC0000u, 16u}, // mwr -> Deva - {0xD6CC0000u, 40u}, // mwv -> Latn - {0x8AEC0000u, 40u}, // mxc -> Latn - {0xB2EC0000u, 40u}, // mxm -> Latn - {0x6D790000u, 52u}, // my -> Mymr - {0xAB0C0000u, 40u}, // myk -> Latn - {0xB30C0000u, 18u}, // mym -> Ethi - {0xD70C0000u, 15u}, // myv -> Cyrl - {0xDB0C0000u, 40u}, // myw -> Latn - {0xDF0C0000u, 40u}, // myx -> Latn - {0xE70C0000u, 46u}, // myz -> Mand - {0xAB2C0000u, 40u}, // mzk -> Latn - {0xB32C0000u, 40u}, // mzm -> Latn + {0xAACC0000u, 44u}, // mwk -> Latn + {0xC6CC0000u, 17u}, // mwr -> Deva + {0xD6CC0000u, 44u}, // mwv -> Latn + {0xDACC0000u, 33u}, // mww -> Hmnp + {0x8AEC0000u, 44u}, // mxc -> Latn + {0xB2EC0000u, 44u}, // mxm -> Latn + {0x6D790000u, 56u}, // my -> Mymr + {0xAB0C0000u, 44u}, // myk -> Latn + {0xB30C0000u, 19u}, // mym -> Ethi + {0xD70C0000u, 16u}, // myv -> Cyrl + {0xDB0C0000u, 44u}, // myw -> Latn + {0xDF0C0000u, 44u}, // myx -> Latn + {0xE70C0000u, 50u}, // myz -> Mand + {0xAB2C0000u, 44u}, // mzk -> Latn + {0xB32C0000u, 44u}, // mzm -> Latn {0xB72C0000u, 1u}, // mzn -> Arab - {0xBF2C0000u, 40u}, // mzp -> Latn - {0xDB2C0000u, 40u}, // mzw -> Latn - {0xE72C0000u, 40u}, // mzz -> Latn - {0x6E610000u, 40u}, // na -> Latn - {0x880D0000u, 40u}, // nac -> Latn - {0x940D0000u, 40u}, // naf -> Latn - {0xA80D0000u, 40u}, // nak -> Latn - {0xB40D0000u, 24u}, // nan -> Hans - {0xBC0D0000u, 40u}, // nap -> Latn - {0xC00D0000u, 40u}, // naq -> Latn - {0xC80D0000u, 40u}, // nas -> Latn - {0x6E620000u, 40u}, // nb -> Latn - {0x804D0000u, 40u}, // nca -> Latn - {0x904D0000u, 40u}, // nce -> Latn - {0x944D0000u, 40u}, // ncf -> Latn - {0x9C4D0000u, 40u}, // nch -> Latn - {0xB84D0000u, 40u}, // nco -> Latn - {0xD04D0000u, 40u}, // ncu -> Latn - {0x6E640000u, 40u}, // nd -> Latn - {0x886D0000u, 40u}, // ndc -> Latn - {0xC86D0000u, 40u}, // nds -> Latn - {0x6E650000u, 16u}, // ne -> Deva - {0x848D0000u, 40u}, // neb -> Latn - {0xD88D0000u, 16u}, // new -> Deva - {0xDC8D0000u, 40u}, // nex -> Latn - {0xC4AD0000u, 40u}, // nfr -> Latn - {0x6E670000u, 40u}, // ng -> Latn - {0x80CD0000u, 40u}, // nga -> Latn - {0x84CD0000u, 40u}, // ngb -> Latn - {0xACCD0000u, 40u}, // ngl -> Latn - {0x84ED0000u, 40u}, // nhb -> Latn - {0x90ED0000u, 40u}, // nhe -> Latn - {0xD8ED0000u, 40u}, // nhw -> Latn - {0x950D0000u, 40u}, // nif -> Latn - {0xA10D0000u, 40u}, // nii -> Latn - {0xA50D0000u, 40u}, // nij -> Latn - {0xB50D0000u, 40u}, // nin -> Latn - {0xD10D0000u, 40u}, // niu -> Latn - {0xE10D0000u, 40u}, // niy -> Latn - {0xE50D0000u, 40u}, // niz -> Latn - {0xB92D0000u, 40u}, // njo -> Latn - {0x994D0000u, 40u}, // nkg -> Latn - {0xB94D0000u, 40u}, // nko -> Latn - {0x6E6C0000u, 40u}, // nl -> Latn - {0x998D0000u, 40u}, // nmg -> Latn - {0xE58D0000u, 40u}, // nmz -> Latn - {0x6E6E0000u, 40u}, // nn -> Latn - {0x95AD0000u, 40u}, // nnf -> Latn - {0x9DAD0000u, 40u}, // nnh -> Latn - {0xA9AD0000u, 40u}, // nnk -> Latn - {0xB1AD0000u, 40u}, // nnm -> Latn - {0x6E6F0000u, 40u}, // no -> Latn - {0x8DCD0000u, 38u}, // nod -> Lana - {0x91CD0000u, 16u}, // noe -> Deva - {0xB5CD0000u, 64u}, // non -> Runr - {0xBDCD0000u, 40u}, // nop -> Latn - {0xD1CD0000u, 40u}, // nou -> Latn - {0xBA0D0000u, 54u}, // nqo -> Nkoo - {0x6E720000u, 40u}, // nr -> Latn - {0x862D0000u, 40u}, // nrb -> Latn - {0xAA4D0000u, 9u}, // nsk -> Cans - {0xB64D0000u, 40u}, // nsn -> Latn - {0xBA4D0000u, 40u}, // nso -> Latn - {0xCA4D0000u, 40u}, // nss -> Latn - {0xB26D0000u, 40u}, // ntm -> Latn - {0xC66D0000u, 40u}, // ntr -> Latn - {0xA28D0000u, 40u}, // nui -> Latn - {0xBE8D0000u, 40u}, // nup -> Latn - {0xCA8D0000u, 40u}, // nus -> Latn - {0xD68D0000u, 40u}, // nuv -> Latn - {0xDE8D0000u, 40u}, // nux -> Latn - {0x6E760000u, 40u}, // nv -> Latn - {0x86CD0000u, 40u}, // nwb -> Latn - {0xC2ED0000u, 40u}, // nxq -> Latn - {0xC6ED0000u, 40u}, // nxr -> Latn - {0x6E790000u, 40u}, // ny -> Latn - {0xB30D0000u, 40u}, // nym -> Latn - {0xB70D0000u, 40u}, // nyn -> Latn - {0xA32D0000u, 40u}, // nzi -> Latn - {0x6F630000u, 40u}, // oc -> Latn - {0x88CE0000u, 40u}, // ogc -> Latn - {0xC54E0000u, 40u}, // okr -> Latn - {0xD54E0000u, 40u}, // okv -> Latn - {0x6F6D0000u, 40u}, // om -> Latn - {0x99AE0000u, 40u}, // ong -> Latn - {0xB5AE0000u, 40u}, // onn -> Latn - {0xC9AE0000u, 40u}, // ons -> Latn - {0xB1EE0000u, 40u}, // opm -> Latn - {0x6F720000u, 57u}, // or -> Orya - {0xBA2E0000u, 40u}, // oro -> Latn + {0xBF2C0000u, 44u}, // mzp -> Latn + {0xDB2C0000u, 44u}, // mzw -> Latn + {0xE72C0000u, 44u}, // mzz -> Latn + {0x6E610000u, 44u}, // na -> Latn + {0x880D0000u, 44u}, // nac -> Latn + {0x940D0000u, 44u}, // naf -> Latn + {0xA80D0000u, 44u}, // nak -> Latn + {0xB40D0000u, 27u}, // nan -> Hans + {0xBC0D0000u, 44u}, // nap -> Latn + {0xC00D0000u, 44u}, // naq -> Latn + {0xC80D0000u, 44u}, // nas -> Latn + {0x6E620000u, 44u}, // nb -> Latn + {0x804D0000u, 44u}, // nca -> Latn + {0x904D0000u, 44u}, // nce -> Latn + {0x944D0000u, 44u}, // ncf -> Latn + {0x9C4D0000u, 44u}, // nch -> Latn + {0xB84D0000u, 44u}, // nco -> Latn + {0xD04D0000u, 44u}, // ncu -> Latn + {0x6E640000u, 44u}, // nd -> Latn + {0x886D0000u, 44u}, // ndc -> Latn + {0xC86D0000u, 44u}, // nds -> Latn + {0x6E650000u, 17u}, // ne -> Deva + {0x848D0000u, 44u}, // neb -> Latn + {0xD88D0000u, 17u}, // new -> Deva + {0xDC8D0000u, 44u}, // nex -> Latn + {0xC4AD0000u, 44u}, // nfr -> Latn + {0x6E670000u, 44u}, // ng -> Latn + {0x80CD0000u, 44u}, // nga -> Latn + {0x84CD0000u, 44u}, // ngb -> Latn + {0xACCD0000u, 44u}, // ngl -> Latn + {0x84ED0000u, 44u}, // nhb -> Latn + {0x90ED0000u, 44u}, // nhe -> Latn + {0xD8ED0000u, 44u}, // nhw -> Latn + {0x950D0000u, 44u}, // nif -> Latn + {0xA10D0000u, 44u}, // nii -> Latn + {0xA50D0000u, 44u}, // nij -> Latn + {0xB50D0000u, 44u}, // nin -> Latn + {0xD10D0000u, 44u}, // niu -> Latn + {0xE10D0000u, 44u}, // niy -> Latn + {0xE50D0000u, 44u}, // niz -> Latn + {0xB92D0000u, 44u}, // njo -> Latn + {0x994D0000u, 44u}, // nkg -> Latn + {0xB94D0000u, 44u}, // nko -> Latn + {0x6E6C0000u, 44u}, // nl -> Latn + {0x998D0000u, 44u}, // nmg -> Latn + {0xE58D0000u, 44u}, // nmz -> Latn + {0x6E6E0000u, 44u}, // nn -> Latn + {0x95AD0000u, 44u}, // nnf -> Latn + {0x9DAD0000u, 44u}, // nnh -> Latn + {0xA9AD0000u, 44u}, // nnk -> Latn + {0xB1AD0000u, 44u}, // nnm -> Latn + {0xBDAD0000u, 91u}, // nnp -> Wcho + {0x6E6F0000u, 44u}, // no -> Latn + {0x8DCD0000u, 42u}, // nod -> Lana + {0x91CD0000u, 17u}, // noe -> Deva + {0xB5CD0000u, 69u}, // non -> Runr + {0xBDCD0000u, 44u}, // nop -> Latn + {0xD1CD0000u, 44u}, // nou -> Latn + {0xBA0D0000u, 58u}, // nqo -> Nkoo + {0x6E720000u, 44u}, // nr -> Latn + {0x862D0000u, 44u}, // nrb -> Latn + {0xAA4D0000u, 10u}, // nsk -> Cans + {0xB64D0000u, 44u}, // nsn -> Latn + {0xBA4D0000u, 44u}, // nso -> Latn + {0xCA4D0000u, 44u}, // nss -> Latn + {0xB26D0000u, 44u}, // ntm -> Latn + {0xC66D0000u, 44u}, // ntr -> Latn + {0xA28D0000u, 44u}, // nui -> Latn + {0xBE8D0000u, 44u}, // nup -> Latn + {0xCA8D0000u, 44u}, // nus -> Latn + {0xD68D0000u, 44u}, // nuv -> Latn + {0xDE8D0000u, 44u}, // nux -> Latn + {0x6E760000u, 44u}, // nv -> Latn + {0x86CD0000u, 44u}, // nwb -> Latn + {0xC2ED0000u, 44u}, // nxq -> Latn + {0xC6ED0000u, 44u}, // nxr -> Latn + {0x6E790000u, 44u}, // ny -> Latn + {0xB30D0000u, 44u}, // nym -> Latn + {0xB70D0000u, 44u}, // nyn -> Latn + {0xA32D0000u, 44u}, // nzi -> Latn + {0x6F630000u, 44u}, // oc -> Latn + {0x88CE0000u, 44u}, // ogc -> Latn + {0xC54E0000u, 44u}, // okr -> Latn + {0xD54E0000u, 44u}, // okv -> Latn + {0x6F6D0000u, 44u}, // om -> Latn + {0x99AE0000u, 44u}, // ong -> Latn + {0xB5AE0000u, 44u}, // onn -> Latn + {0xC9AE0000u, 44u}, // ons -> Latn + {0xB1EE0000u, 44u}, // opm -> Latn + {0x6F720000u, 62u}, // or -> Orya + {0xBA2E0000u, 44u}, // oro -> Latn {0xD22E0000u, 1u}, // oru -> Arab - {0x6F730000u, 15u}, // os -> Cyrl - {0x824E0000u, 58u}, // osa -> Osge + {0x6F730000u, 16u}, // os -> Cyrl + {0x824E0000u, 63u}, // osa -> Osge {0x826E0000u, 1u}, // ota -> Arab - {0xAA6E0000u, 56u}, // otk -> Orkh - {0xB32E0000u, 40u}, // ozm -> Latn - {0x70610000u, 23u}, // pa -> Guru + {0xAA6E0000u, 61u}, // otk -> Orkh + {0xB32E0000u, 44u}, // ozm -> Latn + {0x70610000u, 26u}, // pa -> Guru {0x7061504Bu, 1u}, // pa-PK -> Arab - {0x980F0000u, 40u}, // pag -> Latn - {0xAC0F0000u, 60u}, // pal -> Phli - {0xB00F0000u, 40u}, // pam -> Latn - {0xBC0F0000u, 40u}, // pap -> Latn - {0xD00F0000u, 40u}, // pau -> Latn - {0xA02F0000u, 40u}, // pbi -> Latn - {0x8C4F0000u, 40u}, // pcd -> Latn - {0xB04F0000u, 40u}, // pcm -> Latn - {0x886F0000u, 40u}, // pdc -> Latn - {0xCC6F0000u, 40u}, // pdt -> Latn - {0x8C8F0000u, 40u}, // ped -> Latn - {0xB88F0000u, 84u}, // peo -> Xpeo - {0xDC8F0000u, 40u}, // pex -> Latn - {0xACAF0000u, 40u}, // pfl -> Latn + {0x980F0000u, 44u}, // pag -> Latn + {0xAC0F0000u, 65u}, // pal -> Phli + {0xB00F0000u, 44u}, // pam -> Latn + {0xBC0F0000u, 44u}, // pap -> Latn + {0xD00F0000u, 44u}, // pau -> Latn + {0xA02F0000u, 44u}, // pbi -> Latn + {0x8C4F0000u, 44u}, // pcd -> Latn + {0xB04F0000u, 44u}, // pcm -> Latn + {0x886F0000u, 44u}, // pdc -> Latn + {0xCC6F0000u, 44u}, // pdt -> Latn + {0x8C8F0000u, 44u}, // ped -> Latn + {0xB88F0000u, 92u}, // peo -> Xpeo + {0xDC8F0000u, 44u}, // pex -> Latn + {0xACAF0000u, 44u}, // pfl -> Latn {0xACEF0000u, 1u}, // phl -> Arab - {0xB4EF0000u, 61u}, // phn -> Phnx - {0xAD0F0000u, 40u}, // pil -> Latn - {0xBD0F0000u, 40u}, // pip -> Latn + {0xB4EF0000u, 66u}, // phn -> Phnx + {0xAD0F0000u, 44u}, // pil -> Latn + {0xBD0F0000u, 44u}, // pip -> Latn {0x814F0000u, 8u}, // pka -> Brah - {0xB94F0000u, 40u}, // pko -> Latn - {0x706C0000u, 40u}, // pl -> Latn - {0x816F0000u, 40u}, // pla -> Latn - {0xC98F0000u, 40u}, // pms -> Latn - {0x99AF0000u, 40u}, // png -> Latn - {0xB5AF0000u, 40u}, // pnn -> Latn - {0xCDAF0000u, 21u}, // pnt -> Grek - {0xB5CF0000u, 40u}, // pon -> Latn - {0xB9EF0000u, 40u}, // ppo -> Latn - {0x822F0000u, 34u}, // pra -> Khar + {0xB94F0000u, 44u}, // pko -> Latn + {0x706C0000u, 44u}, // pl -> Latn + {0x816F0000u, 44u}, // pla -> Latn + {0xC98F0000u, 44u}, // pms -> Latn + {0x99AF0000u, 44u}, // png -> Latn + {0xB5AF0000u, 44u}, // pnn -> Latn + {0xCDAF0000u, 24u}, // pnt -> Grek + {0xB5CF0000u, 44u}, // pon -> Latn + {0xB9EF0000u, 44u}, // ppo -> Latn + {0x822F0000u, 38u}, // pra -> Khar {0x8E2F0000u, 1u}, // prd -> Arab - {0x9A2F0000u, 40u}, // prg -> Latn + {0x9A2F0000u, 44u}, // prg -> Latn {0x70730000u, 1u}, // ps -> Arab - {0xCA4F0000u, 40u}, // pss -> Latn - {0x70740000u, 40u}, // pt -> Latn - {0xBE6F0000u, 40u}, // ptp -> Latn - {0xD28F0000u, 40u}, // puu -> Latn - {0x82CF0000u, 40u}, // pwa -> Latn - {0x71750000u, 40u}, // qu -> Latn - {0x8A900000u, 40u}, // quc -> Latn - {0x9A900000u, 40u}, // qug -> Latn - {0xA0110000u, 40u}, // rai -> Latn - {0xA4110000u, 16u}, // raj -> Deva - {0xB8110000u, 40u}, // rao -> Latn - {0x94510000u, 40u}, // rcf -> Latn - {0xA4910000u, 40u}, // rej -> Latn - {0xAC910000u, 40u}, // rel -> Latn - {0xC8910000u, 40u}, // res -> Latn - {0xB4D10000u, 40u}, // rgn -> Latn + {0xCA4F0000u, 44u}, // pss -> Latn + {0x70740000u, 44u}, // pt -> Latn + {0xBE6F0000u, 44u}, // ptp -> Latn + {0xD28F0000u, 44u}, // puu -> Latn + {0x82CF0000u, 44u}, // pwa -> Latn + {0x71750000u, 44u}, // qu -> Latn + {0x8A900000u, 44u}, // quc -> Latn + {0x9A900000u, 44u}, // qug -> Latn + {0xA0110000u, 44u}, // rai -> Latn + {0xA4110000u, 17u}, // raj -> Deva + {0xB8110000u, 44u}, // rao -> Latn + {0x94510000u, 44u}, // rcf -> Latn + {0xA4910000u, 44u}, // rej -> Latn + {0xAC910000u, 44u}, // rel -> Latn + {0xC8910000u, 44u}, // res -> Latn + {0xB4D10000u, 44u}, // rgn -> Latn {0x98F10000u, 1u}, // rhg -> Arab - {0x81110000u, 40u}, // ria -> Latn - {0x95110000u, 78u}, // rif -> Tfng - {0x95114E4Cu, 40u}, // rif-NL -> Latn - {0xC9310000u, 16u}, // rjs -> Deva + {0x81110000u, 44u}, // ria -> Latn + {0x95110000u, 85u}, // rif -> Tfng + {0x95114E4Cu, 44u}, // rif-NL -> Latn + {0xC9310000u, 17u}, // rjs -> Deva {0xCD510000u, 7u}, // rkt -> Beng - {0x726D0000u, 40u}, // rm -> Latn - {0x95910000u, 40u}, // rmf -> Latn - {0xB9910000u, 40u}, // rmo -> Latn + {0x726D0000u, 44u}, // rm -> Latn + {0x95910000u, 44u}, // rmf -> Latn + {0xB9910000u, 44u}, // rmo -> Latn {0xCD910000u, 1u}, // rmt -> Arab - {0xD1910000u, 40u}, // rmu -> Latn - {0x726E0000u, 40u}, // rn -> Latn - {0x81B10000u, 40u}, // rna -> Latn - {0x99B10000u, 40u}, // rng -> Latn - {0x726F0000u, 40u}, // ro -> Latn - {0x85D10000u, 40u}, // rob -> Latn - {0x95D10000u, 40u}, // rof -> Latn - {0xB9D10000u, 40u}, // roo -> Latn - {0xBA310000u, 40u}, // rro -> Latn - {0xB2710000u, 40u}, // rtm -> Latn - {0x72750000u, 15u}, // ru -> Cyrl - {0x92910000u, 15u}, // rue -> Cyrl - {0x9A910000u, 40u}, // rug -> Latn - {0x72770000u, 40u}, // rw -> Latn - {0xAAD10000u, 40u}, // rwk -> Latn - {0xBAD10000u, 40u}, // rwo -> Latn - {0xD3110000u, 33u}, // ryu -> Kana - {0x73610000u, 16u}, // sa -> Deva - {0x94120000u, 40u}, // saf -> Latn - {0x9C120000u, 15u}, // sah -> Cyrl - {0xC0120000u, 40u}, // saq -> Latn - {0xC8120000u, 40u}, // sas -> Latn - {0xCC120000u, 40u}, // sat -> Latn - {0xE4120000u, 67u}, // saz -> Saur - {0x80320000u, 40u}, // sba -> Latn - {0x90320000u, 40u}, // sbe -> Latn - {0xBC320000u, 40u}, // sbp -> Latn - {0x73630000u, 40u}, // sc -> Latn - {0xA8520000u, 16u}, // sck -> Deva + {0xD1910000u, 44u}, // rmu -> Latn + {0x726E0000u, 44u}, // rn -> Latn + {0x81B10000u, 44u}, // rna -> Latn + {0x99B10000u, 44u}, // rng -> Latn + {0x726F0000u, 44u}, // ro -> Latn + {0x85D10000u, 44u}, // rob -> Latn + {0x95D10000u, 44u}, // rof -> Latn + {0xB9D10000u, 44u}, // roo -> Latn + {0xBA310000u, 44u}, // rro -> Latn + {0xB2710000u, 44u}, // rtm -> Latn + {0x72750000u, 16u}, // ru -> Cyrl + {0x92910000u, 16u}, // rue -> Cyrl + {0x9A910000u, 44u}, // rug -> Latn + {0x72770000u, 44u}, // rw -> Latn + {0xAAD10000u, 44u}, // rwk -> Latn + {0xBAD10000u, 44u}, // rwo -> Latn + {0xD3110000u, 37u}, // ryu -> Kana + {0x73610000u, 17u}, // sa -> Deva + {0x94120000u, 44u}, // saf -> Latn + {0x9C120000u, 16u}, // sah -> Cyrl + {0xC0120000u, 44u}, // saq -> Latn + {0xC8120000u, 44u}, // sas -> Latn + {0xCC120000u, 44u}, // sat -> Latn + {0xD4120000u, 44u}, // sav -> Latn + {0xE4120000u, 72u}, // saz -> Saur + {0x80320000u, 44u}, // sba -> Latn + {0x90320000u, 44u}, // sbe -> Latn + {0xBC320000u, 44u}, // sbp -> Latn + {0x73630000u, 44u}, // sc -> Latn + {0xA8520000u, 17u}, // sck -> Deva {0xAC520000u, 1u}, // scl -> Arab - {0xB4520000u, 40u}, // scn -> Latn - {0xB8520000u, 40u}, // sco -> Latn - {0xC8520000u, 40u}, // scs -> Latn + {0xB4520000u, 44u}, // scn -> Latn + {0xB8520000u, 44u}, // sco -> Latn + {0xC8520000u, 44u}, // scs -> Latn {0x73640000u, 1u}, // sd -> Arab - {0x88720000u, 40u}, // sdc -> Latn + {0x88720000u, 44u}, // sdc -> Latn {0x9C720000u, 1u}, // sdh -> Arab - {0x73650000u, 40u}, // se -> Latn - {0x94920000u, 40u}, // sef -> Latn - {0x9C920000u, 40u}, // seh -> Latn - {0xA0920000u, 40u}, // sei -> Latn - {0xC8920000u, 40u}, // ses -> Latn - {0x73670000u, 40u}, // sg -> Latn - {0x80D20000u, 55u}, // sga -> Ogam - {0xC8D20000u, 40u}, // sgs -> Latn - {0xD8D20000u, 18u}, // sgw -> Ethi - {0xE4D20000u, 40u}, // sgz -> Latn - {0x73680000u, 40u}, // sh -> Latn - {0xA0F20000u, 78u}, // shi -> Tfng - {0xA8F20000u, 40u}, // shk -> Latn - {0xB4F20000u, 52u}, // shn -> Mymr + {0x73650000u, 44u}, // se -> Latn + {0x94920000u, 44u}, // sef -> Latn + {0x9C920000u, 44u}, // seh -> Latn + {0xA0920000u, 44u}, // sei -> Latn + {0xC8920000u, 44u}, // ses -> Latn + {0x73670000u, 44u}, // sg -> Latn + {0x80D20000u, 60u}, // sga -> Ogam + {0xC8D20000u, 44u}, // sgs -> Latn + {0xD8D20000u, 19u}, // sgw -> Ethi + {0xE4D20000u, 44u}, // sgz -> Latn + {0x73680000u, 44u}, // sh -> Latn + {0xA0F20000u, 85u}, // shi -> Tfng + {0xA8F20000u, 44u}, // shk -> Latn + {0xB4F20000u, 56u}, // shn -> Mymr {0xD0F20000u, 1u}, // shu -> Arab - {0x73690000u, 69u}, // si -> Sinh - {0x8D120000u, 40u}, // sid -> Latn - {0x99120000u, 40u}, // sig -> Latn - {0xAD120000u, 40u}, // sil -> Latn - {0xB1120000u, 40u}, // sim -> Latn - {0xC5320000u, 40u}, // sjr -> Latn - {0x736B0000u, 40u}, // sk -> Latn - {0x89520000u, 40u}, // skc -> Latn + {0x73690000u, 74u}, // si -> Sinh + {0x8D120000u, 44u}, // sid -> Latn + {0x99120000u, 44u}, // sig -> Latn + {0xAD120000u, 44u}, // sil -> Latn + {0xB1120000u, 44u}, // sim -> Latn + {0xC5320000u, 44u}, // sjr -> Latn + {0x736B0000u, 44u}, // sk -> Latn + {0x89520000u, 44u}, // skc -> Latn {0xC5520000u, 1u}, // skr -> Arab - {0xC9520000u, 40u}, // sks -> Latn - {0x736C0000u, 40u}, // sl -> Latn - {0x8D720000u, 40u}, // sld -> Latn - {0xA1720000u, 40u}, // sli -> Latn - {0xAD720000u, 40u}, // sll -> Latn - {0xE1720000u, 40u}, // sly -> Latn - {0x736D0000u, 40u}, // sm -> Latn - {0x81920000u, 40u}, // sma -> Latn - {0xA5920000u, 40u}, // smj -> Latn - {0xB5920000u, 40u}, // smn -> Latn - {0xBD920000u, 65u}, // smp -> Samr - {0xC1920000u, 40u}, // smq -> Latn - {0xC9920000u, 40u}, // sms -> Latn - {0x736E0000u, 40u}, // sn -> Latn - {0x89B20000u, 40u}, // snc -> Latn - {0xA9B20000u, 40u}, // snk -> Latn - {0xBDB20000u, 40u}, // snp -> Latn - {0xDDB20000u, 40u}, // snx -> Latn - {0xE1B20000u, 40u}, // sny -> Latn - {0x736F0000u, 40u}, // so -> Latn - {0xA9D20000u, 40u}, // sok -> Latn - {0xC1D20000u, 40u}, // soq -> Latn - {0xD1D20000u, 80u}, // sou -> Thai - {0xE1D20000u, 40u}, // soy -> Latn - {0x8DF20000u, 40u}, // spd -> Latn - {0xADF20000u, 40u}, // spl -> Latn - {0xC9F20000u, 40u}, // sps -> Latn - {0x73710000u, 40u}, // sq -> Latn - {0x73720000u, 15u}, // sr -> Cyrl - {0x73724D45u, 40u}, // sr-ME -> Latn - {0x7372524Fu, 40u}, // sr-RO -> Latn - {0x73725255u, 40u}, // sr-RU -> Latn - {0x73725452u, 40u}, // sr-TR -> Latn - {0x86320000u, 70u}, // srb -> Sora - {0xB6320000u, 40u}, // srn -> Latn - {0xC6320000u, 40u}, // srr -> Latn - {0xDE320000u, 16u}, // srx -> Deva - {0x73730000u, 40u}, // ss -> Latn - {0x8E520000u, 40u}, // ssd -> Latn - {0x9A520000u, 40u}, // ssg -> Latn - {0xE2520000u, 40u}, // ssy -> Latn - {0x73740000u, 40u}, // st -> Latn - {0xAA720000u, 40u}, // stk -> Latn - {0xC2720000u, 40u}, // stq -> Latn - {0x73750000u, 40u}, // su -> Latn - {0x82920000u, 40u}, // sua -> Latn - {0x92920000u, 40u}, // sue -> Latn - {0xAA920000u, 40u}, // suk -> Latn - {0xC6920000u, 40u}, // sur -> Latn - {0xCA920000u, 40u}, // sus -> Latn - {0x73760000u, 40u}, // sv -> Latn - {0x73770000u, 40u}, // sw -> Latn + {0xC9520000u, 44u}, // sks -> Latn + {0x736C0000u, 44u}, // sl -> Latn + {0x8D720000u, 44u}, // sld -> Latn + {0xA1720000u, 44u}, // sli -> Latn + {0xAD720000u, 44u}, // sll -> Latn + {0xE1720000u, 44u}, // sly -> Latn + {0x736D0000u, 44u}, // sm -> Latn + {0x81920000u, 44u}, // sma -> Latn + {0xA5920000u, 44u}, // smj -> Latn + {0xB5920000u, 44u}, // smn -> Latn + {0xBD920000u, 70u}, // smp -> Samr + {0xC1920000u, 44u}, // smq -> Latn + {0xC9920000u, 44u}, // sms -> Latn + {0x736E0000u, 44u}, // sn -> Latn + {0x89B20000u, 44u}, // snc -> Latn + {0xA9B20000u, 44u}, // snk -> Latn + {0xBDB20000u, 44u}, // snp -> Latn + {0xDDB20000u, 44u}, // snx -> Latn + {0xE1B20000u, 44u}, // sny -> Latn + {0x736F0000u, 44u}, // so -> Latn + {0x99D20000u, 75u}, // sog -> Sogd + {0xA9D20000u, 44u}, // sok -> Latn + {0xC1D20000u, 44u}, // soq -> Latn + {0xD1D20000u, 87u}, // sou -> Thai + {0xE1D20000u, 44u}, // soy -> Latn + {0x8DF20000u, 44u}, // spd -> Latn + {0xADF20000u, 44u}, // spl -> Latn + {0xC9F20000u, 44u}, // sps -> Latn + {0x73710000u, 44u}, // sq -> Latn + {0x73720000u, 16u}, // sr -> Cyrl + {0x73724D45u, 44u}, // sr-ME -> Latn + {0x7372524Fu, 44u}, // sr-RO -> Latn + {0x73725255u, 44u}, // sr-RU -> Latn + {0x73725452u, 44u}, // sr-TR -> Latn + {0x86320000u, 76u}, // srb -> Sora + {0xB6320000u, 44u}, // srn -> Latn + {0xC6320000u, 44u}, // srr -> Latn + {0xDE320000u, 17u}, // srx -> Deva + {0x73730000u, 44u}, // ss -> Latn + {0x8E520000u, 44u}, // ssd -> Latn + {0x9A520000u, 44u}, // ssg -> Latn + {0xE2520000u, 44u}, // ssy -> Latn + {0x73740000u, 44u}, // st -> Latn + {0xAA720000u, 44u}, // stk -> Latn + {0xC2720000u, 44u}, // stq -> Latn + {0x73750000u, 44u}, // su -> Latn + {0x82920000u, 44u}, // sua -> Latn + {0x92920000u, 44u}, // sue -> Latn + {0xAA920000u, 44u}, // suk -> Latn + {0xC6920000u, 44u}, // sur -> Latn + {0xCA920000u, 44u}, // sus -> Latn + {0x73760000u, 44u}, // sv -> Latn + {0x73770000u, 44u}, // sw -> Latn {0x86D20000u, 1u}, // swb -> Arab - {0x8AD20000u, 40u}, // swc -> Latn - {0x9AD20000u, 40u}, // swg -> Latn - {0xBED20000u, 40u}, // swp -> Latn - {0xD6D20000u, 16u}, // swv -> Deva - {0xB6F20000u, 40u}, // sxn -> Latn - {0xDAF20000u, 40u}, // sxw -> Latn + {0x8AD20000u, 44u}, // swc -> Latn + {0x9AD20000u, 44u}, // swg -> Latn + {0xBED20000u, 44u}, // swp -> Latn + {0xD6D20000u, 17u}, // swv -> Deva + {0xB6F20000u, 44u}, // sxn -> Latn + {0xDAF20000u, 44u}, // sxw -> Latn {0xAF120000u, 7u}, // syl -> Beng - {0xC7120000u, 71u}, // syr -> Syrc - {0xAF320000u, 40u}, // szl -> Latn - {0x74610000u, 74u}, // ta -> Taml - {0xA4130000u, 16u}, // taj -> Deva - {0xAC130000u, 40u}, // tal -> Latn - {0xB4130000u, 40u}, // tan -> Latn - {0xC0130000u, 40u}, // taq -> Latn - {0x88330000u, 40u}, // tbc -> Latn - {0x8C330000u, 40u}, // tbd -> Latn - {0x94330000u, 40u}, // tbf -> Latn - {0x98330000u, 40u}, // tbg -> Latn - {0xB8330000u, 40u}, // tbo -> Latn - {0xD8330000u, 40u}, // tbw -> Latn - {0xE4330000u, 40u}, // tbz -> Latn - {0xA0530000u, 40u}, // tci -> Latn - {0xE0530000u, 36u}, // tcy -> Knda - {0x8C730000u, 72u}, // tdd -> Tale - {0x98730000u, 16u}, // tdg -> Deva - {0x9C730000u, 16u}, // tdh -> Deva - {0x74650000u, 77u}, // te -> Telu - {0x8C930000u, 40u}, // ted -> Latn - {0xB0930000u, 40u}, // tem -> Latn - {0xB8930000u, 40u}, // teo -> Latn - {0xCC930000u, 40u}, // tet -> Latn - {0xA0B30000u, 40u}, // tfi -> Latn - {0x74670000u, 15u}, // tg -> Cyrl + {0xC7120000u, 78u}, // syr -> Syrc + {0xAF320000u, 44u}, // szl -> Latn + {0x74610000u, 81u}, // ta -> Taml + {0xA4130000u, 17u}, // taj -> Deva + {0xAC130000u, 44u}, // tal -> Latn + {0xB4130000u, 44u}, // tan -> Latn + {0xC0130000u, 44u}, // taq -> Latn + {0x88330000u, 44u}, // tbc -> Latn + {0x8C330000u, 44u}, // tbd -> Latn + {0x94330000u, 44u}, // tbf -> Latn + {0x98330000u, 44u}, // tbg -> Latn + {0xB8330000u, 44u}, // tbo -> Latn + {0xD8330000u, 44u}, // tbw -> Latn + {0xE4330000u, 44u}, // tbz -> Latn + {0xA0530000u, 44u}, // tci -> Latn + {0xE0530000u, 40u}, // tcy -> Knda + {0x8C730000u, 79u}, // tdd -> Tale + {0x98730000u, 17u}, // tdg -> Deva + {0x9C730000u, 17u}, // tdh -> Deva + {0x74650000u, 84u}, // te -> Telu + {0x8C930000u, 44u}, // ted -> Latn + {0xB0930000u, 44u}, // tem -> Latn + {0xB8930000u, 44u}, // teo -> Latn + {0xCC930000u, 44u}, // tet -> Latn + {0xA0B30000u, 44u}, // tfi -> Latn + {0x74670000u, 16u}, // tg -> Cyrl {0x7467504Bu, 1u}, // tg-PK -> Arab - {0x88D30000u, 40u}, // tgc -> Latn - {0xB8D30000u, 40u}, // tgo -> Latn - {0xD0D30000u, 40u}, // tgu -> Latn - {0x74680000u, 80u}, // th -> Thai - {0xACF30000u, 16u}, // thl -> Deva - {0xC0F30000u, 16u}, // thq -> Deva - {0xC4F30000u, 16u}, // thr -> Deva - {0x74690000u, 18u}, // ti -> Ethi - {0x95130000u, 40u}, // tif -> Latn - {0x99130000u, 18u}, // tig -> Ethi - {0xA9130000u, 40u}, // tik -> Latn - {0xB1130000u, 40u}, // tim -> Latn - {0xB9130000u, 40u}, // tio -> Latn - {0xD5130000u, 40u}, // tiv -> Latn - {0x746B0000u, 40u}, // tk -> Latn - {0xAD530000u, 40u}, // tkl -> Latn - {0xC5530000u, 40u}, // tkr -> Latn - {0xCD530000u, 16u}, // tkt -> Deva - {0x746C0000u, 40u}, // tl -> Latn - {0x95730000u, 40u}, // tlf -> Latn - {0xDD730000u, 40u}, // tlx -> Latn - {0xE1730000u, 40u}, // tly -> Latn - {0x9D930000u, 40u}, // tmh -> Latn - {0xE1930000u, 40u}, // tmy -> Latn - {0x746E0000u, 40u}, // tn -> Latn - {0x9DB30000u, 40u}, // tnh -> Latn - {0x746F0000u, 40u}, // to -> Latn - {0x95D30000u, 40u}, // tof -> Latn - {0x99D30000u, 40u}, // tog -> Latn - {0xC1D30000u, 40u}, // toq -> Latn - {0xA1F30000u, 40u}, // tpi -> Latn - {0xB1F30000u, 40u}, // tpm -> Latn - {0xE5F30000u, 40u}, // tpz -> Latn - {0xBA130000u, 40u}, // tqo -> Latn - {0x74720000u, 40u}, // tr -> Latn - {0xD2330000u, 40u}, // tru -> Latn - {0xD6330000u, 40u}, // trv -> Latn + {0x88D30000u, 44u}, // tgc -> Latn + {0xB8D30000u, 44u}, // tgo -> Latn + {0xD0D30000u, 44u}, // tgu -> Latn + {0x74680000u, 87u}, // th -> Thai + {0xACF30000u, 17u}, // thl -> Deva + {0xC0F30000u, 17u}, // thq -> Deva + {0xC4F30000u, 17u}, // thr -> Deva + {0x74690000u, 19u}, // ti -> Ethi + {0x95130000u, 44u}, // tif -> Latn + {0x99130000u, 19u}, // tig -> Ethi + {0xA9130000u, 44u}, // tik -> Latn + {0xB1130000u, 44u}, // tim -> Latn + {0xB9130000u, 44u}, // tio -> Latn + {0xD5130000u, 44u}, // tiv -> Latn + {0x746B0000u, 44u}, // tk -> Latn + {0xAD530000u, 44u}, // tkl -> Latn + {0xC5530000u, 44u}, // tkr -> Latn + {0xCD530000u, 17u}, // tkt -> Deva + {0x746C0000u, 44u}, // tl -> Latn + {0x95730000u, 44u}, // tlf -> Latn + {0xDD730000u, 44u}, // tlx -> Latn + {0xE1730000u, 44u}, // tly -> Latn + {0x9D930000u, 44u}, // tmh -> Latn + {0xE1930000u, 44u}, // tmy -> Latn + {0x746E0000u, 44u}, // tn -> Latn + {0x9DB30000u, 44u}, // tnh -> Latn + {0x746F0000u, 44u}, // to -> Latn + {0x95D30000u, 44u}, // tof -> Latn + {0x99D30000u, 44u}, // tog -> Latn + {0xC1D30000u, 44u}, // toq -> Latn + {0xA1F30000u, 44u}, // tpi -> Latn + {0xB1F30000u, 44u}, // tpm -> Latn + {0xE5F30000u, 44u}, // tpz -> Latn + {0xBA130000u, 44u}, // tqo -> Latn + {0x74720000u, 44u}, // tr -> Latn + {0xD2330000u, 44u}, // tru -> Latn + {0xD6330000u, 44u}, // trv -> Latn {0xDA330000u, 1u}, // trw -> Arab - {0x74730000u, 40u}, // ts -> Latn - {0x8E530000u, 21u}, // tsd -> Grek - {0x96530000u, 16u}, // tsf -> Deva - {0x9A530000u, 40u}, // tsg -> Latn - {0xA6530000u, 81u}, // tsj -> Tibt - {0xDA530000u, 40u}, // tsw -> Latn - {0x74740000u, 15u}, // tt -> Cyrl - {0x8E730000u, 40u}, // ttd -> Latn - {0x92730000u, 40u}, // tte -> Latn - {0xA6730000u, 40u}, // ttj -> Latn - {0xC6730000u, 40u}, // ttr -> Latn - {0xCA730000u, 80u}, // tts -> Thai - {0xCE730000u, 40u}, // ttt -> Latn - {0x9E930000u, 40u}, // tuh -> Latn - {0xAE930000u, 40u}, // tul -> Latn - {0xB2930000u, 40u}, // tum -> Latn - {0xC2930000u, 40u}, // tuq -> Latn - {0x8EB30000u, 40u}, // tvd -> Latn - {0xAEB30000u, 40u}, // tvl -> Latn - {0xD2B30000u, 40u}, // tvu -> Latn - {0x9ED30000u, 40u}, // twh -> Latn - {0xC2D30000u, 40u}, // twq -> Latn - {0x9AF30000u, 75u}, // txg -> Tang - {0x74790000u, 40u}, // ty -> Latn - {0x83130000u, 40u}, // tya -> Latn - {0xD7130000u, 15u}, // tyv -> Cyrl - {0xB3330000u, 40u}, // tzm -> Latn - {0xD0340000u, 40u}, // ubu -> Latn - {0xB0740000u, 15u}, // udm -> Cyrl + {0x74730000u, 44u}, // ts -> Latn + {0x8E530000u, 24u}, // tsd -> Grek + {0x96530000u, 17u}, // tsf -> Deva + {0x9A530000u, 44u}, // tsg -> Latn + {0xA6530000u, 88u}, // tsj -> Tibt + {0xDA530000u, 44u}, // tsw -> Latn + {0x74740000u, 16u}, // tt -> Cyrl + {0x8E730000u, 44u}, // ttd -> Latn + {0x92730000u, 44u}, // tte -> Latn + {0xA6730000u, 44u}, // ttj -> Latn + {0xC6730000u, 44u}, // ttr -> Latn + {0xCA730000u, 87u}, // tts -> Thai + {0xCE730000u, 44u}, // ttt -> Latn + {0x9E930000u, 44u}, // tuh -> Latn + {0xAE930000u, 44u}, // tul -> Latn + {0xB2930000u, 44u}, // tum -> Latn + {0xC2930000u, 44u}, // tuq -> Latn + {0x8EB30000u, 44u}, // tvd -> Latn + {0xAEB30000u, 44u}, // tvl -> Latn + {0xD2B30000u, 44u}, // tvu -> Latn + {0x9ED30000u, 44u}, // twh -> Latn + {0xC2D30000u, 44u}, // twq -> Latn + {0x9AF30000u, 82u}, // txg -> Tang + {0x74790000u, 44u}, // ty -> Latn + {0x83130000u, 44u}, // tya -> Latn + {0xD7130000u, 16u}, // tyv -> Cyrl + {0xB3330000u, 44u}, // tzm -> Latn + {0xD0340000u, 44u}, // ubu -> Latn + {0xB0740000u, 16u}, // udm -> Cyrl {0x75670000u, 1u}, // ug -> Arab - {0x75674B5Au, 15u}, // ug-KZ -> Cyrl - {0x75674D4Eu, 15u}, // ug-MN -> Cyrl - {0x80D40000u, 82u}, // uga -> Ugar - {0x756B0000u, 15u}, // uk -> Cyrl - {0xA1740000u, 40u}, // uli -> Latn - {0x85940000u, 40u}, // umb -> Latn + {0x75674B5Au, 16u}, // ug-KZ -> Cyrl + {0x75674D4Eu, 16u}, // ug-MN -> Cyrl + {0x80D40000u, 89u}, // uga -> Ugar + {0x756B0000u, 16u}, // uk -> Cyrl + {0xA1740000u, 44u}, // uli -> Latn + {0x85940000u, 44u}, // umb -> Latn {0xC5B40000u, 7u}, // unr -> Beng - {0xC5B44E50u, 16u}, // unr-NP -> Deva + {0xC5B44E50u, 17u}, // unr-NP -> Deva {0xDDB40000u, 7u}, // unx -> Beng {0x75720000u, 1u}, // ur -> Arab - {0xA2340000u, 40u}, // uri -> Latn - {0xCE340000u, 40u}, // urt -> Latn - {0xDA340000u, 40u}, // urw -> Latn - {0x82540000u, 40u}, // usa -> Latn - {0xC6740000u, 40u}, // utr -> Latn - {0x9EB40000u, 40u}, // uvh -> Latn - {0xAEB40000u, 40u}, // uvl -> Latn - {0x757A0000u, 40u}, // uz -> Latn + {0xA2340000u, 44u}, // uri -> Latn + {0xCE340000u, 44u}, // urt -> Latn + {0xDA340000u, 44u}, // urw -> Latn + {0x82540000u, 44u}, // usa -> Latn + {0xC6740000u, 44u}, // utr -> Latn + {0x9EB40000u, 44u}, // uvh -> Latn + {0xAEB40000u, 44u}, // uvl -> Latn + {0x757A0000u, 44u}, // uz -> Latn {0x757A4146u, 1u}, // uz-AF -> Arab - {0x757A434Eu, 15u}, // uz-CN -> Cyrl - {0x98150000u, 40u}, // vag -> Latn - {0xA0150000u, 83u}, // vai -> Vaii - {0xB4150000u, 40u}, // van -> Latn - {0x76650000u, 40u}, // ve -> Latn - {0x88950000u, 40u}, // vec -> Latn - {0xBC950000u, 40u}, // vep -> Latn - {0x76690000u, 40u}, // vi -> Latn - {0x89150000u, 40u}, // vic -> Latn - {0xD5150000u, 40u}, // viv -> Latn - {0xC9750000u, 40u}, // vls -> Latn - {0x95950000u, 40u}, // vmf -> Latn - {0xD9950000u, 40u}, // vmw -> Latn - {0x766F0000u, 40u}, // vo -> Latn - {0xCDD50000u, 40u}, // vot -> Latn - {0xBA350000u, 40u}, // vro -> Latn - {0xB6950000u, 40u}, // vun -> Latn - {0xCE950000u, 40u}, // vut -> Latn - {0x77610000u, 40u}, // wa -> Latn - {0x90160000u, 40u}, // wae -> Latn - {0xA4160000u, 40u}, // waj -> Latn - {0xAC160000u, 18u}, // wal -> Ethi - {0xB4160000u, 40u}, // wan -> Latn - {0xC4160000u, 40u}, // war -> Latn - {0xBC360000u, 40u}, // wbp -> Latn - {0xC0360000u, 77u}, // wbq -> Telu - {0xC4360000u, 16u}, // wbr -> Deva - {0xA0560000u, 40u}, // wci -> Latn - {0xC4960000u, 40u}, // wer -> Latn - {0xA0D60000u, 40u}, // wgi -> Latn - {0x98F60000u, 40u}, // whg -> Latn - {0x85160000u, 40u}, // wib -> Latn - {0xD1160000u, 40u}, // wiu -> Latn - {0xD5160000u, 40u}, // wiv -> Latn - {0x81360000u, 40u}, // wja -> Latn - {0xA1360000u, 40u}, // wji -> Latn - {0xC9760000u, 40u}, // wls -> Latn - {0xB9960000u, 40u}, // wmo -> Latn - {0x89B60000u, 40u}, // wnc -> Latn + {0x757A434Eu, 16u}, // uz-CN -> Cyrl + {0x98150000u, 44u}, // vag -> Latn + {0xA0150000u, 90u}, // vai -> Vaii + {0xB4150000u, 44u}, // van -> Latn + {0x76650000u, 44u}, // ve -> Latn + {0x88950000u, 44u}, // vec -> Latn + {0xBC950000u, 44u}, // vep -> Latn + {0x76690000u, 44u}, // vi -> Latn + {0x89150000u, 44u}, // vic -> Latn + {0xD5150000u, 44u}, // viv -> Latn + {0xC9750000u, 44u}, // vls -> Latn + {0x95950000u, 44u}, // vmf -> Latn + {0xD9950000u, 44u}, // vmw -> Latn + {0x766F0000u, 44u}, // vo -> Latn + {0xCDD50000u, 44u}, // vot -> Latn + {0xBA350000u, 44u}, // vro -> Latn + {0xB6950000u, 44u}, // vun -> Latn + {0xCE950000u, 44u}, // vut -> Latn + {0x77610000u, 44u}, // wa -> Latn + {0x90160000u, 44u}, // wae -> Latn + {0xA4160000u, 44u}, // waj -> Latn + {0xAC160000u, 19u}, // wal -> Ethi + {0xB4160000u, 44u}, // wan -> Latn + {0xC4160000u, 44u}, // war -> Latn + {0xBC360000u, 44u}, // wbp -> Latn + {0xC0360000u, 84u}, // wbq -> Telu + {0xC4360000u, 17u}, // wbr -> Deva + {0xA0560000u, 44u}, // wci -> Latn + {0xC4960000u, 44u}, // wer -> Latn + {0xA0D60000u, 44u}, // wgi -> Latn + {0x98F60000u, 44u}, // whg -> Latn + {0x85160000u, 44u}, // wib -> Latn + {0xD1160000u, 44u}, // wiu -> Latn + {0xD5160000u, 44u}, // wiv -> Latn + {0x81360000u, 44u}, // wja -> Latn + {0xA1360000u, 44u}, // wji -> Latn + {0xC9760000u, 44u}, // wls -> Latn + {0xB9960000u, 44u}, // wmo -> Latn + {0x89B60000u, 44u}, // wnc -> Latn {0xA1B60000u, 1u}, // wni -> Arab - {0xD1B60000u, 40u}, // wnu -> Latn - {0x776F0000u, 40u}, // wo -> Latn - {0x85D60000u, 40u}, // wob -> Latn - {0xC9D60000u, 40u}, // wos -> Latn - {0xCA360000u, 40u}, // wrs -> Latn - {0xAA560000u, 40u}, // wsk -> Latn - {0xB2760000u, 16u}, // wtm -> Deva - {0xD2960000u, 24u}, // wuu -> Hans - {0xD6960000u, 40u}, // wuv -> Latn - {0x82D60000u, 40u}, // wwa -> Latn - {0xD4170000u, 40u}, // xav -> Latn - {0xA0370000u, 40u}, // xbi -> Latn - {0xC4570000u, 10u}, // xcr -> Cari - {0xC8970000u, 40u}, // xes -> Latn - {0x78680000u, 40u}, // xh -> Latn - {0x81770000u, 40u}, // xla -> Latn - {0x89770000u, 44u}, // xlc -> Lyci - {0x8D770000u, 45u}, // xld -> Lydi - {0x95970000u, 19u}, // xmf -> Geor - {0xB5970000u, 47u}, // xmn -> Mani - {0xC5970000u, 48u}, // xmr -> Merc - {0x81B70000u, 53u}, // xna -> Narb - {0xC5B70000u, 16u}, // xnr -> Deva - {0x99D70000u, 40u}, // xog -> Latn - {0xB5D70000u, 40u}, // xon -> Latn - {0xC5F70000u, 63u}, // xpr -> Prti - {0x86370000u, 40u}, // xrb -> Latn - {0x82570000u, 66u}, // xsa -> Sarb - {0xA2570000u, 40u}, // xsi -> Latn - {0xB2570000u, 40u}, // xsm -> Latn - {0xC6570000u, 16u}, // xsr -> Deva - {0x92D70000u, 40u}, // xwe -> Latn - {0xB0180000u, 40u}, // yam -> Latn - {0xB8180000u, 40u}, // yao -> Latn - {0xBC180000u, 40u}, // yap -> Latn - {0xC8180000u, 40u}, // yas -> Latn - {0xCC180000u, 40u}, // yat -> Latn - {0xD4180000u, 40u}, // yav -> Latn - {0xE0180000u, 40u}, // yay -> Latn - {0xE4180000u, 40u}, // yaz -> Latn - {0x80380000u, 40u}, // yba -> Latn - {0x84380000u, 40u}, // ybb -> Latn - {0xE0380000u, 40u}, // yby -> Latn - {0xC4980000u, 40u}, // yer -> Latn - {0xC4D80000u, 40u}, // ygr -> Latn - {0xD8D80000u, 40u}, // ygw -> Latn - {0x79690000u, 27u}, // yi -> Hebr - {0xB9580000u, 40u}, // yko -> Latn - {0x91780000u, 40u}, // yle -> Latn - {0x99780000u, 40u}, // ylg -> Latn - {0xAD780000u, 40u}, // yll -> Latn - {0xAD980000u, 40u}, // yml -> Latn - {0x796F0000u, 40u}, // yo -> Latn - {0xB5D80000u, 40u}, // yon -> Latn - {0x86380000u, 40u}, // yrb -> Latn - {0x92380000u, 40u}, // yre -> Latn - {0xAE380000u, 40u}, // yrl -> Latn - {0xCA580000u, 40u}, // yss -> Latn - {0x82980000u, 40u}, // yua -> Latn - {0x92980000u, 25u}, // yue -> Hant - {0x9298434Eu, 24u}, // yue-CN -> Hans - {0xA6980000u, 40u}, // yuj -> Latn - {0xCE980000u, 40u}, // yut -> Latn - {0xDA980000u, 40u}, // yuw -> Latn - {0x7A610000u, 40u}, // za -> Latn - {0x98190000u, 40u}, // zag -> Latn + {0xD1B60000u, 44u}, // wnu -> Latn + {0x776F0000u, 44u}, // wo -> Latn + {0x85D60000u, 44u}, // wob -> Latn + {0xC9D60000u, 44u}, // wos -> Latn + {0xCA360000u, 44u}, // wrs -> Latn + {0x9A560000u, 21u}, // wsg -> Gong + {0xAA560000u, 44u}, // wsk -> Latn + {0xB2760000u, 17u}, // wtm -> Deva + {0xD2960000u, 27u}, // wuu -> Hans + {0xD6960000u, 44u}, // wuv -> Latn + {0x82D60000u, 44u}, // wwa -> Latn + {0xD4170000u, 44u}, // xav -> Latn + {0xA0370000u, 44u}, // xbi -> Latn + {0xC4570000u, 11u}, // xcr -> Cari + {0xC8970000u, 44u}, // xes -> Latn + {0x78680000u, 44u}, // xh -> Latn + {0x81770000u, 44u}, // xla -> Latn + {0x89770000u, 48u}, // xlc -> Lyci + {0x8D770000u, 49u}, // xld -> Lydi + {0x95970000u, 20u}, // xmf -> Geor + {0xB5970000u, 51u}, // xmn -> Mani + {0xC5970000u, 52u}, // xmr -> Merc + {0x81B70000u, 57u}, // xna -> Narb + {0xC5B70000u, 17u}, // xnr -> Deva + {0x99D70000u, 44u}, // xog -> Latn + {0xB5D70000u, 44u}, // xon -> Latn + {0xC5F70000u, 68u}, // xpr -> Prti + {0x86370000u, 44u}, // xrb -> Latn + {0x82570000u, 71u}, // xsa -> Sarb + {0xA2570000u, 44u}, // xsi -> Latn + {0xB2570000u, 44u}, // xsm -> Latn + {0xC6570000u, 17u}, // xsr -> Deva + {0x92D70000u, 44u}, // xwe -> Latn + {0xB0180000u, 44u}, // yam -> Latn + {0xB8180000u, 44u}, // yao -> Latn + {0xBC180000u, 44u}, // yap -> Latn + {0xC8180000u, 44u}, // yas -> Latn + {0xCC180000u, 44u}, // yat -> Latn + {0xD4180000u, 44u}, // yav -> Latn + {0xE0180000u, 44u}, // yay -> Latn + {0xE4180000u, 44u}, // yaz -> Latn + {0x80380000u, 44u}, // yba -> Latn + {0x84380000u, 44u}, // ybb -> Latn + {0xE0380000u, 44u}, // yby -> Latn + {0xC4980000u, 44u}, // yer -> Latn + {0xC4D80000u, 44u}, // ygr -> Latn + {0xD8D80000u, 44u}, // ygw -> Latn + {0x79690000u, 30u}, // yi -> Hebr + {0xB9580000u, 44u}, // yko -> Latn + {0x91780000u, 44u}, // yle -> Latn + {0x99780000u, 44u}, // ylg -> Latn + {0xAD780000u, 44u}, // yll -> Latn + {0xAD980000u, 44u}, // yml -> Latn + {0x796F0000u, 44u}, // yo -> Latn + {0xB5D80000u, 44u}, // yon -> Latn + {0x86380000u, 44u}, // yrb -> Latn + {0x92380000u, 44u}, // yre -> Latn + {0xAE380000u, 44u}, // yrl -> Latn + {0xCA580000u, 44u}, // yss -> Latn + {0x82980000u, 44u}, // yua -> Latn + {0x92980000u, 28u}, // yue -> Hant + {0x9298434Eu, 27u}, // yue-CN -> Hans + {0xA6980000u, 44u}, // yuj -> Latn + {0xCE980000u, 44u}, // yut -> Latn + {0xDA980000u, 44u}, // yuw -> Latn + {0x7A610000u, 44u}, // za -> Latn + {0x98190000u, 44u}, // zag -> Latn {0xA4790000u, 1u}, // zdj -> Arab - {0x80990000u, 40u}, // zea -> Latn - {0x9CD90000u, 78u}, // zgh -> Tfng - {0x7A680000u, 24u}, // zh -> Hans - {0x7A684155u, 25u}, // zh-AU -> Hant - {0x7A68424Eu, 25u}, // zh-BN -> Hant - {0x7A684742u, 25u}, // zh-GB -> Hant - {0x7A684746u, 25u}, // zh-GF -> Hant - {0x7A68484Bu, 25u}, // zh-HK -> Hant - {0x7A684944u, 25u}, // zh-ID -> Hant - {0x7A684D4Fu, 25u}, // zh-MO -> Hant - {0x7A684D59u, 25u}, // zh-MY -> Hant - {0x7A685041u, 25u}, // zh-PA -> Hant - {0x7A685046u, 25u}, // zh-PF -> Hant - {0x7A685048u, 25u}, // zh-PH -> Hant - {0x7A685352u, 25u}, // zh-SR -> Hant - {0x7A685448u, 25u}, // zh-TH -> Hant - {0x7A685457u, 25u}, // zh-TW -> Hant - {0x7A685553u, 25u}, // zh-US -> Hant - {0x7A68564Eu, 25u}, // zh-VN -> Hant - {0x81190000u, 40u}, // zia -> Latn - {0xB1790000u, 40u}, // zlm -> Latn - {0xA1990000u, 40u}, // zmi -> Latn - {0x91B90000u, 40u}, // zne -> Latn - {0x7A750000u, 40u}, // zu -> Latn - {0x83390000u, 40u}, // zza -> Latn + {0x80990000u, 44u}, // zea -> Latn + {0x9CD90000u, 85u}, // zgh -> Tfng + {0x7A680000u, 27u}, // zh -> Hans + {0x7A684155u, 28u}, // zh-AU -> Hant + {0x7A68424Eu, 28u}, // zh-BN -> Hant + {0x7A684742u, 28u}, // zh-GB -> Hant + {0x7A684746u, 28u}, // zh-GF -> Hant + {0x7A68484Bu, 28u}, // zh-HK -> Hant + {0x7A684944u, 28u}, // zh-ID -> Hant + {0x7A684D4Fu, 28u}, // zh-MO -> Hant + {0x7A684D59u, 28u}, // zh-MY -> Hant + {0x7A685041u, 28u}, // zh-PA -> Hant + {0x7A685046u, 28u}, // zh-PF -> Hant + {0x7A685048u, 28u}, // zh-PH -> Hant + {0x7A685352u, 28u}, // zh-SR -> Hant + {0x7A685448u, 28u}, // zh-TH -> Hant + {0x7A685457u, 28u}, // zh-TW -> Hant + {0x7A685553u, 28u}, // zh-US -> Hant + {0x7A68564Eu, 28u}, // zh-VN -> Hant + {0xDCF90000u, 59u}, // zhx -> Nshu + {0x81190000u, 44u}, // zia -> Latn + {0xB1790000u, 44u}, // zlm -> Latn + {0xA1990000u, 44u}, // zmi -> Latn + {0x91B90000u, 44u}, // zne -> Latn + {0x7A750000u, 44u}, // zu -> Latn + {0x83390000u, 44u}, // zza -> Latn }); std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ @@ -1517,6 +1544,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xB5014E474C61746ELLU, // bin_Latn_NG 0xA521494E44657661LLU, // bjj_Deva_IN 0xB52149444C61746ELLU, // bjn_Latn_ID + 0xCD21534E4C61746ELLU, // bjt_Latn_SN 0xB141434D4C61746ELLU, // bkm_Latn_CM 0xD14150484C61746ELLU, // bku_Latn_PH 0xCD61564E54617674LLU, // blt_Tavt_VN @@ -1546,7 +1574,6 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x93214D4C4C61746ELLU, // bze_Latn_ML 0x636145534C61746ELLU, // ca_Latn_ES 0x9C424E474C61746ELLU, // cch_Latn_NG - 0xBC42494E42656E67LLU, // ccp_Beng_IN 0xBC42424443616B6DLLU, // ccp_Cakm_BD 0x636552554379726CLLU, // ce_Cyrl_RU 0x848250484C61746ELLU, // ceb_Latn_PH @@ -1560,10 +1587,12 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x81224B4841726162LLU, // cja_Arab_KH 0xB122564E4368616DLLU, // cjm_Cham_VN 0x8542495141726162LLU, // ckb_Arab_IQ + 0x99824D4E536F796FLLU, // cmg_Soyo_MN 0x636F46524C61746ELLU, // co_Latn_FR 0xBDC24547436F7074LLU, // cop_Copt_EG 0xC9E250484C61746ELLU, // cps_Latn_PH 0x6372434143616E73LLU, // cr_Cans_CA + 0x9E2255414379726CLLU, // crh_Cyrl_UA 0xA622434143616E73LLU, // crj_Cans_CA 0xAA22434143616E73LLU, // crk_Cans_CA 0xAE22434143616E73LLU, // crl_Cans_CA @@ -1610,6 +1639,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x657345534C61746ELLU, // es_Latn_ES 0x65734D584C61746ELLU, // es_Latn_MX 0x657355534C61746ELLU, // es_Latn_US + 0x9A44494E476F6E6DLLU, // esg_Gonm_IN 0xD24455534C61746ELLU, // esu_Latn_US 0x657445454C61746ELLU, // et_Latn_EE 0xCE6449544974616CLLU, // ett_Ital_IT @@ -1700,10 +1730,10 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x687548554C61746ELLU, // hu_Latn_HU 0x6879414D41726D6ELLU, // hy_Armn_AM 0x687A4E414C61746ELLU, // hz_Latn_NA - 0x696146524C61746ELLU, // ia_Latn_FR 0x80284D594C61746ELLU, // iba_Latn_MY 0x84284E474C61746ELLU, // ibb_Latn_NG 0x696449444C61746ELLU, // id_Latn_ID + 0x90A854474C61746ELLU, // ife_Latn_TG 0x69674E474C61746ELLU, // ig_Latn_NG 0x6969434E59696969LLU, // ii_Yiii_CN 0x696B55534C61746ELLU, // ik_Latn_US @@ -1764,6 +1794,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x6B6D4B484B686D72LLU, // km_Khmr_KH 0x858A414F4C61746ELLU, // kmb_Latn_AO 0x6B6E494E4B6E6461LLU, // kn_Knda_IN + 0x95AA47574C61746ELLU, // knf_Latn_GW 0x6B6F4B524B6F7265LLU, // ko_Kore_KR 0xA1CA52554379726CLLU, // koi_Cyrl_RU 0xA9CA494E44657661LLU, // kok_Deva_IN @@ -1854,6 +1885,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x6D694E5A4C61746ELLU, // mi_Latn_NZ 0xB50C49444C61746ELLU, // min_Latn_ID 0xC90C495148617472LLU, // mis_Hatr_IQ + 0xC90C4E474D656466LLU, // mis_Medf_NG 0x6D6B4D4B4379726CLLU, // mk_Cyrl_MK 0x6D6C494E4D6C796DLLU, // ml_Mlym_IN 0xC96C53444C61746ELLU, // mls_Latn_SD @@ -1877,6 +1909,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xAACC4D4C4C61746ELLU, // mwk_Latn_ML 0xC6CC494E44657661LLU, // mwr_Deva_IN 0xD6CC49444C61746ELLU, // mwv_Latn_ID + 0xDACC5553486D6E70LLU, // mww_Hmnp_US 0x8AEC5A574C61746ELLU, // mxc_Latn_ZW 0x6D794D4D4D796D72LLU, // my_Mymr_MM 0xD70C52554379726CLLU, // myv_Cyrl_RU @@ -1905,6 +1938,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x998D434D4C61746ELLU, // nmg_Latn_CM 0x6E6E4E4F4C61746ELLU, // nn_Latn_NO 0x9DAD434D4C61746ELLU, // nnh_Latn_CM + 0xBDAD494E5763686FLLU, // nnp_Wcho_IN 0x6E6F4E4F4C61746ELLU, // no_Latn_NO 0x8DCD54484C616E61LLU, // nod_Lana_TH 0x91CD494E44657661LLU, // noe_Deva_IN @@ -1959,6 +1993,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x945152454C61746ELLU, // rcf_Latn_RE 0xA49149444C61746ELLU, // rej_Latn_ID 0xB4D149544C61746ELLU, // rgn_Latn_IT + 0x98F14D4D41726162LLU, // rhg_Arab_MM 0x8111494E4C61746ELLU, // ria_Latn_IN 0x95114D4154666E67LLU, // rif_Tfng_MA 0xC9314E5044657661LLU, // rjs_Deva_NP @@ -1986,6 +2021,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xC0124B454C61746ELLU, // saq_Latn_KE 0xC81249444C61746ELLU, // sas_Latn_ID 0xCC12494E4C61746ELLU, // sat_Latn_IN + 0xD412534E4C61746ELLU, // sav_Latn_SN 0xE412494E53617572LLU, // saz_Saur_IN 0xBC32545A4C61746ELLU, // sbp_Latn_TZ 0x736349544C61746ELLU, // sc_Latn_IT @@ -2025,6 +2061,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x736E5A574C61746ELLU, // sn_Latn_ZW 0xA9B24D4C4C61746ELLU, // snk_Latn_ML 0x736F534F4C61746ELLU, // so_Latn_SO + 0x99D2555A536F6764LLU, // sog_Sogd_UZ 0xD1D2544854686169LLU, // sou_Thai_TH 0x7371414C4C61746ELLU, // sq_Latn_AL 0x737252534379726CLLU, // sr_Cyrl_RS @@ -2135,6 +2172,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0xC97657464C61746ELLU, // wls_Latn_WF 0xA1B64B4D41726162LLU, // wni_Arab_KM 0x776F534E4C61746ELLU, // wo_Latn_SN + 0x9A56494E476F6E67LLU, // wsg_Gong_IN 0xB276494E44657661LLU, // wtm_Deva_IN 0xD296434E48616E73LLU, // wuu_Hans_CN 0xD41742524C61746ELLU, // xav_Latn_BR @@ -2169,6 +2207,7 @@ std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({ 0x7A68545748616E62LLU, // zh_Hanb_TW 0x7A68434E48616E73LLU, // zh_Hans_CN 0x7A68545748616E74LLU, // zh_Hant_TW + 0xDCF9434E4E736875LLU, // zhx_Nshu_CN 0xB17954474C61746ELLU, // zlm_Latn_TG 0xA1994D594C61746ELLU, // zmi_Latn_MY 0x7A755A414C61746ELLU, // zu_Latn_ZA @@ -2194,7 +2233,7 @@ const std::unordered_map<uint32_t, uint32_t> LATN_PARENTS({ {0x656E4154u, 0x656E80A1u}, // en-AT -> en-150 {0x656E4155u, 0x656E8400u}, // en-AU -> en-001 {0x656E4242u, 0x656E8400u}, // en-BB -> en-001 - {0x656E4245u, 0x656E8400u}, // en-BE -> en-001 + {0x656E4245u, 0x656E80A1u}, // en-BE -> en-150 {0x656E424Du, 0x656E8400u}, // en-BM -> en-001 {0x656E4253u, 0x656E8400u}, // en-BS -> en-001 {0x656E4257u, 0x656E8400u}, // en-BW -> en-001 @@ -2285,6 +2324,7 @@ const std::unordered_map<uint32_t, uint32_t> LATN_PARENTS({ {0x65734152u, 0x6573A424u}, // es-AR -> es-419 {0x6573424Fu, 0x6573A424u}, // es-BO -> es-419 {0x65734252u, 0x6573A424u}, // es-BR -> es-419 + {0x6573425Au, 0x6573A424u}, // es-BZ -> es-419 {0x6573434Cu, 0x6573A424u}, // es-CL -> es-419 {0x6573434Fu, 0x6573A424u}, // es-CO -> es-419 {0x65734352u, 0x6573A424u}, // es-CR -> es-419 @@ -2315,6 +2355,10 @@ const std::unordered_map<uint32_t, uint32_t> LATN_PARENTS({ {0x7074544Cu, 0x70745054u}, // pt-TL -> pt-PT }); +const std::unordered_map<uint32_t, uint32_t> ___B_PARENTS({ + {0x61725842u, 0x61729420u}, // ar-XB -> ar-015 +}); + const struct { const char script[4]; const std::unordered_map<uint32_t, uint32_t>* map; @@ -2322,6 +2366,7 @@ const struct { {{'A', 'r', 'a', 'b'}, &ARAB_PARENTS}, {{'H', 'a', 'n', 't'}, &HANT_PARENTS}, {{'L', 'a', 't', 'n'}, &LATN_PARENTS}, + {{'~', '~', '~', 'B'}, &___B_PARENTS}, }; const size_t MAX_PARENT_DEPTH = 3; diff --git a/media/Android.bp b/media/Android.bp index e2bdad1f21b9..75ccb227e5c5 100644 --- a/media/Android.bp +++ b/media/Android.bp @@ -77,20 +77,13 @@ filegroup { path: "apex/java" } -metalava_updatable_media_args = " --error UnhiddenSystemApi " + - "--hide RequiresPermission " + - "--hide MissingPermission --hide BroadcastBehavior " + - "--hide HiddenSuperclass --hide DeprecationMismatch --hide UnavailableSymbol " + - "--hide SdkConstant --hide HiddenTypeParameter --hide Todo --hide Typo " + - "--hide HiddenTypedefConstant --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS,process=android.annotation.SystemApi.Process.ALL\\) " - droidstubs { name: "updatable-media-stubs", srcs: [ ":updatable-media-srcs", ":framework-media-annotation-srcs", ], - args: metalava_updatable_media_args, + defaults: [ "framework-module-stubs-defaults-systemapi" ], aidl: { // TODO(b/135922046) remove this include_dirs: ["frameworks/base/core/java"], @@ -109,3 +102,67 @@ java_library { srcs: [":framework-media-annotation-srcs"], installable: false, } + +aidl_interface { + name: "audio_common-aidl", + local_include_dir: "java", + srcs: [ + "java/android/media/audio/common/AudioChannelMask.aidl", + "java/android/media/audio/common/AudioConfig.aidl", + "java/android/media/audio/common/AudioFormat.aidl", + "java/android/media/audio/common/AudioOffloadInfo.aidl", + "java/android/media/audio/common/AudioStreamType.aidl", + "java/android/media/audio/common/AudioUsage.aidl", + ], + backend: + { + cpp: { + enabled: true, + }, + java: { + // Already generated as part of the entire media java library. + enabled: false, + }, + }, +} + +aidl_interface { + name: "soundtrigger_middleware-aidl", + local_include_dir: "java", + srcs: [ + "java/android/media/soundtrigger_middleware/ConfidenceLevel.aidl", + "java/android/media/soundtrigger_middleware/ISoundTriggerCallback.aidl", + "java/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl", + "java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl", + "java/android/media/soundtrigger_middleware/ModelParameter.aidl", + "java/android/media/soundtrigger_middleware/ModelParameterRange.aidl", + "java/android/media/soundtrigger_middleware/Phrase.aidl", + "java/android/media/soundtrigger_middleware/PhraseRecognitionEvent.aidl", + "java/android/media/soundtrigger_middleware/PhraseRecognitionExtra.aidl", + "java/android/media/soundtrigger_middleware/PhraseSoundModel.aidl", + "java/android/media/soundtrigger_middleware/RecognitionConfig.aidl", + "java/android/media/soundtrigger_middleware/RecognitionEvent.aidl", + "java/android/media/soundtrigger_middleware/RecognitionMode.aidl", + "java/android/media/soundtrigger_middleware/RecognitionStatus.aidl", + "java/android/media/soundtrigger_middleware/SoundModel.aidl", + "java/android/media/soundtrigger_middleware/SoundModelType.aidl", + "java/android/media/soundtrigger_middleware/SoundTriggerModuleDescriptor.aidl", + "java/android/media/soundtrigger_middleware/SoundTriggerModuleProperties.aidl", + "java/android/media/soundtrigger_middleware/Status.aidl", + ], + backend: + { + cpp: { + enabled: true, + }, + java: { + // Already generated as part of the entire media java library. + enabled: false, + }, + ndk: { + // Not currently needed, and disabled because of b/146172425 + enabled: false, + }, + }, + imports: [ "audio_common-aidl" ], +} diff --git a/media/java/android/media/MediaMetrics.java b/media/java/android/media/MediaMetrics.java new file mode 100644 index 000000000000..88a829546989 --- /dev/null +++ b/media/java/android/media/MediaMetrics.java @@ -0,0 +1,634 @@ +/* + * Copyright 2019 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.media; + +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.os.Bundle; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * MediaMetrics is the Java interface to the MediaMetrics service. + * + * This is used to collect media statistics by the framework. + * It is not intended for direct application use. + * + * @hide + */ +public class MediaMetrics { + public static final String TAG = "MediaMetrics"; + + /** + * The TYPE constants below should match those in native MediaMetricsItem.h + */ + private static final int TYPE_NONE = 0; + private static final int TYPE_INT32 = 1; // Java integer + private static final int TYPE_INT64 = 2; // Java long + private static final int TYPE_DOUBLE = 3; // Java double + private static final int TYPE_CSTRING = 4; // Java string + private static final int TYPE_RATE = 5; // Two longs, ignored in Java + + // The charset used for encoding Strings to bytes. + private static final Charset MEDIAMETRICS_CHARSET = StandardCharsets.UTF_8; + + /** + * Item records properties and delivers to the MediaMetrics service + * + */ + public static class Item { + + /* + * MediaMetrics Item + * + * Creates a Byte String and sends to the MediaMetrics service. + * The Byte String serves as a compact form for logging data + * with low overhead for storage. + * + * The Byte String format is as follows: + * + * For Java + * int64 corresponds to long + * int32, uint32 corresponds to int + * uint16 corresponds to char + * uint8, int8 corresponds to byte + * + * For items transmitted from Java, uint8 and uint32 values are limited + * to INT8_MAX and INT32_MAX. This constrains the size of large items + * to 2GB, which is consistent with ByteBuffer max size. A native item + * can conceivably have size of 4GB. + * + * Physical layout of integers and doubles within the MediaMetrics byte string + * is in Native / host order, which is usually little endian. + * + * Note that primitive data (ints, doubles) within a Byte String has + * no extra padding or alignment requirements, like ByteBuffer. + * + * -- begin of item + * -- begin of header + * (uint32) item size: including the item size field + * (uint32) header size, including the item size and header size fields. + * (uint16) version: exactly 0 + * (uint16) key size, that is key strlen + 1 for zero termination. + * (int8)+ key, a string which is 0 terminated (UTF-8). + * (int32) pid + * (int32) uid + * (int64) timestamp + * -- end of header + * -- begin body + * (uint32) number of properties + * -- repeat for number of properties + * (uint16) property size, including property size field itself + * (uint8) type of property + * (int8)+ key string, including 0 termination + * based on type of property (given above), one of: + * (int32) + * (int64) + * (double) + * (int8)+ for TYPE_CSTRING, including 0 termination + * (int64, int64) for rate + * -- end body + * -- end of item + * + * To record a MediaMetrics event, one creates a new item with an id, + * then use a series of puts to add properties + * and then a record() to send to the MediaMetrics service. + * + * The properties may not be unique, and putting a later property with + * the same name as an earlier property will overwrite the value and type + * of the prior property. + * + * The timestamp can only be recorded by a system service (and is ignored otherwise; + * the MediaMetrics service will fill in the timestamp as needed). + * + * The units of time are in SystemClock.elapsedRealtimeNanos(). + * + * A clear() may be called to reset the properties to empty, the time to 0, but keep + * the other entries the same. This may be called after record(). + * Additional properties may be added after calling record(). Changing the same property + * repeatedly is discouraged as - for this particular implementation - extra data + * is stored per change. + * + * new MediaMetrics.Item(mSomeId) + * .putString("event", "javaCreate") + * .putInt("value", intValue) + * .record(); + */ + + /** + * Creates an Item with server added uid, time. + * + * This is the typical way to record a MediaMetrics item. + * + * @param key the Metrics ID associated with the item. + */ + public Item(String key) { + this(key, -1 /* pid */, -1 /* uid */, 0 /* SystemClock.elapsedRealtimeNanos() */, + 2048 /* capacity */); + } + + /** + * Creates an Item specifying pid, uid, time, and initial Item capacity. + * + * This might be used by a service to specify a different PID or UID for a client. + * + * @param key the Metrics ID associated with the item. + * An app may only set properties on an item which has already been + * logged previously by a service. + * @param pid the process ID corresponding to the item. + * A value of -1 (or a record() from an app instead of a service) causes + * the MediaMetrics service to fill this in. + * @param uid the user ID corresponding to the item. + * A value of -1 (or a record() from an app instead of a service) causes + * the MediaMetrics service to fill this in. + * @param timeNs the time when the item occurred (may be in the past). + * A value of 0 (or a record() from an app instead of a service) causes + * the MediaMetrics service to fill it in. + * Should be obtained from SystemClock.elapsedRealtimeNanos(). + * @param capacity the anticipated size to use for the buffer. + * If the capacity is too small, the buffer will be resized to accommodate. + * This is amortized to copy data no more than twice. + */ + public Item(String key, int pid, int uid, long timeNs, int capacity) { + final byte[] keyBytes = key.getBytes(MEDIAMETRICS_CHARSET); + final int keyLength = keyBytes.length; + if (keyLength > Character.MAX_VALUE - 1) { + throw new IllegalArgumentException("Key length too large"); + } + + // Version 0 - compute the header offsets here. + mHeaderSize = 4 + 4 + 2 + 2 + keyLength + 1 + 4 + 4 + 8; // see format above. + mPidOffset = mHeaderSize - 16; + mUidOffset = mHeaderSize - 12; + mTimeNsOffset = mHeaderSize - 8; + mPropertyCountOffset = mHeaderSize; + mPropertyStartOffset = mHeaderSize + 4; + + mKey = key; + mBuffer = ByteBuffer.allocateDirect( + Math.max(capacity, mHeaderSize + MINIMUM_PAYLOAD_SIZE)); + + // Version 0 - fill the ByteBuffer with the header (some details updated later). + mBuffer.order(ByteOrder.nativeOrder()) + .putInt((int) 0) // total size in bytes (filled in later) + .putInt((int) mHeaderSize) // size of header + .putChar((char) FORMAT_VERSION) // version + .putChar((char) (keyLength + 1)) // length, with zero termination + .put(keyBytes).put((byte) 0) + .putInt(pid) + .putInt(uid) + .putLong(timeNs); + if (mHeaderSize != mBuffer.position()) { + throw new IllegalStateException("Mismatched sizing"); + } + mBuffer.putInt(0); // number of properties (to be later filled in by record()). + } + + /** + * Sets the property with key to an integer (32 bit) value. + * + * @param key + * @param value + * @return itself + */ + public Item putInt(String key, int value) { + final byte[] keyBytes = key.getBytes(MEDIAMETRICS_CHARSET); + final char propSize = (char) reserveProperty(keyBytes, 4 /* payloadSize */); + final int estimatedFinalPosition = mBuffer.position() + propSize; + mBuffer.putChar(propSize) + .put((byte) TYPE_INT32) + .put(keyBytes).put((byte) 0) // key, zero terminated + .putInt(value); + ++mPropertyCount; + if (mBuffer.position() != estimatedFinalPosition) { + throw new IllegalStateException("Final position " + mBuffer.position() + + " != estimatedFinalPosition " + estimatedFinalPosition); + } + return this; + } + + /** + * Sets the property with key to a long (64 bit) value. + * + * @param key + * @param value + * @return itself + */ + public Item putLong(String key, long value) { + final byte[] keyBytes = key.getBytes(MEDIAMETRICS_CHARSET); + final char propSize = (char) reserveProperty(keyBytes, 8 /* payloadSize */); + final int estimatedFinalPosition = mBuffer.position() + propSize; + mBuffer.putChar(propSize) + .put((byte) TYPE_INT64) + .put(keyBytes).put((byte) 0) // key, zero terminated + .putLong(value); + ++mPropertyCount; + if (mBuffer.position() != estimatedFinalPosition) { + throw new IllegalStateException("Final position " + mBuffer.position() + + " != estimatedFinalPosition " + estimatedFinalPosition); + } + return this; + } + + /** + * Sets the property with key to a double value. + * + * @param key + * @param value + * @return itself + */ + public Item putDouble(String key, double value) { + final byte[] keyBytes = key.getBytes(MEDIAMETRICS_CHARSET); + final char propSize = (char) reserveProperty(keyBytes, 8 /* payloadSize */); + final int estimatedFinalPosition = mBuffer.position() + propSize; + mBuffer.putChar(propSize) + .put((byte) TYPE_DOUBLE) + .put(keyBytes).put((byte) 0) // key, zero terminated + .putDouble(value); + ++mPropertyCount; + if (mBuffer.position() != estimatedFinalPosition) { + throw new IllegalStateException("Final position " + mBuffer.position() + + " != estimatedFinalPosition " + estimatedFinalPosition); + } + return this; + } + + /** + * Sets the property with key to a String value. + * + * @param key + * @param value + * @return itself + */ + public Item putString(String key, String value) { + final byte[] keyBytes = key.getBytes(MEDIAMETRICS_CHARSET); + final byte[] valueBytes = value.getBytes(MEDIAMETRICS_CHARSET); + final char propSize = (char) reserveProperty(keyBytes, valueBytes.length + 1); + final int estimatedFinalPosition = mBuffer.position() + propSize; + mBuffer.putChar(propSize) + .put((byte) TYPE_CSTRING) + .put(keyBytes).put((byte) 0) // key, zero terminated + .put(valueBytes).put((byte) 0); // value, zero term. + ++mPropertyCount; + if (mBuffer.position() != estimatedFinalPosition) { + throw new IllegalStateException("Final position " + mBuffer.position() + + " != estimatedFinalPosition " + estimatedFinalPosition); + } + return this; + } + + /** + * Sets the pid to the provided value. + * + * @param pid which can be -1 if the service is to fill it in from the calling info. + * @return itself + */ + public Item setPid(int pid) { + mBuffer.putInt(mPidOffset, pid); // pid location in byte string. + return this; + } + + /** + * Sets the uid to the provided value. + * + * The UID represents the client associated with the property. This must be the UID + * of the application if it comes from the application client. + * + * Trusted services are allowed to set the uid for a client-related item. + * + * @param uid which can be -1 if the service is to fill it in from calling info. + * @return itself + */ + public Item setUid(int uid) { + mBuffer.putInt(mUidOffset, uid); // uid location in byte string. + return this; + } + + /** + * Sets the timestamp to the provided value. + * + * The time is referenced by the Boottime obtained by SystemClock.elapsedRealtimeNanos(). + * This should be associated with the occurrence of the event. It is recommended that + * the event be registered immediately when it occurs, and no later than 500ms + * (and certainly not in the future). + * + * @param timeNs which can be 0 if the service is to fill it in at the time of call. + * @return itself + */ + public Item setTimestamp(long timeNs) { + mBuffer.putLong(mTimeNsOffset, timeNs); // time location in byte string. + return this; + } + + /** + * Clears the properties and resets the time to 0. + * + * No other values are changed. + * + * @return itself + */ + public Item clear() { + mBuffer.position(mPropertyStartOffset); + mBuffer.limit(mBuffer.capacity()); + mBuffer.putLong(mTimeNsOffset, 0); // reset time. + mPropertyCount = 0; + return this; + } + + /** + * Sends the item to the MediaMetrics service. + * + * The item properties are unchanged, hence record() may be called more than once + * to send the same item twice. Also, record() may be called without any properties. + * + * @return true if successful. + */ + public boolean record() { + updateHeader(); + return native_submit_bytebuffer(mBuffer, mBuffer.limit()) >= 0; + } + + /** + * Converts the Item to a Bundle. + * + * This is primarily used as a test API for CTS. + * + * @return a Bundle with the keys set according to data in the Item's buffer. + */ + @TestApi + public Bundle toBundle() { + updateHeader(); + + final ByteBuffer buffer = mBuffer.duplicate(); + buffer.order(ByteOrder.nativeOrder()) // restore order property + .flip(); // convert from write buffer to read buffer + + return toBundle(buffer); + } + + // The following constants are used for tests to extract + // the content of the Bundle for CTS testing. + @TestApi + public static final String BUNDLE_TOTAL_SIZE = "_totalSize"; + @TestApi + public static final String BUNDLE_HEADER_SIZE = "_headerSize"; + @TestApi + public static final String BUNDLE_VERSION = "_version"; + @TestApi + public static final String BUNDLE_KEY_SIZE = "_keySize"; + @TestApi + public static final String BUNDLE_KEY = "_key"; + @TestApi + public static final String BUNDLE_PID = "_pid"; + @TestApi + public static final String BUNDLE_UID = "_uid"; + @TestApi + public static final String BUNDLE_TIMESTAMP = "_timestamp"; + @TestApi + public static final String BUNDLE_PROPERTY_COUNT = "_propertyCount"; + + /** + * Converts a buffer contents to a bundle + * + * This is primarily used as a test API for CTS. + * + * @param buffer contains the byte data serialized according to the byte string version. + * @return a Bundle with the keys set according to data in the buffer. + */ + @TestApi + public static Bundle toBundle(ByteBuffer buffer) { + final Bundle bundle = new Bundle(); + + final int totalSize = buffer.getInt(); + final int headerSize = buffer.getInt(); + final char version = buffer.getChar(); + final char keySize = buffer.getChar(); // includes zero termination, i.e. keyLength + 1 + + if (totalSize < 0 || headerSize < 0) { + throw new IllegalArgumentException("Item size cannot be > " + Integer.MAX_VALUE); + } + final String key; + if (keySize > 0) { + key = getStringFromBuffer(buffer, keySize); + } else { + throw new IllegalArgumentException("Illegal null key"); + } + + final int pid = buffer.getInt(); + final int uid = buffer.getInt(); + final long timestamp = buffer.getLong(); + + // Verify header size (depending on version). + final int headerRead = buffer.position(); + if (version == 0) { + if (headerRead != headerSize) { + throw new IllegalArgumentException( + "Item key:" + key + + " headerRead:" + headerRead + " != headerSize:" + headerSize); + } + } else { + // future versions should only increase header size + // by adding to the end. + if (headerRead > headerSize) { + throw new IllegalArgumentException( + "Item key:" + key + + " headerRead:" + headerRead + " > headerSize:" + headerSize); + } else if (headerRead < headerSize) { + buffer.position(headerSize); + } + } + + // Body always starts with properties. + final int propertyCount = buffer.getInt(); + if (propertyCount < 0) { + throw new IllegalArgumentException( + "Cannot have more than " + Integer.MAX_VALUE + " properties"); + } + bundle.putInt(BUNDLE_TOTAL_SIZE, totalSize); + bundle.putInt(BUNDLE_HEADER_SIZE, headerSize); + bundle.putChar(BUNDLE_VERSION, version); + bundle.putChar(BUNDLE_KEY_SIZE, keySize); + bundle.putString(BUNDLE_KEY, key); + bundle.putInt(BUNDLE_PID, pid); + bundle.putInt(BUNDLE_UID, uid); + bundle.putLong(BUNDLE_TIMESTAMP, timestamp); + bundle.putInt(BUNDLE_PROPERTY_COUNT, propertyCount); + + for (int i = 0; i < propertyCount; ++i) { + final int initialBufferPosition = buffer.position(); + final char propSize = buffer.getChar(); + final byte type = buffer.get(); + + // Log.d(TAG, "(" + i + ") propSize:" + ((int)propSize) + " type:" + type); + final String propKey = getStringFromBuffer(buffer); + switch (type) { + case TYPE_INT32: + bundle.putInt(propKey, buffer.getInt()); + break; + case TYPE_INT64: + bundle.putLong(propKey, buffer.getLong()); + break; + case TYPE_DOUBLE: + bundle.putDouble(propKey, buffer.getDouble()); + break; + case TYPE_CSTRING: + bundle.putString(propKey, getStringFromBuffer(buffer)); + break; + case TYPE_NONE: + break; // ignore on Java side + case TYPE_RATE: + buffer.getLong(); // consume the first int64_t of rate + buffer.getLong(); // consume the second int64_t of rate + break; // ignore on Java side + default: + // These are unsupported types for version 0 + // We ignore them if the version is greater than 0. + if (version == 0) { + throw new IllegalArgumentException( + "Property " + propKey + " has unsupported type " + type); + } + buffer.position(initialBufferPosition + propSize); // advance and skip + break; + } + final int deltaPosition = buffer.position() - initialBufferPosition; + if (deltaPosition != propSize) { + throw new IllegalArgumentException("propSize:" + propSize + + " != deltaPosition:" + deltaPosition); + } + } + + final int finalPosition = buffer.position(); + if (finalPosition != totalSize) { + throw new IllegalArgumentException("totalSize:" + totalSize + + " != finalPosition:" + finalPosition); + } + return bundle; + } + + // Version 0 byte offsets for the header. + private static final int FORMAT_VERSION = 0; + private static final int TOTAL_SIZE_OFFSET = 0; + private static final int HEADER_SIZE_OFFSET = 4; + private static final int MINIMUM_PAYLOAD_SIZE = 4; + private final int mPidOffset; // computed in constructor + private final int mUidOffset; // computed in constructor + private final int mTimeNsOffset; // computed in constructor + private final int mPropertyCountOffset; // computed in constructor + private final int mPropertyStartOffset; // computed in constructor + private final int mHeaderSize; // computed in constructor + + private final String mKey; + + private ByteBuffer mBuffer; // may be reallocated if capacity is insufficient. + private int mPropertyCount = 0; // overflow not checked (mBuffer would overflow first). + + private int reserveProperty(byte[] keyBytes, int payloadSize) { + final int keyLength = keyBytes.length; + if (keyLength > Character.MAX_VALUE) { + throw new IllegalStateException("property key too long " + + new String(keyBytes, MEDIAMETRICS_CHARSET)); + } + if (payloadSize > Character.MAX_VALUE) { + throw new IllegalStateException("payload too large " + payloadSize); + } + + // See the byte string property format above. + final int size = 2 /* length */ + + 1 /* type */ + + keyLength + 1 /* key length with zero termination */ + + payloadSize; /* payload size */ + + if (size > Character.MAX_VALUE) { + throw new IllegalStateException("Item property " + + new String(keyBytes, MEDIAMETRICS_CHARSET) + " is too large to send"); + } + + if (mBuffer.remaining() < size) { + int newCapacity = mBuffer.position() + size; + if (newCapacity > Integer.MAX_VALUE >> 1) { + throw new IllegalStateException( + "Item memory requirements too large: " + newCapacity); + } + newCapacity <<= 1; + ByteBuffer buffer = ByteBuffer.allocateDirect(newCapacity); + buffer.order(ByteOrder.nativeOrder()); + + // Copy data from old buffer to new buffer. + mBuffer.flip(); + buffer.put(mBuffer); + + // set buffer to new buffer + mBuffer = buffer; + } + return size; + } + + // Used for test + private static String getStringFromBuffer(ByteBuffer buffer) { + return getStringFromBuffer(buffer, Integer.MAX_VALUE); + } + + // Used for test + private static String getStringFromBuffer(ByteBuffer buffer, int size) { + int i = buffer.position(); + int limit = buffer.limit(); + if (size < Integer.MAX_VALUE - i && i + size < limit) { + limit = i + size; + } + for (; i < limit; ++i) { + if (buffer.get(i) == 0) { + final int newPosition = i + 1; + if (size != Integer.MAX_VALUE && newPosition - buffer.position() != size) { + throw new IllegalArgumentException("chars consumed at " + i + ": " + + (newPosition - buffer.position()) + " != size: " + size); + } + final String found; + if (buffer.hasArray()) { + found = new String( + buffer.array(), buffer.position() + buffer.arrayOffset(), + i - buffer.position(), MEDIAMETRICS_CHARSET); + buffer.position(newPosition); + } else { + final byte[] array = new byte[i - buffer.position()]; + buffer.get(array); + found = new String(array, MEDIAMETRICS_CHARSET); + buffer.get(); // remove 0. + } + return found; + } + } + throw new IllegalArgumentException( + "No zero termination found in string position: " + + buffer.position() + " end: " + i); + } + + /** + * May be called multiple times - just makes the header consistent with the current + * properties written. + */ + private void updateHeader() { + // Buffer sized properly in constructor. + mBuffer.putInt(TOTAL_SIZE_OFFSET, mBuffer.position()) // set total length + .putInt(mPropertyCountOffset, (char) mPropertyCount); // set number of properties + } + } + + private static native int native_submit_bytebuffer(@NonNull ByteBuffer buffer, int length); +} diff --git a/media/java/android/media/MediaScannerConnection.java b/media/java/android/media/MediaScannerConnection.java index 515f6a8ce8a2..40e90731f2a2 100644 --- a/media/java/android/media/MediaScannerConnection.java +++ b/media/java/android/media/MediaScannerConnection.java @@ -19,6 +19,7 @@ package android.media; import android.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.ContentProviderClient; +import android.content.ContentResolver; import android.content.Context; import android.content.ServiceConnection; import android.net.Uri; @@ -197,7 +198,7 @@ public class MediaScannerConnection implements ServiceConnection { private static Uri scanFileQuietly(ContentProviderClient client, File file) { Uri uri = null; try { - uri = MediaStore.scanFile(client, file.getCanonicalFile()); + uri = MediaStore.scanFile(ContentResolver.wrap(client), file.getCanonicalFile()); Log.d(TAG, "Scanned " + file + " to " + uri); } catch (Exception e) { Log.w(TAG, "Failed to scan " + file + ": " + e); diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java index 9064e6891be6..a1e159174f95 100644 --- a/media/java/android/media/RingtoneManager.java +++ b/media/java/android/media/RingtoneManager.java @@ -908,7 +908,7 @@ public class RingtoneManager { } // Tell MediaScanner about the new file. Wait for it to assign a {@link Uri}. - return MediaStore.scanFile(mContext, outFile); + return MediaStore.scanFile(mContext.getContentResolver(), outFile); } private static final String getExternalDirectoryForType(final int type) { diff --git a/media/java/android/media/RouteSessionController.java b/media/java/android/media/RouteSessionController.java new file mode 100644 index 000000000000..5ff721837573 --- /dev/null +++ b/media/java/android/media/RouteSessionController.java @@ -0,0 +1,208 @@ +/* + * Copyright 2019 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.media; + +import android.annotation.NonNull; + +import com.android.internal.annotations.GuardedBy; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; + +/** + * A class to control media route session in media route provider. + * For example, adding/removing/transferring routes to session can be done through this class. + * Instances are created by {@link MediaRouter2}. + * + * TODO: When session is introduced, change Javadoc of all methods/classes by using [@link Session]. + * + * @hide + */ +public class RouteSessionController { + private final int mSessionId; + private final String mCategory; + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private final CopyOnWriteArrayList<CallbackRecord> mCallbackRecords = + new CopyOnWriteArrayList<>(); + + private volatile boolean mIsReleased; + + /** + * @param sessionId the ID of the session. + * @param category The category of media routes that the session includes. + */ + RouteSessionController(int sessionId, @NonNull String category) { + mSessionId = sessionId; + mCategory = category; + } + + /** + * @return the ID of this controller + */ + public int getSessionId() { + return mSessionId; + } + + /** + * @return the category of routes that the session includes. + */ + @NonNull + public String getCategory() { + return mCategory; + } + + /** + * @return the list of currently connected routes + */ + @NonNull + public List<MediaRoute2Info> getRoutes() { + // TODO: Implement this when SessionInfo is introduced. + return null; + } + + /** + * Returns true if the session is released, false otherwise. + * If it is released, then all other getters from this instance may return invalid values. + * Also, any operations to this instance will be ignored once released. + * + * @see #release + * @see Callback#onReleased + */ + public boolean isReleased() { + return mIsReleased; + } + + /** + * Add routes to the remote session. + * + * @see #getRoutes() + * @see Callback#onSessionInfoChanged + */ + public void addRoutes(List<MediaRoute2Info> routes) { + // TODO: Implement this when the actual connection logic is implemented. + } + + /** + * Remove routes from this session. Media may be stopped on those devices. + * Route removal requests that are not currently in {@link #getRoutes()} will be ignored. + * + * @see #getRoutes() + * @see Callback#onSessionInfoChanged + */ + public void removeRoutes(List<MediaRoute2Info> routes) { + // TODO: Implement this when the actual connection logic is implemented. + } + + /** + * Registers a {@link Callback} for monitoring route changes. + * If the same callback is registered previously, previous executor will be overwritten with the + * new one. + */ + public void registerCallback(Executor executor, Callback callback) { + if (mIsReleased) { + return; + } + Objects.requireNonNull(executor, "executor must not be null"); + Objects.requireNonNull(callback, "callback must not be null"); + + synchronized (mLock) { + CallbackRecord recordWithSameCallback = null; + for (CallbackRecord record : mCallbackRecords) { + if (callback == record.mCallback) { + recordWithSameCallback = record; + break; + } + } + + if (recordWithSameCallback != null) { + recordWithSameCallback.mExecutor = executor; + } else { + mCallbackRecords.add(new CallbackRecord(executor, callback)); + } + } + } + + /** + * Unregisters a previously registered {@link Callback}. + */ + public void unregisterCallback(Callback callback) { + Objects.requireNonNull(callback, "callback must not be null"); + + synchronized (mLock) { + CallbackRecord recordToRemove = null; + for (CallbackRecord record : mCallbackRecords) { + if (callback == record.mCallback) { + recordToRemove = record; + break; + } + } + + if (recordToRemove != null) { + mCallbackRecords.remove(recordToRemove); + } + } + } + + /** + * Release this session. + * Any operation on this session after calling this method will be ignored. + * + * @param stopMedia Should the device where the media is played + * be stopped after this session is released. + */ + public void release(boolean stopMedia) { + mIsReleased = true; + mCallbackRecords.clear(); + // TODO: Use stopMedia variable when the actual connection logic is implemented. + } + + /** + * Callback class for getting updates on routes and session release. + */ + public static class Callback { + + /** + * Called when the session info has changed. + * TODO: When SessionInfo is introduced, uncomment below argument. + */ + void onSessionInfoChanged(/* SessionInfo info */) {} + + /** + * Called when the session is released. Session can be released by the controller using + * {@link #release(boolean)}, or by the {@link MediaRoute2ProviderService} itself. + * One can do clean-ups here. + * + * TODO: When SessionInfo is introduced, change the javadoc of releasing session on + * provider side. + */ + void onReleased(int reason, boolean shouldStop) {} + } + + private class CallbackRecord { + public final Callback mCallback; + public Executor mExecutor; + + CallbackRecord(@NonNull Executor executor, @NonNull Callback callback) { + mExecutor = executor; + mCallback = callback; + } + } +} diff --git a/media/java/android/media/ThumbnailUtils.java b/media/java/android/media/ThumbnailUtils.java index a315c1eefb52..6705b0cb79f0 100644 --- a/media/java/android/media/ThumbnailUtils.java +++ b/media/java/android/media/ThumbnailUtils.java @@ -33,14 +33,13 @@ import android.graphics.ImageDecoder; import android.graphics.ImageDecoder.ImageInfo; import android.graphics.ImageDecoder.Source; import android.graphics.Matrix; -import android.graphics.Point; import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.CancellationSignal; import android.os.Environment; import android.os.ParcelFileDescriptor; -import android.provider.MediaStore.ThumbnailConstants; +import android.provider.MediaStore; import android.util.Log; import android.util.Size; @@ -77,15 +76,7 @@ public class ThumbnailUtils { public static final int OPTIONS_RECYCLE_INPUT = 0x2; private static Size convertKind(int kind) { - if (kind == ThumbnailConstants.MICRO_KIND) { - return Point.convert(ThumbnailConstants.MICRO_SIZE); - } else if (kind == ThumbnailConstants.FULL_SCREEN_KIND) { - return Point.convert(ThumbnailConstants.FULL_SCREEN_SIZE); - } else if (kind == ThumbnailConstants.MINI_KIND) { - return Point.convert(ThumbnailConstants.MINI_SIZE); - } else { - throw new IllegalArgumentException("Unsupported kind: " + kind); - } + return MediaStore.Images.Thumbnails.getKindSize(kind); } private static class Resizer implements ImageDecoder.OnHeaderDecodedListener { diff --git a/media/java/android/media/audio/common/AudioChannelMask.aidl b/media/java/android/media/audio/common/AudioChannelMask.aidl new file mode 100644 index 000000000000..b9b08e6921bc --- /dev/null +++ b/media/java/android/media/audio/common/AudioChannelMask.aidl @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2019 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. + */ + + // This file has been semi-automatically generated using hidl2aidl from its counterpart in + // hardware/interfaces/audio/common/5.0/types.hal + +package android.media.audio.common; + +/** + * A channel mask per se only defines the presence or absence of a channel, not + * the order. + * + * The channel order convention is that channels are interleaved in order from + * least significant channel mask bit to most significant channel mask bit, + * with unused bits skipped. For example for stereo, LEFT would be first, + * followed by RIGHT. + * Any exceptions to this convention are noted at the appropriate API. + * + * AudioChannelMask is an opaque type and its internal layout should not be + * assumed as it may change in the future. Instead, always use functions + * to examine it. + * + * These are the current representations: + * + * REPRESENTATION_POSITION + * is a channel mask representation for position assignment. Each low-order + * bit corresponds to the spatial position of a transducer (output), or + * interpretation of channel (input). The user of a channel mask needs to + * know the context of whether it is for output or input. The constants + * OUT_* or IN_* apply to the bits portion. It is not permitted for no bits + * to be set. + * + * REPRESENTATION_INDEX + * is a channel mask representation for index assignment. Each low-order + * bit corresponds to a selected channel. There is no platform + * interpretation of the various bits. There is no concept of output or + * input. It is not permitted for no bits to be set. + * + * All other representations are reserved for future use. + * + * Warning: current representation distinguishes between input and output, but + * this will not the be case in future revisions of the platform. Wherever there + * is an ambiguity between input and output that is currently resolved by + * checking the channel mask, the implementer should look for ways to fix it + * with additional information outside of the mask. + * + * {@hide} + */ +@Backing(type="int") +enum AudioChannelMask { + /** + * must be 0 for compatibility + */ + REPRESENTATION_POSITION = 0, + /** + * 1 is reserved for future use + */ + REPRESENTATION_INDEX = 2, + /** + * 3 is reserved for future use + * + * + * These can be a complete value of AudioChannelMask + */ + NONE = 0x0, + INVALID = 0xC0000000, + /** + * These can be the bits portion of an AudioChannelMask + * with representation REPRESENTATION_POSITION. + * + * + * output channels + */ + OUT_FRONT_LEFT = 0x1, + OUT_FRONT_RIGHT = 0x2, + OUT_FRONT_CENTER = 0x4, + OUT_LOW_FREQUENCY = 0x8, + OUT_BACK_LEFT = 0x10, + OUT_BACK_RIGHT = 0x20, + OUT_FRONT_LEFT_OF_CENTER = 0x40, + OUT_FRONT_RIGHT_OF_CENTER = 0x80, + OUT_BACK_CENTER = 0x100, + OUT_SIDE_LEFT = 0x200, + OUT_SIDE_RIGHT = 0x400, + OUT_TOP_CENTER = 0x800, + OUT_TOP_FRONT_LEFT = 0x1000, + OUT_TOP_FRONT_CENTER = 0x2000, + OUT_TOP_FRONT_RIGHT = 0x4000, + OUT_TOP_BACK_LEFT = 0x8000, + OUT_TOP_BACK_CENTER = 0x10000, + OUT_TOP_BACK_RIGHT = 0x20000, + OUT_TOP_SIDE_LEFT = 0x40000, + OUT_TOP_SIDE_RIGHT = 0x80000, + /** + * Haptic channel characteristics are specific to a device and + * only used to play device specific resources (eg: ringtones). + * The HAL can freely map A and B to haptic controllers, the + * framework shall not interpret those values and forward them + * from the device audio assets. + */ + OUT_HAPTIC_A = 0x20000000, + OUT_HAPTIC_B = 0x10000000, +// TODO(ytai): Aliases not currently supported in AIDL - can inline the values. +// OUT_MONO = OUT_FRONT_LEFT, +// OUT_STEREO = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT), +// OUT_2POINT1 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_LOW_FREQUENCY), +// OUT_2POINT0POINT2 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_TOP_SIDE_LEFT | OUT_TOP_SIDE_RIGHT), +// OUT_2POINT1POINT2 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_TOP_SIDE_LEFT | OUT_TOP_SIDE_RIGHT | OUT_LOW_FREQUENCY), +// OUT_3POINT0POINT2 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_FRONT_CENTER | OUT_TOP_SIDE_LEFT | OUT_TOP_SIDE_RIGHT), +// OUT_3POINT1POINT2 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_FRONT_CENTER | OUT_TOP_SIDE_LEFT | OUT_TOP_SIDE_RIGHT | OUT_LOW_FREQUENCY), +// OUT_QUAD = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_BACK_LEFT | OUT_BACK_RIGHT), +// OUT_QUAD_BACK = OUT_QUAD, +// /** +// * like OUT_QUAD_BACK with *_SIDE_* instead of *_BACK_* +// */ +// OUT_QUAD_SIDE = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_SIDE_LEFT | OUT_SIDE_RIGHT), +// OUT_SURROUND = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_FRONT_CENTER | OUT_BACK_CENTER), +// OUT_PENTA = (OUT_QUAD | OUT_FRONT_CENTER), +// OUT_5POINT1 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_FRONT_CENTER | OUT_LOW_FREQUENCY | OUT_BACK_LEFT | OUT_BACK_RIGHT), +// OUT_5POINT1_BACK = OUT_5POINT1, +// /** +// * like OUT_5POINT1_BACK with *_SIDE_* instead of *_BACK_* +// */ +// OUT_5POINT1_SIDE = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_FRONT_CENTER | OUT_LOW_FREQUENCY | OUT_SIDE_LEFT | OUT_SIDE_RIGHT), +// OUT_5POINT1POINT2 = (OUT_5POINT1 | OUT_TOP_SIDE_LEFT | OUT_TOP_SIDE_RIGHT), +// OUT_5POINT1POINT4 = (OUT_5POINT1 | OUT_TOP_FRONT_LEFT | OUT_TOP_FRONT_RIGHT | OUT_TOP_BACK_LEFT | OUT_TOP_BACK_RIGHT), +// OUT_6POINT1 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_FRONT_CENTER | OUT_LOW_FREQUENCY | OUT_BACK_LEFT | OUT_BACK_RIGHT | OUT_BACK_CENTER), +// /** +// * matches the correct AudioFormat.CHANNEL_OUT_7POINT1_SURROUND +// */ +// OUT_7POINT1 = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_FRONT_CENTER | OUT_LOW_FREQUENCY | OUT_BACK_LEFT | OUT_BACK_RIGHT | OUT_SIDE_LEFT | OUT_SIDE_RIGHT), +// OUT_7POINT1POINT2 = (OUT_7POINT1 | OUT_TOP_SIDE_LEFT | OUT_TOP_SIDE_RIGHT), +// OUT_7POINT1POINT4 = (OUT_7POINT1 | OUT_TOP_FRONT_LEFT | OUT_TOP_FRONT_RIGHT | OUT_TOP_BACK_LEFT | OUT_TOP_BACK_RIGHT), +// OUT_MONO_HAPTIC_A = (OUT_FRONT_LEFT | OUT_HAPTIC_A), +// OUT_STEREO_HAPTIC_A = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_HAPTIC_A), +// OUT_HAPTIC_AB = (OUT_HAPTIC_A | OUT_HAPTIC_B), +// OUT_MONO_HAPTIC_AB = (OUT_FRONT_LEFT | OUT_HAPTIC_A | OUT_HAPTIC_B), +// OUT_STEREO_HAPTIC_AB = (OUT_FRONT_LEFT | OUT_FRONT_RIGHT | OUT_HAPTIC_A | OUT_HAPTIC_B), + /** + * These are bits only, not complete values + * + * + * input channels + */ + IN_LEFT = 0x4, + IN_RIGHT = 0x8, + IN_FRONT = 0x10, + IN_BACK = 0x20, + IN_LEFT_PROCESSED = 0x40, + IN_RIGHT_PROCESSED = 0x80, + IN_FRONT_PROCESSED = 0x100, + IN_BACK_PROCESSED = 0x200, + IN_PRESSURE = 0x400, + IN_X_AXIS = 0x800, + IN_Y_AXIS = 0x1000, + IN_Z_AXIS = 0x2000, + IN_BACK_LEFT = 0x10000, + IN_BACK_RIGHT = 0x20000, + IN_CENTER = 0x40000, + IN_LOW_FREQUENCY = 0x100000, + IN_TOP_LEFT = 0x200000, + IN_TOP_RIGHT = 0x400000, + IN_VOICE_UPLINK = 0x4000, + IN_VOICE_DNLINK = 0x8000, +// TODO(ytai): Aliases not currently supported in AIDL - can inline the values. +// IN_MONO = IN_FRONT, +// IN_STEREO = (IN_LEFT | IN_RIGHT), +// IN_FRONT_BACK = (IN_FRONT | IN_BACK), +// IN_6 = (IN_LEFT | IN_RIGHT | IN_FRONT | IN_BACK | IN_LEFT_PROCESSED | IN_RIGHT_PROCESSED), +// IN_2POINT0POINT2 = (IN_LEFT | IN_RIGHT | IN_TOP_LEFT | IN_TOP_RIGHT), +// IN_2POINT1POINT2 = (IN_LEFT | IN_RIGHT | IN_TOP_LEFT | IN_TOP_RIGHT | IN_LOW_FREQUENCY), +// IN_3POINT0POINT2 = (IN_LEFT | IN_CENTER | IN_RIGHT | IN_TOP_LEFT | IN_TOP_RIGHT), +// IN_3POINT1POINT2 = (IN_LEFT | IN_CENTER | IN_RIGHT | IN_TOP_LEFT | IN_TOP_RIGHT | IN_LOW_FREQUENCY), +// IN_5POINT1 = (IN_LEFT | IN_CENTER | IN_RIGHT | IN_BACK_LEFT | IN_BACK_RIGHT | IN_LOW_FREQUENCY), +// IN_VOICE_UPLINK_MONO = (IN_VOICE_UPLINK | IN_MONO), +// IN_VOICE_DNLINK_MONO = (IN_VOICE_DNLINK | IN_MONO), +// IN_VOICE_CALL_MONO = (IN_VOICE_UPLINK_MONO | IN_VOICE_DNLINK_MONO), +// COUNT_MAX = 30, +// INDEX_HDR = REPRESENTATION_INDEX << COUNT_MAX, +// INDEX_MASK_1 = INDEX_HDR | ((1 << 1) - 1), +// INDEX_MASK_2 = INDEX_HDR | ((1 << 2) - 1), +// INDEX_MASK_3 = INDEX_HDR | ((1 << 3) - 1), +// INDEX_MASK_4 = INDEX_HDR | ((1 << 4) - 1), +// INDEX_MASK_5 = INDEX_HDR | ((1 << 5) - 1), +// INDEX_MASK_6 = INDEX_HDR | ((1 << 6) - 1), +// INDEX_MASK_7 = INDEX_HDR | ((1 << 7) - 1), +// INDEX_MASK_8 = INDEX_HDR | ((1 << 8) - 1), +// INDEX_MASK_9 = INDEX_HDR | ((1 << 9) - 1), +// INDEX_MASK_10 = INDEX_HDR | ((1 << 10) - 1), +// INDEX_MASK_11 = INDEX_HDR | ((1 << 11) - 1), +// INDEX_MASK_12 = INDEX_HDR | ((1 << 12) - 1), +// INDEX_MASK_13 = INDEX_HDR | ((1 << 13) - 1), +// INDEX_MASK_14 = INDEX_HDR | ((1 << 14) - 1), +// INDEX_MASK_15 = INDEX_HDR | ((1 << 15) - 1), +// INDEX_MASK_16 = INDEX_HDR | ((1 << 16) - 1), +// INDEX_MASK_17 = INDEX_HDR | ((1 << 17) - 1), +// INDEX_MASK_18 = INDEX_HDR | ((1 << 18) - 1), +// INDEX_MASK_19 = INDEX_HDR | ((1 << 19) - 1), +// INDEX_MASK_20 = INDEX_HDR | ((1 << 20) - 1), +// INDEX_MASK_21 = INDEX_HDR | ((1 << 21) - 1), +// INDEX_MASK_22 = INDEX_HDR | ((1 << 22) - 1), +// INDEX_MASK_23 = INDEX_HDR | ((1 << 23) - 1), +// INDEX_MASK_24 = INDEX_HDR | ((1 << 24) - 1), +} diff --git a/media/java/android/media/audio/common/AudioConfig.aidl b/media/java/android/media/audio/common/AudioConfig.aidl new file mode 100644 index 000000000000..50dd796e1fa0 --- /dev/null +++ b/media/java/android/media/audio/common/AudioConfig.aidl @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2019 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. + */ + + // This file has been semi-automatically generated using hidl2aidl from its counterpart in + // hardware/interfaces/audio/common/5.0/types.hal + +package android.media.audio.common; + +import android.media.audio.common.AudioFormat; +import android.media.audio.common.AudioOffloadInfo; + +/** + * Commonly used audio stream configuration parameters. + * + * {@hide} + */ +parcelable AudioConfig { + int sampleRateHz; + int channelMask; + AudioFormat format; + AudioOffloadInfo offloadInfo; + long frameCount; +} diff --git a/media/java/android/media/audio/common/AudioFormat.aidl b/media/java/android/media/audio/common/AudioFormat.aidl new file mode 100644 index 000000000000..aadc8e26cce3 --- /dev/null +++ b/media/java/android/media/audio/common/AudioFormat.aidl @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2019 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. + */ + + // This file has been semi-automatically generated using hidl2aidl from its counterpart in + // hardware/interfaces/audio/common/5.0/types.hal + +package android.media.audio.common; + +/** + * Audio format is a 32-bit word that consists of: + * main format field (upper 8 bits) + * sub format field (lower 24 bits). + * + * The main format indicates the main codec type. The sub format field indicates + * options and parameters for each format. The sub format is mainly used for + * record to indicate for instance the requested bitrate or profile. It can + * also be used for certain formats to give informations not present in the + * encoded audio stream (e.g. octet alignement for AMR). + * + * {@hide} + */ +@Backing(type="int") +enum AudioFormat { + INVALID = 0xFFFFFFFF, + DEFAULT = 0, + PCM = 0x00000000, + MP3 = 0x01000000, + AMR_NB = 0x02000000, + AMR_WB = 0x03000000, + AAC = 0x04000000, + /** + * Deprecated, Use AAC_HE_V1 + */ + HE_AAC_V1 = 0x05000000, + /** + * Deprecated, Use AAC_HE_V2 + */ + HE_AAC_V2 = 0x06000000, + VORBIS = 0x07000000, + OPUS = 0x08000000, + AC3 = 0x09000000, + E_AC3 = 0x0A000000, + DTS = 0x0B000000, + DTS_HD = 0x0C000000, + /** + * IEC61937 is encoded audio wrapped in 16-bit PCM. + */ + IEC61937 = 0x0D000000, + DOLBY_TRUEHD = 0x0E000000, + EVRC = 0x10000000, + EVRCB = 0x11000000, + EVRCWB = 0x12000000, + EVRCNW = 0x13000000, + AAC_ADIF = 0x14000000, + WMA = 0x15000000, + WMA_PRO = 0x16000000, + AMR_WB_PLUS = 0x17000000, + MP2 = 0x18000000, + QCELP = 0x19000000, + DSD = 0x1A000000, + FLAC = 0x1B000000, + ALAC = 0x1C000000, + APE = 0x1D000000, + AAC_ADTS = 0x1E000000, + SBC = 0x1F000000, + APTX = 0x20000000, + APTX_HD = 0x21000000, + AC4 = 0x22000000, + LDAC = 0x23000000, + /** + * Dolby Metadata-enhanced Audio Transmission + */ + MAT = 0x24000000, + AAC_LATM = 0x25000000, + CELT = 0x26000000, + APTX_ADAPTIVE = 0x27000000, + LHDC = 0x28000000, + LHDC_LL = 0x29000000, + APTX_TWSP = 0x2A000000, + /** + * Deprecated + */ + MAIN_MASK = 0xFF000000, + SUB_MASK = 0x00FFFFFF, + /** + * Subformats + */ + PCM_SUB_16_BIT = 0x1, + PCM_SUB_8_BIT = 0x2, + PCM_SUB_32_BIT = 0x3, + PCM_SUB_8_24_BIT = 0x4, + PCM_SUB_FLOAT = 0x5, + PCM_SUB_24_BIT_PACKED = 0x6, + MP3_SUB_NONE = 0x0, + AMR_SUB_NONE = 0x0, + AAC_SUB_MAIN = 0x1, + AAC_SUB_LC = 0x2, + AAC_SUB_SSR = 0x4, + AAC_SUB_LTP = 0x8, + AAC_SUB_HE_V1 = 0x10, + AAC_SUB_SCALABLE = 0x20, + AAC_SUB_ERLC = 0x40, + AAC_SUB_LD = 0x80, + AAC_SUB_HE_V2 = 0x100, + AAC_SUB_ELD = 0x200, + AAC_SUB_XHE = 0x300, + VORBIS_SUB_NONE = 0x0, + E_AC3_SUB_JOC = 0x1, + MAT_SUB_1_0 = 0x1, + MAT_SUB_2_0 = 0x2, + MAT_SUB_2_1 = 0x3, +// TODO(ytai): Aliases not currently supported in AIDL - can inline the values. +// /** +// * Aliases +// * +// * +// * note != AudioFormat.ENCODING_PCM_16BIT +// */ +// PCM_16_BIT = (PCM | PCM_SUB_16_BIT), +// /** +// * note != AudioFormat.ENCODING_PCM_8BIT +// */ +// PCM_8_BIT = (PCM | PCM_SUB_8_BIT), +// PCM_32_BIT = (PCM | PCM_SUB_32_BIT), +// PCM_8_24_BIT = (PCM | PCM_SUB_8_24_BIT), +// PCM_FLOAT = (PCM | PCM_SUB_FLOAT), +// PCM_24_BIT_PACKED = (PCM | PCM_SUB_24_BIT_PACKED), +// AAC_MAIN = (AAC | AAC_SUB_MAIN), +// AAC_LC = (AAC | AAC_SUB_LC), +// AAC_SSR = (AAC | AAC_SUB_SSR), +// AAC_LTP = (AAC | AAC_SUB_LTP), +// AAC_HE_V1 = (AAC | AAC_SUB_HE_V1), +// AAC_SCALABLE = (AAC | AAC_SUB_SCALABLE), +// AAC_ERLC = (AAC | AAC_SUB_ERLC), +// AAC_LD = (AAC | AAC_SUB_LD), +// AAC_HE_V2 = (AAC | AAC_SUB_HE_V2), +// AAC_ELD = (AAC | AAC_SUB_ELD), +// AAC_XHE = (AAC | AAC_SUB_XHE), +// AAC_ADTS_MAIN = (AAC_ADTS | AAC_SUB_MAIN), +// AAC_ADTS_LC = (AAC_ADTS | AAC_SUB_LC), +// AAC_ADTS_SSR = (AAC_ADTS | AAC_SUB_SSR), +// AAC_ADTS_LTP = (AAC_ADTS | AAC_SUB_LTP), +// AAC_ADTS_HE_V1 = (AAC_ADTS | AAC_SUB_HE_V1), +// AAC_ADTS_SCALABLE = (AAC_ADTS | AAC_SUB_SCALABLE), +// AAC_ADTS_ERLC = (AAC_ADTS | AAC_SUB_ERLC), +// AAC_ADTS_LD = (AAC_ADTS | AAC_SUB_LD), +// AAC_ADTS_HE_V2 = (AAC_ADTS | AAC_SUB_HE_V2), +// AAC_ADTS_ELD = (AAC_ADTS | AAC_SUB_ELD), +// AAC_ADTS_XHE = (AAC_ADTS | AAC_SUB_XHE), +// E_AC3_JOC = (E_AC3 | E_AC3_SUB_JOC), +// MAT_1_0 = (MAT | MAT_SUB_1_0), +// MAT_2_0 = (MAT | MAT_SUB_2_0), +// MAT_2_1 = (MAT | MAT_SUB_2_1), +// AAC_LATM_LC = (AAC_LATM | AAC_SUB_LC), +// AAC_LATM_HE_V1 = (AAC_LATM | AAC_SUB_HE_V1), +// AAC_LATM_HE_V2 = (AAC_LATM | AAC_SUB_HE_V2), +} diff --git a/media/java/android/media/audio/common/AudioOffloadInfo.aidl b/media/java/android/media/audio/common/AudioOffloadInfo.aidl new file mode 100644 index 000000000000..ec10d71135ae --- /dev/null +++ b/media/java/android/media/audio/common/AudioOffloadInfo.aidl @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 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. + */ + + // This file has been semi-automatically generated using hidl2aidl from its counterpart in + // hardware/interfaces/audio/common/5.0/types.hal + +package android.media.audio.common; + +import android.media.audio.common.AudioFormat; +import android.media.audio.common.AudioStreamType; +import android.media.audio.common.AudioUsage; + +/** + * Additional information about the stream passed to hardware decoders. + * + * {@hide} + */ +parcelable AudioOffloadInfo { + int sampleRateHz; + int channelMask; + AudioFormat format; + AudioStreamType streamType; + int bitRatePerSecond; + long durationMicroseconds; + boolean hasVideo; + boolean isStreaming; + int bitWidth; + int bufferSize; + AudioUsage usage; +} + diff --git a/media/java/android/media/audio/common/AudioStreamType.aidl b/media/java/android/media/audio/common/AudioStreamType.aidl new file mode 100644 index 000000000000..c54566726350 --- /dev/null +++ b/media/java/android/media/audio/common/AudioStreamType.aidl @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 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. + */ + + // This file has been semi-automatically generated using hidl2aidl from its counterpart in + // hardware/interfaces/audio/common/5.0/types.hal + +package android.media.audio.common; + +/** + * Audio streams + * + * Audio stream type describing the intended use case of a stream. + * + * {@hide} + */ +@Backing(type="int") +enum AudioStreamType { + DEFAULT = -1, + MIN = 0, + VOICE_CALL = 0, + SYSTEM = 1, + RING = 2, + MUSIC = 3, + ALARM = 4, + NOTIFICATION = 5, + BLUETOOTH_SCO = 6, + ENFORCED_AUDIBLE = 7, + DTMF = 8, + TTS = 9, + ACCESSIBILITY = 10, +} diff --git a/media/java/android/media/audio/common/AudioUsage.aidl b/media/java/android/media/audio/common/AudioUsage.aidl new file mode 100644 index 000000000000..ef348165b22c --- /dev/null +++ b/media/java/android/media/audio/common/AudioUsage.aidl @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 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. + */ + + // This file has been semi-automatically generated using hidl2aidl from its counterpart in + // hardware/interfaces/audio/common/5.0/types.hal + +package android.media.audio.common; + +/** + * {@hide} + */ +@Backing(type="int") +enum AudioUsage { + UNKNOWN = 0, + MEDIA = 1, + VOICE_COMMUNICATION = 2, + VOICE_COMMUNICATION_SIGNALLING = 3, + ALARM = 4, + NOTIFICATION = 5, + NOTIFICATION_TELEPHONY_RINGTONE = 6, + ASSISTANCE_ACCESSIBILITY = 11, + ASSISTANCE_NAVIGATION_GUIDANCE = 12, + ASSISTANCE_SONIFICATION = 13, + GAME = 14, + VIRTUAL_SOURCE = 15, + ASSISTANT = 16, +} diff --git a/media/java/android/media/session/ICallback.aidl b/media/java/android/media/session/IOnMediaKeyEventDispatchedListener.aidl index 322bffa96c9e..90d913459edc 100644 --- a/media/java/android/media/session/ICallback.aidl +++ b/media/java/android/media/session/IOnMediaKeyEventDispatchedListener.aidl @@ -1,4 +1,5 @@ -/* Copyright (C) 2016 The Android Open Source Project +/* + * Copyright 2019 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. @@ -15,21 +16,13 @@ package android.media.session; -import android.app.PendingIntent; -import android.content.ComponentName; import android.media.session.MediaSession; import android.view.KeyEvent; /** * @hide */ -oneway interface ICallback { - void onMediaKeyEventDispatchedToMediaSession(in KeyEvent event, +oneway interface IOnMediaKeyEventDispatchedListener { + void onMediaKeyEventDispatched(in KeyEvent event, in String packageName, in MediaSession.Token sessionToken); - void onMediaKeyEventDispatchedToMediaButtonReceiver(in KeyEvent event, - in ComponentName mediaButtonReceiver); - - void onAddressedPlayerChangedToMediaSession(in MediaSession.Token sessionToken); - void onAddressedPlayerChangedToMediaButtonReceiver(in ComponentName mediaButtonReceiver); } - diff --git a/media/java/android/media/session/IOnMediaKeyEventSessionChangedListener.aidl b/media/java/android/media/session/IOnMediaKeyEventSessionChangedListener.aidl new file mode 100644 index 000000000000..9566e756162c --- /dev/null +++ b/media/java/android/media/session/IOnMediaKeyEventSessionChangedListener.aidl @@ -0,0 +1,28 @@ +/* + * Copyright 2019 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.media.session; + +import android.media.session.MediaSession; + +/** + * @hide + */ +oneway interface IOnMediaKeyEventSessionChangedListener { + void onMediaKeyEventSessionChanged(in String packageName, + in MediaSession.Token mediaKeyEventSessionToken); +} + diff --git a/media/java/android/media/session/ISessionManager.aidl b/media/java/android/media/session/ISessionManager.aidl index 01e6ed5e9f35..c8502a51ee23 100644 --- a/media/java/android/media/session/ISessionManager.aidl +++ b/media/java/android/media/session/ISessionManager.aidl @@ -20,7 +20,8 @@ import android.content.pm.ParceledListSlice; import android.media.IRemoteVolumeController; import android.media.Session2Token; import android.media.session.IActiveSessionsListener; -import android.media.session.ICallback; +import android.media.session.IOnMediaKeyEventDispatchedListener; +import android.media.session.IOnMediaKeyEventSessionChangedListener; import android.media.session.IOnMediaKeyListener; import android.media.session.IOnVolumeKeyLongPressListener; import android.media.session.ISession; @@ -62,8 +63,12 @@ interface ISessionManager { // For PhoneWindowManager to precheck media keys boolean isGlobalPriorityActive(); - void registerCallback(in ICallback callback); - void unregisterCallback(in ICallback callback); + void addOnMediaKeyEventDispatchedListener(in IOnMediaKeyEventDispatchedListener listener); + void removeOnMediaKeyEventDispatchedListener(in IOnMediaKeyEventDispatchedListener listener); + void addOnMediaKeyEventSessionChangedListener( + in IOnMediaKeyEventSessionChangedListener listener); + void removeOnMediaKeyEventSessionChangedListener( + in IOnMediaKeyEventSessionChangedListener listener); void setOnVolumeKeyLongPressListener(in IOnVolumeKeyLongPressListener listener); void setOnMediaKeyListener(in IOnMediaKeyListener listener); diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java index 92fb31bd4de8..a89dc5f1d2af 100644 --- a/media/java/android/media/session/MediaSessionManager.java +++ b/media/java/android/media/session/MediaSessionManager.java @@ -32,7 +32,6 @@ import android.media.MediaSession2; import android.media.Session2Token; import android.os.Bundle; import android.os.Handler; -import android.os.HandlerExecutor; import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; @@ -78,29 +77,33 @@ public final class MediaSessionManager { */ public static final int RESULT_MEDIA_KEY_HANDLED = 1; private final ISessionManager mService; + private final OnMediaKeyEventDispatchedListenerStub mOnMediaKeyEventDispatchedListenerStub = + new OnMediaKeyEventDispatchedListenerStub(); + private final OnMediaKeyEventSessionChangedListenerStub + mOnMediaKeyEventSessionChangedListenerStub = + new OnMediaKeyEventSessionChangedListenerStub(); private final Object mLock = new Object(); @GuardedBy("mLock") - private final ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper> mListeners - = new ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper>(); + private final ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper> mListeners = + new ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper>(); @GuardedBy("mLock") private final ArrayMap<OnSession2TokensChangedListener, Session2TokensChangedWrapper> mSession2TokensListeners = new ArrayMap<>(); @GuardedBy("mLock") - private final CallbackStub mCbStub = new CallbackStub(); + private final Map<OnMediaKeyEventDispatchedListener, Executor> + mOnMediaKeyEventDispatchedListeners = new HashMap<>(); @GuardedBy("mLock") - private final Map<Callback, Executor> mCallbacks = new HashMap<>(); + private final Map<OnMediaKeyEventSessionChangedListener, Executor> + mMediaKeyEventSessionChangedCallbacks = new HashMap<>(); @GuardedBy("mLock") - private MediaSession.Token mCurMediaButtonSession; + private String mCurMediaKeyEventSessionPackage; @GuardedBy("mLock") - private ComponentName mCurMediaButtonReceiver; + private MediaSession.Token mCurMediaKeyEventSession; private Context mContext; private OnVolumeKeyLongPressListenerImpl mOnVolumeKeyLongPressListener; private OnMediaKeyListenerImpl mOnMediaKeyListener; - // TODO: Remove mLegacyCallback once Bluetooth app stop calling setCallback() method. - @GuardedBy("mLock") - private Callback mLegacyCallback; /** * @hide @@ -756,89 +759,118 @@ public final class MediaSessionManager { } /** - * Set a {@link Callback}. - * - * <p>System can only have a single callback, and the callback can only be set by - * Bluetooth service process. + * Add a {@link OnMediaKeyEventDispatchedListener}. * - * @param callback A {@link Callback}. {@code null} to reset. - * @param handler The handler on which the callback should be invoked, or {@code null} - * if the callback should be invoked on the calling thread's looper. + * @param executor The executor on which the callback should be invoked + * @param listener A {@link OnMediaKeyEventDispatchedListener}. * @hide */ - // TODO: Remove this method once Bluetooth app stop calling it. - public void setCallback(@Nullable Callback callback, @Nullable Handler handler) { - if (handler == null) { - handler = new Handler(); + @SystemApi + @RequiresPermission(value = android.Manifest.permission.MEDIA_CONTENT_CONTROL) + public void addOnMediaKeyEventDispatchedListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull OnMediaKeyEventDispatchedListener listener) { + if (executor == null) { + throw new NullPointerException("executor shouldn't be null"); + } + if (listener == null) { + throw new NullPointerException("listener shouldn't be null"); } synchronized (mLock) { - if (mLegacyCallback != null) { - unregisterCallback(mLegacyCallback); + try { + mOnMediaKeyEventDispatchedListeners.put(listener, executor); + if (mOnMediaKeyEventDispatchedListeners.size() == 1) { + mService.addOnMediaKeyEventDispatchedListener( + mOnMediaKeyEventDispatchedListenerStub); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to set media key listener", e); } - mLegacyCallback = callback; - if (callback != null) { - registerCallback(new HandlerExecutor(handler), callback); + } + } + + /** + * Remove a {@link OnMediaKeyEventDispatchedListener}. + * + * @param listener A {@link OnMediaKeyEventDispatchedListener}. + * @hide + */ + @SystemApi + @RequiresPermission(value = android.Manifest.permission.MEDIA_CONTENT_CONTROL) + public void removeOnMediaKeyEventDispatchedListener( + @NonNull OnMediaKeyEventDispatchedListener listener) { + if (listener == null) { + throw new NullPointerException("listener shouldn't be null"); + } + synchronized (mLock) { + try { + mOnMediaKeyEventDispatchedListeners.remove(listener); + if (mOnMediaKeyEventDispatchedListeners.size() == 0) { + mService.removeOnMediaKeyEventDispatchedListener( + mOnMediaKeyEventDispatchedListenerStub); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to set media key event dispatched listener", e); } } } /** - * Register a {@link Callback}. + * Add a {@link OnMediaKeyEventDispatchedListener}. * * @param executor The executor on which the callback should be invoked - * @param callback A {@link Callback}. + * @param listener A {@link OnMediaKeyEventSessionChangedListener}. * @hide */ @SystemApi @RequiresPermission(value = android.Manifest.permission.MEDIA_CONTENT_CONTROL) - public void registerCallback(@NonNull @CallbackExecutor Executor executor, - @NonNull Callback callback) { + public void addOnMediaKeyEventSessionChangedListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull OnMediaKeyEventSessionChangedListener listener) { if (executor == null) { throw new NullPointerException("executor shouldn't be null"); } - if (callback == null) { - throw new NullPointerException("callback shouldn't be null"); + if (listener == null) { + throw new NullPointerException("listener shouldn't be null"); } synchronized (mLock) { try { - mCallbacks.put(callback, executor); - if (mCurMediaButtonSession != null) { - executor.execute( - () -> callback.onAddressedPlayerChanged(mCurMediaButtonSession)); - } else if (mCurMediaButtonReceiver != null) { - executor.execute( - () -> callback.onAddressedPlayerChanged(mCurMediaButtonReceiver)); - } - - if (mCallbacks.size() == 1) { - mService.registerCallback(mCbStub); + mMediaKeyEventSessionChangedCallbacks.put(listener, executor); + executor.execute( + () -> listener.onMediaKeyEventSessionChanged( + mCurMediaKeyEventSessionPackage, mCurMediaKeyEventSession)); + if (mMediaKeyEventSessionChangedCallbacks.size() == 1) { + mService.addOnMediaKeyEventSessionChangedListener( + mOnMediaKeyEventSessionChangedListenerStub); } } catch (RemoteException e) { - Log.e(TAG, "Failed to set media key callback", e); + Log.e(TAG, "Failed to set media key listener", e); } } } /** - * Unregister a {@link Callback}. + * Remove a {@link OnMediaKeyEventSessionChangedListener}. * - * @param callback A {@link Callback}. + * @param listener A {@link OnMediaKeyEventSessionChangedListener}. * @hide */ @SystemApi @RequiresPermission(value = android.Manifest.permission.MEDIA_CONTENT_CONTROL) - public void unregisterCallback(@NonNull Callback callback) { - if (callback == null) { - throw new NullPointerException("callback shouldn't be null"); + public void removeOnMediaKeyEventSessionChangedListener( + @NonNull OnMediaKeyEventSessionChangedListener listener) { + if (listener == null) { + throw new NullPointerException("listener shouldn't be null"); } synchronized (mLock) { try { - mCallbacks.remove(callback); - if (mCallbacks.size() == 0) { - mService.unregisterCallback(mCbStub); + mMediaKeyEventSessionChangedCallbacks.remove(listener); + if (mMediaKeyEventSessionChangedCallbacks.size() == 0) { + mService.removeOnMediaKeyEventSessionChangedListener( + mOnMediaKeyEventSessionChangedListenerStub); } } catch (RemoteException e) { - Log.e(TAG, "Failed to set media key callback", e); + Log.e(TAG, "Failed to set media key listener", e); } } } @@ -900,54 +932,46 @@ public final class MediaSessionManager { } /** - * Callbacks for the media session service. - * - * <p>Called when a media key event is dispatched or the addressed player is changed. - * The addressed player is either the media session or the media button receiver that will - * receive media key events. + * Listener to receive when the media session service * @hide */ @SystemApi - public static abstract class Callback { + public interface OnMediaKeyEventDispatchedListener { /** - * Called when a media key event is dispatched to the media session - * through the media session service. + * Called when a media key event is dispatched through the media session service. The + * session token can be {@link null} if the framework has sent the media key event to the + * media button receiver to revive the media app's playback. * - * @param event Dispatched media key event. - * @param sessionToken The media session's token. - */ - public abstract void onMediaKeyEventDispatched(KeyEvent event, - MediaSession.Token sessionToken); - - /** - * Called when a media key event is dispatched to the media button receiver - * through the media session service. - * <p>MediaSessionService may broadcast key events to the media button receiver - * when reviving playback after the media session is released. + * the session is dead when , but the framework sent * * @param event Dispatched media key event. - * @param mediaButtonReceiver The media button receiver. + * @param packageName Package + * @param sessionToken The media session's token. Can be {@code null}. */ - public abstract void onMediaKeyEventDispatched(KeyEvent event, - ComponentName mediaButtonReceiver); - - /** - * Called when the addressed player is changed to a media session. - * <p>One of the {@ #onAddressedPlayerChanged} will be also called immediately after - * {@link #registerCallback} if the addressed player exists. - * - * @param sessionToken The media session's token. - */ - public abstract void onAddressedPlayerChanged(MediaSession.Token sessionToken); + default void onMediaKeyEventDispatched(@NonNull KeyEvent event, @NonNull String packageName, + @NonNull MediaSession.Token sessionToken) { } + } + /** + * Listener to receive changes in the media key event session, which would receive the media key + * event unless specified. + * @hide + */ + @SystemApi + public interface OnMediaKeyEventSessionChangedListener { /** - * Called when the addressed player is changed to the media button receiver. - * <p>One of the {@ #onAddressedPlayerChanged} will be also called immediately after - * {@link #registerCallback} if the addressed player exists. + * Called when the media key session is changed to the given media session. The key event + * session is the media session which would receive key event by default, unless the caller + * has specified the target. + * <p> + * The session token can be {@link null} if the media button session is unset. In that case, + * framework would dispatch to the last sessions's media button receiver. * - * @param mediaButtonReceiver The media button receiver. + * @param packageName The package name who would receive the media key event. Can be empty. + * @param sessionToken The media session's token. Can be {@code null.} */ - public abstract void onAddressedPlayerChanged(ComponentName mediaButtonReceiver); + default void onMediaKeyEventSessionChanged(@NonNull String packageName, + @Nullable MediaSession.Token sessionToken) { } } /** @@ -1149,50 +1173,35 @@ public final class MediaSessionManager { } } - private final class CallbackStub extends ICallback.Stub { + private final class OnMediaKeyEventDispatchedListenerStub + extends IOnMediaKeyEventDispatchedListener.Stub { @Override - public void onMediaKeyEventDispatchedToMediaSession(KeyEvent event, + public void onMediaKeyEventDispatched(KeyEvent event, String packageName, MediaSession.Token sessionToken) { synchronized (mLock) { - for (Map.Entry<Callback, Executor> e : mCallbacks.entrySet()) { - e.getValue().execute( - () -> e.getKey().onMediaKeyEventDispatched(event, sessionToken)); - } - } - } - - @Override - public void onMediaKeyEventDispatchedToMediaButtonReceiver(KeyEvent event, - ComponentName mediaButtonReceiver) { - synchronized (mLock) { - for (Map.Entry<Callback, Executor> e : mCallbacks.entrySet()) { + for (Map.Entry<OnMediaKeyEventDispatchedListener, Executor> e + : mOnMediaKeyEventDispatchedListeners.entrySet()) { e.getValue().execute( - () -> e.getKey().onMediaKeyEventDispatched(event, mediaButtonReceiver)); - } - } - } - - @Override - public void onAddressedPlayerChangedToMediaSession(MediaSession.Token sessionToken) { - synchronized (mLock) { - mCurMediaButtonSession = sessionToken; - mCurMediaButtonReceiver = null; - for (Map.Entry<Callback, Executor> e : mCallbacks.entrySet()) { - e.getValue().execute(() -> e.getKey().onAddressedPlayerChanged(sessionToken)); + () -> e.getKey().onMediaKeyEventDispatched(event, packageName, + sessionToken)); } } } + } + private final class OnMediaKeyEventSessionChangedListenerStub + extends IOnMediaKeyEventSessionChangedListener.Stub { @Override - public void onAddressedPlayerChangedToMediaButtonReceiver( - ComponentName mediaButtonReceiver) { + public void onMediaKeyEventSessionChanged(String packageName, + MediaSession.Token sessionToken) { synchronized (mLock) { - mCurMediaButtonSession = null; - mCurMediaButtonReceiver = mediaButtonReceiver; - for (Map.Entry<Callback, Executor> e : mCallbacks.entrySet()) { - e.getValue().execute(() -> e.getKey().onAddressedPlayerChanged( - mediaButtonReceiver)); + mCurMediaKeyEventSessionPackage = packageName; + mCurMediaKeyEventSession = sessionToken; + for (Map.Entry<OnMediaKeyEventSessionChangedListener, Executor> e + : mMediaKeyEventSessionChangedCallbacks.entrySet()) { + e.getValue().execute(() -> e.getKey().onMediaKeyEventSessionChanged(packageName, + sessionToken)); } } } diff --git a/media/java/android/media/soundtrigger_middleware/ConfidenceLevel.aidl b/media/java/android/media/soundtrigger_middleware/ConfidenceLevel.aidl new file mode 100644 index 000000000000..3dbc70556bd3 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/ConfidenceLevel.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +/** + * A recognition confidence level. + * This type is used to represent either a threshold or an actual detection confidence level. + * + * {@hide} + */ +parcelable ConfidenceLevel { + /** user ID. */ + int userId; + /** + * Confidence level in percent (0 - 100). + * <ul> + * <li>Min level for recognition configuration + * <li>Detected level for recognition event. + * </ul> + */ + int levelPercent; +} diff --git a/media/java/android/media/soundtrigger_middleware/ISoundTriggerCallback.aidl b/media/java/android/media/soundtrigger_middleware/ISoundTriggerCallback.aidl new file mode 100644 index 000000000000..149c1cd0447b --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/ISoundTriggerCallback.aidl @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.RecognitionEvent; +import android.media.soundtrigger_middleware.PhraseRecognitionEvent; + +/** + * Main interface for a client to get notifications of events coming from this module. + * + * {@hide} + */ +oneway interface ISoundTriggerCallback { + /** + * Invoked whenever a recognition event is triggered (typically, on recognition, but also in + * case of external aborting of a recognition or a forced recognition event - see the status + * code in the event for determining). + */ + void onRecognition(int modelHandle, in RecognitionEvent event); + /** + * Invoked whenever a phrase recognition event is triggered (typically, on recognition, but + * also in case of external aborting of a recognition or a forced recognition event - see the + * status code in the event for determining). + */ + void onPhraseRecognition(int modelHandle, in PhraseRecognitionEvent event); + /** + * Notifies the client the recognition has become available after previously having been + * unavailable, or vice versa. This method will always be invoked once immediately after + * attachment, and then every time there is a change in availability. + * When availability changes from available to unavailable, all active recognitions are aborted, + * and this event will be sent in addition to the abort event. + */ + void onRecognitionAvailabilityChange(boolean available); +} diff --git a/media/java/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl b/media/java/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl new file mode 100644 index 000000000000..80333070b7ce --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.ISoundTriggerModule; +import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; + +/** + * Main entry point into this module. + * + * Allows the client to enumerate the available soundtrigger devices and their capabilities, then + * attach to either one of them in order to use it. + * + * {@hide} + */ +interface ISoundTriggerMiddlewareService { + /** + * Query the available modules and their capabilities. + */ + SoundTriggerModuleDescriptor[] listModules(); + + /** + * Attach to one of the available modules. + * listModules() must be called prior to calling this method and the provided handle must be + * one of the handles from the returned list. + */ + ISoundTriggerModule attach(int handle, ISoundTriggerCallback callback); + + /** + * Notify the service that external input capture is taking place. This may cause some of the + * active recognitions to be aborted. + */ + void setExternalCaptureState(boolean active); +}
\ No newline at end of file diff --git a/media/java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl b/media/java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl new file mode 100644 index 000000000000..c4a57857dd3d --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.ModelParameter; +import android.media.soundtrigger_middleware.ModelParameterRange; +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionConfig; + +/** + * A sound-trigger module. + * + * This interface allows a client to operate a sound-trigger device, intended for low-power + * detection of various sound patterns, represented by a "sound model". + * + * Basic operation is to load a sound model (either a generic one or a "phrase" model), then + * initiate recognition on this model. A trigger will be delivered asynchronously via a callback + * provided by the caller earlier, when attaching to this interface. + * + * In additon to recognition events, this module will also produce abort events in cases where + * recognition has been externally preempted. + * + * {@hide} + */ +interface ISoundTriggerModule { + /** + * Load a sound model. Will return a handle to the model on success or will throw a + * ServiceSpecificException with one of the {@link Status} error codes upon a recoverable error + * (for example, lack of resources of loading a model at the time of call. + * Model must eventually be unloaded using {@link #unloadModel(int)} prior to detaching. + * + * May throw a ServiceSpecificException with an RESOURCE_CONTENTION status to indicate that + * resources required for loading the model are currently consumed by other clients. + */ + int loadModel(in SoundModel model); + + /** + * Load a phrase sound model. Will return a handle to the model on success or will throw a + * ServiceSpecificException with one of the {@link Status} error codes upon a recoverable error + * (for example, lack of resources of loading a model at the time of call. + * Model must eventually be unloaded using unloadModel prior to detaching. + * + * May throw a ServiceSpecificException with an RESOURCE_CONTENTION status to indicate that + * resources required for loading the model are currently consumed by other clients. + */ + int loadPhraseModel(in PhraseSoundModel model); + + /** + * Unload a model, previously loaded with loadModel or loadPhraseModel. After unloading, model + * can no longer be used for recognition and the resources occupied by it are released. + * Model must not be active at the time of unloading. Cient may call stopRecognition to ensure + * that. + */ + void unloadModel(int modelHandle); + + /** + * Initiate recognition on a previously loaded model. + * Recognition event would eventually be delivered via the client-provided callback, typically + * supplied during attachment to this interface. + * + * Once a recognition event is passed to the client, the recognition automatically become + * inactive, unless the event is of the RecognitionStatus.FORCED kind. Client can also shut down + * the recognition explicitly, via stopRecognition. + */ + void startRecognition(int modelHandle, in RecognitionConfig config); + + /** + * Stop a recognition of a previously active recognition. Will NOT generate a recognition event. + * This call is idempotent - calling it on an inactive model has no effect. However, it must + * only be used with a loaded model handle. + */ + void stopRecognition(int modelHandle); + + /** + * Force generation of a recognition event. Handle must be that of a loaded model. If + * recognition is inactive, will do nothing. If recognition is active, will asynchronously + * deliever an event with RecognitionStatus.FORCED status and leave recognition in active state. + * To avoid any race conditions, once an event signalling the automatic stopping of recognition + * is sent, no more forced events will get sent (even if previously requested) until recognition + * is explicitly started again. + * + * Since not all module implementations support this feature, may throw a + * ServiceSpecificException with an OPERATION_NOT_SUPPORTED status. + */ + void forceRecognitionEvent(int modelHandle); + + /** + * Set a model specific parameter with the given value. This parameter + * will keep its value for the duration the model is loaded regardless of starting and stopping + * recognition. Once the model is unloaded, the value will be lost. + * It is expected to check if the handle supports the parameter via the + * queryModelParameterSupport API prior to calling this method. + * + * @param modelHandle The sound model handle indicating which model to modify parameters + * @param modelParam Parameter to set which will be validated against the + * ModelParameter type. + * @param value The value to set for the given model parameter + */ + void setModelParameter(int modelHandle, ModelParameter modelParam, int value); + + /** + * Get a model specific parameter. This parameter will keep its value + * for the duration the model is loaded regardless of starting and stopping recognition. + * Once the model is unloaded, the value will be lost. If the value is not set, a default + * value is returned. See ModelParameter for parameter default values. + * It is expected to check if the handle supports the parameter via the + * queryModelParameterSupport API prior to calling this method. + * + * @param modelHandle The sound model associated with given modelParam + * @param modelParam Parameter to set which will be validated against the + * ModelParameter type. + * @return Value set to the requested parameter. + */ + int getModelParameter(int modelHandle, ModelParameter modelParam); + + /** + * Determine if parameter control is supported for the given model handle, and its valid value + * range if it is. + * + * @param modelHandle The sound model handle indicating which model to query + * @param modelParam Parameter to set which will be validated against the + * ModelParameter type. + * @return If parameter is supported, the return value is its valid range, otherwise null. + */ + @nullable ModelParameterRange queryModelParameterSupport(int modelHandle, + ModelParameter modelParam); + + /** + * Detach from the module, releasing any active resources. + * This will ensure the client callback is no longer called after this call returns. + * All models must have been unloaded prior to calling this method. + */ + void detach(); +}
\ No newline at end of file diff --git a/media/java/android/media/soundtrigger_middleware/ModelParameter.aidl b/media/java/android/media/soundtrigger_middleware/ModelParameter.aidl new file mode 100644 index 000000000000..09936278e93a --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/ModelParameter.aidl @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +/** + * Model specific parameters to be used with parameter set and get APIs. + * + * {@hide} + */ +@Backing(type="int") +enum ModelParameter { + /** + * Placeholder for invalid model parameter used for returning error or + * passing an invalid value. + */ + INVALID = -1, + + /** + * Controls the sensitivity threshold adjustment factor for a given model. + * Negative value corresponds to less sensitive model (high threshold) and + * a positive value corresponds to a more sensitive model (low threshold). + * Default value is 0. + */ + THRESHOLD_FACTOR = 0, +} diff --git a/media/java/android/media/soundtrigger_middleware/ModelParameterRange.aidl b/media/java/android/media/soundtrigger_middleware/ModelParameterRange.aidl new file mode 100644 index 000000000000..d6948a87dc6d --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/ModelParameterRange.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +/** + * Value range for a model parameter. + * + * {@hide} + */ +parcelable ModelParameterRange { + /** Minimum (inclusive) */ + int minInclusive; + /** Maximum (inclusive) */ + int maxInclusive; +} diff --git a/media/java/android/media/soundtrigger_middleware/Phrase.aidl b/media/java/android/media/soundtrigger_middleware/Phrase.aidl new file mode 100644 index 000000000000..98a489f8a6a9 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/Phrase.aidl @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +/** + * Key phrase descriptor. + * + * {@hide} + */ +parcelable Phrase { + /** Unique keyphrase ID assigned at enrollment time. */ + int id; + /** Recognition modes supported by this key phrase (bitfield of RecognitionMode enum). */ + int recognitionModes; + /** List of users IDs associated with this key phrase. */ + int[] users; + /** Locale - Java Locale style (e.g. en_US). */ + String locale; + /** Phrase text. */ + String text; +} diff --git a/media/java/android/media/soundtrigger_middleware/PhraseRecognitionEvent.aidl b/media/java/android/media/soundtrigger_middleware/PhraseRecognitionEvent.aidl new file mode 100644 index 000000000000..6a3ec61d1ebf --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/PhraseRecognitionEvent.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.PhraseRecognitionExtra; +import android.media.soundtrigger_middleware.RecognitionEvent; + +/** + * An event that gets sent to indicate a phrase recognition (or aborting of the recognition + process). + * {@hide} + */ +parcelable PhraseRecognitionEvent { + /** Common recognition event. */ + RecognitionEvent common; + /** List of descriptors for each recognized key phrase */ + PhraseRecognitionExtra[] phraseExtras; +} diff --git a/media/java/android/media/soundtrigger_middleware/PhraseRecognitionExtra.aidl b/media/java/android/media/soundtrigger_middleware/PhraseRecognitionExtra.aidl new file mode 100644 index 000000000000..cb96bf37a95d --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/PhraseRecognitionExtra.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.ConfidenceLevel; + +/** + * Specialized recognition event for key phrase detection. + * {@hide} + */ +parcelable PhraseRecognitionExtra { + // TODO(ytai): Constants / enums. + + /** keyphrase ID */ + int id; + /** recognition modes used for this keyphrase */ + int recognitionModes; + /** confidence level for mode RECOGNITION_MODE_VOICE_TRIGGER */ + int confidenceLevel; + /** number of user confidence levels */ + ConfidenceLevel[] levels; +} diff --git a/media/java/android/media/soundtrigger_middleware/PhraseSoundModel.aidl b/media/java/android/media/soundtrigger_middleware/PhraseSoundModel.aidl new file mode 100644 index 000000000000..81028c1608ea --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/PhraseSoundModel.aidl @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.Phrase; + +/** + * Specialized sound model for key phrase detection. + * Proprietary representation of key phrases in binary data must match + * information indicated by phrases field. + * {@hide} + */ +parcelable PhraseSoundModel { + /** Common part of sound model descriptor */ + SoundModel common; + /** List of descriptors for key phrases supported by this sound model */ + Phrase[] phrases; +} diff --git a/media/java/android/media/soundtrigger_middleware/RecognitionConfig.aidl b/media/java/android/media/soundtrigger_middleware/RecognitionConfig.aidl new file mode 100644 index 000000000000..c7642e8c1241 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/RecognitionConfig.aidl @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.PhraseRecognitionExtra; + +/** + * Configuration for tuning behavior of an active recognition process. + * {@hide} + */ +parcelable RecognitionConfig { + /* Capture and buffer audio for this recognition instance. */ + boolean captureRequested; + + /* Configuration for each key phrase. */ + PhraseRecognitionExtra[] phraseRecognitionExtras; + + /** Opaque capture configuration data. */ + byte[] data; +} diff --git a/media/java/android/media/soundtrigger_middleware/RecognitionEvent.aidl b/media/java/android/media/soundtrigger_middleware/RecognitionEvent.aidl new file mode 100644 index 000000000000..de4d060ce484 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/RecognitionEvent.aidl @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +import android.media.audio.common.AudioConfig; +import android.media.soundtrigger_middleware.RecognitionStatus; +import android.media.soundtrigger_middleware.SoundModelType; + +/** + * An event that gets sent to indicate a recognition (or aborting of the recognition process). + * {@hide} + */ +parcelable RecognitionEvent { + /** Recognition status. */ + RecognitionStatus status; + /** Event type, same as sound model type. */ + SoundModelType type; + /** Is it possible to capture audio from this utterance buffered by the implementation. */ + boolean captureAvailable; + /* Audio session ID. framework use. */ + int captureSession; + /** + * Delay in ms between end of model detection and start of audio available for capture. + * A negative value is possible (e.g. if key phrase is also available for Capture. + */ + int captureDelayMs; + /** Duration in ms of audio captured before the start of the trigger. 0 if none. */ + int capturePreambleMs; + /** If true, the 'data' field below contains the capture of the trigger sound. */ + boolean triggerInData; + /** + * Audio format of either the trigger in event data or to use for capture of the rest of the + * utterance. + */ + AudioConfig audioConfig; + /** Additional data. */ + byte[] data; +} diff --git a/media/java/android/media/soundtrigger_middleware/RecognitionMode.aidl b/media/java/android/media/soundtrigger_middleware/RecognitionMode.aidl new file mode 100644 index 000000000000..d8bfff4bec6f --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/RecognitionMode.aidl @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +/** + * Recognition mode. + * {@hide} + */ +@Backing(type="int") +enum RecognitionMode { + /** Simple voice trigger. */ + VOICE_TRIGGER = 0x1, + /** Trigger only if one user in model identified. */ + USER_IDENTIFICATION = 0x2, + /** Trigger only if one user in model authenticated. */ + USER_AUTHENTICATION = 0x4, + /** Generic sound trigger. */ + GENERIC_TRIGGER = 0x8, +} diff --git a/media/java/android/media/soundtrigger_middleware/RecognitionStatus.aidl b/media/java/android/media/soundtrigger_middleware/RecognitionStatus.aidl new file mode 100644 index 000000000000..d563edca547d --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/RecognitionStatus.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +/** + * A status for indicating the type of a recognition event. + * {@hide} + */ +@Backing(type="int") +enum RecognitionStatus { + /** Recognition success. */ + SUCCESS = 0, + /** Recognition aborted (e.g. capture preempted by another use-case. */ + ABORTED = 1, + /** Recognition failure. */ + FAILURE = 2, + /** + * Recognition event was triggered by a forceRecognitionEvent request, not by the DSP. + * Note that forced detections *do not* stop the active recognition, unlike the other types. + */ + FORCED = 3 +} diff --git a/media/java/android/media/soundtrigger_middleware/SoundModel.aidl b/media/java/android/media/soundtrigger_middleware/SoundModel.aidl new file mode 100644 index 000000000000..fba1ee507836 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/SoundModel.aidl @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.SoundModelType; + +/** + * Base sound model descriptor. This struct can be extended for various specific types by way of + * aggregation. + * {@hide} + */ +parcelable SoundModel { + /** Model type. */ + SoundModelType type; + /** Unique sound model ID. */ + String uuid; + /** + * Unique vendor ID. Identifies the engine the sound model + * was build for */ + String vendorUuid; + /** Opaque data transparent to Android framework */ + byte[] data; +} diff --git a/media/java/android/media/soundtrigger_middleware/SoundModelType.aidl b/media/java/android/media/soundtrigger_middleware/SoundModelType.aidl new file mode 100644 index 000000000000..f2abc9af7780 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/SoundModelType.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +/** + * Sound model type. + * {@hide} + */ +@Backing(type="int") +enum SoundModelType { + /** Unspecified sound model type */ + UNKNOWN = -1, + /** Key phrase sound models */ + KEYPHRASE = 0, + /** All models other than keyphrase */ + GENERIC = 1, +} diff --git a/media/java/android/media/soundtrigger_middleware/SoundTriggerModuleDescriptor.aidl b/media/java/android/media/soundtrigger_middleware/SoundTriggerModuleDescriptor.aidl new file mode 100644 index 000000000000..667135ff61b9 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/SoundTriggerModuleDescriptor.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.SoundTriggerModuleProperties; + +/** + * A descriptor of an available sound trigger module, containing the handle used to reference the + * module, as well its capabilities. + * {@hide} + */ +parcelable SoundTriggerModuleDescriptor { + /** Module handle to be used for attaching to it. */ + int handle; + /** Module capabilities. */ + SoundTriggerModuleProperties properties; +} + diff --git a/media/java/android/media/soundtrigger_middleware/SoundTriggerModuleProperties.aidl b/media/java/android/media/soundtrigger_middleware/SoundTriggerModuleProperties.aidl new file mode 100644 index 000000000000..1a3b40261a62 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/SoundTriggerModuleProperties.aidl @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +/** + * Capabilities of a sound trigger module. + * {@hide} + */ +parcelable SoundTriggerModuleProperties { + /** Implementor name */ + String implementor; + /** Implementation description */ + String description; + /** Implementation version */ + int version; + /** + * Unique implementation ID. The UUID must change with each version of + the engine implementation */ + String uuid; + /** Maximum number of concurrent sound models loaded */ + int maxSoundModels; + /** Maximum number of key phrases */ + int maxKeyPhrases; + /** Maximum number of concurrent users detected */ + int maxUsers; + /** All supported modes. e.g RecognitionMode.VOICE_TRIGGER */ + int recognitionModes; + /** Supports seamless transition from detection to capture */ + boolean captureTransition; + /** Maximum buffering capacity in ms if captureTransition is true */ + int maxBufferMs; + /** Supports capture by other use cases while detection is active */ + boolean concurrentCapture; + /** Returns the trigger capture in event */ + boolean triggerInEvent; + /** + * Rated power consumption when detection is active with TDB + * silence/sound/speech ratio */ + int powerConsumptionMw; +} diff --git a/media/java/android/media/soundtrigger_middleware/Status.aidl b/media/java/android/media/soundtrigger_middleware/Status.aidl new file mode 100644 index 000000000000..d8f9d8f7e891 --- /dev/null +++ b/media/java/android/media/soundtrigger_middleware/Status.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2019 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.media.soundtrigger_middleware; + +/** + * {@hide} + */ +@Backing(type="int") +enum Status { + /** Success. */ + SUCCESS = 0, + /** Failure due to resource contention. This is typically a temporary condition. */ + RESOURCE_CONTENTION = 1, + /** Operation is not supported in this implementation. This is a permanent condition. */ + OPERATION_NOT_SUPPORTED = 2, +} diff --git a/media/java/android/mtp/MtpDatabase.java b/media/java/android/mtp/MtpDatabase.java index 16ba63bf031b..f3c071a06eba 100755 --- a/media/java/android/mtp/MtpDatabase.java +++ b/media/java/android/mtp/MtpDatabase.java @@ -16,8 +16,10 @@ package android.mtp; +import android.annotation.NonNull; import android.content.BroadcastReceiver; import android.content.ContentProviderClient; +import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -422,13 +424,13 @@ public class MtpDatabase implements AutoCloseable { } // Add the new file to MediaProvider if (succeeded) { - MediaStore.scanFile(mContext, obj.getPath().toFile()); + MediaStore.scanFile(mContext.getContentResolver(), obj.getPath().toFile()); } } @VisibleForNative private void rescanFile(String path, int handle, int format) { - MediaStore.scanFile(mContext, new File(path)); + MediaStore.scanFile(mContext.getContentResolver(), new File(path)); } @VisibleForNative @@ -577,7 +579,7 @@ public class MtpDatabase implements AutoCloseable { try { // note - we are relying on a special case in MediaProvider.update() to update // the paths for all children in the case where this is a directory. - final Uri objectsUri = MediaStore.Files.getMtpObjectsUri(obj.getVolumeName()); + final Uri objectsUri = MediaStore.Files.getContentUri(obj.getVolumeName()); mMediaProvider.update(objectsUri, values, PATH_WHERE, whereArgs); } catch (RemoteException e) { Log.e(TAG, "RemoteException in mMediaProvider.update", e); @@ -587,13 +589,13 @@ public class MtpDatabase implements AutoCloseable { if (obj.isDir()) { // for directories, check if renamed from something hidden to something non-hidden if (oldPath.getFileName().startsWith(".") && !newPath.startsWith(".")) { - MediaStore.scanFile(mContext, newPath.toFile()); + MediaStore.scanFile(mContext.getContentResolver(), newPath.toFile()); } } else { // for files, check if renamed from .nomedia to something else if (oldPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA) && !newPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)) { - MediaStore.scanFile(mContext, newPath.getParent().toFile()); + MediaStore.scanFile(mContext.getContentResolver(), newPath.getParent().toFile()); } } return MtpConstants.RESPONSE_OK; @@ -658,11 +660,11 @@ public class MtpDatabase implements AutoCloseable { // Old parent exists in MediaProvider - perform a move // note - we are relying on a special case in MediaProvider.update() to update // the paths for all children in the case where this is a directory. - final Uri objectsUri = MediaStore.Files.getMtpObjectsUri(obj.getVolumeName()); + final Uri objectsUri = MediaStore.Files.getContentUri(obj.getVolumeName()); mMediaProvider.update(objectsUri, values, PATH_WHERE, whereArgs); } else { // Old parent doesn't exist - add the object - MediaStore.scanFile(mContext, path.toFile()); + MediaStore.scanFile(mContext.getContentResolver(), path.toFile()); } } catch (RemoteException e) { Log.e(TAG, "RemoteException in mMediaProvider.update", e); @@ -689,7 +691,7 @@ public class MtpDatabase implements AutoCloseable { if (!success) { return; } - MediaStore.scanFile(mContext, obj.getPath().toFile()); + MediaStore.scanFile(mContext.getContentResolver(), obj.getPath().toFile()); } @VisibleForNative @@ -873,7 +875,7 @@ public class MtpDatabase implements AutoCloseable { } private int findInMedia(MtpStorageManager.MtpObject obj, Path path) { - final Uri objectsUri = MediaStore.Files.getMtpObjectsUri(obj.getVolumeName()); + final Uri objectsUri = MediaStore.Files.getContentUri(obj.getVolumeName()); int ret = -1; Cursor c = null; @@ -893,7 +895,7 @@ public class MtpDatabase implements AutoCloseable { } private void deleteFromMedia(MtpStorageManager.MtpObject obj, Path path, boolean isDir) { - final Uri objectsUri = MediaStore.Files.getMtpObjectsUri(obj.getVolumeName()); + final Uri objectsUri = MediaStore.Files.getContentUri(obj.getVolumeName()); try { // Delete the object(s) from MediaProvider, but ignore errors. if (isDir) { @@ -909,7 +911,7 @@ public class MtpDatabase implements AutoCloseable { String[] whereArgs = new String[]{path.toString()}; if (mMediaProvider.delete(objectsUri, PATH_WHERE, whereArgs) > 0) { if (!isDir && path.toString().toLowerCase(Locale.US).endsWith(NO_MEDIA)) { - MediaStore.scanFile(mContext, path.getParent().toFile()); + MediaStore.scanFile(mContext.getContentResolver(), path.getParent().toFile()); } } else { Log.i(TAG, "Mediaprovider didn't delete " + path); @@ -921,71 +923,12 @@ public class MtpDatabase implements AutoCloseable { @VisibleForNative private int[] getObjectReferences(int handle) { - MtpStorageManager.MtpObject obj = mManager.getObject(handle); - if (obj == null) - return null; - // Translate this handle to the MediaProvider Handle - handle = findInMedia(obj, obj.getPath()); - if (handle == -1) - return null; - Uri uri = Files.getMtpReferencesUri(obj.getVolumeName(), handle); - Cursor c = null; - try { - c = mMediaProvider.query(uri, PATH_PROJECTION, null, null, null, null); - if (c == null) { - return null; - } - ArrayList<Integer> result = new ArrayList<>(); - while (c.moveToNext()) { - // Translate result handles back into handles for this session. - String refPath = c.getString(0); - MtpStorageManager.MtpObject refObj = mManager.getByPath(refPath); - if (refObj != null) { - result.add(refObj.getId()); - } - } - return result.stream().mapToInt(Integer::intValue).toArray(); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getObjectList", e); - } finally { - if (c != null) { - c.close(); - } - } return null; } @VisibleForNative private int setObjectReferences(int handle, int[] references) { - MtpStorageManager.MtpObject obj = mManager.getObject(handle); - if (obj == null) - return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; - // Translate this handle to the MediaProvider Handle - handle = findInMedia(obj, obj.getPath()); - if (handle == -1) - return MtpConstants.RESPONSE_GENERAL_ERROR; - Uri uri = Files.getMtpReferencesUri(obj.getVolumeName(), handle); - ArrayList<ContentValues> valuesList = new ArrayList<>(); - for (int id : references) { - // Translate each reference id to the MediaProvider Id - MtpStorageManager.MtpObject refObj = mManager.getObject(id); - if (refObj == null) - continue; - int refHandle = findInMedia(refObj, refObj.getPath()); - if (refHandle == -1) - continue; - ContentValues values = new ContentValues(); - values.put(Files.FileColumns._ID, refHandle); - valuesList.add(values); - } - try { - if (mMediaProvider.bulkInsert(uri, valuesList.toArray(new ContentValues[0])) > 0) { - return MtpConstants.RESPONSE_OK; - } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in setObjectReferences", e); - } - return MtpConstants.RESPONSE_GENERAL_ERROR; + return MtpConstants.RESPONSE_OPERATION_NOT_SUPPORTED; } @VisibleForNative diff --git a/media/java/android/mtp/MtpStorage.java b/media/java/android/mtp/MtpStorage.java index 65d0fef74b25..c7dbca61f90a 100644 --- a/media/java/android/mtp/MtpStorage.java +++ b/media/java/android/mtp/MtpStorage.java @@ -41,11 +41,7 @@ public class MtpStorage { mDescription = volume.getDescription(null); mRemovable = volume.isRemovable(); mMaxFileSize = volume.getMaxFileSize(); - if (volume.isPrimary()) { - mVolumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY; - } else { - mVolumeName = volume.getNormalizedUuid(); - } + mVolumeName = volume.getMediaStoreVolumeName(); } /** diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp index 05aaa82f8ac8..20980a90e9fe 100644 --- a/media/jni/android_media_MediaCodec.cpp +++ b/media/jni/android_media_MediaCodec.cpp @@ -742,11 +742,11 @@ status_t JMediaCodec::getCodecInfo(JNIEnv *env, jobject *codecInfoObject) const return OK; } -status_t JMediaCodec::getMetrics(JNIEnv *, MediaAnalyticsItem * &reply) const { - mediametrics_handle_t reply2 = MediaAnalyticsItem::convert(reply); +status_t JMediaCodec::getMetrics(JNIEnv *, mediametrics::Item * &reply) const { + mediametrics_handle_t reply2 = mediametrics::Item::convert(reply); status_t status = mCodec->getMetrics(reply2); // getMetrics() updates reply2, pass the converted update along to our caller. - reply = MediaAnalyticsItem::convert(reply2); + reply = mediametrics::Item::convert(reply2); return status; } @@ -1850,7 +1850,7 @@ android_media_MediaCodec_native_getMetrics(JNIEnv *env, jobject thiz) } // get what we have for the metrics from the codec - MediaAnalyticsItem *item = 0; + mediametrics::Item *item = 0; status_t err = codec->getMetrics(env, item); if (err != OK) { diff --git a/media/jni/android_media_MediaCodec.h b/media/jni/android_media_MediaCodec.h index dfe30a3f5909..ce1c805b6366 100644 --- a/media/jni/android_media_MediaCodec.h +++ b/media/jni/android_media_MediaCodec.h @@ -21,7 +21,7 @@ #include "jni.h" -#include <media/MediaAnalyticsItem.h> +#include <media/MediaMetricsItem.h> #include <media/hardware/CryptoAPI.h> #include <media/stagefright/foundation/ABase.h> #include <media/stagefright/foundation/AHandler.h> @@ -124,7 +124,7 @@ struct JMediaCodec : public AHandler { status_t getCodecInfo(JNIEnv *env, jobject *codecInfo) const; - status_t getMetrics(JNIEnv *env, MediaAnalyticsItem * &reply) const; + status_t getMetrics(JNIEnv *env, mediametrics::Item * &reply) const; status_t setParameters(const sp<AMessage> ¶ms); diff --git a/media/jni/android_media_MediaExtractor.cpp b/media/jni/android_media_MediaExtractor.cpp index f5ae9d0d5d2f..528dc62c3016 100644 --- a/media/jni/android_media_MediaExtractor.cpp +++ b/media/jni/android_media_MediaExtractor.cpp @@ -913,7 +913,7 @@ android_media_MediaExtractor_native_getMetrics(JNIEnv * env, jobject thiz) } // build and return the Bundle - std::unique_ptr<MediaAnalyticsItem> item(MediaAnalyticsItem::create()); + std::unique_ptr<mediametrics::Item> item(mediametrics::Item::create()); item->readFromParcel(reply); jobject mybundle = MediaMetricsJNI::writeMetricsToBundle(env, item.get(), NULL); diff --git a/media/jni/android_media_MediaMetricsJNI.cpp b/media/jni/android_media_MediaMetricsJNI.cpp index d3151545333c..e17a6173ba4d 100644 --- a/media/jni/android_media_MediaMetricsJNI.cpp +++ b/media/jni/android_media_MediaMetricsJNI.cpp @@ -18,11 +18,12 @@ #include <binder/Parcel.h> #include <jni.h> -#include <media/MediaAnalyticsItem.h> +#include <media/MediaMetricsItem.h> #include <nativehelper/JNIHelp.h> #include "android_media_MediaMetricsJNI.h" #include "android_os_Parcel.h" +#include "android_runtime/AndroidRuntime.h" // This source file is compiled and linked into: // core/jni/ (libandroid_runtime.so) @@ -52,7 +53,7 @@ struct BundleHelper { const jmethodID constructID; jobject const bundle; - // We use templated put to access MediaAnalyticsItem based on data type not type enum. + // We use templated put to access mediametrics::Item based on data type not type enum. // See std::variant and std::visit. template<typename T> void put(jstring keyName, const T& value) = delete; @@ -97,7 +98,7 @@ struct BundleHelper { // place the attributes into a java PersistableBundle object jobject MediaMetricsJNI::writeMetricsToBundle( - JNIEnv* env, MediaAnalyticsItem *item, jobject bundle) + JNIEnv* env, mediametrics::Item *item, jobject bundle) { BundleHelper bh(env, bundle); @@ -124,6 +125,28 @@ jobject MediaMetricsJNI::writeMetricsToBundle( return bh.bundle; } +// Implementation of MediaMetrics.native_submit_bytebuffer(), +// Delivers the byte buffer to the mediametrics service. +static jint android_media_MediaMetrics_submit_bytebuffer( + JNIEnv* env, jobject thiz, jobject byteBuffer, jint length) +{ + const jbyte* buffer = + reinterpret_cast<const jbyte*>(env->GetDirectBufferAddress(byteBuffer)); + if (buffer == nullptr) { + ALOGE("Error retrieving source of audio data to play, can't play"); + return (jint)BAD_VALUE; + } + + // TODO: directly record item to MediaMetrics service. + mediametrics::Item item; + if (item.readFromByteString((char *)buffer, length) != NO_ERROR) { + ALOGW("%s: cannot read from byte string", __func__); + return (jint)BAD_VALUE; + } + item.selfrecord(); + return (jint)NO_ERROR; +} + // Helper function to convert a native PersistableBundle to a Java // PersistableBundle. jobject MediaMetricsJNI::nativeToJavaPersistableBundle(JNIEnv *env, @@ -191,5 +214,18 @@ jobject MediaMetricsJNI::nativeToJavaPersistableBundle(JNIEnv *env, return newBundle; } -}; // namespace android +// ---------------------------------------------------------------------------- +static constexpr JNINativeMethod gMethods[] = { + {"native_submit_bytebuffer", "(Ljava/nio/ByteBuffer;I)I", + (void *)android_media_MediaMetrics_submit_bytebuffer}, +}; + +// Registers the native methods, called from core/jni/AndroidRuntime.cpp +int register_android_media_MediaMetrics(JNIEnv *env) +{ + return AndroidRuntime::registerNativeMethods( + env, "android/media/MediaMetrics", gMethods, std::size(gMethods)); +} + +}; // namespace android diff --git a/media/jni/android_media_MediaMetricsJNI.h b/media/jni/android_media_MediaMetricsJNI.h index 63ec27aa58ee..bcad5587d909 100644 --- a/media/jni/android_media_MediaMetricsJNI.h +++ b/media/jni/android_media_MediaMetricsJNI.h @@ -19,7 +19,7 @@ #include <jni.h> #include <nativehelper/JNIHelp.h> -#include <media/MediaAnalyticsItem.h> +#include <media/MediaMetricsItem.h> #include <binder/PersistableBundle.h> // Copeid from core/jni/ (libandroid_runtime.so) @@ -27,7 +27,7 @@ namespace android { class MediaMetricsJNI { public: - static jobject writeMetricsToBundle(JNIEnv* env, MediaAnalyticsItem *item, jobject mybundle); + static jobject writeMetricsToBundle(JNIEnv* env, mediametrics::Item *item, jobject mybundle); static jobject nativeToJavaPersistableBundle(JNIEnv*, os::PersistableBundle*); }; diff --git a/media/jni/android_media_MediaPlayer.cpp b/media/jni/android_media_MediaPlayer.cpp index b4fa07bd7420..963b650292e4 100644 --- a/media/jni/android_media_MediaPlayer.cpp +++ b/media/jni/android_media_MediaPlayer.cpp @@ -23,7 +23,7 @@ #include <media/AudioResamplerPublic.h> #include <media/IMediaHTTPService.h> #include <media/MediaPlayerInterface.h> -#include <media/MediaAnalyticsItem.h> +#include <media/MediaMetricsItem.h> #include <media/stagefright/foundation/ByteUtils.h> // for FOURCC definition #include <stdio.h> #include <assert.h> @@ -682,7 +682,7 @@ android_media_MediaPlayer_native_getMetrics(JNIEnv *env, jobject thiz) return (jobject) NULL; } - std::unique_ptr<MediaAnalyticsItem> item(MediaAnalyticsItem::create()); + std::unique_ptr<mediametrics::Item> item(mediametrics::Item::create()); item->readFromParcel(p); jobject mybundle = MediaMetricsJNI::writeMetricsToBundle(env, item.get(), NULL); diff --git a/media/jni/android_media_MediaRecorder.cpp b/media/jni/android_media_MediaRecorder.cpp index f8ba36d99de7..6eeccf0e5a77 100644 --- a/media/jni/android_media_MediaRecorder.cpp +++ b/media/jni/android_media_MediaRecorder.cpp @@ -29,7 +29,7 @@ #include <gui/Surface.h> #include <camera/Camera.h> #include <media/mediarecorder.h> -#include <media/MediaAnalyticsItem.h> +#include <media/MediaMetricsItem.h> #include <media/MicrophoneInfo.h> #include <media/stagefright/PersistentSurface.h> #include <utils/threads.h> @@ -694,7 +694,7 @@ android_media_MediaRecorder_native_getMetrics(JNIEnv *env, jobject thiz) } // build and return the Bundle - std::unique_ptr<MediaAnalyticsItem> item(MediaAnalyticsItem::create()); + std::unique_ptr<mediametrics::Item> item(mediametrics::Item::create()); item->readFromParcel(reply); jobject mybundle = MediaMetricsJNI::writeMetricsToBundle(env, item.get(), NULL); diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CarrierActionUtils.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CarrierActionUtils.java index 3258d57ba8e0..2697a1066ed2 100644 --- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CarrierActionUtils.java +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CarrierActionUtils.java @@ -29,6 +29,7 @@ import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; + import com.android.internal.telephony.PhoneConstants; /** @@ -138,7 +139,7 @@ public class CarrierActionUtils { SubscriptionManager.getDefaultVoiceSubscriptionId()); logd("onRegisterDefaultNetworkAvail subId: " + subId); final TelephonyManager telephonyMgr = context.getSystemService(TelephonyManager.class); - telephonyMgr.carrierActionReportDefaultNetworkStatus(subId, true); + telephonyMgr.createForSubscriptionId(subId).reportDefaultNetworkStatus(true); } private static void onDeregisterDefaultNetworkAvail(Intent intent, Context context) { @@ -146,7 +147,7 @@ public class CarrierActionUtils { SubscriptionManager.getDefaultVoiceSubscriptionId()); logd("onDeregisterDefaultNetworkAvail subId: " + subId); final TelephonyManager telephonyMgr = context.getSystemService(TelephonyManager.class); - telephonyMgr.carrierActionReportDefaultNetworkStatus(subId, false); + telephonyMgr.createForSubscriptionId(subId).reportDefaultNetworkStatus(false); } private static void onDisableRadio(Intent intent, Context context) { @@ -154,7 +155,7 @@ public class CarrierActionUtils { SubscriptionManager.getDefaultVoiceSubscriptionId()); logd("onDisableRadio subId: " + subId); final TelephonyManager telephonyMgr = context.getSystemService(TelephonyManager.class); - telephonyMgr.carrierActionSetRadioEnabled(subId, !ENABLE); + telephonyMgr.createForSubscriptionId(subId).setRadioEnabled(!ENABLE); } private static void onEnableRadio(Intent intent, Context context) { @@ -162,7 +163,7 @@ public class CarrierActionUtils { SubscriptionManager.getDefaultVoiceSubscriptionId()); logd("onEnableRadio subId: " + subId); final TelephonyManager telephonyMgr = context.getSystemService(TelephonyManager.class); - telephonyMgr.carrierActionSetRadioEnabled(subId, ENABLE); + telephonyMgr.createForSubscriptionId(subId).setRadioEnabled(ENABLE); } private static void onShowCaptivePortalNotification(Intent intent, Context context) { @@ -205,7 +206,7 @@ public class CarrierActionUtils { SubscriptionManager.getDefaultVoiceSubscriptionId()); logd("onResetAllCarrierActions subId: " + subId); final TelephonyManager telephonyMgr = context.getSystemService(TelephonyManager.class); - telephonyMgr.carrierActionResetAll(subId); + telephonyMgr.createForSubscriptionId(subId).resetAllCarrierActions(); } private static Notification getNotification(Context context, int titleId, int textId, diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpSinkProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpSinkProfile.java index 9c896c80b409..bc03c343a909 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpSinkProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpSinkProfile.java @@ -164,13 +164,13 @@ final class A2dpSinkProfile implements LocalBluetoothProfile { } } - boolean isA2dpPlaying() { + boolean isAudioPlaying() { if (mService == null) { return false; } List<BluetoothDevice> srcs = mService.getConnectedDevices(); if (!srcs.isEmpty()) { - if (mService.isA2dpPlaying(srcs.get(0))) { + if (mService.isAudioPlaying(srcs.get(0))) { return true; } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java index 8f40ab47fe1b..80b03a43b055 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java @@ -175,7 +175,7 @@ public class LocalBluetoothAdapter { return; } A2dpSinkProfile a2dpSink = mProfileManager.getA2dpSinkProfile(); - if ((a2dpSink != null) && (a2dpSink.isA2dpPlaying())){ + if ((a2dpSink != null) && (a2dpSink.isAudioPlaying())) { return; } } diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java index 443543b89a66..b13c483fc2c2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java @@ -36,7 +36,6 @@ import android.net.ScoredNetwork; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiConfiguration.KeyMgmt; -import android.net.wifi.WifiEnterpriseConfig; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiNetworkScoreCache; @@ -181,9 +180,6 @@ public class AccessPoint implements Comparable<AccessPoint> { static final String KEY_CONFIG = "key_config"; static final String KEY_FQDN = "key_fqdn"; static final String KEY_PROVIDER_FRIENDLY_NAME = "key_provider_friendly_name"; - static final String KEY_IS_CARRIER_AP = "key_is_carrier_ap"; - static final String KEY_CARRIER_AP_EAP_TYPE = "key_carrier_ap_eap_type"; - static final String KEY_CARRIER_NAME = "key_carrier_name"; static final String KEY_EAPTYPE = "eap_psktype"; static final String KEY_SUBSCRIPTION_EXPIRATION_TIME_IN_MILLIS = "key_subscription_expiration_time_in_millis"; @@ -265,8 +261,6 @@ public class AccessPoint implements Comparable<AccessPoint> { @PasspointConfigurationVersion private int mPasspointConfigurationVersion = PasspointConfigurationVersion.INVALID; - private boolean mIsCarrierAp = false; - private OsuProvider mOsuProvider; private String mOsuStatus; @@ -276,12 +270,6 @@ public class AccessPoint implements Comparable<AccessPoint> { private boolean mIsPskSaeTransitionMode = false; private boolean mIsOweTransitionMode = false; - /** - * The EAP type {@link WifiEnterpriseConfig.Eap} associated with this AP if it is a carrier AP. - */ - private int mCarrierApEapType = WifiEnterpriseConfig.Eap.NONE; - private String mCarrierName = null; - public AccessPoint(Context context, Bundle savedState) { mContext = context; @@ -330,15 +318,6 @@ public class AccessPoint implements Comparable<AccessPoint> { if (savedState.containsKey(KEY_PROVIDER_FRIENDLY_NAME)) { mProviderFriendlyName = savedState.getString(KEY_PROVIDER_FRIENDLY_NAME); } - if (savedState.containsKey(KEY_IS_CARRIER_AP)) { - mIsCarrierAp = savedState.getBoolean(KEY_IS_CARRIER_AP); - } - if (savedState.containsKey(KEY_CARRIER_AP_EAP_TYPE)) { - mCarrierApEapType = savedState.getInt(KEY_CARRIER_AP_EAP_TYPE); - } - if (savedState.containsKey(KEY_CARRIER_NAME)) { - mCarrierName = savedState.getString(KEY_CARRIER_NAME); - } if (savedState.containsKey(KEY_SUBSCRIPTION_EXPIRATION_TIME_IN_MILLIS)) { mSubscriptionExpirationTimeInMillis = savedState.getLong(KEY_SUBSCRIPTION_EXPIRATION_TIME_IN_MILLIS); @@ -966,10 +945,6 @@ public class AccessPoint implements Comparable<AccessPoint> { mIsPskSaeTransitionMode = AccessPoint.isPskSaeTransitionMode(bestResult); mIsOweTransitionMode = AccessPoint.isOweTransitionMode(bestResult); - - mIsCarrierAp = bestResult.isCarrierAp; - mCarrierApEapType = bestResult.carrierApEapType; - mCarrierName = bestResult.carrierName; } // Update the config SSID of a Passpoint network to that of the best RSSI if (isPasspoint()) { @@ -1094,18 +1069,6 @@ public class AccessPoint implements Comparable<AccessPoint> { return null; } - public boolean isCarrierAp() { - return mIsCarrierAp; - } - - public int getCarrierApEapType() { - return mCarrierApEapType; - } - - public String getCarrierName() { - return mCarrierName; - } - public String getSavedNetworkSummary() { WifiConfiguration config = mConfig; if (config != null) { @@ -1181,15 +1144,9 @@ public class AccessPoint implements Comparable<AccessPoint> { summary.append(mContext.getString(R.string.tap_to_sign_up)); } } else if (isActive()) { - if (getDetailedState() == DetailedState.CONNECTED && mIsCarrierAp) { - // This is the active connection on a carrier AP - summary.append(String.format(mContext.getString(R.string.connected_via_carrier), - mCarrierName)); - } else { - summary.append(getSummary(mContext, /* ssid */ null, getDetailedState(), - mInfo != null && mInfo.isEphemeral(), - mInfo != null ? mInfo.getAppPackageName() : null)); - } + summary.append(getSummary(mContext, /* ssid */ null, getDetailedState(), + mInfo != null && mInfo.isEphemeral(), + mInfo != null ? mInfo.getAppPackageName() : null)); } else { // not active if (mConfig != null && mConfig.hasNoInternetAccess()) { int messageID = mConfig.getNetworkSelectionStatus().isNetworkPermanentlyDisabled() @@ -1213,9 +1170,6 @@ public class AccessPoint implements Comparable<AccessPoint> { summary.append(mContext.getString(R.string.wifi_disabled_generic)); break; } - } else if (mIsCarrierAp) { - summary.append(String.format(mContext.getString( - R.string.available_via_carrier), mCarrierName)); } else if (!isReachable()) { // Wifi out of range summary.append(mContext.getString(R.string.wifi_not_in_range)); } else { // In range, not disabled. @@ -1427,9 +1381,6 @@ public class AccessPoint implements Comparable<AccessPoint> { if (mProviderFriendlyName != null) { savedState.putString(KEY_PROVIDER_FRIENDLY_NAME, mProviderFriendlyName); } - savedState.putBoolean(KEY_IS_CARRIER_AP, mIsCarrierAp); - savedState.putInt(KEY_CARRIER_AP_EAP_TYPE, mCarrierApEapType); - savedState.putString(KEY_CARRIER_NAME, mCarrierName); savedState.putLong(KEY_SUBSCRIPTION_EXPIRATION_TIME_IN_MILLIS, mSubscriptionExpirationTimeInMillis); savedState.putInt(KEY_PASSPOINT_CONFIGURATION_VERSION, mPasspointConfigurationVersion); diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java b/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java index 17a73acb9bda..4a4f0e66cfe8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java @@ -56,8 +56,6 @@ public class TestAccessPointBuilder { private int mSecurity = AccessPoint.SECURITY_NONE; private WifiConfiguration mWifiConfig; private WifiInfo mWifiInfo; - private boolean mIsCarrierAp = false; - private String mCarrierName = null; Context mContext; private ArrayList<ScanResult> mScanResults; @@ -99,10 +97,6 @@ public class TestAccessPointBuilder { } bundle.putInt(AccessPoint.KEY_SECURITY, mSecurity); bundle.putInt(AccessPoint.KEY_SPEED, mSpeed); - bundle.putBoolean(AccessPoint.KEY_IS_CARRIER_AP, mIsCarrierAp); - if (mCarrierName != null) { - bundle.putString(AccessPoint.KEY_CARRIER_NAME, mCarrierName); - } AccessPoint ap = new AccessPoint(mContext, bundle); ap.setRssi(mRssi); @@ -241,16 +235,6 @@ public class TestAccessPointBuilder { return this; } - public TestAccessPointBuilder setIsCarrierAp(boolean isCarrierAp) { - mIsCarrierAp = isCarrierAp; - return this; - } - - public TestAccessPointBuilder setCarrierName(String carrierName) { - mCarrierName = carrierName; - return this; - } - public TestAccessPointBuilder setScoredNetworkCache( ArrayList<TimestampedScoredNetwork> scoredNetworkCache) { mScoredNetworkCache = scoredNetworkCache; diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java index 3818057e8535..61cbbd3eb0a4 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Resources; @@ -40,7 +41,6 @@ import android.net.WifiKey; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiConfiguration.KeyMgmt; -import android.net.wifi.WifiEnterpriseConfig; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiNetworkScoreCache; @@ -473,51 +473,6 @@ public class AccessPointTest { } @Test - public void testSummaryString_showsAvaiableViaCarrier() { - String carrierName = "Test Carrier"; - ScanResult result = new ScanResult(); - result.BSSID = "00:11:22:33:44:55"; - result.capabilities = "EAP"; - result.isCarrierAp = true; - result.carrierApEapType = WifiEnterpriseConfig.Eap.SIM; - result.carrierName = carrierName; - - AccessPoint ap = new AccessPoint(mContext, Collections.singletonList(result)); - assertThat(ap.getSummary()).isEqualTo(String.format(mContext.getString( - R.string.available_via_carrier), carrierName)); - assertThat(ap.isCarrierAp()).isEqualTo(true); - assertThat(ap.getCarrierApEapType()).isEqualTo(WifiEnterpriseConfig.Eap.SIM); - assertThat(ap.getCarrierName()).isEqualTo(carrierName); - } - - @Test - public void testSummaryString_showsConnectedViaCarrier() { - int networkId = 123; - int rssi = -55; - String carrierName = "Test Carrier"; - WifiConfiguration config = new WifiConfiguration(); - config.networkId = networkId; - WifiInfo wifiInfo = new WifiInfo(); - wifiInfo.setNetworkId(networkId); - wifiInfo.setRssi(rssi); - - NetworkInfo networkInfo = - new NetworkInfo(ConnectivityManager.TYPE_WIFI, 0 /* subtype */, "WIFI", ""); - networkInfo.setDetailedState(NetworkInfo.DetailedState.CONNECTED, "", ""); - - AccessPoint ap = new TestAccessPointBuilder(mContext) - .setNetworkInfo(networkInfo) - .setNetworkId(networkId) - .setRssi(rssi) - .setWifiInfo(wifiInfo) - .setIsCarrierAp(true) - .setCarrierName(carrierName) - .build(); - assertThat(ap.getSummary()).isEqualTo(String.format(mContext.getString( - R.string.connected_via_carrier), carrierName)); - } - - @Test public void testSummaryString_showsDisconnected() { AccessPoint ap = createAccessPointWithScanResultCache(); ap.update(new WifiConfiguration()); @@ -580,31 +535,6 @@ public class AccessPointTest { assertThat(ap.getSummary()).isEqualTo("Connected via Test App"); } - @Test - public void testSetScanResultWithCarrierInfo() { - String ssid = "ssid"; - AccessPoint ap = new TestAccessPointBuilder(mContext).setSsid(ssid).build(); - assertThat(ap.isCarrierAp()).isEqualTo(false); - assertThat(ap.getCarrierApEapType()).isEqualTo(WifiEnterpriseConfig.Eap.NONE); - assertThat(ap.getCarrierName()).isEqualTo(null); - - int carrierApEapType = WifiEnterpriseConfig.Eap.SIM; - String carrierName = "Test Carrier"; - ScanResult scanResult = new ScanResult(); - scanResult.SSID = ssid; - scanResult.BSSID = "00:11:22:33:44:55"; - scanResult.capabilities = ""; - scanResult.isCarrierAp = true; - scanResult.carrierApEapType = carrierApEapType; - scanResult.carrierName = carrierName; - - - ap.setScanResults(Collections.singletonList(scanResult)); - assertThat(ap.isCarrierAp()).isEqualTo(true); - assertThat(ap.getCarrierApEapType()).isEqualTo(carrierApEapType); - assertThat(ap.getCarrierName()).isEqualTo(carrierName); - } - private ScoredNetwork buildScoredNetworkWithMockBadgeCurve() { return buildScoredNetworkWithGivenBadgeCurve(mockBadgeCurve); } @@ -1435,8 +1365,15 @@ public class AccessPointTest { */ @Test public void testOsuAccessPointSummary_showsProvisioningUpdates() { - AccessPoint osuAccessPoint = new AccessPoint(mContext, createOsuProvider(), + OsuProvider provider = createOsuProvider(); + Context spyContext = spy(new ContextWrapper(mContext)); + AccessPoint osuAccessPoint = new AccessPoint(spyContext, provider, mScanResults); + Map<OsuProvider, PasspointConfiguration> osuProviderConfigMap = new HashMap<>(); + osuProviderConfigMap.put(provider, null); + when(spyContext.getSystemService(Context.WIFI_SERVICE)).thenReturn(mMockWifiManager); + when(mMockWifiManager.getMatchingPasspointConfigsForOsuProviders( + Collections.singleton(provider))).thenReturn(osuProviderConfigMap); osuAccessPoint.setListener(mMockAccessPointListener); diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 76e7ab4c321a..22d843b1dc90 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -142,6 +142,8 @@ public class SecureSettings { Settings.Secure.LOCK_SCREEN_WHEN_TRUST_LOST, Settings.Secure.SKIP_DIRECTION, Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, + Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT, + Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT, Settings.Secure.NAVIGATION_MODE, Settings.Secure.SKIP_GESTURE_COUNT, Settings.Secure.SKIP_TOUCH_COUNT, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index ae07598bf7ad..4b10557e34d9 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -216,6 +216,10 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, JSON_OBJECT_VALIDATOR); VALIDATORS.put( Secure.NAVIGATION_MODE, new DiscreteValueValidator(new String[] {"0", "1", "2"})); + VALIDATORS.put(Secure.BACK_GESTURE_INSET_SCALE_LEFT, + new InclusiveFloatRangeValidator(0.0f, Float.MAX_VALUE)); + VALIDATORS.put(Secure.BACK_GESTURE_INSET_SCALE_RIGHT, + new InclusiveFloatRangeValidator(0.0f, Float.MAX_VALUE)); VALIDATORS.put(Secure.AWARE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SKIP_GESTURE_COUNT, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.SKIP_TOUCH_COUNT, NON_NEGATIVE_INTEGER_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 80077c802def..016896f52a75 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -2191,6 +2191,14 @@ class SettingsProtoDumpUtil { Settings.Secure.NAVIGATION_MODE, SecureSettingsProto.NAVIGATION_MODE); + dumpSetting(s, p, + Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT, + SecureSettingsProto.GestureNavigation.BACK_GESTURE_INSET_SCALE_LEFT); + + dumpSetting(s, p, + Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT, + SecureSettingsProto.GestureNavigation.BACK_GESTURE_INSET_SCALE_RIGHT); + final long nfcPaymentToken = p.start(SecureSettingsProto.NFC_PAYMENT); dumpSetting(s, p, Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT, diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 4a10e85206d3..19ff2444e6ca 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -215,7 +215,6 @@ public class SettingsBackupTest { Settings.Global.DEFAULT_DNS_SERVER, Settings.Global.DEFAULT_INSTALL_LOCATION, Settings.Global.DEFAULT_RESTRICT_BACKGROUND_DATA, - Settings.Global.DEFAULT_USER_ID_TO_BOOT_INTO, Settings.Global.DESK_DOCK_SOUND, Settings.Global.DESK_UNDOCK_SOUND, Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, @@ -734,7 +733,8 @@ public class SettingsBackupTest { Settings.Secure.DOZE_WAKE_LOCK_SCREEN_GESTURE, Settings.Secure.DOZE_WAKE_DISPLAY_GESTURE, Settings.Secure.FACE_UNLOCK_RE_ENROLL, - Settings.Secure.TAP_GESTURE); + Settings.Secure.TAP_GESTURE, + Settings.Secure.WINDOW_MAGNIFICATION); @Test public void systemSettingsBackedUpOrBlacklisted() { diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index b89f1412c6a6..51bf441fe119 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -224,6 +224,9 @@ <!-- Permission requried for CTS test - CellBroadcastIntentsTest --> <uses-permission android:name="android.permission.GRANT_RUNTIME_PERMISSIONS_TO_TELEPHONY_DEFAULTS"/> + <!-- Permission required for CTS test - TetheringManagerTest --> + <uses-permission android:name="android.permission.TETHER_PRIVILEGED"/> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" android:defaultToDeviceProtectedStorage="true" diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtoneOverlayService.java b/packages/SoundPicker/src/com/android/soundpicker/RingtoneOverlayService.java index 2d37b4c9c342..15fd1f7f4957 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/RingtoneOverlayService.java +++ b/packages/SoundPicker/src/com/android/soundpicker/RingtoneOverlayService.java @@ -101,7 +101,7 @@ public class RingtoneOverlayService extends Service { } private Uri scanFile(@NonNull final File file) { - return MediaStore.scanFile(this, file); + return MediaStore.scanFile(getContentResolver(), file); } private void set(@NonNull final String name, @NonNull final Uri uri) { diff --git a/packages/SystemUI/res/drawable/ic_control_magnification_grey.xml b/packages/SystemUI/res/drawable/ic_control_magnification_grey.xml new file mode 100644 index 000000000000..80ce8c110041 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_control_magnification_grey.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2019 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. +--> + +<vector android:height="96dp" android:viewportHeight="24" + android:viewportWidth="24" android:width="96dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#757575" android:pathData="M15.54,5.54L13.77,7.3 12,5.54 10.23,7.3 8.46,5.54 12,2zM18.46,15.54l-1.76,-1.77L18.46,12l-1.76,-1.77 1.76,-1.77L22,12zM8.46,18.46l1.77,-1.76L12,18.46l1.77,-1.76 1.77,1.76L12,22zM5.54,8.46l1.76,1.77L5.54,12l1.76,1.77 -1.76,1.77L2,12z"/> + <path android:fillColor="#757575" android:pathData="M12,12m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"/> +</vector> diff --git a/packages/SystemUI/res/layout/magnifier_controllers.xml b/packages/SystemUI/res/layout/magnifier_controllers.xml new file mode 100644 index 000000000000..0203cd4f506b --- /dev/null +++ b/packages/SystemUI/res/layout/magnifier_controllers.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2019 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. + --> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/magnification_controls_size" + android:layout_alignParentBottom="true" + android:layout_alignParentEnd="true" + android:layout_height="@dimen/magnification_controls_size" + android:gravity="center"> + + <RelativeLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <ImageView + android:focusable="true" + android:id="@+id/controller" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/ic_control_magnification_grey" /> + + <View + android:id="@+id/left_control" + android:layout_width="@dimen/magnifier_left_right_controls_width" + android:layout_height="@dimen/magnifier_left_right_controls_height" + android:layout_alignLeft="@+id/controller" + android:layout_centerVertical="true" /> + + <View + android:id="@+id/up_control" + android:layout_width="@dimen/magnifier_up_down_controls_width" + android:layout_height="@dimen/magnifier_up_down_controls_height" + android:layout_alignTop="@+id/controller" + android:layout_centerHorizontal="true" /> + + <View + android:id="@+id/right_control" + android:layout_width="@dimen/magnifier_left_right_controls_width" + android:layout_height="@dimen/magnifier_left_right_controls_height" + android:layout_alignRight="@+id/controller" + android:layout_centerVertical="true" /> + + <View + android:id="@+id/down_control" + android:layout_width="@dimen/magnifier_up_down_controls_width" + android:layout_height="@dimen/magnifier_up_down_controls_height" + android:layout_alignBottom="@+id/controller" + android:layout_centerHorizontal="true" /> + </RelativeLayout> + +</FrameLayout> diff --git a/packages/SystemUI/res/layout/window_magnifier_view.xml b/packages/SystemUI/res/layout/window_magnifier_view.xml new file mode 100644 index 000000000000..f8186129f111 --- /dev/null +++ b/packages/SystemUI/res/layout/window_magnifier_view.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2019 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. + --> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <SurfaceView + android:layout_marginStart="@dimen/magnification_border_size" + android:layout_marginTop="@dimen/magnification_border_size" + android:id="@+id/surface_view" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <View + android:id="@+id/left_handle" + android:layout_width="@dimen/magnification_border_size" + android:layout_height="match_parent" + android:layout_above="@+id/drag_handle" + android:background="@color/magnification_border_color" /> + + <View + android:id="@+id/top_handle" + android:layout_width="match_parent" + android:layout_height="@dimen/magnification_border_size" + android:background="@color/magnification_border_color" /> + + <View + android:id="@+id/right_handle" + android:layout_width="@dimen/magnification_border_size" + android:layout_height="match_parent" + android:layout_above="@+id/drag_handle" + android:layout_alignParentEnd="true" + android:background="@color/magnification_border_color" /> + + <View + android:id="@+id/bottom_handle" + android:layout_width="match_parent" + android:layout_height="@dimen/magnification_border_size" + android:layout_above="@+id/drag_handle" + android:background="@color/magnification_border_color" /> + + <View + android:id="@+id/drag_handle" + android:layout_width="@dimen/magnification_drag_view_width" + android:layout_height="@dimen/magnification_drag_view_height" + android:layout_alignParentBottom="true" + android:layout_centerHorizontal="true" + android:background="@color/magnification_border_color" /> + + </RelativeLayout> +</FrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 92c74770a3c4..c1424657cf4f 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -211,4 +211,5 @@ <color name="GM2_green_500">#FF34A853</color> <color name="GM2_blue_500">#FF4285F4</color> + <color name="magnification_border_color">#FF9900</color> </resources> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index e896c162af7d..640f31bc9fe8 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -295,6 +295,7 @@ <item>com.android.systemui.SizeCompatModeActivityController</item> <item>com.android.systemui.statusbar.notification.InstantAppNotifier</item> <item>com.android.systemui.theme.ThemeOverlayController</item> + <item>com.android.systemui.accessibility.WindowMagnification</item> </string-array> <!-- SystemUI vender service, used in config_systemUIServiceComponents. --> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index c9481164c202..da0323ac7357 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1139,4 +1139,16 @@ <dimen name="qs_media_width">350dp</dimen> <dimen name="qs_media_padding">8dp</dimen> <dimen name="qs_media_corner_radius">10dp</dimen> + + <dimen name="magnification_border_size">5dp</dimen> + <dimen name="magnification_frame_move_short">5dp</dimen> + <dimen name="magnification_frame_move_long">25dp</dimen> + <dimen name="magnification_drag_view_width">100dp</dimen> + <dimen name="magnification_drag_view_height">35dp</dimen> + <dimen name="magnification_controls_size">90dp</dimen> + <dimen name="magnifier_left_right_controls_width">35dp</dimen> + <dimen name="magnifier_left_right_controls_height">45dp</dimen> + <dimen name="magnifier_up_down_controls_width">45dp</dimen> + <dimen name="magnifier_up_down_controls_height">40dp</dimen> + </resources> diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml index deae7e2c181c..c1cf7b420078 100644 --- a/packages/SystemUI/res/values/integers.xml +++ b/packages/SystemUI/res/values/integers.xml @@ -32,4 +32,6 @@ <!-- Ratio of "right" end of status bar that will swipe to QS. --> <integer name="qs_split_fraction">2</integer> + <integer name="magnification_default_scale">2</integer> + </resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 1053750a5538..45318fde4f0e 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -914,8 +914,8 @@ <string name="quick_settings_secondary_label_until">Until <xliff:g id="time" example="7 am">%s</xliff:g></string> <!-- QuickSettings: Label for the toggle to activate dark theme (A.K.A Dark Mode). [CHAR LIMIT=20] --> <string name="quick_settings_ui_mode_night_label">Dark theme</string> - <!-- QuickSettings: Label for the dark theme tile when enabled by battery saver. [CHAR LIMIT=40] --> - <string name="quick_settings_ui_mode_night_label_battery_saver">Dark theme\nBattery saver</string> + <!-- QuickSettings: Secondary text for the dark theme tile when enabled by battery saver. [CHAR LIMIT=20] --> + <string name="quick_settings_dark_mode_secondary_label_battery_saver">Battery Saver</string> <!-- QuickSettings: Secondary text for when the Dark Mode will be enabled at sunset. [CHAR LIMIT=20] --> <string name="quick_settings_dark_mode_secondary_label_on_at_sunset">On at sunset</string> <!-- QuickSettings: Secondary text for when the Dark Mode will be on until sunrise. [CHAR LIMIT=20] --> @@ -2492,4 +2492,12 @@ <!-- Title of the overlay warning the user to interact with the device or it will go to sleep. [CHAR LIMIT=25] --> <string name="inattentive_sleep_warning_title">Standby</string> + + <!-- Window Magnification strings --> + <!-- Title for Magnification Overlay Window [CHAR LIMIT=NONE] --> + <string name="magnification_overlay_title">Magnification Overlay Window</string> + <!-- Title for Magnification Window [CHAR LIMIT=NONE] --> + <string name="magnification_window_title">Magnification Window</string> + <!-- Title for Magnification Controls Window [CHAR LIMIT=NONE] --> + <string name="magnification_controls_title">Magnification Window Controls</string> </resources> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/WindowManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/WindowManagerWrapper.java index 22d1675cf17e..9f13718ec236 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/WindowManagerWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/WindowManagerWrapper.java @@ -27,6 +27,7 @@ import android.graphics.Rect; import android.os.Handler; import android.os.RemoteException; import android.util.Log; +import android.view.SurfaceControl; import android.view.WindowManager; import android.view.WindowManagerGlobal; @@ -215,4 +216,23 @@ public class WindowManagerWrapper { public void removePinnedStackListener(PinnedStackListener listener) { mPinnedStackListenerForwarder.removeListener(listener); } + + /** + * Mirrors a specified display. The SurfaceControl returned is the root of the mirrored + * hierarchy. + * + * @param displayId The id of the display to mirror + * @return The SurfaceControl for the root of the mirrored hierarchy. + */ + public SurfaceControl mirrorDisplay(final int displayId) { + try { + SurfaceControl outSurfaceControl = new SurfaceControl(); + WindowManagerGlobal.getWindowManagerService().mirrorDisplay(displayId, + outSurfaceControl); + return outSurfaceControl; + } catch (RemoteException e) { + Log.e(TAG, "Unable to reach window manager", e); + } + return null; + } } diff --git a/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java b/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java index 5d084e7f3272..c2cedad0dc48 100644 --- a/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java +++ b/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java @@ -30,6 +30,7 @@ import android.os.UserHandle; import android.telecom.TelecomManager; import android.telephony.TelephonyManager; import android.util.AttributeSet; +import android.util.Log; import android.util.Slog; import android.view.MotionEvent; import android.view.View; @@ -114,11 +115,7 @@ public class EmergencyButton extends Button { super.onFinishInflate(); mLockPatternUtils = new LockPatternUtils(mContext); mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); - setOnClickListener(new OnClickListener() { - public void onClick(View v) { - takeEmergencyCallAction(); - } - }); + setOnClickListener(v -> takeEmergencyCallAction()); setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { @@ -168,9 +165,9 @@ public class EmergencyButton extends Button { */ public void takeEmergencyCallAction() { MetricsLogger.action(mContext, MetricsEvent.ACTION_EMERGENCY_CALL); - // TODO: implement a shorter timeout once new PowerManager API is ready. - // should be the equivalent to the old userActivity(EMERGENCY_CALL_TIMEOUT) - mPowerManager.userActivity(SystemClock.uptimeMillis(), true); + if (mPowerManager != null) { + mPowerManager.userActivity(SystemClock.uptimeMillis(), true); + } try { ActivityTaskManager.getService().stopSystemLockTaskMode(); } catch (RemoteException e) { @@ -182,10 +179,19 @@ public class EmergencyButton extends Button { mEmergencyButtonCallback.onEmergencyButtonClickedWhenInCall(); } } else { - Dependency.get(KeyguardUpdateMonitor.class).reportEmergencyCallAction( - true /* bypassHandler */); + KeyguardUpdateMonitor updateMonitor = Dependency.get(KeyguardUpdateMonitor.class); + if (updateMonitor != null) { + updateMonitor.reportEmergencyCallAction(true /* bypassHandler */); + } else { + Log.w(LOG_TAG, "KeyguardUpdateMonitor was null, launching intent anyway."); + } + TelecomManager telecomManager = getTelecommManager(); + if (telecomManager == null) { + Log.wtf(LOG_TAG, "TelecomManager was null, cannot launch emergency dialer"); + return; + } Intent emergencyDialIntent = - getTelecommManager().createLaunchEmergencyDialerIntent(null /* number*/) + telecomManager.createLaunchEmergencyDialerIntent(null /* number*/) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS | Intent.FLAG_ACTIVITY_CLEAR_TOP) diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java new file mode 100644 index 000000000000..6178ff2ab2cb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility; + +import android.content.Context; +import android.database.ContentObserver; +import android.os.Handler; +import android.provider.Settings; + +import com.android.systemui.SystemUI; +import com.android.systemui.dagger.qualifiers.MainHandler; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Class to handle changes to setting window_magnification value. + */ +@Singleton +public class WindowMagnification extends SystemUI { + private WindowMagnificationController mWindowMagnificationController; + private final Handler mHandler; + + @Inject + public WindowMagnification(Context context, @MainHandler Handler mainHandler) { + super(context); + mHandler = mainHandler; + } + + @Override + public void start() { + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.WINDOW_MAGNIFICATION), + true, new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + updateWindowMagnification(); + } + }); + } + + private void updateWindowMagnification() { + try { + boolean enable = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.WINDOW_MAGNIFICATION) != 0; + if (enable) { + enableMagnification(); + } else { + disableMagnification(); + } + } catch (Settings.SettingNotFoundException e) { + disableMagnification(); + } + } + + private void enableMagnification() { + if (mWindowMagnificationController == null) { + mWindowMagnificationController = new WindowMagnificationController(mContext, mHandler); + } + mWindowMagnificationController.createWindowMagnification(); + } + + private void disableMagnification() { + if (mWindowMagnificationController != null) { + mWindowMagnificationController.deleteWindowMagnification(); + } + mWindowMagnificationController = null; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java new file mode 100644 index 000000000000..e3694ac532fe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Binder; +import android.os.Handler; +import android.view.Display; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.WindowManager; + +import com.android.systemui.R; +import com.android.systemui.shared.system.WindowManagerWrapper; + +/** + * Class to handle adding and removing a window magnification. + */ +public class WindowMagnificationController implements View.OnClickListener, + View.OnLongClickListener, View.OnTouchListener, SurfaceHolder.Callback { + private final int mBorderSize; + private final int mMoveFrameAmountShort; + private final int mMoveFrameAmountLong; + + private final Context mContext; + private final Point mDisplaySize = new Point(); + private final int mDisplayId; + private final Handler mHandler; + private final Rect mMagnificationFrame = new Rect(); + private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); + + private final WindowManager mWm; + + private float mScale; + + private final Rect mTmpRect = new Rect(); + + // The root of the mirrored content + private SurfaceControl mMirrorSurface; + + private boolean mIsPressedDown; + + private View mLeftControl; + private View mUpControl; + private View mRightControl; + private View mBottomControl; + + private View mDragView; + private View mLeftDrag; + private View mTopDrag; + private View mRightDrag; + private View mBottomDrag; + + private final PointF mLastDrag = new PointF(); + private final Point mMoveWindowOffset = new Point(); + + private View mMirrorView; + private SurfaceView mMirrorSurfaceView; + private View mControlsView; + private View mOverlayView; + + private MoveMirrorRunnable mMoveMirrorRunnable = new MoveMirrorRunnable(); + + WindowMagnificationController(Context context, Handler handler) { + mContext = context; + mHandler = handler; + Display display = mContext.getDisplay(); + display.getSize(mDisplaySize); + mDisplayId = mContext.getDisplayId(); + + mWm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + + Resources r = context.getResources(); + mBorderSize = (int) r.getDimension(R.dimen.magnification_border_size); + mMoveFrameAmountShort = (int) r.getDimension(R.dimen.magnification_frame_move_short); + mMoveFrameAmountLong = (int) r.getDimension(R.dimen.magnification_frame_move_long); + + mScale = r.getInteger(R.integer.magnification_default_scale); + } + + /** + * Creates a magnification window if it doesn't already exist. + */ + void createWindowMagnification() { + if (mMirrorView != null) { + return; + } + createOverlayWindow(); + } + + private void createOverlayWindow() { + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSPARENT); + params.gravity = Gravity.TOP | Gravity.LEFT; + params.token = new Binder(); + params.setTitle(mContext.getString(R.string.magnification_overlay_title)); + + mOverlayView = new View(mContext); + mOverlayView.getViewTreeObserver().addOnWindowAttachListener( + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + mOverlayView.getViewTreeObserver().removeOnWindowAttachListener(this); + createMirrorWindow(); + createControls(); + } + + @Override + public void onWindowDetached() { + + } + }); + + mOverlayView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + + mWm.addView(mOverlayView, params); + } + + /** + * Deletes the magnification window. + */ + void deleteWindowMagnification() { + if (mMirrorSurface != null) { + mTransaction.remove(mMirrorSurface).apply(); + mMirrorSurface = null; + } + + if (mOverlayView != null) { + mWm.removeView(mOverlayView); + mOverlayView = null; + } + + if (mMirrorView != null) { + mWm.removeView(mMirrorView); + mMirrorView = null; + } + + if (mControlsView != null) { + mWm.removeView(mControlsView); + mControlsView = null; + } + } + + private void createMirrorWindow() { + setInitialStartBounds(); + + // The window should be the size the mirrored surface will be but also add room for the + // border and the drag handle. + int dragViewHeight = (int) mContext.getResources().getDimension( + R.dimen.magnification_drag_view_height); + int windowWidth = mMagnificationFrame.width() + 2 * mBorderSize; + int windowHeight = mMagnificationFrame.height() + dragViewHeight + 2 * mBorderSize; + + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + windowWidth, windowHeight, + WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSPARENT); + params.gravity = Gravity.TOP | Gravity.LEFT; + params.token = mOverlayView.getWindowToken(); + params.x = mMagnificationFrame.left; + params.y = mMagnificationFrame.top; + params.setTitle(mContext.getString(R.string.magnification_window_title)); + + mMirrorView = LayoutInflater.from(mContext).inflate(R.layout.window_magnifier_view, null); + mMirrorSurfaceView = mMirrorView.findViewById(R.id.surface_view); + // This places the SurfaceView's SurfaceControl above the ViewRootImpl's SurfaceControl to + // ensure the mirrored area can get touch instead of going to the window + mMirrorSurfaceView.setZOrderOnTop(true); + + mMirrorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + mWm.addView(mMirrorView, params); + + SurfaceHolder holder = mMirrorSurfaceView.getHolder(); + holder.addCallback(this); + holder.setFormat(PixelFormat.RGBA_8888); + + addDragTouchListeners(); + } + + private void createControls() { + int controlsSize = (int) mContext.getResources().getDimension( + R.dimen.magnification_controls_size); + + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(controlsSize, controlsSize, + WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL, + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.RGBA_8888); + lp.gravity = Gravity.BOTTOM | Gravity.RIGHT; + lp.token = mOverlayView.getWindowToken(); + lp.setTitle(mContext.getString(R.string.magnification_controls_title)); + + mControlsView = LayoutInflater.from(mContext).inflate(R.layout.magnifier_controllers, null); + mWm.addView(mControlsView, lp); + + mLeftControl = mControlsView.findViewById(R.id.left_control); + mUpControl = mControlsView.findViewById(R.id.up_control); + mRightControl = mControlsView.findViewById(R.id.right_control); + mBottomControl = mControlsView.findViewById(R.id.down_control); + + mLeftControl.setOnClickListener(this); + mUpControl.setOnClickListener(this); + mRightControl.setOnClickListener(this); + mBottomControl.setOnClickListener(this); + + mLeftControl.setOnLongClickListener(this); + mUpControl.setOnLongClickListener(this); + mRightControl.setOnLongClickListener(this); + mBottomControl.setOnLongClickListener(this); + + mLeftControl.setOnTouchListener(this); + mUpControl.setOnTouchListener(this); + mRightControl.setOnTouchListener(this); + mBottomControl.setOnTouchListener(this); + } + + private void setInitialStartBounds() { + // Sets the initial frame area for the mirror and places it in the center of the display. + int initSize = Math.min(mDisplaySize.x, mDisplaySize.y) / 2; + int initX = mDisplaySize.x / 2 - initSize / 2; + int initY = mDisplaySize.y / 2 - initSize / 2; + mMagnificationFrame.set(initX, initY, initX + initSize, initY + initSize); + } + + /** + * This is called once the surfaceView is created so the mirrored content can be placed as a + * child of the surfaceView. + */ + private void createMirror() { + mMirrorSurface = WindowManagerWrapper.getInstance().mirrorDisplay(mDisplayId); + if (!mMirrorSurface.isValid()) { + return; + } + mTransaction.show(mMirrorSurface) + .reparent(mMirrorSurface, mMirrorSurfaceView.getSurfaceControl()); + + modifyWindowMagnification(mTransaction); + mTransaction.apply(); + } + + private void addDragTouchListeners() { + mDragView = mMirrorView.findViewById(R.id.drag_handle); + mLeftDrag = mMirrorView.findViewById(R.id.left_handle); + mTopDrag = mMirrorView.findViewById(R.id.top_handle); + mRightDrag = mMirrorView.findViewById(R.id.right_handle); + mBottomDrag = mMirrorView.findViewById(R.id.bottom_handle); + + mDragView.setOnTouchListener(this); + mLeftDrag.setOnTouchListener(this); + mTopDrag.setOnTouchListener(this); + mRightDrag.setOnTouchListener(this); + mBottomDrag.setOnTouchListener(this); + } + + /** + * Modifies the placement of the mirrored content. + */ + private void modifyWindowMagnification(SurfaceControl.Transaction t) { + Rect sourceBounds = getSourceBounds(mMagnificationFrame, mScale); + // The final destination for the magnification surface should be at 0,0 since the + // ViewRootImpl's position will change + mTmpRect.set(0, 0, mMagnificationFrame.width(), mMagnificationFrame.height()); + + WindowManager.LayoutParams params = + (WindowManager.LayoutParams) mMirrorView.getLayoutParams(); + params.x = mMagnificationFrame.left; + params.y = mMagnificationFrame.top; + mWm.updateViewLayout(mMirrorView, params); + + t.setGeometry(mMirrorSurface, sourceBounds, mTmpRect, Surface.ROTATION_0); + } + + @Override + public void onClick(View v) { + setMoveOffset(v, mMoveFrameAmountShort); + moveMirrorFromControls(); + } + + @Override + public boolean onLongClick(View v) { + mIsPressedDown = true; + setMoveOffset(v, mMoveFrameAmountLong); + mHandler.post(mMoveMirrorRunnable); + return true; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (v == mLeftControl || v == mUpControl || v == mRightControl || v == mBottomControl) { + return handleControlTouchEvent(event); + } else if (v == mDragView || v == mLeftDrag || v == mTopDrag || v == mRightDrag + || v == mBottomDrag) { + return handleDragTouchEvent(event); + } + return false; + } + + private boolean handleControlTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mIsPressedDown = false; + break; + } + return false; + } + + private boolean handleDragTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mLastDrag.set(event.getRawX(), event.getRawY()); + return true; + case MotionEvent.ACTION_MOVE: + int xDiff = (int) (event.getRawX() - mLastDrag.x); + int yDiff = (int) (event.getRawY() - mLastDrag.y); + mMagnificationFrame.offset(xDiff, yDiff); + mLastDrag.set(event.getRawX(), event.getRawY()); + modifyWindowMagnification(mTransaction); + mTransaction.apply(); + return true; + } + return false; + } + + private void setMoveOffset(View v, int moveFrameAmount) { + mMoveWindowOffset.set(0, 0); + + if (v == mLeftControl) { + mMoveWindowOffset.x = -moveFrameAmount; + } else if (v == mUpControl) { + mMoveWindowOffset.y = -moveFrameAmount; + } else if (v == mRightControl) { + mMoveWindowOffset.x = moveFrameAmount; + } else if (v == mBottomControl) { + mMoveWindowOffset.y = moveFrameAmount; + } + } + + private void moveMirrorFromControls() { + mMagnificationFrame.offset(mMoveWindowOffset.x, mMoveWindowOffset.y); + + modifyWindowMagnification(mTransaction); + mTransaction.apply(); + } + + /** + * Calculates the desired source bounds. This will be the area under from the center of the + * displayFrame, factoring in scale. + */ + private Rect getSourceBounds(Rect displayFrame, float scale) { + int halfWidth = displayFrame.width() / 2; + int halfHeight = displayFrame.height() / 2; + int left = displayFrame.left + (halfWidth - (int) (halfWidth / scale)); + int right = displayFrame.right - (halfWidth - (int) (halfWidth / scale)); + int top = displayFrame.top + (halfHeight - (int) (halfHeight / scale)); + int bottom = displayFrame.bottom - (halfHeight - (int) (halfHeight / scale)); + return new Rect(left, top, right, bottom); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + createMirror(); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + } + + class MoveMirrorRunnable implements Runnable { + @Override + public void run() { + if (mIsPressedDown) { + moveMirrorFromControls(); + mHandler.postDelayed(mMoveMirrorRunnable, 100); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/OWNERS b/packages/SystemUI/src/com/android/systemui/biometrics/OWNERS new file mode 100644 index 000000000000..8765c9a64b77 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/OWNERS @@ -0,0 +1,7 @@ +set noparent + +kchyn@google.com +jaggies@google.com +curtislb@google.com +ilyamaty@google.com +joshmccloskey@google.com diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index 1277736a798b..c82bc30338a7 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java @@ -103,7 +103,6 @@ import java.lang.annotation.Target; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.function.Consumer; import javax.inject.Inject; @@ -735,7 +734,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @SuppressWarnings("FieldCanBeLocal") private final NotificationEntryListener mEntryListener = new NotificationEntryListener() { @Override - public void onPendingEntryAdded(NotificationEntry entry) { + public void onNotificationAdded(NotificationEntry entry) { boolean previouslyUserCreated = mUserCreatedBubbles.contains(entry.getKey()); boolean userBlocked = mUserBlockedBubbles.contains(entry.getKey()); boolean wasAdjusted = BubbleExperimentConfig.adjustForExperiments( diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java index 21471ec8182b..4c1cf49ecf9e 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java @@ -32,6 +32,9 @@ import android.content.Context; import android.content.Intent; import android.content.pm.LauncherApps; import android.content.pm.ShortcutInfo; +import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Bundle; import android.os.Parcelable; @@ -39,8 +42,11 @@ import android.os.UserHandle; import android.provider.Settings; import android.util.Log; +import com.android.internal.graphics.ColorUtils; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.ContrastColorUtil; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.people.PeopleHubNotificationListenerKt; import java.util.ArrayList; import java.util.Arrays; @@ -227,15 +233,29 @@ public class BubbleExperimentConfig { List<Person> personList = getPeopleFromNotification(entry); if (personList.size() > 0) { final Person person = personList.get(0); - if (person != null) { icon = person.getIcon(); + if (icon == null) { + // Lets try and grab the icon constructed by the layout + Drawable d = PeopleHubNotificationListenerKt.extractAvatarFromRow(entry); + if (d instanceof BitmapDrawable) { + icon = Icon.createWithBitmap(((BitmapDrawable) d).getBitmap()); + } + } } } if (icon == null) { - icon = notification.getLargeIcon() != null - ? notification.getLargeIcon() - : notification.getSmallIcon(); + boolean shouldTint = notification.getLargeIcon() == null; + icon = shouldTint + ? notification.getSmallIcon() + : notification.getLargeIcon(); + if (shouldTint) { + int notifColor = entry.getSbn().getNotification().color; + notifColor = ColorUtils.setAlphaComponent(notifColor, 255); + notifColor = ContrastColorUtil.findContrastColor(notifColor, Color.WHITE, + true /* findFg */, 3f); + icon.setTint(notifColor); + } } if (intent != null) { return new Notification.BubbleMetadata.Builder() @@ -285,7 +305,7 @@ public class BubbleExperimentConfig { } static boolean isShortcutIntent(PendingIntent intent) { - return intent.equals(sDummyShortcutIntent); + return intent != null && intent.equals(sDummyShortcutIntent); } static List<Person> getPeopleFromNotification(NotificationEntry entry) { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java index f032a33c45a9..534f350e4898 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java @@ -23,6 +23,7 @@ import android.app.AlarmManager; import android.app.IActivityManager; import android.app.IWallpaperManager; import android.app.KeyguardManager; +import android.app.NotificationManager; import android.app.WallpaperManager; import android.app.admin.DevicePolicyManager; import android.content.Context; @@ -131,6 +132,12 @@ public class SystemServicesModule { @Singleton @Provides + static NotificationManager provideNotificationManager(Context context) { + return context.getSystemService(NotificationManager.class); + } + + @Singleton + @Provides static PackageManagerWrapper providePackageManagerWrapper() { return PackageManagerWrapper.getInstance(); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java index 3cf14d65e5b8..99dd5e2356d6 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java @@ -21,6 +21,7 @@ import com.android.systemui.ScreenDecorations; import com.android.systemui.SizeCompatModeActivityController; import com.android.systemui.SliceBroadcastRelayHandler; import com.android.systemui.SystemUI; +import com.android.systemui.accessibility.WindowMagnification; import com.android.systemui.biometrics.AuthController; import com.android.systemui.globalactions.GlobalActionsComponent; import com.android.systemui.keyguard.KeyguardViewMediator; @@ -156,4 +157,10 @@ public abstract class SystemUIBinder { @IntoMap @ClassKey(VolumeUI.class) public abstract SystemUI bindVolumeUI(VolumeUI sysui); + + /** Inject into WindowMagnification. */ + @Binds + @IntoMap + @ClassKey(WindowMagnification.class) + public abstract SystemUI bindWindowMagnification(WindowMagnification sysui); } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java index 05a234f1e431..d008e665d171 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java @@ -80,8 +80,7 @@ public class DozeSensors { public DozeSensors(Context context, AlarmManager alarmManager, AsyncSensorManager sensorManager, DozeParameters dozeParameters, AmbientDisplayConfiguration config, WakeLock wakeLock, - Callback callback, Consumer<Boolean> proxCallback, AlwaysOnDisplayPolicy policy, - DozeLog dozeLog) { + Callback callback, Consumer<Boolean> proxCallback, DozeLog dozeLog) { mContext = context; mAlarmManager = alarmManager; mSensorManager = sensorManager; @@ -154,7 +153,7 @@ public class DozeSensors { }; mProximitySensor = new ProximitySensor(context.getResources(), sensorManager); - + setProxListening(false); // Don't immediately start listening when we register. mProximitySensor.register( proximityEvent -> { if (proximityEvent != null) { diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java index 2d6b946fc67e..722dc038f853 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java @@ -103,8 +103,7 @@ public class DozeTriggers implements DozeMachine.Part { mWakeLock = wakeLock; mAllowPulseTriggers = allowPulseTriggers; mDozeSensors = new DozeSensors(context, alarmManager, mSensorManager, dozeParameters, - config, wakeLock, this::onSensor, this::onProximityFar, - dozeParameters.getPolicy(), dozeLog); + config, wakeLock, this::onSensor, this::onProximityFar, dozeLog); mUiModeManager = mContext.getSystemService(UiModeManager.class); mDockManager = dockManager; mProxCheck = new ProximitySensor.ProximityCheck(proximitySensor, handler); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java index d79e38332af5..7bc2a0d5c97d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java @@ -92,7 +92,10 @@ public class UiModeNightTile extends QSTileImpl<QSTile.BooleanState> implements boolean nightMode = (mContext.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; - if (isAuto && !powerSave) { + if (powerSave) { + state.secondaryLabel = mContext.getResources().getString( + R.string.quick_settings_dark_mode_secondary_label_battery_saver); + } else if (isAuto) { state.secondaryLabel = mContext.getResources().getString(nightMode ? R.string.quick_settings_dark_mode_secondary_label_until_sunrise : R.string.quick_settings_dark_mode_secondary_label_on_at_sunset); @@ -100,9 +103,7 @@ public class UiModeNightTile extends QSTileImpl<QSTile.BooleanState> implements state.secondaryLabel = null; } state.value = nightMode; - state.label = mContext.getString(powerSave - ? R.string.quick_settings_ui_mode_night_label_battery_saver - : R.string.quick_settings_ui_mode_night_label); + state.label = mContext.getString(R.string.quick_settings_ui_mode_night_label); state.icon = mIcon; state.contentDescription = TextUtils.isEmpty(state.secondaryLabel) ? state.label diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index 0383dee4f9c3..d3ccbeb0afb1 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -371,7 +371,8 @@ public class RecordingService extends Service { Bitmap thumbnailBitmap = null; try { ContentResolver resolver = getContentResolver(); - Size size = Point.convert(MediaStore.ThumbnailConstants.MINI_SIZE); + DisplayMetrics metrics = getResources().getDisplayMetrics(); + Size size = new Size(metrics.widthPixels, metrics.heightPixels / 2); thumbnailBitmap = resolver.loadThumbnail(uri, size, null); } catch (IOException e) { Log.e(TAG, "Error creating thumbnail: " + e.getMessage()); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index 76925b43cfb8..2f401e5271d8 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -23,6 +23,8 @@ import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipDescription; import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.UserInfo; @@ -48,7 +50,9 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.DeviceConfig; import android.provider.MediaStore; +import android.provider.MediaStore.MediaColumns; import android.text.TextUtils; +import android.text.format.DateUtils; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; @@ -275,6 +279,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND); Context context = mParams.context; + ContentResolver resolver = context.getContentResolver(); Bitmap image = mParams.image; Resources r = context.getResources(); @@ -284,23 +289,27 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { mSmartActionsProvider, mSmartActionsEnabled, isManagedProfile(context)); // Save the screenshot to the MediaStore - final MediaStore.PendingParams params = new MediaStore.PendingParams( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mImageFileName, "image/png"); - params.setRelativePath(Environment.DIRECTORY_PICTURES + File.separator - + Environment.DIRECTORY_SCREENSHOTS); - - final Uri uri = MediaStore.createPending(context, params); - final MediaStore.PendingSession session = MediaStore.openPending(context, uri); + final ContentValues values = new ContentValues(); + values.put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + + File.separator + Environment.DIRECTORY_SCREENSHOTS); + values.put(MediaColumns.DISPLAY_NAME, mImageFileName); + values.put(MediaColumns.MIME_TYPE, "image/png"); + values.put(MediaColumns.DATE_ADDED, mImageTime / 1000); + values.put(MediaColumns.DATE_MODIFIED, mImageTime / 1000); + values.put(MediaColumns.DATE_EXPIRES, (mImageTime + DateUtils.DAY_IN_MILLIS) / 1000); + values.put(MediaColumns.IS_PENDING, 1); + + final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); try { // First, write the actual data for our screenshot - try (OutputStream out = session.openOutputStream()) { + try (OutputStream out = resolver.openOutputStream(uri)) { if (!image.compress(Bitmap.CompressFormat.PNG, 100, out)) { throw new IOException("Failed to compress"); } } // Next, write metadata to help index the screenshot - try (ParcelFileDescriptor pfd = session.open()) { + try (ParcelFileDescriptor pfd = resolver.openFile(uri, "rw", null)) { final ExifInterface exif = new ExifInterface(pfd.getFileDescriptor()); exif.setAttribute(ExifInterface.TAG_SOFTWARE, @@ -327,12 +336,15 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { exif.saveAttributes(); } - session.publish(); + + // Everything went well above, publish it! + values.clear(); + values.put(MediaColumns.IS_PENDING, 0); + values.putNull(MediaColumns.DATE_EXPIRES); + resolver.update(uri, values, null, null); } catch (Exception e) { - session.abandon(); + resolver.delete(uri, null); throw e; - } finally { - IoUtils.closeQuietly(session); } populateNotificationActions(context, r, uri, smartActionsFuture, mNotificationBuilder); diff --git a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserTracker.java b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserTracker.java index 9f79785be56e..077d2602e43f 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserTracker.java +++ b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserTracker.java @@ -21,6 +21,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.UserHandle; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -97,7 +98,7 @@ public abstract class CurrentUserTracker { if (!mReceiverRegistered) { mCurrentUserId = ActivityManager.getCurrentUser(); IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED); - mBroadcastDispatcher.registerReceiver(this, filter); + mBroadcastDispatcher.registerReceiver(this, filter, null, UserHandle.ALL); mReceiverRegistered = true; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java index 9a64b30ed918..97dd3daae341 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java @@ -19,9 +19,7 @@ package com.android.systemui.statusbar; import static com.android.systemui.statusbar.RemoteInputController.processForRemoteInput; import static com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON; import static com.android.systemui.statusbar.phone.StatusBar.DEBUG; -import static com.android.systemui.statusbar.phone.StatusBar.ENABLE_CHILD_NOTIFICATIONS; -import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.NotificationManager; import android.content.ComponentName; @@ -33,8 +31,6 @@ import android.service.notification.StatusBarNotification; import android.util.Log; import com.android.systemui.dagger.qualifiers.MainHandler; -import com.android.systemui.statusbar.notification.NotificationEntryManager; -import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins; import java.util.ArrayList; @@ -52,31 +48,33 @@ import javax.inject.Singleton; public class NotificationListener extends NotificationListenerWithPlugins { private static final String TAG = "NotificationListener"; - // Dependencies: - private final NotificationEntryManager mEntryManager; - private final NotificationGroupManager mGroupManager; - private final Context mContext; + private final NotificationManager mNotificationManager; private final Handler mMainHandler; + private final List<NotifServiceListener> mNotificationListeners = new ArrayList<>(); private final ArrayList<NotificationSettingsListener> mSettingsListeners = new ArrayList<>(); - @Nullable private NotifServiceListener mDownstreamListener; @Inject - public NotificationListener(Context context, @MainHandler Handler mainHandler, - NotificationEntryManager notificationEntryManager, - NotificationGroupManager notificationGroupManager) { + public NotificationListener( + Context context, + NotificationManager notificationManager, + @MainHandler Handler mainHandler) { mContext = context; + mNotificationManager = notificationManager; mMainHandler = mainHandler; - mEntryManager = notificationEntryManager; - mGroupManager = notificationGroupManager; } - public void addNotificationSettingsListener(NotificationSettingsListener listener) { - mSettingsListeners.add(listener); + /** Registers a listener that's notified when notifications are added/removed/etc. */ + public void addNotificationListener(NotifServiceListener listener) { + if (mNotificationListeners.contains(listener)) { + throw new IllegalArgumentException("Listener is already added"); + } + mNotificationListeners.add(listener); } - public void setDownstreamListener(NotifServiceListener downstreamListener) { - mDownstreamListener = downstreamListener; + /** Registers a listener that's notified when any notification-related settings change. */ + public void addNotificationSettingsListener(NotificationSettingsListener listener) { + mSettingsListeners.add(listener); } @Override @@ -102,14 +100,13 @@ public class NotificationListener extends NotificationListenerWithPlugins { final RankingMap completeMap = new RankingMap(newRankings.toArray(new Ranking[0])); for (StatusBarNotification sbn : notifications) { - if (mDownstreamListener != null) { - mDownstreamListener.onNotificationPosted(sbn, completeMap); + for (NotifServiceListener listener : mNotificationListeners) { + listener.onNotificationPosted(sbn, completeMap); } - mEntryManager.addNotification(sbn, completeMap); } }); - NotificationManager noMan = mContext.getSystemService(NotificationManager.class); - onSilentStatusBarIconsVisibilityChanged(noMan.shouldHideSilentStatusBarIcons()); + onSilentStatusBarIconsVisibilityChanged( + mNotificationManager.shouldHideSilentStatusBarIcons()); } @Override @@ -120,34 +117,8 @@ public class NotificationListener extends NotificationListenerWithPlugins { mMainHandler.post(() -> { processForRemoteInput(sbn.getNotification(), mContext); - if (mDownstreamListener != null) { - mDownstreamListener.onNotificationPosted(sbn, rankingMap); - } - - String key = sbn.getKey(); - boolean isUpdate = mEntryManager.getActiveNotificationUnfiltered(key) != null; - // In case we don't allow child notifications, we ignore children of - // notifications that have a summary, since` we're not going to show them - // anyway. This is true also when the summary is canceled, - // because children are automatically canceled by NoMan in that case. - if (!ENABLE_CHILD_NOTIFICATIONS - && mGroupManager.isChildInGroupWithSummary(sbn)) { - if (DEBUG) { - Log.d(TAG, "Ignoring group child due to existing summary: " + sbn); - } - - // Remove existing notification to avoid stale data. - if (isUpdate) { - mEntryManager.removeNotification(key, rankingMap, UNDEFINED_DISMISS_REASON); - } else { - mEntryManager.updateRanking(rankingMap, "onNotificationPosted"); - } - return; - } - if (isUpdate) { - mEntryManager.updateNotification(sbn, rankingMap); - } else { - mEntryManager.addNotification(sbn, rankingMap); + for (NotifServiceListener listener : mNotificationListeners) { + listener.onNotificationPosted(sbn, rankingMap); } }); } @@ -158,12 +129,10 @@ public class NotificationListener extends NotificationListenerWithPlugins { int reason) { if (DEBUG) Log.d(TAG, "onNotificationRemoved: " + sbn + " reason: " + reason); if (sbn != null && !onPluginNotificationRemoved(sbn, rankingMap)) { - final String key = sbn.getKey(); mMainHandler.post(() -> { - if (mDownstreamListener != null) { - mDownstreamListener.onNotificationRemoved(sbn, rankingMap, reason); + for (NotifServiceListener listener : mNotificationListeners) { + listener.onNotificationRemoved(sbn, rankingMap, reason); } - mEntryManager.removeNotification(key, rankingMap, reason); }); } } @@ -179,10 +148,9 @@ public class NotificationListener extends NotificationListenerWithPlugins { if (rankingMap != null) { RankingMap r = onPluginRankingUpdate(rankingMap); mMainHandler.post(() -> { - if (mDownstreamListener != null) { - mDownstreamListener.onNotificationRankingUpdate(rankingMap); + for (NotifServiceListener listener : mNotificationListeners) { + listener.onNotificationRankingUpdate(r); } - mEntryManager.updateNotificationRanking(r); }); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java index dbefc7b4514e..43b9fbc909dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java @@ -33,6 +33,8 @@ import com.android.internal.statusbar.NotificationVisibility; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.statusbar.NotificationLifetimeExtender; +import com.android.systemui.statusbar.NotificationListener; +import com.android.systemui.statusbar.NotificationListener.NotifServiceListener; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationRemoveInterceptor; @@ -174,6 +176,11 @@ public class NotificationEntryManager implements mKeyguardEnvironment = keyguardEnvironment; } + /** Once called, the NEM will start processing notification events from system server. */ + public void attach(NotificationListener notificationListener) { + notificationListener.addNotificationListener(mNotifListener); + } + /** Adds a {@link NotificationEntryListener}. */ public void addNotificationEntryListener(NotificationEntryListener listener) { mNotificationEntryListeners.add(listener); @@ -318,6 +325,36 @@ public class NotificationEntryManager implements } } + private final NotifServiceListener mNotifListener = new NotifServiceListener() { + @Override + public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { + final boolean isUpdate = mActiveNotifications.containsKey(sbn.getKey()); + if (isUpdate) { + updateNotification(sbn, rankingMap); + } else { + addNotification(sbn, rankingMap); + } + } + + @Override + public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) { + removeNotification(sbn.getKey(), rankingMap, UNDEFINED_DISMISS_REASON); + } + + @Override + public void onNotificationRemoved( + StatusBarNotification sbn, + RankingMap rankingMap, + int reason) { + removeNotification(sbn.getKey(), rankingMap, reason); + } + + @Override + public void onNotificationRankingUpdate(RankingMap rankingMap) { + updateNotificationRanking(rankingMap); + } + }; + /** * Equivalent to the old NotificationData#add * @param entry - an entry which is prepared for display diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java index 6c61923332ea..3afd6235b287 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java @@ -32,7 +32,6 @@ import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.ShadeController; -import com.android.systemui.statusbar.phone.StatusBar; import javax.inject.Inject; import javax.inject.Singleton; @@ -120,11 +119,6 @@ public class NotificationFilter { return true; } - if (!StatusBar.ENABLE_CHILD_NOTIFICATIONS - && mGroupManager.isChildInGroupWithSummary(sbn)) { - return true; - } - if (getFsc().isDisclosureNotification(sbn) && !getFsc().isDisclosureNeededForUser(sbn.getUserId())) { // this is a foreground-service disclosure for a user that does not need to show one diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java index 9ae3882e8e82..ec1efa58868e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/GroupEntry.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Objects; /** * Represents a set of grouped notifications. The final notification list is usually a mix of @@ -57,15 +58,22 @@ public class GroupEntry extends ListEntry { @VisibleForTesting public void setSummary(@Nullable NotificationEntry summary) { - mSummary = summary; + if (!Objects.equals(mSummary, summary)) { + mSummary = summary; + onGroupingUpdated(); + } } void clearChildren() { - mChildren.clear(); + if (mChildren.size() != 0) { + mChildren.clear(); + onGroupingUpdated(); + } } void addChild(NotificationEntry child) { mChildren.add(child); + onGroupingUpdated(); } void sortChildren(Comparator<? super NotificationEntry> c) { @@ -77,4 +85,5 @@ public class GroupEntry extends ListEntry { } public static final GroupEntry ROOT_ENTRY = new GroupEntry("<root>"); + } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java index 6ce7fd96e6a0..052473ae21d9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java @@ -19,6 +19,14 @@ package com.android.systemui.statusbar.notification.collection; import android.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.Dependency; +import com.android.systemui.statusbar.notification.collection.provider.DerivedMember; +import com.android.systemui.statusbar.notification.collection.provider.IsHighPriorityProvider; +import com.android.systemui.statusbar.phone.NotificationGroupManager; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; /** * Abstract superclass for top-level entries, i.e. things that can appear in the final notification @@ -26,14 +34,23 @@ import com.android.internal.annotations.VisibleForTesting; */ public abstract class ListEntry { private final String mKey; + private final IsHighPriorityProvider mIsHighPriorityProvider = new IsHighPriorityProvider(); + private final List<DerivedMember> mDerivedMemberList = Arrays.asList(mIsHighPriorityProvider); @Nullable private GroupEntry mParent; @Nullable private GroupEntry mPreviousParent; private int mSection; int mFirstAddedIteration = -1; + // TODO: (b/145659174) remove groupManager when moving to NewNotifPipeline. Logic + // replaced in GroupEntry and NotifListBuilderImpl + private final NotificationGroupManager mGroupManager; + ListEntry(String key) { mKey = key; + + // TODO: (b/145659174) remove + mGroupManager = Dependency.get(NotificationGroupManager.class); } public String getKey() { @@ -53,7 +70,11 @@ public abstract class ListEntry { @VisibleForTesting public void setParent(@Nullable GroupEntry parent) { - mParent = parent; + if (!Objects.equals(mParent, parent)) { + invalidateParent(); + mParent = parent; + onGroupingUpdated(); + } } @Nullable public GroupEntry getPreviousParent() { @@ -72,4 +93,58 @@ public abstract class ListEntry { void setSection(int section) { mSection = section; } + + /** + * Resets the cached values of DerivedMembers. + */ + void invalidateDerivedMembers() { + for (int i = 0; i < mDerivedMemberList.size(); i++) { + mDerivedMemberList.get(i).invalidate(); + } + } + + /** + * Whether this notification is shown to the user as a high priority notification: visible on + * the lock screen/status bar and in the top section in the shade. + */ + public boolean isHighPriority() { + return mIsHighPriorityProvider.get(this); + } + + private void invalidateParent() { + // invalidate our parent (GroupEntry) since DerivedMembers may be dependent on children + if (getParent() != null) { + getParent().invalidateDerivedMembers(); + } + + // TODO: (b/145659174) remove + final NotificationEntry notifEntry = getRepresentativeEntry(); + if (notifEntry != null && mGroupManager.isGroupChild(notifEntry.getSbn())) { + NotificationEntry summary = mGroupManager.getLogicalGroupSummary(notifEntry.getSbn()); + if (summary != null) { + summary.invalidateDerivedMembers(); + } + } + } + + void onGroupingUpdated() { + for (int i = 0; i < mDerivedMemberList.size(); i++) { + mDerivedMemberList.get(i).onGroupingUpdated(); + } + invalidateParent(); + } + + void onSbnUpdated() { + for (int i = 0; i < mDerivedMemberList.size(); i++) { + mDerivedMemberList.get(i).onSbnUpdated(); + } + invalidateParent(); + } + + void onRankingUpdated() { + for (int i = 0; i < mDerivedMemberList.size(); i++) { + mDerivedMemberList.get(i).onRankingUpdated(); + } + invalidateParent(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java index 6f085c0ce7c0..7f85c88865f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java @@ -116,7 +116,7 @@ public class NotifCollection { } mAttached = true; - listenerService.setDownstreamListener(mNotifServiceListener); + listenerService.addNotificationListener(mNotifServiceListener); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 232fb6d3cf67..de16ef5a6d40 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -21,7 +21,6 @@ import static android.app.Notification.CATEGORY_CALL; import static android.app.Notification.CATEGORY_EVENT; import static android.app.Notification.CATEGORY_MESSAGE; import static android.app.Notification.CATEGORY_REMINDER; -import static android.app.Notification.EXTRA_MESSAGES; import static android.app.Notification.FLAG_BUBBLE; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE; @@ -42,7 +41,6 @@ import android.app.Person; import android.content.Context; import android.graphics.drawable.Icon; import android.os.Bundle; -import android.os.Parcelable; import android.os.SystemClock; import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.SnoozeCriterion; @@ -92,7 +90,6 @@ public final class NotificationEntry extends ListEntry { private StatusBarNotification mSbn; private Ranking mRanking; - /* * Bookkeeping members */ @@ -120,7 +117,6 @@ public final class NotificationEntry extends ListEntry { public int targetSdk; private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET; public CharSequence remoteInputText; - private final List<Person> mAssociatedPeople = new ArrayList<>(); private Notification.BubbleMetadata mBubbleMetadata; /** @@ -157,12 +153,6 @@ public final class NotificationEntry extends ListEntry { */ private boolean hasSentReply; - /** - * Whether this notification is shown to the user as a high priority notification: visible on - * the lock screen/status bar and in the top section in the shade. - */ - private boolean mHighPriority; - private boolean mSensitive = true; private Runnable mOnSensitiveChangedListener; private boolean mAutoHeadsUp; @@ -212,9 +202,11 @@ public final class NotificationEntry extends ListEntry { + " doesn't match existing key " + mKey); } - mSbn = sbn; - mBubbleMetadata = mSbn.getNotification().getBubbleMetadata(); - updatePeopleList(); + if (!Objects.equals(mSbn, sbn)) { + mSbn = sbn; + mBubbleMetadata = mSbn.getNotification().getBubbleMetadata(); + onSbnUpdated(); + } } /** @@ -239,10 +231,12 @@ public final class NotificationEntry extends ListEntry { + " doesn't match existing key " + mKey); } - mRanking = ranking; + if (!Objects.equals(mRanking, ranking)) { + mRanking = ranking; + onRankingUpdated(); + } } - /* * Convenience getters for SBN and Ranking members */ @@ -304,49 +298,10 @@ public final class NotificationEntry extends ListEntry { return interruption; } - public boolean isHighPriority() { - return mHighPriority; - } - - public void setIsHighPriority(boolean highPriority) { - this.mHighPriority = highPriority; - } - public boolean isBubble() { return (mSbn.getNotification().flags & FLAG_BUBBLE) != 0; } - private void updatePeopleList() { - mAssociatedPeople.clear(); - - Bundle extras = mSbn.getNotification().extras; - if (extras == null) { - return; - } - - List<Person> p = extras.getParcelableArrayList(Notification.EXTRA_PEOPLE_LIST); - - if (p != null) { - mAssociatedPeople.addAll(p); - } - - if (Notification.MessagingStyle.class.equals( - mSbn.getNotification().getNotificationStyle())) { - final Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES); - if (!ArrayUtils.isEmpty(messages)) { - for (Notification.MessagingStyle.Message message : - Notification.MessagingStyle.Message - .getMessagesFromBundleArray(messages)) { - mAssociatedPeople.add(message.getSenderPerson()); - } - } - } - } - - boolean hasAssociatedPeople() { - return mAssociatedPeople.size() > 0; - } - /** * Returns the data needed for a bubble for this notification, if it exists. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt index 48a4882bcf82..7010943559ba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManager.kt @@ -16,14 +16,11 @@ package com.android.systemui.statusbar.notification.collection -import android.app.NotificationManager.IMPORTANCE_DEFAULT import android.app.NotificationManager.IMPORTANCE_HIGH -import android.app.NotificationManager.IMPORTANCE_LOW import android.app.NotificationManager.IMPORTANCE_MIN import android.service.notification.NotificationListenerService.Ranking import android.service.notification.NotificationListenerService.RankingMap import android.service.notification.StatusBarNotification -import com.android.internal.annotations.VisibleForTesting import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.statusbar.notification.NotificationFilter import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager @@ -105,44 +102,6 @@ open class NotificationRankingManager @Inject constructor( return entry.key == mediaManager.mediaNotificationKey && importance > IMPORTANCE_MIN } - @VisibleForTesting - protected fun isHighPriority(entry: NotificationEntry): Boolean { - if (entry.importance >= IMPORTANCE_DEFAULT || - hasHighPriorityCharacteristics(entry)) { - return true - } - - if (groupManager.isSummaryOfGroup(entry.sbn)) { - val logicalChildren = groupManager.getLogicalChildren(entry.sbn) - for (child in logicalChildren) { - if (isHighPriority(child)) { - return true - } - } - } - - return false - } - - private fun hasHighPriorityCharacteristics(entry: NotificationEntry): Boolean { - val c = entry.channel - val n = entry.sbn.notification - - if ((n.isForegroundService && entry.ranking.importance >= IMPORTANCE_LOW) || - n.hasMediaSession() || - entry.isPeopleNotification()) { - // Users who have long pressed and demoted to silent should not see the notification - // in the top section - if (c != null && c.hasUserSetImportance()) { - return false - } - - return true - } - - return false - } - fun updateRanking( newRankingMap: RankingMap?, entries: Collection<NotificationEntry>, @@ -219,7 +178,10 @@ open class NotificationRankingManager @Inject constructor( // TODO: notify group manager here? groupManager.onEntryUpdated(entry, oldSbn) } - entry.setIsHighPriority(isHighPriority(entry)) + + // TODO: (b/145659174) remove after moving to new NotifPipeline + // (should be able to remove all groupManager code post-migration) + entry.invalidateDerivedMembers() } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java index 4413dc46724a..232246e4f704 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java @@ -37,6 +37,8 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.notification.NotificationUtils; +import com.android.systemui.statusbar.notification.collection.GroupEntry; +import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifCollection; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.listbuilder.NotifListBuilder; @@ -133,8 +135,8 @@ public class KeyguardCoordinator implements Coordinator { // to be shown on the lockscreen // TODO: grouping hasn't happened yet (b/145134683) if (entry.getParent() != null) { - final NotificationEntry summary = entry.getParent().getRepresentativeEntry(); - if (priorityExceedsLockscreenShowingThreshold(summary)) { + final GroupEntry parent = entry.getParent(); + if (priorityExceedsLockscreenShowingThreshold(parent)) { return false; } } @@ -144,7 +146,7 @@ public class KeyguardCoordinator implements Coordinator { } }; - private boolean priorityExceedsLockscreenShowingThreshold(NotificationEntry entry) { + private boolean priorityExceedsLockscreenShowingThreshold(ListEntry entry) { if (entry == null) { return false; } @@ -154,7 +156,7 @@ public class KeyguardCoordinator implements Coordinator { // correctly updated before reaching this point (b/145134683) return entry.isHighPriority(); } else { - return !entry.getRanking().isAmbient(); + return !entry.getRepresentativeEntry().getRanking().isAmbient(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DerivedMember.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DerivedMember.java new file mode 100644 index 000000000000..815e6f7eaa46 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DerivedMember.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.provider; +/** + * Caches a computed value until invalidate() is called + * @param <Parent> Object used to computeValue + * @param <Value> type of value to cache until invalidate is called + */ +public abstract class DerivedMember<Parent, Value> { + private Value mValue; + protected abstract Value computeValue(Parent parent); + + /** + * Gets the last cached value, else recomputes the value. + */ + public Value get(Parent parent) { + if (mValue == null) { + mValue = computeValue(parent); + } + return mValue; + } + + /** + * Resets the cached value. + * Next time "get" is called, the value is recomputed. + */ + public void invalidate() { + mValue = null; + } + + /** + * Called when a NotificationEntry's status bar notification has updated. + * Derived members can invalidate here. + */ + public void onSbnUpdated() {} + + /** + * Called when a NotificationEntry's Ranking has updated. + * Derived members can invalidate here. + */ + public void onRankingUpdated() {} + + /** + * Called when a ListEntry's grouping information (parent or children) has changed. + * Derived members can invalidate here. + */ + public void onGroupingUpdated() {} +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/IsHighPriorityProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/IsHighPriorityProvider.java new file mode 100644 index 000000000000..76e256b9be2d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/IsHighPriorityProvider.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.provider; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Person; + +import com.android.systemui.Dependency; +import com.android.systemui.statusbar.notification.collection.GroupEntry; +import com.android.systemui.statusbar.notification.collection.ListEntry; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.phone.NotificationGroupManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * Whether the ListEntry is shown to the user as a high priority notification: visible on + * the lock screen/status bar and in the top section in the shade. + * + * A NotificationEntry is considered high priority if it: + * - has importance greater than or equal to IMPORTANCE_DEFAULT + * OR + * - their importance has NOT been set to a low priority option by the user AND the notification + * fulfills one of the following: + * - has a person associated with it + * - has a media session associated with it + * - has messaging style + * + * A GroupEntry is considered high priority if its representativeEntry (summary) or children are + * high priority + */ +public class IsHighPriorityProvider extends DerivedMember<ListEntry, Boolean> { + // TODO: (b/145659174) remove groupManager when moving to NewNotifPipeline. Logic + // replaced in GroupEntry and NotifListBuilderImpl + private final NotificationGroupManager mGroupManager; + + + public IsHighPriorityProvider() { + // TODO: (b/145659174) remove + mGroupManager = Dependency.get(NotificationGroupManager.class); + } + + @Override + protected Boolean computeValue(ListEntry entry) { + if (entry == null) { + return false; + } + + return isHighPriority(entry); + } + + private boolean isHighPriority(ListEntry listEntry) { + // requires groups have been set (AFTER PipelineState.STATE_TRANSFORMING) + final NotificationEntry notifEntry = listEntry.getRepresentativeEntry(); + return notifEntry.getRanking().getImportance() >= NotificationManager.IMPORTANCE_DEFAULT + || hasHighPriorityCharacteristics(notifEntry) + || hasHighPriorityChild(listEntry); + + } + + private boolean hasHighPriorityChild(ListEntry entry) { + // TODO: (b/145659174) remove + if (entry instanceof NotificationEntry) { + NotificationEntry notifEntry = (NotificationEntry) entry; + if (mGroupManager.isSummaryOfGroup(notifEntry.getSbn())) { + List<NotificationEntry> logicalChildren = + mGroupManager.getLogicalChildren(notifEntry.getSbn()); + for (NotificationEntry child : logicalChildren) { + if (child.isHighPriority()) { + return true; + } + } + } + } + + if (entry instanceof GroupEntry) { + for (NotificationEntry child : ((GroupEntry) entry).getChildren()) { + if (child.isHighPriority()) { + return true; + } + } + } + return false; + } + + private boolean hasHighPriorityCharacteristics(NotificationEntry entry) { + return !hasUserSetImportance(entry) + && (isImportantOngoing(entry) + || entry.getSbn().getNotification().hasMediaSession() + || hasPerson(entry) + || isMessagingStyle(entry)); + } + + private boolean isImportantOngoing(NotificationEntry entry) { + return entry.getSbn().getNotification().isForegroundService() + && entry.getRanking().getImportance() >= NotificationManager.IMPORTANCE_LOW; + } + + private boolean isMessagingStyle(NotificationEntry entry) { + return Notification.MessagingStyle.class.equals( + entry.getSbn().getNotification().getNotificationStyle()); + } + + private boolean hasPerson(NotificationEntry entry) { + // TODO: cache favorite and recent contacts to check contact affinity + Notification notification = entry.getSbn().getNotification(); + ArrayList<Person> people = notification.extras != null + ? notification.extras.getParcelableArrayList(Notification.EXTRA_PEOPLE_LIST) + : new ArrayList<>(); + return people != null && !people.isEmpty(); + } + + private boolean hasUserSetImportance(NotificationEntry entry) { + return entry.getRanking().getChannel() != null + && entry.getRanking().getChannel().hasUserSetImportance(); + } + + @Override + public void onSbnUpdated() { + invalidate(); + } + + @Override + public void onRankingUpdated() { + invalidate(); + } + + @Override + public void onGroupingUpdated() { + invalidate(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt index 987b52dbc2e8..784673e64ff8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt @@ -249,7 +249,7 @@ private fun addBadgeToDrawable( } } -private fun extractAvatarFromRow(entry: NotificationEntry): Drawable? = +fun extractAvatarFromRow(entry: NotificationEntry): Drawable? = entry.row ?.childrenWithId(R.id.expanded) ?.mapNotNull { it as? ViewGroup } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 9ac5b44f8639..65423e970cf1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -813,15 +813,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * @param parent the new parent notification */ public void setIsChildInGroup(boolean isChildInGroup, ExpandableNotificationRow parent) { - boolean childInGroup = StatusBar.ENABLE_CHILD_NOTIFICATIONS && isChildInGroup; if (mExpandAnimationRunning && !isChildInGroup && mNotificationParent != null) { mNotificationParent.setChildIsExpanding(false); mNotificationParent.setExtraWidthForClipping(0.0f); mNotificationParent.setMinimumHeightForClipping(0); } - mNotificationParent = childInGroup ? parent : null; - mPrivateLayout.setIsChildInGroup(childInGroup); - mNotificationInflater.setIsChildInGroup(childInGroup); + mNotificationParent = isChildInGroup ? parent : null; + mPrivateLayout.setIsChildInGroup(isChildInGroup); + mNotificationInflater.setIsChildInGroup(isChildInGroup); resetBackgroundAlpha(); updateBackgroundForGroupState(); updateClickAndFocus(); @@ -2354,8 +2353,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } private void onChildrenCountChanged() { - mIsSummaryWithChildren = StatusBar.ENABLE_CHILD_NOTIFICATIONS - && mChildrenContainer != null && mChildrenContainer.getNotificationChildCount() > 0; + mIsSummaryWithChildren = mChildrenContainer != null + && mChildrenContainer.getNotificationChildCount() > 0; if (mIsSummaryWithChildren && mChildrenContainer.getHeaderView() == null) { mChildrenContainer.recreateNotificationHeader(mExpandClickListener ); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java index f3e9b6b3d155..183adeb037d6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java @@ -37,12 +37,19 @@ import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; +import androidx.annotation.NonNull; + +import com.android.internal.util.IndentingPrintWriter; import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.systemui.Dependency; +import com.android.systemui.DumpController; +import com.android.systemui.Dumpable; +import com.android.systemui.dagger.qualifiers.MainHandler; import com.android.systemui.statusbar.NotificationMediaManager; import libcore.io.IoUtils; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.util.Objects; import javax.inject.Inject; @@ -52,16 +59,15 @@ import javax.inject.Singleton; * Manages the lockscreen wallpaper. */ @Singleton -public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implements Runnable { +public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implements Runnable, + Dumpable { private static final String TAG = "LockscreenWallpaper"; - private final NotificationMediaManager mMediaManager = - Dependency.get(NotificationMediaManager.class); - + private final NotificationMediaManager mMediaManager; private final WallpaperManager mWallpaperManager; - private Handler mH; private final KeyguardUpdateMonitor mUpdateMonitor; + private final Handler mH; private boolean mCached; private Bitmap mCache; @@ -74,10 +80,16 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen @Inject public LockscreenWallpaper(WallpaperManager wallpaperManager, @Nullable IWallpaperManager iWallpaperManager, - KeyguardUpdateMonitor keyguardUpdateMonitor) { + KeyguardUpdateMonitor keyguardUpdateMonitor, + DumpController dumpController, + NotificationMediaManager mediaManager, + @MainHandler Handler mainHandler) { + dumpController.registerDumpable(getClass().getSimpleName(), this); mWallpaperManager = wallpaperManager; mCurrentUserId = ActivityManager.getCurrentUser(); mUpdateMonitor = keyguardUpdateMonitor; + mMediaManager = mediaManager; + mH = mainHandler; if (iWallpaperManager != null) { // Service is disabled on some devices like Automotive @@ -89,14 +101,6 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen } } - void setHandler(Handler handler) { - if (mH != null) { - Log.wtfStack(TAG, "Handler has already been set. Trying to double initialize?"); - return; - } - mH = handler; - } - public Bitmap getBitmap() { if (mCached) { return mCache; @@ -227,6 +231,16 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } + @Override + public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { + pw.println(getClass().getSimpleName() + ":"); + IndentingPrintWriter iPw = new IndentingPrintWriter(pw, " ").increaseIndent(); + iPw.println("mCached=" + mCached); + iPw.println("mCache=" + mCache); + iPw.println("mCurrentUserId=" + mCurrentUserId); + iPw.println("mSelectedUser=" + mSelectedUser); + } + private static class LoaderResult { public final boolean success; public final Bitmap bitmap; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index b8aea9b2cd59..4c5bbce05261 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -561,7 +561,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo } protected void scheduleUpdate() { - if (mUpdatePending) return; + if (mUpdatePending || mScrimBehind == null) return; // Make sure that a frame gets scheduled. mScrimBehind.invalidate(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index a9d760185fd6..f4c7e23f9cda 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -246,9 +246,6 @@ public class StatusBar extends SystemUI implements DemoMode, StatusBarStateController.StateListener, ActivityLaunchAnimator.Callback { public static final boolean MULTIUSER_DEBUG = false; - public static final boolean ENABLE_CHILD_NOTIFICATIONS - = SystemProperties.getBoolean("debug.child_notifs", true); - protected static final int MSG_HIDE_RECENT_APPS = 1020; protected static final int MSG_PRELOAD_RECENT_APPS = 1022; protected static final int MSG_CANCEL_PRELOAD_RECENT_APPS = 1023; @@ -813,6 +810,8 @@ public class StatusBar extends SystemUI implements DemoMode, ServiceManager.getService(Context.STATUS_BAR_SERVICE)); mKeyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); + mWallpaperSupported = + mContext.getSystemService(WallpaperManager.class).isWallpaperSupported(); // Connect in to the status bar manager service mCommandQueue.addCallback(this); @@ -826,9 +825,6 @@ public class StatusBar extends SystemUI implements DemoMode, createAndAddWindows(result); - mWallpaperSupported = - mContext.getSystemService(WallpaperManager.class).isWallpaperSupported(); - if (mWallpaperSupported) { // Make sure we always have the most current wallpaper info. IntentFilter wallpaperChangedFilter = new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED); @@ -1061,7 +1057,6 @@ public class StatusBar extends SystemUI implements DemoMode, if (ENABLE_LOCKSCREEN_WALLPAPER && mWallpaperSupported) { mLockscreenWallpaper = mLockscreenWallpaperLazy.get(); - mLockscreenWallpaper.setHandler(mHandler); } mKeyguardIndicationController = @@ -1271,6 +1266,7 @@ public class StatusBar extends SystemUI implements DemoMode, if (mFeatureFlags.isNewNotifPipelineEnabled()) { mNewNotifPipeline.get().initialize(mNotificationListener); } + mEntryManager.attach(mNotificationListener); } /** diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt new file mode 100644 index 000000000000..62ae7b9d620f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt @@ -0,0 +1,717 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.animation + +import android.os.Looper +import android.util.ArrayMap +import android.util.Log +import android.view.View +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.FlingAnimation +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce +import com.android.systemui.util.animation.PhysicsAnimator.Companion.getInstance +import java.util.WeakHashMap + +/** + * Extension function for all objects which will return a PhysicsAnimator instance for that object. + */ +val <T : View> T.physicsAnimator: PhysicsAnimator<T> get() { return getInstance(this) } + +private const val TAG = "PhysicsAnimator" + +typealias EndAction = () -> Unit + +/** A map of Property -> AnimationUpdate, which is provided to update listeners on each frame. */ +typealias UpdateMap<T> = + ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate> + +/** + * Map of the animators associated with a given object. This ensures that only one animator + * per object exists. + */ +internal val animators = WeakHashMap<Any, PhysicsAnimator<*>>() + +/** + * Default spring configuration to use for animations where stiffness and/or damping ratio + * were not provided. + */ +private val defaultSpring = PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_MEDIUM, + SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) + +/** Default fling configuration to use for animations where friction was not provided. */ +private val defaultFling = PhysicsAnimator.FlingConfig( + friction = 1f, min = -Float.MAX_VALUE, max = Float.MAX_VALUE) + +/** Whether to log helpful debug information about animations. */ +private var verboseLogging = false + +/** + * Animator that uses physics-based animations to animate properties on views and objects. Physics + * animations use real-world physical concepts, such as momentum and mass, to realistically simulate + * motion. PhysicsAnimator is heavily inspired by [android.view.ViewPropertyAnimator], and + * also uses the builder pattern to configure and start animations. + * + * The physics animations are backed by [DynamicAnimation]. + * + * @param T The type of the object being animated. + */ +class PhysicsAnimator<T> private constructor (val target: T) { + + /** Data class for representing animation frame updates. */ + data class AnimationUpdate(val value: Float, val velocity: Float) + + /** [DynamicAnimation] instances for the given properties. */ + private val springAnimations = ArrayMap<FloatPropertyCompat<in T>, SpringAnimation>() + private val flingAnimations = ArrayMap<FloatPropertyCompat<in T>, FlingAnimation>() + + /** + * Spring and fling configurations for the properties to be animated on the target. We'll + * configure and start the DynamicAnimations for these properties according to the provided + * configurations. + */ + private val springConfigs = ArrayMap<FloatPropertyCompat<in T>, SpringConfig>() + private val flingConfigs = ArrayMap<FloatPropertyCompat<in T>, FlingConfig>() + + /** + * Animation listeners for the animation. These will be notified when each property animation + * updates or ends. + */ + private val updateListeners = ArrayList<UpdateListener<T>>() + private val endListeners = ArrayList<EndListener<T>>() + + /** End actions to run when all animations have completed. */ + private val endActions = ArrayList<EndAction>() + + /** + * Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to + * the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add + * just one permanent update and end listener to the DynamicAnimations. + */ + internal var internalListeners = ArrayList<InternalListener>() + + /** + * Action to run when [start] is called. This can be changed by + * [PhysicsAnimatorTestUtils.prepareForTest] to enable animators to run under test and provide + * helpful test utilities. + */ + internal var startAction: () -> Unit = ::startInternal + + /** + * Springs a property to the given value, using the provided configuration settings. + * + * Springs are used when you know the exact value to which you want to animate. They can be + * configured with a start velocity (typically used when the spring is initiated by a touch + * event), but this velocity will be realistically attenuated as forces are applied to move the + * property towards the end value. + * + * If you find yourself repeating the same stiffness and damping ratios many times, consider + * storing a single [SpringConfig] instance and passing that in instead of individual values. + * + * @param property The property to spring to the given value. The property must be an instance + * of FloatPropertyCompat<? super T>. For example, if this is a + * PhysicsAnimator<FrameLayout>, you can use a FloatPropertyCompat<FrameLayout>, as + * well as a FloatPropertyCompat<ViewGroup>, and so on. + * @param toPosition The value to spring the given property to. + * @param startVelocity The initial velocity to use for the animation. + * @param stiffness The stiffness to use for the spring. Higher stiffness values result in + * faster animations, while lower stiffness means a slower animation. Reasonable values for + * low, medium, and high stiffness can be found as constants in [SpringForce]. + * @param dampingRatio The damping ratio (bounciness) to use for the spring. Higher values + * result in a less 'springy' animation, while lower values allow the animation to bounce + * back and forth for a longer time after reaching the final position. Reasonable values for + * low, medium, and high damping can be found in [SpringForce]. + */ + fun spring( + property: FloatPropertyCompat<in T>, + toPosition: Float, + startVelocity: Float = 0f, + stiffness: Float = defaultSpring.stiffness, + dampingRatio: Float = defaultSpring.dampingRatio + ): PhysicsAnimator<T> { + if (verboseLogging) { + Log.d(TAG, "Springing ${getReadablePropertyName(property)} to $toPosition.") + } + + springConfigs[property] = + SpringConfig(stiffness, dampingRatio, startVelocity, toPosition) + return this + } + + /** + * Springs a property to a given value using the provided start velocity and configuration + * options. + * + * @see spring + */ + fun spring( + property: FloatPropertyCompat<in T>, + toPosition: Float, + startVelocity: Float, + config: SpringConfig = defaultSpring + ): PhysicsAnimator<T> { + return spring( + property, toPosition, startVelocity, config.stiffness, config.dampingRatio) + } + + /** + * Springs a property to a given value using the provided configuration options, and a start + * velocity of 0f. + * + * @see spring + */ + fun spring( + property: FloatPropertyCompat<in T>, + toPosition: Float, + config: SpringConfig = defaultSpring + ): PhysicsAnimator<T> { + return spring(property, toPosition, 0f, config) + } + + /** + * Flings a property using the given start velocity, using a [FlingAnimation] configured using + * the provided configuration settings. + * + * Flings are used when you have a start velocity, and want the property value to realistically + * decrease as friction is applied until the velocity reaches zero. Flings do not have a + * deterministic end value. If you are attempting to animate to a specific end value, use + * [spring]. + * + * If you find yourself repeating the same friction/min/max values, consider storing a single + * [FlingConfig] and passing that in instead. + * + * @param property The property to fling using the given start velocity. + * @param startVelocity The start velocity (in pixels per second) with which to start the fling. + * @param friction Friction value applied to slow down the animation over time. Higher values + * will more quickly slow the animation. Typical friction values range from 1f to 10f. + * @param min The minimum value allowed for the animation. If this value is reached, the + * animation will end abruptly. + * @param max The maximum value allowed for the animation. If this value is reached, the + * animation will end abruptly. + */ + fun fling( + property: FloatPropertyCompat<in T>, + startVelocity: Float, + friction: Float = defaultFling.friction, + min: Float = defaultFling.min, + max: Float = defaultFling.max + ): PhysicsAnimator<T> { + if (verboseLogging) { + Log.d(TAG, "Flinging ${getReadablePropertyName(property)} " + + "with velocity $startVelocity.") + } + + flingConfigs[property] = FlingConfig(friction, min, max, startVelocity) + return this + } + + /** + * Flings a property using the given start velocity, using a [FlingAnimation] configured using + * the provided configuration settings. + * + * @see fling + */ + fun fling( + property: FloatPropertyCompat<in T>, + startVelocity: Float, + config: FlingConfig = defaultFling + ): PhysicsAnimator<T> { + return fling(property, startVelocity, config.friction, config.min, config.max) + } + + /** + * Adds a listener that will be called whenever any property on the animated object is updated. + * This will be called on every animation frame, with the current value of the animated object + * and the new property values. + */ + fun addUpdateListener(listener: UpdateListener<T>): PhysicsAnimator<T> { + updateListeners.add(listener) + return this + } + + /** + * Adds a listener that will be called whenever a property's animation ends. This is useful if + * you care about a specific property ending, or want to use the end value/end velocity from a + * particular property's animation. If you just want to run an action when all property + * animations have ended, use [withEndActions]. + */ + fun addEndListener(listener: EndListener<T>): PhysicsAnimator<T> { + endListeners.add(listener) + return this + } + + /** + * Adds end actions that will be run sequentially when animations for every property involved in + * this specific animation have ended (unless they were explicitly canceled). For example, if + * you call: + * + * animator + * .spring(TRANSLATION_X, ...) + * .spring(TRANSLATION_Y, ...) + * .withEndAction(action) + * .start() + * + * 'action' will be run when both TRANSLATION_X and TRANSLATION_Y end. + * + * Other properties may still be animating, if those animations were not started in the same + * call. For example: + * + * animator + * .spring(ALPHA, ...) + * .start() + * + * animator + * .spring(TRANSLATION_X, ...) + * .spring(TRANSLATION_Y, ...) + * .withEndAction(action) + * .start() + * + * 'action' will still be run as soon as TRANSLATION_X and TRANSLATION_Y end, even if ALPHA is + * still animating. + * + * If you want to run actions as soon as a subset of property animations have ended, you want + * access to the animation's end value/velocity, or you want to run these actions even if the + * animation is explicitly canceled, use [addEndListener]. End listeners have an allEnded param, + * which indicates that all relevant animations have ended. + */ + fun withEndActions(vararg endActions: EndAction): PhysicsAnimator<T> { + this.endActions.addAll(endActions) + return this + } + + /** Starts the animations! */ + fun start() { + startAction() + } + + /** + * Starts the animations for real! This is typically called immediately by [start] unless this + * animator is under test. + */ + internal fun startInternal() { + if (!Looper.getMainLooper().isCurrentThread) { + Log.e(TAG, "Animations can only be started on the main thread. If you are seeing " + + "this message in a test, call PhysicsAnimatorTestUtils#prepareForTest in " + + "your test setup.") + } + + // Add an internal listener that will dispatch animation events to the provided listeners. + internalListeners.add(InternalListener( + getAnimatedProperties(), + ArrayList(updateListeners), + ArrayList(endListeners), + ArrayList(endActions))) + + for ((property, config) in flingConfigs) { + val currentValue = property.getValue(target) + + // If the fling is already out of bounds, don't start it. + if (currentValue <= config.min || currentValue >= config.max) { + continue + } + + val flingAnim = getFlingAnimation(property) + config.applyToAnimation(flingAnim) + flingAnim.start() + } + + for ((property, config) in springConfigs) { + val springAnim = getSpringAnimation(property) + config.applyToAnimation(springAnim) + springAnim.start() + } + + clearAnimator() + } + + /** Clear the animator's builder variables. */ + private fun clearAnimator() { + springConfigs.clear() + flingConfigs.clear() + + updateListeners.clear() + endListeners.clear() + endActions.clear() + } + + /** Retrieves a spring animation for the given property, building one if needed. */ + private fun getSpringAnimation(property: FloatPropertyCompat<in T>): SpringAnimation { + return springAnimations.getOrPut( + property, + { configureDynamicAnimation(SpringAnimation(target, property), property) + as SpringAnimation }) + } + + /** Retrieves a fling animation for the given property, building one if needed. */ + private fun getFlingAnimation(property: FloatPropertyCompat<in T>): FlingAnimation { + return flingAnimations.getOrPut( + property, + { configureDynamicAnimation(FlingAnimation(target, property), property) + as FlingAnimation }) + } + + /** + * Adds update and end listeners to the DynamicAnimation which will dispatch to the internal + * listeners. + */ + private fun configureDynamicAnimation( + anim: DynamicAnimation<*>, + property: FloatPropertyCompat<in T> + ): DynamicAnimation<*> { + anim.addUpdateListener { _, value, velocity -> + for (i in 0 until internalListeners.size) { + internalListeners[i].onInternalAnimationUpdate(property, value, velocity) + } + } + anim.addEndListener { _, canceled, value, velocity -> + internalListeners.removeAll { + it.onInternalAnimationEnd(property, canceled, value, velocity) } } + return anim + } + + /** + * Internal listener class that receives updates from DynamicAnimation listeners, and dispatches + * them to the appropriate update/end listeners. This class is also aware of which properties + * were being animated when the end listeners were passed in, so that we can provide the + * appropriate value for allEnded to [EndListener.onAnimationEnd]. + */ + internal inner class InternalListener constructor( + private var properties: Set<FloatPropertyCompat<in T>>, + private var updateListeners: List<UpdateListener<T>>, + private var endListeners: List<EndListener<T>>, + private var endActions: List<EndAction> + ) { + + /** The number of properties whose animations haven't ended. */ + private var numPropertiesAnimating = properties.size + + /** + * Update values that haven't yet been dispatched because not all property animations have + * updated yet. + */ + private val undispatchedUpdates = + ArrayMap<FloatPropertyCompat<in T>, AnimationUpdate>() + + /** Called when a DynamicAnimation updates. */ + internal fun onInternalAnimationUpdate( + property: FloatPropertyCompat<in T>, + value: Float, + velocity: Float + ) { + + // If this property animation isn't relevant to this listener, ignore it. + if (!properties.contains(property)) { + return + } + + undispatchedUpdates[property] = AnimationUpdate(value, velocity) + maybeDispatchUpdates() + } + + /** + * Called when a DynamicAnimation ends. + * + * @return True if this listener should be removed from the list of internal listeners, so + * it no longer receives updates from DynamicAnimations. + */ + internal fun onInternalAnimationEnd( + property: FloatPropertyCompat<in T>, + canceled: Boolean, + finalValue: Float, + finalVelocity: Float + ): Boolean { + + // If this property animation isn't relevant to this listener, ignore it. + if (!properties.contains(property)) { + return false + } + + // Dispatch updates if we have one for each property. + numPropertiesAnimating-- + maybeDispatchUpdates() + + // If we didn't have an update for each property, dispatch the update for the ending + // property. This guarantees that an update isn't sent for this property *after* we call + // onAnimationEnd for that property. + if (undispatchedUpdates.contains(property)) { + updateListeners.forEach { updateListener -> + updateListener.onAnimationUpdateForProperty( + target, + UpdateMap<T>().also { it[property] = undispatchedUpdates[property] }) + } + + undispatchedUpdates.remove(property) + } + + val allEnded = !arePropertiesAnimating(properties) + endListeners.forEach { + it.onAnimationEnd(target, property, canceled, finalValue, finalVelocity, allEnded) } + + // If all of the animations that this listener cares about have ended, run the end + // actions unless the animation was canceled. + if (allEnded && !canceled) { + endActions.forEach { it() } + } + + return allEnded + } + + /** + * Dispatch undispatched values if we've received an update from each of the animating + * properties. + */ + private fun maybeDispatchUpdates() { + if (undispatchedUpdates.size >= numPropertiesAnimating && + undispatchedUpdates.size > 0) { + updateListeners.forEach { + it.onAnimationUpdateForProperty(target, ArrayMap(undispatchedUpdates)) + } + + undispatchedUpdates.clear() + } + } + } + + /** Return true if any animations are running on the object. */ + fun isRunning(): Boolean { + return arePropertiesAnimating(springAnimations.keys.union(flingAnimations.keys)) + } + + /** Returns whether the given property is animating. */ + fun isPropertyAnimating(property: FloatPropertyCompat<in T>): Boolean { + return springAnimations[property]?.isRunning ?: false + } + + /** Returns whether any of the given properties are animating. */ + fun arePropertiesAnimating(properties: Set<FloatPropertyCompat<in T>>): Boolean { + return properties.any { isPropertyAnimating(it) } + } + + /** Return the set of properties that will begin animating upon calling [start]. */ + internal fun getAnimatedProperties(): Set<FloatPropertyCompat<in T>> { + return springConfigs.keys.union(flingConfigs.keys) + } + + /** Cancels all in progress animations on all properties. */ + fun cancel() { + for (dynamicAnim in flingAnimations.values.union(springAnimations.values)) { + dynamicAnim.cancel() + } + } + + /** + * Container object for spring animation configuration settings. This allows you to store + * default stiffness and damping ratio values in a single configuration object, which you can + * pass to [spring]. + */ + data class SpringConfig internal constructor( + internal var stiffness: Float, + internal var dampingRatio: Float, + internal var startVel: Float = 0f, + internal var finalPosition: Float = -Float.MAX_VALUE + ) { + + constructor() : + this(defaultSpring.stiffness, defaultSpring.dampingRatio) + + constructor(stiffness: Float, dampingRatio: Float) : + this(stiffness = stiffness, dampingRatio = dampingRatio, startVel = 0f) + + /** Apply these configuration settings to the given SpringAnimation. */ + internal fun applyToAnimation(anim: SpringAnimation) { + val springForce = anim.spring ?: SpringForce() + anim.spring = springForce.apply { + stiffness = this@SpringConfig.stiffness + dampingRatio = this@SpringConfig.dampingRatio + finalPosition = this@SpringConfig.finalPosition + } + + if (startVel != 0f) anim.setStartVelocity(startVel) + } + } + + /** + * Container object for fling animation configuration settings. This allows you to store default + * friction values (as well as optional min/max values) in a single configuration object, which + * you can pass to [fling] and related methods. + */ + data class FlingConfig internal constructor( + internal var friction: Float, + internal var min: Float, + internal var max: Float, + internal var startVel: Float + ) { + + constructor() : this(defaultFling.friction) + + constructor(friction: Float) : + this(friction, defaultFling.min, defaultFling.max) + + constructor(friction: Float, min: Float, max: Float) : + this(friction, min, max, startVel = 0f) + + /** Apply these configuration settings to the given FlingAnimation. */ + internal fun applyToAnimation(anim: FlingAnimation) { + anim.apply { + friction = this@FlingConfig.friction + setMinValue(min) + setMaxValue(max) + setStartVelocity(startVel) + } + } + } + + /** + * Listener for receiving values from in progress animations. Used with + * [PhysicsAnimator.addUpdateListener]. + * + * @param <T> The type of the object being animated. + </T> */ + interface UpdateListener<T> { + + /** + * Called on each animation frame with the target object, and a map of FloatPropertyCompat + * -> AnimationUpdate, containing the latest value and velocity for that property. When + * multiple properties are animating together, the map will typically contain one entry for + * each property. However, you should never assume that this is the case - when a property + * animation ends earlier than the others, you'll receive an UpdateMap containing only that + * property's final update. Subsequently, you'll only receive updates for the properties + * that are still animating. + * + * Always check that the map contains an update for the property you're interested in before + * accessing it. + * + * @param target The animated object itself. + * @param values Map of property to AnimationUpdate, which contains that property + * animation's latest value and velocity. You should never assume that a particular property + * is present in this map. + */ + fun onAnimationUpdateForProperty( + target: T, + values: UpdateMap<T> + ) + } + + /** + * Listener for receiving callbacks when animations end. + * + * @param <T> The type of the object being animated. + </T> */ + interface EndListener<T> { + + /** + * Called with the final animation values as each property animation ends. This can be used + * to respond to specific property animations concluding (such as hiding a view when ALPHA + * ends, even if the corresponding TRANSLATION animations have not ended). + * + * If you just want to run an action when all of the property animations have ended, you can + * use [PhysicsAnimator.withEndActions]. + * + * @param target The animated object itself. + * @param property The property whose animation has just ended. + * @param canceled Whether the animation was explicitly canceled before it naturally ended. + * @param finalValue The final value of the animated property. + * @param finalVelocity The final velocity (in pixels per second) of the ended animation. + * This is typically zero, unless this was a fling animation which ended abruptly due to + * reaching its configured min/max values. + * @param allRelevantPropertyAnimsEnded Whether all properties relevant to this end listener + * have ended. Relevant properties are those which were animated alongside the + * [addEndListener] call where this animator was passed in. For example: + * + * animator + * .spring(TRANSLATION_X, 100f) + * .spring(TRANSLATION_Y, 200f) + * .withEndListener(firstEndListener) + * .start() + * + * firstEndListener will be called first for TRANSLATION_X, with allEnded = false, + * because TRANSLATION_Y is still running. When TRANSLATION_Y ends, it'll be called with + * allEnded = true. + * + * If a subsequent call to start() is made with other properties, those properties are not + * considered relevant and allEnded will still equal true when only TRANSLATION_X and + * TRANSLATION_Y end. For example, if immediately after the prior example, while + * TRANSLATION_X and TRANSLATION_Y are still animating, we called: + * + * animator. + * .spring(SCALE_X, 2f, stiffness = 10f) // That will take awhile... + * .withEndListener(secondEndListener) + * .start() + * + * firstEndListener will still be called with allEnded = true when TRANSLATION_X/Y end, even + * though SCALE_X is still animating. Similarly, secondEndListener will be called with + * allEnded = true as soon as SCALE_X ends, even if the translation animations are still + * running. + */ + fun onAnimationEnd( + target: T, + property: FloatPropertyCompat<in T>, + canceled: Boolean, + finalValue: Float, + finalVelocity: Float, + allRelevantPropertyAnimsEnded: Boolean + ) + } + + companion object { + + /** + * Constructor to use to for new physics animator instances in [getInstance]. This is + * typically the default constructor, but [PhysicsAnimatorTestUtils] can change it so that + * all code using the physics animator is given testable instances instead. + */ + internal var instanceConstructor: (Any) -> PhysicsAnimator<*> = ::PhysicsAnimator + + @JvmStatic + fun <T : Any> getInstance(target: T): PhysicsAnimator<T> { + if (!animators.containsKey(target)) { + animators[target] = instanceConstructor(target) + } + + return animators[target] as PhysicsAnimator<T> + } + + /** + * Set whether all physics animators should log a lot of information about animations. + * Useful for debugging! + */ + @JvmStatic + fun setVerboseLogging(debug: Boolean) { + verboseLogging = debug + } + + @JvmStatic + fun getReadablePropertyName(property: FloatPropertyCompat<*>): String { + return when (property) { + DynamicAnimation.TRANSLATION_X -> "translationX" + DynamicAnimation.TRANSLATION_Y -> "translationY" + DynamicAnimation.TRANSLATION_Z -> "translationZ" + DynamicAnimation.SCALE_X -> "scaleX" + DynamicAnimation.SCALE_Y -> "scaleY" + DynamicAnimation.ROTATION -> "rotation" + DynamicAnimation.ROTATION_X -> "rotationX" + DynamicAnimation.ROTATION_Y -> "rotationY" + DynamicAnimation.SCROLL_X -> "scrollX" + DynamicAnimation.SCROLL_Y -> "scrollY" + DynamicAnimation.ALPHA -> "alpha" + else -> "Custom FloatPropertyCompat instance" + } + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimatorTestUtils.kt b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimatorTestUtils.kt new file mode 100644 index 000000000000..a1f74eb40cad --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimatorTestUtils.kt @@ -0,0 +1,457 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.util.animation + +import android.os.Handler +import android.os.Looper +import android.util.ArrayMap +import androidx.dynamicanimation.animation.FloatPropertyCompat +import java.util.ArrayDeque +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +typealias UpdateMatcher = (PhysicsAnimator.AnimationUpdate) -> Boolean +typealias UpdateFramesPerProperty<T> = + ArrayMap<FloatPropertyCompat<in T>, ArrayList<PhysicsAnimator.AnimationUpdate>> + +/** + * Utilities for testing code that uses [PhysicsAnimator]. + * + * Start by calling [prepareForTest] at the beginning of each test - this will modify the behavior + * of all PhysicsAnimator instances so that they post animations to the main thread (so they don't + * crash). It'll also enable the use of the other static helper methods in this class, which you can + * use to do things like block the test until animations complete (so you can test end states), or + * verify keyframes. + */ +object PhysicsAnimatorTestUtils { + var timeoutMs: Long = 2000 + private var startBlocksUntilAnimationsEnd = false + private val animationThreadHandler = Handler(Looper.getMainLooper()) + private val allAnimatedObjects = HashSet<Any>() + private val animatorTestHelpers = HashMap<PhysicsAnimator<*>, AnimatorTestHelper<*>>() + + /** + * Modifies the behavior of all [PhysicsAnimator] instances so that they post animations to the + * main thread, and report all of their + */ + @JvmStatic + fun prepareForTest() { + val defaultConstructor = PhysicsAnimator.instanceConstructor + PhysicsAnimator.instanceConstructor = fun(target: Any): PhysicsAnimator<*> { + val animator = defaultConstructor(target) + allAnimatedObjects.add(target) + animatorTestHelpers[animator] = AnimatorTestHelper(animator) + return animator + } + + timeoutMs = 2000 + startBlocksUntilAnimationsEnd = false + allAnimatedObjects.clear() + } + + @JvmStatic + fun tearDown() { + val latch = CountDownLatch(1) + animationThreadHandler.post { + animatorTestHelpers.keys.forEach { it.cancel() } + latch.countDown() + } + + latch.await() + + animatorTestHelpers.clear() + animators.clear() + allAnimatedObjects.clear() + } + + /** + * Sets the maximum time (in milliseconds) to block the test thread while waiting for animations + * before throwing an exception. + */ + @JvmStatic + fun setBlockTimeout(timeoutMs: Long) { + this.timeoutMs = timeoutMs + } + + /** + * Sets whether all animations should block the test thread until they end. This is typically + * the desired behavior, since you can invoke code that runs an animation and then assert things + * about its end state. + */ + @JvmStatic + fun setAllAnimationsBlock(block: Boolean) { + startBlocksUntilAnimationsEnd = block + } + + /** + * Blocks the calling thread until animations of the given property on the target object end. + */ + @JvmStatic + @Throws(InterruptedException::class) + fun <T : Any> blockUntilAnimationsEnd( + animator: PhysicsAnimator<T>, + vararg properties: FloatPropertyCompat<in T> + ) { + val animatingProperties = HashSet<FloatPropertyCompat<in T>>() + for (property in properties) { + if (animator.isPropertyAnimating(property)) { + animatingProperties.add(property) + } + } + + if (animatingProperties.size > 0) { + val latch = CountDownLatch(animatingProperties.size) + getAnimationTestHelper(animator).addTestEndListener( + object : PhysicsAnimator.EndListener<T> { + override fun onAnimationEnd( + target: T, + property: FloatPropertyCompat<in T>, + canceled: Boolean, + finalValue: Float, + finalVelocity: Float, + allRelevantPropertyAnimsEnded: Boolean + ) { + if (animatingProperties.contains(property)) { + latch.countDown() + } + } + }) + + latch.await(timeoutMs, TimeUnit.MILLISECONDS) + } + } + + /** + * Blocks the calling thread until all animations of the given property (on all target objects) + * have ended. Useful when you don't have access to the objects being animated, but still need + * to wait for them to end so that other testable side effects occur (such as update/end + * listeners). + */ + @JvmStatic + @Throws(InterruptedException::class) + fun <T : Any> blockUntilAnimationsEnd( + properties: FloatPropertyCompat<in T> + ) { + for (target in allAnimatedObjects) { + try { + blockUntilAnimationsEnd( + PhysicsAnimator.getInstance(target) as PhysicsAnimator<T>, properties) + } catch (e: ClassCastException) { + // Keep checking the other objects for ones whose types match the provided + // properties. + } + } + } + + /** + * Blocks the calling thread until the first animation frame in which predicate returns true. If + * the given object isn't animating, returns without blocking. + */ + @JvmStatic + @Throws(InterruptedException::class) + fun <T : Any> blockUntilFirstAnimationFrameWhereTrue( + animator: PhysicsAnimator<T>, + predicate: (T) -> Boolean + ) { + if (animator.isRunning()) { + val latch = CountDownLatch(1) + getAnimationTestHelper(animator).addTestUpdateListener(object : PhysicsAnimator + .UpdateListener<T> { + override fun onAnimationUpdateForProperty( + target: T, + values: UpdateMap<T> + ) { + if (predicate(target)) { + latch.countDown() + } + } + }) + + latch.await(timeoutMs, TimeUnit.MILLISECONDS) + } + } + + /** + * Verifies that the animator reported animation frame values to update listeners that satisfy + * the given matchers, in order. Not all frames need to satisfy a matcher - we'll run through + * all animation frames, and check them against the current predicate. If it returns false, we + * continue through the frames until it returns true, and then move on to the next matcher. + * Verification fails if we run out of frames while unsatisfied matchers remain. + * + * If verification is successful, all frames to this point are considered 'verified' and will be + * cleared. Subsequent calls to this method will start verification at the next animation frame. + * + * Example: Verify that an animation surpassed x = 50f before going negative. + * verifyAnimationUpdateFrames( + * animator, TRANSLATION_X, + * { u -> u.value > 50f }, + * { u -> u.value < 0f }) + * + * Example: verify that an animation went backwards at some point while still being on-screen. + * verifyAnimationUpdateFrames( + * animator, TRANSLATION_X, + * { u -> u.velocity < 0f && u.value >= 0f }) + * + * This method is intended to help you test longer, more complicated animations where it's + * critical that certain values were reached. Using this method to test short animations can + * fail due to the animation having fewer frames than provided matchers. For example, an + * animation from x = 1f to x = 5f might only have two frames, at x = 3f and x = 5f. The + * following would then fail despite it seeming logically sound: + * + * verifyAnimationUpdateFrames( + * animator, TRANSLATION_X, + * { u -> u.value > 1f }, + * { u -> u.value > 2f }, + * { u -> u.value > 3f }) + * + * Tests might also fail if your matchers are too granular, such as this example test after an + * animation from x = 0f to x = 100f. It's unlikely there was a frame specifically between 2f + * and 3f. + * + * verifyAnimationUpdateFrames( + * animator, TRANSLATION_X, + * { u -> u.value > 2f && u.value < 3f }, + * { u -> u.value >= 50f }) + * + * Failures will print a helpful log of all animation frames so you can see what caused the test + * to fail. + */ + fun <T : Any> verifyAnimationUpdateFrames( + animator: PhysicsAnimator<T>, + property: FloatPropertyCompat<in T>, + firstUpdateMatcher: UpdateMatcher, + vararg additionalUpdateMatchers: UpdateMatcher + ) { + val updateFrames: UpdateFramesPerProperty<T> = getAnimationUpdateFrames(animator) + val matchers = ArrayDeque<UpdateMatcher>( + additionalUpdateMatchers.toList()) + val frameTraceMessage = StringBuilder() + + var curMatcher = firstUpdateMatcher + + // Loop through the updates from the testable animator. + for (update in updateFrames[property] + ?: error("No frames for given target object and property.")) { + + // Check whether this frame satisfies the current matcher. + if (curMatcher(update)) { + + // If that was the last unsatisfied matcher, we're good here. 'Verify' all remaining + // frames and return without failing. + if (matchers.size == 0) { + getAnimationUpdateFrames(animator).remove(property) + return + } + + frameTraceMessage.append("$update\t(satisfied matcher)\n") + curMatcher = matchers.pop() // Get the next matcher and keep going. + } else { + frameTraceMessage.append("${update}\n") + } + } + + val readablePropertyName = PhysicsAnimator.getReadablePropertyName(property) + getAnimationUpdateFrames(animator).remove(property) + + throw RuntimeException( + "Failed to verify animation frames for property $readablePropertyName: " + + "Provided ${additionalUpdateMatchers.size + 1} matchers, " + + "however ${matchers.size + 1} remained unsatisfied.\n\n" + + "All frames:\n$frameTraceMessage") + } + + /** + * Overload of [verifyAnimationUpdateFrames] that builds matchers for you, from given float + * values. For example, to verify that an animations passed from 0f to 50f to 100f back to 50f: + * + * verifyAnimationUpdateFrames(animator, TRANSLATION_X, 0f, 50f, 100f, 50f) + * + * This verifies that update frames were received with values of >= 0f, >= 50f, >= 100f, and + * <= 50f. + * + * The same caveats apply: short animations might not have enough frames to satisfy all of the + * matchers, and overly specific calls (such as 0f, 1f, 2f, 3f, etc. for an animation from + * x = 0f to x = 100f) might fail as the animation only had frames at 0f, 25f, 50f, 75f, and + * 100f. As with [verifyAnimationUpdateFrames], failures will print a helpful log of all frames + * so you can see what caused the test to fail. + */ + fun <T : Any> verifyAnimationUpdateFrames( + animator: PhysicsAnimator<T>, + property: FloatPropertyCompat<in T>, + startValue: Float, + firstTargetValue: Float, + vararg additionalTargetValues: Float + ) { + val matchers = ArrayList<UpdateMatcher>() + + val values = ArrayList<Float>().also { + it.add(firstTargetValue) + it.addAll(additionalTargetValues.toList()) + } + + var prevVal = startValue + for (value in values) { + if (value > prevVal) { + matchers.add { update -> update.value >= value } + } else { + matchers.add { update -> update.value <= value } + } + + prevVal = value + } + + verifyAnimationUpdateFrames( + animator, property, matchers[0], *matchers.drop(0).toTypedArray()) + } + + /** + * Returns all of the values that have ever been reported to update listeners, per property. + */ + fun <T : Any> getAnimationUpdateFrames(animator: PhysicsAnimator<T>): + UpdateFramesPerProperty<T> { + return animatorTestHelpers[animator]?.getUpdates() as UpdateFramesPerProperty<T> + } + + /** + * Clears animation frame updates from the given animator so they aren't used the next time its + * passed to [verifyAnimationUpdateFrames]. + */ + fun <T : Any> clearAnimationUpdateFrames(animator: PhysicsAnimator<T>) { + animatorTestHelpers[animator]?.clearUpdates() + } + + private fun <T> getAnimationTestHelper(animator: PhysicsAnimator<T>): AnimatorTestHelper<T> { + return animatorTestHelpers[animator] as AnimatorTestHelper<T> + } + + /** + * Helper class for testing an animator. This replaces the animator's start action with + * [startForTest] and adds test listeners to enable other test utility behaviors. We build one + * these for each Animator and keep them around so we can access the updates. + */ + class AnimatorTestHelper<T> (private val animator: PhysicsAnimator<T>) { + + /** All updates received for each property animation. */ + private val allUpdates = + ArrayMap<FloatPropertyCompat<in T>, ArrayList<PhysicsAnimator.AnimationUpdate>>() + + private val testEndListeners = ArrayList<PhysicsAnimator.EndListener<T>>() + private val testUpdateListeners = ArrayList<PhysicsAnimator.UpdateListener<T>>() + + init { + animator.startAction = ::startForTest + } + + internal fun addTestEndListener(listener: PhysicsAnimator.EndListener<T>) { + testEndListeners.add(listener) + } + + internal fun addTestUpdateListener(listener: PhysicsAnimator.UpdateListener<T>) { + testUpdateListeners.add(listener) + } + + internal fun getUpdates(): UpdateFramesPerProperty<T> { + return allUpdates + } + + internal fun clearUpdates() { + allUpdates.clear() + } + + private fun startForTest() { + // The testable animator needs to block the main thread until super.start() has been + // called, since callers expect .start() to be synchronous but we're posting it to a + // handler here. We may also continue blocking until all animations end, if + // startBlocksUntilAnimationsEnd = true. + val unblockLatch = CountDownLatch(if (startBlocksUntilAnimationsEnd) 2 else 1) + + animationThreadHandler.post { + val animatedProperties = animator.getAnimatedProperties() + + // Add an update listener that dispatches to any test update listeners added by + // tests. + animator.addUpdateListener(object : PhysicsAnimator.UpdateListener<T> { + override fun onAnimationUpdateForProperty( + target: T, + values: ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate> + ) { + for (listener in testUpdateListeners) { + listener.onAnimationUpdateForProperty(target, values) + } + } + }) + + // Add an end listener that dispatches to any test end listeners added by tests, and + // unblocks the main thread if required. + animator.addEndListener(object : PhysicsAnimator.EndListener<T> { + override fun onAnimationEnd( + target: T, + property: FloatPropertyCompat<in T>, + canceled: Boolean, + finalValue: Float, + finalVelocity: Float, + allRelevantPropertyAnimsEnded: Boolean + ) { + for (listener in testEndListeners) { + listener.onAnimationEnd( + target, property, canceled, finalValue, finalVelocity, + allRelevantPropertyAnimsEnded) + } + + if (allRelevantPropertyAnimsEnded) { + testEndListeners.clear() + testUpdateListeners.clear() + + if (startBlocksUntilAnimationsEnd) { + unblockLatch.countDown() + } + } + } + }) + + val updateListeners = ArrayList<PhysicsAnimator.UpdateListener<T>>().also { + it.add(object : PhysicsAnimator.UpdateListener<T> { + override fun onAnimationUpdateForProperty( + target: T, + values: ArrayMap<FloatPropertyCompat<in T>, + PhysicsAnimator.AnimationUpdate> + ) { + values.forEach { (property, value) -> + allUpdates.getOrPut(property, { ArrayList() }).add(value) + } + } + }) + } + + /** + * Add an internal listener at the head of the list that captures update values + * directly from DynamicAnimation. We use this to build a list of all updates so we + * can verify that InternalListener dispatches to the real listeners properly. + */ + animator.internalListeners.add(0, animator.InternalListener( + animatedProperties, + updateListeners, + ArrayList(), + ArrayList())) + + animator.startInternal() + unblockLatch.countDown() + } + + unblockLatch.await(timeoutMs, TimeUnit.MILLISECONDS) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java index 24f49ff99879..3e90581292ce 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java @@ -18,11 +18,12 @@ package com.android.systemui.util.concurrency; import android.content.Context; import android.os.Handler; +import android.os.Looper; import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.BgHandler; +import com.android.systemui.dagger.qualifiers.BgLooper; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.dagger.qualifiers.MainHandler; +import com.android.systemui.dagger.qualifiers.MainLooper; import java.util.concurrent.Executor; @@ -38,8 +39,8 @@ public abstract class ConcurrencyModule { * Provide a Background-Thread Executor by default. */ @Provides - public static Executor provideExecutor(@BgHandler Handler handler) { - return new ExecutorImpl(handler); + public static Executor provideExecutor(@BgLooper Looper looper) { + return new ExecutorImpl(new Handler(looper)); } /** @@ -47,8 +48,8 @@ public abstract class ConcurrencyModule { */ @Provides @Background - public static Executor provideBackgroundExecutor(@BgHandler Handler handler) { - return new ExecutorImpl(handler); + public static Executor provideBackgroundExecutor(@BgLooper Looper looper) { + return new ExecutorImpl(new Handler(looper)); } /** @@ -64,8 +65,8 @@ public abstract class ConcurrencyModule { * Provide a Background-Thread Executor by default. */ @Provides - public static DelayableExecutor provideDelayableExecutor(@BgHandler Handler handler) { - return new ExecutorImpl(handler); + public static DelayableExecutor provideDelayableExecutor(@BgLooper Looper looper) { + return new ExecutorImpl(new Handler(looper)); } /** @@ -73,8 +74,8 @@ public abstract class ConcurrencyModule { */ @Provides @Background - public static DelayableExecutor provideBackgroundDelayableExecutor(@BgHandler Handler handler) { - return new ExecutorImpl(handler); + public static DelayableExecutor provideBackgroundDelayableExecutor(@BgLooper Looper looper) { + return new ExecutorImpl(new Handler(looper)); } /** @@ -82,7 +83,7 @@ public abstract class ConcurrencyModule { */ @Provides @Main - public static DelayableExecutor provideMainDelayableExecutor(@MainHandler Handler handler) { - return new ExecutorImpl(handler); + public static DelayableExecutor provideMainDelayableExecutor(@MainLooper Looper looper) { + return new ExecutorImpl(new Handler(looper)); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java index 2ccececbdd9d..2bf855a27cc4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java @@ -261,7 +261,7 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void testRemoveBubble_withDismissedNotif() { - mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mEntryListener.onNotificationAdded(mRow.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); assertTrue(mBubbleController.hasBubbles()); @@ -304,7 +304,7 @@ public class BubbleControllerTest extends SysuiTestCase { assertFalse(mBubbleController.isStackExpanded()); // Mark it as a bubble and add it explicitly - mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mEntryListener.onNotificationAdded(mRow.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); // We should have bubbles & their notifs should not be suppressed @@ -334,8 +334,8 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void testCollapseAfterChangingExpandedBubble() { // Mark it as a bubble and add it explicitly - mEntryListener.onPendingEntryAdded(mRow.getEntry()); - mEntryListener.onPendingEntryAdded(mRow2.getEntry()); + mEntryListener.onNotificationAdded(mRow.getEntry()); + mEntryListener.onNotificationAdded(mRow2.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); mBubbleController.updateBubble(mRow2.getEntry()); @@ -377,7 +377,7 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void testExpansionRemovesShowInShadeAndDot() { // Mark it as a bubble and add it explicitly - mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mEntryListener.onNotificationAdded(mRow.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); // We should have bubbles & their notifs should not be suppressed @@ -403,7 +403,7 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void testUpdateWhileExpanded_DoesntChangeShowInShadeAndDot() { // Mark it as a bubble and add it explicitly - mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mEntryListener.onNotificationAdded(mRow.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); // We should have bubbles & their notifs should not be suppressed @@ -439,8 +439,8 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void testRemoveLastExpandedCollapses() { // Mark it as a bubble and add it explicitly - mEntryListener.onPendingEntryAdded(mRow.getEntry()); - mEntryListener.onPendingEntryAdded(mRow2.getEntry()); + mEntryListener.onNotificationAdded(mRow.getEntry()); + mEntryListener.onNotificationAdded(mRow2.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); mBubbleController.updateBubble(mRow2.getEntry()); verify(mBubbleStateChangeListener).onHasBubblesChanged(true); @@ -483,7 +483,7 @@ public class BubbleControllerTest extends SysuiTestCase { Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE, false /* enableFlag */); // Add the auto expand bubble - mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mEntryListener.onNotificationAdded(mRow.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); // Expansion shouldn't change @@ -501,7 +501,7 @@ public class BubbleControllerTest extends SysuiTestCase { Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE, true /* enableFlag */); // Add the auto expand bubble - mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mEntryListener.onNotificationAdded(mRow.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); // Expansion should change @@ -519,7 +519,7 @@ public class BubbleControllerTest extends SysuiTestCase { Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, true /* enableFlag */); // Add the suppress notif bubble - mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mEntryListener.onNotificationAdded(mRow.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); // Notif should be suppressed because we were foreground @@ -564,7 +564,7 @@ public class BubbleControllerTest extends SysuiTestCase { public void testExpandStackAndSelectBubble_removedFirst() { final String key = mRow.getEntry().getKey(); - mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mEntryListener.onNotificationAdded(mRow.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); // Simulate notification cancellation. @@ -576,7 +576,7 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void testMarkNewNotificationAsShowInShade() { - mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mEntryListener.onNotificationAdded(mRow.getEntry()); assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade( mRow.getEntry().getKey())); @@ -586,7 +586,7 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void testAddNotif_notBubble() { - mEntryListener.onPendingEntryAdded(mNonBubbleNotifRow.getEntry()); + mEntryListener.onNotificationAdded(mNonBubbleNotifRow.getEntry()); mEntryListener.onPreEntryUpdated(mNonBubbleNotifRow.getEntry()); verify(mBubbleStateChangeListener, never()).onHasBubblesChanged(anyBoolean()); @@ -631,7 +631,7 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void testRemoveBubble_succeeds_appCancel() { - mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mEntryListener.onNotificationAdded(mRow.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); assertTrue(mBubbleController.hasBubbles()); @@ -646,7 +646,7 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void removeBubble_fails_clearAll() { - mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mEntryListener.onNotificationAdded(mRow.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); assertTrue(mBubbleController.hasBubbles()); @@ -669,7 +669,7 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void removeBubble_fails_userDismissNotif() { - mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mEntryListener.onNotificationAdded(mRow.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); assertTrue(mBubbleController.hasBubbles()); @@ -692,7 +692,7 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void removeBubble_succeeds_userDismissBubble_userDimissNotif() { - mEntryListener.onPendingEntryAdded(mRow.getEntry()); + mEntryListener.onNotificationAdded(mRow.getEntry()); mBubbleController.updateBubble(mRow.getEntry()); assertTrue(mBubbleController.hasBubbles()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java index f2665ef3c845..775acdf58c64 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java @@ -33,6 +33,7 @@ import static org.mockito.Mockito.when; import android.app.AlarmManager; import android.database.ContentObserver; +import android.hardware.Sensor; import android.hardware.display.AmbientDisplayConfiguration; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -80,6 +81,8 @@ public class DozeSensorsTest extends SysuiTestCase { private TriggerSensor mTriggerSensor; @Mock private DozeLog mDozeLog; + @Mock + private Sensor mProximitySensor; private SensorManagerPlugin.SensorEventListener mWakeLockScreenListener; private TestableLooper mTestableLooper; private DozeSensors mDozeSensors; @@ -90,6 +93,7 @@ public class DozeSensorsTest extends SysuiTestCase { mTestableLooper = TestableLooper.get(this); when(mAmbientDisplayConfiguration.getWakeLockScreenDebounce()).thenReturn(5000L); when(mAmbientDisplayConfiguration.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)).thenReturn(mProximitySensor); doAnswer(invocation -> { ((Runnable) invocation.getArgument(0)).run(); return null; @@ -98,6 +102,14 @@ public class DozeSensorsTest extends SysuiTestCase { } @Test + public void testRegisterProx() { + // We should not register with the sensor manager initially. + verify(mSensorManager, never()).registerListener(any(), any(Sensor.class), anyInt()); + mDozeSensors.setProxListening(true); + verify(mSensorManager).registerListener(any(), any(Sensor.class), anyInt()); + } + + @Test public void testSensorDebounce() { mDozeSensors.setListening(true); @@ -116,6 +128,7 @@ public class DozeSensorsTest extends SysuiTestCase { @Test public void testSetListening_firstTrue_registerSettingsObserver() { + verify(mSensorManager, never()).registerListener(any(), any(Sensor.class), anyInt()); mDozeSensors.setListening(true); verify(mTriggerSensor).registerSettingsObserver(any(ContentObserver.class)); @@ -123,6 +136,7 @@ public class DozeSensorsTest extends SysuiTestCase { @Test public void testSetListening_twiceTrue_onlyRegisterSettingsObserverOnce() { + verify(mSensorManager, never()).registerListener(any(), any(Sensor.class), anyInt()); mDozeSensors.setListening(true); mDozeSensors.setListening(true); @@ -131,6 +145,7 @@ public class DozeSensorsTest extends SysuiTestCase { @Test public void testSetPaused_doesntPause_sensors() { + verify(mSensorManager, never()).registerListener(any(), any(Sensor.class), anyInt()); mDozeSensors.setListening(true); verify(mTriggerSensor).setListening(eq(true)); @@ -147,8 +162,7 @@ public class DozeSensorsTest extends SysuiTestCase { TestableDozeSensors() { super(getContext(), mAlarmManager, mSensorManager, mDozeParameters, - mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, - mAlwaysOnDisplayPolicy, mDozeLog); + mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog); for (TriggerSensor sensor : mSensors) { if (sensor instanceof PluginSensor && ((PluginSensor) sensor).mPluginSensor.getType() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java index 8e6f4d7dd793..d580234725ac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java @@ -27,7 +27,8 @@ import android.app.Notification; import android.app.NotificationManager; import android.os.Handler; import android.os.UserHandle; -import android.service.notification.NotificationListenerService; +import android.service.notification.NotificationListenerService.Ranking; +import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -35,8 +36,7 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import com.android.systemui.statusbar.notification.NotificationEntryManager; -import com.android.systemui.statusbar.phone.NotificationGroupManager; +import com.android.systemui.statusbar.NotificationListener.NotifServiceListener; import org.junit.Before; import org.junit.Test; @@ -51,52 +51,39 @@ public class NotificationListenerTest extends SysuiTestCase { private static final String TEST_PACKAGE_NAME = "test"; private static final int TEST_UID = 0; - @Mock private NotificationListenerService.RankingMap mRanking; - - // Dependency mocks: - @Mock private NotificationEntryManager mEntryManager; + @Mock private NotifServiceListener mServiceListener; @Mock private NotificationManager mNotificationManager; - @Mock private NotificationGroupManager mNotificationGroupManager; private NotificationListener mListener; private StatusBarNotification mSbn; + private RankingMap mRanking = new RankingMap(new Ranking[0]); @Before public void setUp() { MockitoAnnotations.initMocks(this); - mContext.addMockSystemService(NotificationManager.class, mNotificationManager); - mListener = new NotificationListener(mContext, - new Handler(TestableLooper.get(this).getLooper()), mEntryManager, - mNotificationGroupManager); + mListener = new NotificationListener( + mContext, + mNotificationManager, + new Handler(TestableLooper.get(this).getLooper())); mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0, new Notification(), UserHandle.CURRENT, null, 0); - } - @Test - public void testNotificationAddCallsAddNotification() { - mListener.onNotificationPosted(mSbn, mRanking); - TestableLooper.get(this).processAllMessages(); - verify(mEntryManager).addNotification(mSbn, mRanking); + mListener.addNotificationListener(mServiceListener); } @Test - public void testNotificationUpdateCallsUpdateNotification() { - when(mEntryManager.getActiveNotificationUnfiltered(mSbn.getKey())) - .thenReturn( - new NotificationEntryBuilder() - .setSbn(mSbn) - .build()); + public void testNotificationAddCallsAddNotification() { mListener.onNotificationPosted(mSbn, mRanking); TestableLooper.get(this).processAllMessages(); - verify(mEntryManager).updateNotification(mSbn, mRanking); + verify(mServiceListener).onNotificationPosted(mSbn, mRanking); } @Test public void testNotificationRemovalCallsRemoveNotification() { mListener.onNotificationRemoved(mSbn, mRanking); TestableLooper.get(this).processAllMessages(); - verify(mEntryManager).removeNotification(eq(mSbn.getKey()), eq(mRanking), anyInt()); + verify(mServiceListener).onNotificationRemoved(eq(mSbn), eq(mRanking), anyInt()); } @Test @@ -104,7 +91,7 @@ public class NotificationListenerTest extends SysuiTestCase { mListener.onNotificationRankingUpdate(mRanking); TestableLooper.get(this).processAllMessages(); // RankingMap may be modified by plugins. - verify(mEntryManager).updateNotificationRanking(any()); + verify(mServiceListener).onNotificationRankingUpdate(any()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index ba7b2e2e14c3..cef210cef19a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -26,12 +26,14 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.app.KeyguardManager; +import android.app.Notification; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -198,11 +200,13 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, 0); + final Notification notification = mock(Notification.class); + when(notification.isForegroundService()).thenReturn(true); NotificationEntry entry = new NotificationEntryBuilder() .setImportance(IMPORTANCE_LOW) + .setNotification(notification) .build(); entry.setBucket(BUCKET_SILENT); - entry.setIsHighPriority(true); assertFalse(mLockscreenUserManager.shouldShowOnKeyguard(entry)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/RankingBuilder.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/RankingBuilder.java index d003b994f6dd..5310dd8a61f9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/RankingBuilder.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/RankingBuilder.java @@ -76,6 +76,7 @@ public class RankingBuilder { mSmartActions = copyList(ranking.getSmartActions()); mSmartReplies = copyList(ranking.getSmartReplies()); mCanBubble = ranking.canBubble(); + mIsVisuallyInterruptive = ranking.visuallyInterruptive(); } public Ranking build() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryTest.java new file mode 100644 index 000000000000..721bbdc1f700 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryTest.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection; + +import static android.app.NotificationManager.IMPORTANCE_HIGH; +import static android.app.NotificationManager.IMPORTANCE_MIN; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.NotificationEntryBuilder; +import com.android.systemui.statusbar.RankingBuilder; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class GroupEntryTest extends SysuiTestCase { + @Test + public void testIsHighPriority_addChild() { + // GIVEN a GroupEntry with a lowPrioritySummary and no children + final GroupEntry parentEntry = new GroupEntry("test_group_key"); + final NotificationEntry lowPrioritySummary = createNotifEntry(false); + setSummary(parentEntry, lowPrioritySummary); + assertFalse(parentEntry.isHighPriority()); + + // WHEN we add a high priority child and invalidate derived members + addChild(parentEntry, createNotifEntry(true)); + parentEntry.invalidateDerivedMembers(); + + // THEN the GroupEntry's priority is updated to high even though the summary is still low + // priority + assertTrue(parentEntry.isHighPriority()); + assertFalse(lowPrioritySummary.isHighPriority()); + } + + @Test + public void testIsHighPriority_clearChildren() { + // GIVEN a GroupEntry with a lowPrioritySummary and high priority children + final GroupEntry parentEntry = new GroupEntry("test_group_key"); + setSummary(parentEntry, createNotifEntry(false)); + addChild(parentEntry, createNotifEntry(true)); + addChild(parentEntry, createNotifEntry(true)); + addChild(parentEntry, createNotifEntry(true)); + assertTrue(parentEntry.isHighPriority()); + + // WHEN we clear the children and invalidate derived members + parentEntry.clearChildren(); + parentEntry.invalidateDerivedMembers(); + + // THEN the parentEntry isn't high priority anymore + assertFalse(parentEntry.isHighPriority()); + } + + @Test + public void testIsHighPriority_summaryUpdated() { + // GIVEN a GroupEntry with a lowPrioritySummary and no children + final GroupEntry parentEntry = new GroupEntry("test_group_key"); + final NotificationEntry lowPrioritySummary = createNotifEntry(false); + setSummary(parentEntry, lowPrioritySummary); + assertFalse(parentEntry.isHighPriority()); + + // WHEN the summary changes to high priority and invalidates its derived members + lowPrioritySummary.setRanking( + new RankingBuilder() + .setKey(lowPrioritySummary.getKey()) + .setImportance(IMPORTANCE_HIGH) + .build()); + lowPrioritySummary.invalidateDerivedMembers(); + assertTrue(lowPrioritySummary.isHighPriority()); + + // THEN the GroupEntry's priority is updated to high + assertTrue(parentEntry.isHighPriority()); + } + + @Test + public void testIsHighPriority_checkChildrenToCalculatePriority() { + // GIVEN: + // GroupEntry = parentEntry, summary = lowPrioritySummary + // NotificationEntry = lowPriorityChild + // NotificationEntry = highPriorityChild + final GroupEntry parentEntry = new GroupEntry("test_group_key"); + setSummary(parentEntry, createNotifEntry(false)); + addChild(parentEntry, createNotifEntry(false)); + addChild(parentEntry, createNotifEntry(true)); + + // THEN the GroupEntry parentEntry is high priority since it has a high priority child + assertTrue(parentEntry.isHighPriority()); + } + + @Test + public void testIsHighPriority_childEntryRankingUpdated() { + // GIVEN: + // GroupEntry = parentEntry, summary = lowPrioritySummary + // NotificationEntry = lowPriorityChild + final GroupEntry parentEntry = new GroupEntry("test_group_key"); + final NotificationEntry lowPriorityChild = createNotifEntry(false); + setSummary(parentEntry, createNotifEntry(false)); + addChild(parentEntry, lowPriorityChild); + + // WHEN the child entry ranking changes to high priority and invalidates its derived members + lowPriorityChild.setRanking( + new RankingBuilder() + .setKey(lowPriorityChild.getKey()) + .setImportance(IMPORTANCE_HIGH) + .build()); + lowPriorityChild.invalidateDerivedMembers(); + + // THEN the parent entry's high priority value is updated - but not the parent's summary + assertTrue(parentEntry.isHighPriority()); + assertFalse(parentEntry.getSummary().isHighPriority()); + } + + private NotificationEntry createNotifEntry(boolean highPriority) { + return new NotificationEntryBuilder() + .setImportance(highPriority ? IMPORTANCE_HIGH : IMPORTANCE_MIN) + .build(); + } + + private void setSummary(GroupEntry parent, NotificationEntry summary) { + parent.setSummary(summary); + summary.setParent(parent); + } + + private void addChild(GroupEntry parent, NotificationEntry child) { + parent.addChild(child); + child.setParent(parent); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java index e1beb34db6bd..8d9537d9afc3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java @@ -99,7 +99,7 @@ public class NotifCollectionTest extends SysuiTestCase { // Capture the listener object that the collection registers with the listener service so // we can simulate listener service events in tests below - verify(mListenerService).setDownstreamListener(mListenerCaptor.capture()); + verify(mListenerService).addNotificationListener(mListenerCaptor.capture()); mServiceListener = checkNotNull(mListenerCaptor.getValue()); mNoMan = new NoManSimulator(mServiceListener); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java index 536aeb4d0163..17d556dbd201 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java @@ -21,6 +21,8 @@ import static android.app.Notification.CATEGORY_CALL; import static android.app.Notification.CATEGORY_EVENT; import static android.app.Notification.CATEGORY_MESSAGE; import static android.app.Notification.CATEGORY_REMINDER; +import static android.app.NotificationManager.IMPORTANCE_HIGH; +import static android.app.NotificationManager.IMPORTANCE_MIN; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking; @@ -91,6 +93,44 @@ public class NotificationEntryTest extends SysuiTestCase { } @Test + public void testIsHighPriority_notificationUpdates() { + // GIVEN a notification with high importance + final NotificationEntry entryHigh = new NotificationEntryBuilder() + .setImportance(IMPORTANCE_HIGH) + .build(); + + // WHEN we get the value for the high priority entry, we're caching the high priority value + assertTrue(entryHigh.isHighPriority()); + + // WHEN we change the ranking and derived members (high priority) are invalidated + entryHigh.setRanking( + new RankingBuilder() + .setKey(entryHigh.getKey()) + .setImportance(IMPORTANCE_MIN) + .build()); + entryHigh.invalidateDerivedMembers(); + + // THEN the priority is recalculated and is now low + assertFalse(entryHigh.isHighPriority()); + + // WHEN the sbn is updated to have messaging style (high priority characteristic) + // AND the entry invalidates its derived members + final Notification notification = + new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("")) + .build(); + final StatusBarNotification sbn = entryHigh.getSbn(); + entryHigh.setSbn(new StatusBarNotification( + sbn.getPackageName(), sbn.getPackageName(), + sbn.getId(), sbn.getTag(), sbn.getUid(), sbn.getInitialPid(), + notification, sbn.getUser(), sbn.getOverrideGroupKey(), 0)); + entryHigh.invalidateDerivedMembers(); + + // THEN the priority is recalculated and is now high + assertTrue(entryHigh.isHighPriority()); + } + + @Test public void testIsExemptFromDndVisualSuppression_foreground() { mEntry.getSbn().getNotification().flags = Notification.FLAG_FOREGROUND_SERVICE; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManagerTest.kt index cda1538e899d..1764bef57d27 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationRankingManagerTest.kt @@ -17,17 +17,12 @@ package com.android.systemui.statusbar.notification.collection import android.app.Notification -import android.app.NotificationChannel import android.app.NotificationManager.IMPORTANCE_DEFAULT +import android.app.NotificationManager.IMPORTANCE_HIGH import android.app.NotificationManager.IMPORTANCE_LOW import android.service.notification.NotificationListenerService.RankingMap -import android.service.notification.StatusBarNotification import android.testing.AndroidTestingRunner - -import org.junit.runner.RunWith - import androidx.test.filters.SmallTest - import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.NotificationEntryBuilder import com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking @@ -42,12 +37,9 @@ import com.android.systemui.statusbar.phone.NotificationGroupManager import com.android.systemui.statusbar.policy.HeadsUpManager import dagger.Lazy import junit.framework.Assert.assertEquals -import junit.framework.Assert.assertFalse -import junit.framework.Assert.assertTrue - import org.junit.Before import org.junit.Test -import org.mockito.Mockito.`when` +import org.junit.runner.RunWith import org.mockito.Mockito.mock @SmallTest @@ -76,106 +68,34 @@ class NotificationRankingManagerTest : SysuiTestCase() { } @Test - fun testPeopleNotification_isHighPriority() { - val notification = Notification.Builder(mContext, "test") - .build() - - val sbn = StatusBarNotification("pkg", "pkg", 0, "tag", 0, 0, - notification, mContext.user, "", 0) - - `when`(personNotificationIdentifier.isPeopleNotification(sbn)).thenReturn(true) - - val e = NotificationEntryBuilder() - .setNotification(notification) - .setSbn(sbn) - .build() - - assertTrue(rankingManager.isHighPriority2(e)) - } - - @Test - fun lowForegroundHighPriority() { - val notification = mock(Notification::class.java) - `when`(notification.isForegroundService).thenReturn(true) - - val sbn = StatusBarNotification("pkg", "pkg", 0, "tag", 0, 0, - notification, mContext.user, "", 0) - - val e = NotificationEntryBuilder() - .setNotification(notification) - .setSbn(sbn) - .build() - - modifyRanking(e) - .setImportance(IMPORTANCE_LOW) - .build() - - assertTrue(rankingManager.isHighPriority2(e)) - } - - @Test - fun userChangeTrumpsHighPriorityCharacteristics() { - val notification = Notification.Builder(mContext, "test") - .setStyle(Notification.MessagingStyle("")) - .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true) - .build() - - val sbn = StatusBarNotification("pkg", "pkg", 0, "tag", 0, 0, - notification, mContext.user, "", 0) - - `when`(personNotificationIdentifier.isPeopleNotification(sbn)).thenReturn(true) - - val channel = NotificationChannel("a", "a", IMPORTANCE_LOW) - channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE) - - val e = NotificationEntryBuilder() - .setSbn(sbn) - .setChannel(channel) - .build() - - assertFalse(rankingManager.isHighPriority2(e)) - } - - @Test fun testSort_highPriorityTrumpsNMSRank() { // NMS rank says A and then B. But A is not high priority and B is, so B should sort in // front - val aN = Notification.Builder(mContext, "test") - .setStyle(Notification.MessagingStyle("")) - .build() val a = NotificationEntryBuilder() + .setImportance(IMPORTANCE_LOW) // low priority + .setRank(1) // NMS says rank first .setPkg("pkg") .setOpPkg("pkg") .setTag("tag") - .setNotification(aN) + .setNotification( + Notification.Builder(mContext, "test") + .build()) .setUser(mContext.getUser()) .setOverrideGroupKey("") .build() - a.setIsHighPriority(false) - - modifyRanking(a) - .setImportance(IMPORTANCE_LOW) - .setRank(1) - .build() - - val bN = Notification.Builder(mContext, "test") - .setStyle(Notification.MessagingStyle("")) - .build() val b = NotificationEntryBuilder() + .setImportance(IMPORTANCE_HIGH) // high priority + .setRank(2) // NMS says rank second .setPkg("pkg2") .setOpPkg("pkg2") .setTag("tag") - .setNotification(bN) + .setNotification( + Notification.Builder(mContext, "test") + .build()) .setUser(mContext.getUser()) .setOverrideGroupKey("") .build() - b.setIsHighPriority(true) - - modifyRanking(b) - .setImportance(IMPORTANCE_LOW) - .setRank(2) - .build() assertEquals( listOf(b, a), @@ -189,6 +109,8 @@ class NotificationRankingManagerTest : SysuiTestCase() { .setStyle(Notification.MessagingStyle("")) .build() val a = NotificationEntryBuilder() + .setRank(1) + .setImportance(IMPORTANCE_HIGH) .setPkg("pkg") .setOpPkg("pkg") .setTag("tag") @@ -196,17 +118,13 @@ class NotificationRankingManagerTest : SysuiTestCase() { .setUser(mContext.getUser()) .setOverrideGroupKey("") .build() - a.setIsHighPriority(false) - - modifyRanking(a) - .setImportance(IMPORTANCE_LOW) - .setRank(1) - .build() val bN = Notification.Builder(mContext, "test") .setStyle(Notification.MessagingStyle("")) .build() val b = NotificationEntryBuilder() + .setRank(2) + .setImportance(IMPORTANCE_HIGH) .setPkg("pkg2") .setOpPkg("pkg2") .setTag("tag") @@ -214,12 +132,6 @@ class NotificationRankingManagerTest : SysuiTestCase() { .setUser(mContext.getUser()) .setOverrideGroupKey("") .build() - b.setIsHighPriority(false) - - modifyRanking(b) - .setImportance(IMPORTANCE_LOW) - .setRank(2) - .build() assertEquals( listOf(a, b), @@ -239,7 +151,7 @@ class NotificationRankingManagerTest : SysuiTestCase() { .setOverrideGroupKey("") .build() - modifyRanking(e).setImportance(IMPORTANCE_DEFAULT) .build() + modifyRanking(e).setImportance(IMPORTANCE_DEFAULT).build() rankingManager.updateRanking(RankingMap(arrayOf(e.ranking)), listOf(e), "test") assertEquals(e.bucket, BUCKET_ALERTING) @@ -281,11 +193,6 @@ class NotificationRankingManagerTest : SysuiTestCase() { sectionsFeatureManager, peopleNotificationIdentifier ) { - - fun isHighPriority2(e: NotificationEntry): Boolean { - return isHighPriority(e) - } - fun applyTestRankingMap(r: RankingMap) { rankingMap = r } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/provider/IsHighPriorityProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/provider/IsHighPriorityProviderTest.java new file mode 100644 index 000000000000..11488a0b699e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/provider/IsHighPriorityProviderTest.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.provider; + +import static android.app.NotificationManager.IMPORTANCE_HIGH; +import static android.app.NotificationManager.IMPORTANCE_LOW; +import static android.app.NotificationManager.IMPORTANCE_MIN; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.Person; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.NotificationEntryBuilder; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class IsHighPriorityProviderTest extends SysuiTestCase { + private IsHighPriorityProvider mIsHighPriorityProvider; + + @Before + public void setup() { + mIsHighPriorityProvider = new IsHighPriorityProvider(); + } + + @Test + public void testCache() { + // GIVEN a notification with high importance + final NotificationEntry entryHigh = new NotificationEntryBuilder() + .setImportance(IMPORTANCE_HIGH) + .build(); + + // GIVEN notification with min importance + final NotificationEntry entryMin = new NotificationEntryBuilder() + .setImportance(IMPORTANCE_MIN) + .build(); + + // WHEN we get the value for the high priority entry + assertTrue(mIsHighPriorityProvider.get(entryHigh)); + + // THEN the value is cached, so even when passed an entryMin, we still get high priority + assertTrue(mIsHighPriorityProvider.get(entryMin)); + + // UNTIL the provider is invalidated + mIsHighPriorityProvider.invalidate(); + + // THEN the priority is recalculated + assertFalse(mIsHighPriorityProvider.get(entryMin)); + } + + @Test + public void highImportance() { + // GIVEN notification has high importance + final NotificationEntry entry = new NotificationEntryBuilder() + .setImportance(IMPORTANCE_HIGH) + .build(); + + // THEN it has high priority + assertTrue(mIsHighPriorityProvider.get(entry)); + } + + @Test + public void peopleNotification() { + // GIVEN notification is low importance but has a person associated with it + final Notification notification = new Notification.Builder(mContext, "test") + .addPerson( + new Person.Builder() + .setName("name") + .setKey("abc") + .setUri("uri") + .setBot(true) + .build()) + .build(); + + final NotificationEntry entry = new NotificationEntryBuilder() + .setNotification(notification) + .setImportance(IMPORTANCE_LOW) + .build(); + + // THEN it has high priority + assertTrue(mIsHighPriorityProvider.get(entry)); + } + + @Test + public void messagingStyle() { + // GIVEN notification is low importance but has messaging style + final Notification notification = new Notification.Builder(mContext, "test") + .setStyle(new Notification.MessagingStyle("")) + .build(); + + final NotificationEntry entry = new NotificationEntryBuilder() + .setNotification(notification) + .build(); + + // THEN it has high priority + assertTrue(mIsHighPriorityProvider.get(entry)); + } + + @Test + public void lowImportanceForeground() { + // GIVEN notification is low importance and is associated with a foreground service + final Notification notification = mock(Notification.class); + when(notification.isForegroundService()).thenReturn(true); + + final NotificationEntry entry = new NotificationEntryBuilder() + .setNotification(notification) + .setImportance(IMPORTANCE_LOW) + .build(); + + // THEN it has high priority + assertTrue(mIsHighPriorityProvider.get(entry)); + } + + @Test + public void minImportanceForeground() { + // GIVEN notification is low importance and is associated with a foreground service + final Notification notification = mock(Notification.class); + when(notification.isForegroundService()).thenReturn(true); + + final NotificationEntry entry = new NotificationEntryBuilder() + .setNotification(notification) + .setImportance(IMPORTANCE_MIN) + .build(); + + // THEN it does NOT have high priority + assertFalse(mIsHighPriorityProvider.get(entry)); + } + + @Test + public void userChangeTrumpsHighPriorityCharacteristics() { + // GIVEN notification has high priority characteristics but the user changed the importance + // to less than IMPORTANCE_DEFAULT (ie: IMPORTANCE_LOW or IMPORTANCE_MIN) + final Notification notification = new Notification.Builder(mContext, "test") + .addPerson( + new Person.Builder() + .setName("name") + .setKey("abc") + .setUri("uri") + .setBot(true) + .build()) + .setStyle(new Notification.MessagingStyle("")) + .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true) + .build(); + + final NotificationChannel channel = new NotificationChannel("a", "a", + IMPORTANCE_LOW); + channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); + + final NotificationEntry entry = new NotificationEntryBuilder() + .setNotification(notification) + .setChannel(channel) + .build(); + + // THEN it does NOT have high priority + assertFalse(mIsHighPriorityProvider.get(entry)); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java index 43d39a2f9511..ccc9496368e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java @@ -20,6 +20,7 @@ import static android.app.AppOpsManager.OP_CAMERA; import static android.app.AppOpsManager.OP_RECORD_AUDIO; import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; +import static android.app.NotificationManager.IMPORTANCE_HIGH; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE; import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking; @@ -383,15 +384,14 @@ public class NotificationGutsManagerTest extends SysuiTestCase { NotificationInfo notificationInfoView = mock(NotificationInfo.class); ExpandableNotificationRow row = spy(mHelper.createRow()); row.setBlockingHelperShowing(true); - modifyRanking(row.getEntry()) + final NotificationEntry entry = row.getEntry(); + modifyRanking(entry) .setUserSentiment(USER_SENTIMENT_NEGATIVE) - .setImportance(IMPORTANCE_DEFAULT) + .setImportance(IMPORTANCE_HIGH) .build(); - row.getEntry().setIsHighPriority(true); - when(row.getIsNonblockable()).thenReturn(false); - StatusBarNotification statusBarNotification = row.getEntry().getSbn(); - NotificationEntry entry = row.getEntry(); + when(row.getIsNonblockable()).thenReturn(false); + StatusBarNotification statusBarNotification = entry.getSbn(); mGutsManager.initializeNotificationInfo(row, notificationInfoView); verify(notificationInfoView).bindNotification( @@ -408,7 +408,7 @@ public class NotificationGutsManagerTest extends SysuiTestCase { eq(false), eq(false), eq(true) /* isForBlockingHelper */, - eq(IMPORTANCE_DEFAULT), + eq(IMPORTANCE_HIGH), eq(true) /* wasShownHighPriority */); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/animation/PhysicsAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/animation/PhysicsAnimatorTest.kt new file mode 100644 index 000000000000..a39fbc4c232e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/animation/PhysicsAnimatorTest.kt @@ -0,0 +1,436 @@ +package com.android.systemui.util.animation + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.util.ArrayMap +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.dynamicanimation.animation.SpringForce +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.animation.PhysicsAnimator.EndListener +import com.android.systemui.util.animation.PhysicsAnimator.UpdateListener +import com.android.systemui.util.animation.PhysicsAnimatorTestUtils.clearAnimationUpdateFrames +import com.android.systemui.util.animation.PhysicsAnimatorTestUtils.getAnimationUpdateFrames +import com.android.systemui.util.animation.PhysicsAnimatorTestUtils.verifyAnimationUpdateFrames +import org.junit.After +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.MockitoAnnotations + +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner::class) +@SmallTest +class PhysicsAnimatorTest : SysuiTestCase() { + private lateinit var viewGroup: ViewGroup + private lateinit var testView: View + private lateinit var testView2: View + + private lateinit var animator: PhysicsAnimator<View> + + private val springConfig = PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY) + private val flingConfig = PhysicsAnimator.FlingConfig(2f) + + private lateinit var mockUpdateListener: UpdateListener<View> + private lateinit var mockEndListener: EndListener<View> + private lateinit var mockEndAction: Runnable + + private fun <T> eq(value: T): T = Mockito.eq(value) ?: value + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + mockUpdateListener = mock(UpdateListener::class.java) as UpdateListener<View> + mockEndListener = mock(EndListener::class.java) as EndListener<View> + mockEndAction = mock(Runnable::class.java) + + viewGroup = FrameLayout(context) + testView = View(context) + testView2 = View(context) + viewGroup.addView(testView) + viewGroup.addView(testView2) + + PhysicsAnimatorTestUtils.prepareForTest() + + // Most of our tests involve checking the end state of animations, so we want calls that + // start animations to block the test thread until the animations have ended. + PhysicsAnimatorTestUtils.setAllAnimationsBlock(true) + + animator = PhysicsAnimator.getInstance(testView) + } + + @After + fun tearDown() { + PhysicsAnimatorTestUtils.tearDown() + } + + @Test + fun testOneAnimatorPerView() { + assertEquals(animator, PhysicsAnimator.getInstance(testView)) + assertEquals(PhysicsAnimator.getInstance(testView), PhysicsAnimator.getInstance(testView)) + assertNotEquals(animator, PhysicsAnimator.getInstance(testView2)) + } + + @Test + fun testSpringOneProperty() { + animator + .spring(DynamicAnimation.TRANSLATION_X, 50f, springConfig) + .start() + + assertEquals(testView.translationX, 50f, 1f) + } + + @Test + fun testSpringMultipleProperties() { + animator + .spring(DynamicAnimation.TRANSLATION_X, 10f, springConfig) + .spring(DynamicAnimation.TRANSLATION_Y, 50f, springConfig) + .spring(DynamicAnimation.SCALE_Y, 1.1f, springConfig) + .start() + + assertEquals(10f, testView.translationX, 1f) + assertEquals(50f, testView.translationY, 1f) + assertEquals(1.1f, testView.scaleY, 0.01f) + } + + @Test + fun testFling() { + val startTime = System.currentTimeMillis() + + animator + .fling(DynamicAnimation.TRANSLATION_X, 1000f /* startVelocity */, flingConfig) + .fling(DynamicAnimation.TRANSLATION_Y, 500f, flingConfig) + .start() + + val elapsedTimeSeconds = (System.currentTimeMillis() - startTime) / 1000f + + // If the fling worked, the view should be somewhere between its starting position and the + // and the theoretical no-friction maximum of startVelocity (in pixels per second) + // multiplied by elapsedTimeSeconds. We can't calculate an exact expected location for a + // fling, so this is close enough. + assertTrue(testView.translationX > 0f) + assertTrue(testView.translationX < 1000f * elapsedTimeSeconds) + assertTrue(testView.translationY > 0f) + assertTrue(testView.translationY < 500f * elapsedTimeSeconds) + } + + @Test + @Throws(InterruptedException::class) + fun testEndListenersAndActions() { + PhysicsAnimatorTestUtils.setAllAnimationsBlock(false) + animator + .spring(DynamicAnimation.TRANSLATION_X, 10f, springConfig) + .spring(DynamicAnimation.TRANSLATION_Y, 500f, springConfig) + .addEndListener(mockEndListener) + .withEndActions(mockEndAction::run) + .start() + + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_X) + + // Once TRANSLATION_X is done, the view should be at x = 10... + assertEquals(10f, testView.translationX, 1f) + + // / ...TRANSLATION_Y should still be running... + assertTrue(animator.isPropertyAnimating(DynamicAnimation.TRANSLATION_Y)) + + // ...and our end listener should have been called with x = 10, velocity = 0, and allEnded = + // false since TRANSLATION_Y is still running. + verify(mockEndListener).onAnimationEnd( + testView, + DynamicAnimation.TRANSLATION_X, + canceled = false, + finalValue = 10f, + finalVelocity = 0f, + allRelevantPropertyAnimsEnded = false) + verifyNoMoreInteractions(mockEndListener) + + // The end action should not have been run yet. + verify(mockEndAction, times(0)).run() + + // Block until TRANSLATION_Y finishes. + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_Y) + + // The view should have been moved. + assertEquals(10f, testView.translationX, 1f) + assertEquals(500f, testView.translationY, 1f) + + // The end listener should have been called, this time with TRANSLATION_Y, y = 50, and + // allEnded = true. + verify(mockEndListener).onAnimationEnd( + testView, + DynamicAnimation.TRANSLATION_Y, + canceled = false, + finalValue = 500f, + finalVelocity = 0f, + allRelevantPropertyAnimsEnded = true) + verifyNoMoreInteractions(mockEndListener) + + // Now that all properties are done animating, the end action should have been called. + verify(mockEndAction, times(1)).run() + } + + @Test + fun testUpdateListeners() { + animator + .spring(DynamicAnimation.TRANSLATION_X, 100f, springConfig) + .spring(DynamicAnimation.TRANSLATION_Y, 50f, springConfig) + .addUpdateListener(object : UpdateListener<View> { + override fun onAnimationUpdateForProperty( + target: View, + values: UpdateMap<View> + ) { + mockUpdateListener.onAnimationUpdateForProperty(target, values) + } + }) + .start() + + verifyUpdateListenerCalls(animator, mockUpdateListener) + } + + @Test + fun testListenersNotCalledOnSubsequentAnimations() { + animator + .spring(DynamicAnimation.TRANSLATION_X, 10f, springConfig) + .addUpdateListener(mockUpdateListener) + .addEndListener(mockEndListener) + .withEndActions(mockEndAction::run) + .start() + + verifyUpdateListenerCalls(animator, mockUpdateListener) + verify(mockEndListener, times(1)).onAnimationEnd( + eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), anyFloat(), anyFloat(), + eq(true)) + verify(mockEndAction, times(1)).run() + + animator + .spring(DynamicAnimation.TRANSLATION_X, 0f, springConfig) + .start() + + // We didn't pass any of the listeners/actions to the subsequent animation, so they should + // never have been called. + verifyNoMoreInteractions(mockUpdateListener) + verifyNoMoreInteractions(mockEndListener) + verifyNoMoreInteractions(mockEndAction) + } + + @Test + @Throws(InterruptedException::class) + fun testAnimationsUpdatedWhileInMotion() { + PhysicsAnimatorTestUtils.setAllAnimationsBlock(false) + + // Spring towards x = 100f. + animator + .spring( + DynamicAnimation.TRANSLATION_X, + 100f, + springConfig) + .start() + + // Block until it reaches x = 50f. + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue( + animator) { view -> view.translationX > 50f } + + // Translation X value at the time of reversing the animation to spring to x = 0f. + val reversalTranslationX = testView.translationX + + // Spring back towards 0f. + animator + .spring( + DynamicAnimation.TRANSLATION_X, + 0f, + // Lower the stiffness to ensure the update listener receives at least one + // update frame where the view has continued to move to the right. + springConfig.apply { stiffness = SpringForce.STIFFNESS_LOW }) + .start() + + // Wait for TRANSLATION_X. + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_X) + + // Verify that the animation continued past the X value at the time of reversal, before + // springing back. This ensures the change in direction was not abrupt. + verifyAnimationUpdateFrames( + animator, DynamicAnimation.TRANSLATION_X, + { u -> u.value > reversalTranslationX }, + { u -> u.value < reversalTranslationX }) + + // Verify that the view is where it should be. + assertEquals(0f, testView.translationX, 1f) + } + + @Test + @Throws(InterruptedException::class) + fun testAnimationsUpdatedWhileInMotion_originalListenersStillCalled() { + PhysicsAnimatorTestUtils.setAllAnimationsBlock(false) + + // Spring TRANSLATION_X to 100f, with an update and end listener provided. + animator + .spring(DynamicAnimation.TRANSLATION_X, 100f, springConfig) + .addUpdateListener(mockUpdateListener) + .addEndListener(mockEndListener) + .start() + + // Wait until the animation is halfway there. + PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue( + animator) { view -> view.translationX > 50f } + + // The end listener shouldn't have been called since the animation hasn't ended. + verifyNoMoreInteractions(mockEndListener) + + // Make sure we called the update listener with appropriate values. + verifyAnimationUpdateFrames(animator, DynamicAnimation.TRANSLATION_X, + { u -> u.value > 0f }, + { u -> u.value >= 50f }) + + // Mock a second end listener. + val secondEndListener = mock(EndListener::class.java) as EndListener<View> + val secondUpdateListener = mock(UpdateListener::class.java) as UpdateListener<View> + + // Start a new animation that springs both TRANSLATION_X and TRANSLATION_Y, and provide it + // the second end listener. This new end listener should be called for the end of + // TRANSLATION_X and TRANSLATION_Y, with allEnded = true when both have ended. + animator + .spring(DynamicAnimation.TRANSLATION_X, 200f, springConfig) + .spring(DynamicAnimation.TRANSLATION_Y, 4000f, springConfig) + .addUpdateListener(secondUpdateListener) + .addEndListener(secondEndListener) + .start() + + // Wait for TRANSLATION_X to end. + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_X) + + // The update listener provided to the initial animation call (the one that only animated + // TRANSLATION_X) should have been called with values on the way to x = 200f. This is + // because the second animation call updated the original TRANSLATION_X animation. + verifyAnimationUpdateFrames( + animator, DynamicAnimation.TRANSLATION_X, + { u -> u.value > 100f }, { u -> u.value >= 200f }) + + // The original end listener should also have been called, with allEnded = true since it was + // provided to an animator that animated only TRANSLATION_X. + verify(mockEndListener, times(1)) + .onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X, false, 200f, 0f, true) + verifyNoMoreInteractions(mockEndListener) + + // The second end listener should have been called, but with allEnded = false since it was + // provided to an animator that animated both TRANSLATION_X and TRANSLATION_Y. + verify(secondEndListener, times(1)) + .onAnimationEnd(testView, DynamicAnimation.TRANSLATION_X, false, 200f, 0f, false) + verifyNoMoreInteractions(secondEndListener) + + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(animator, DynamicAnimation.TRANSLATION_Y) + + // The original end listener shouldn't receive any callbacks because it was not provided to + // an animator that animated TRANSLATION_Y. + verifyNoMoreInteractions(mockEndListener) + + verify(secondEndListener, times(1)) + .onAnimationEnd(testView, DynamicAnimation.TRANSLATION_Y, false, 4000f, 0f, true) + verifyNoMoreInteractions(secondEndListener) + } + + @Test + fun testFlingRespectsMinMax() { + animator + .fling(DynamicAnimation.TRANSLATION_X, + startVelocity = 1000f, + friction = 1.1f, + max = 10f) + .addEndListener(mockEndListener) + .start() + + // Ensure that the view stopped at x = 10f, and the end listener was called once with that + // value. + assertEquals(10f, testView.translationX, 1f) + verify(mockEndListener, times(1)) + .onAnimationEnd( + eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), eq(10f), + anyFloat(), eq(true)) + + animator + .fling( + DynamicAnimation.TRANSLATION_X, + startVelocity = -1000f, + friction = 1.1f, + min = -5f) + .addEndListener(mockEndListener) + .start() + + // Ensure that the view stopped at x = -5f, and the end listener was called once with that + // value. + assertEquals(-5f, testView.translationX, 1f) + verify(mockEndListener, times(1)) + .onAnimationEnd( + eq(testView), eq(DynamicAnimation.TRANSLATION_X), eq(false), eq(-5f), + anyFloat(), eq(true)) + } + + @Test + fun testExtensionProperty() { + testView + .physicsAnimator + .spring(DynamicAnimation.TRANSLATION_X, 200f) + .start() + + assertEquals(200f, testView.translationX, 1f) + } + + /** + * Verifies that the calls to the mock update listener match the animation update frames + * reported by the test internal listener, in order. + */ + private fun <T : Any> verifyUpdateListenerCalls( + animator: PhysicsAnimator<T>, + mockUpdateListener: UpdateListener<T> + ) { + val updates = getAnimationUpdateFrames(animator) + + for (invocation in Mockito.mockingDetails(mockUpdateListener).invocations) { + + // Grab the update map of Property -> AnimationUpdate that was passed to the mock update + // listener. + val updateMap = invocation.arguments[1] + as ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate> + + // + for ((property, update) in updateMap) { + val updatesForProperty = updates[property]!! + + // This update should be the next one in the list for this property. + if (update != updatesForProperty[0]) { + Assert.fail("The update listener was called with an unexpected value: $update.") + } + + updatesForProperty.remove(update) + } + + // Mark this invocation verified. + verify(mockUpdateListener).onAnimationUpdateForProperty(animator.target, updateMap) + } + + verifyNoMoreInteractions(mockUpdateListener) + + // Since we were removing values as matching invocations were found, there should no longer + // be any values remaining. If there are, it means the update listener wasn't notified when + // it should have been. + assertEquals(0, + updates.values.fold(0, { count, propertyUpdates -> count + propertyUpdates.size })) + + clearAnimationUpdateFrames(animator) + } +}
\ No newline at end of file diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/AccessibilityGestureDetector.java b/services/accessibility/java/com/android/server/accessibility/gestures/AccessibilityGestureDetector.java deleted file mode 100644 index 3dfe59e142a6..000000000000 --- a/services/accessibility/java/com/android/server/accessibility/gestures/AccessibilityGestureDetector.java +++ /dev/null @@ -1,640 +0,0 @@ -/* - ** Copyright 2015, The Android Open Source Project - ** - ** Licensed under the Apache License, Version 2.0 (the "License"); - ** you may not use this file except in compliance with the License. - ** You may obtain a copy of the License at - ** - ** http://www.apache.org/licenses/LICENSE-2.0 - ** - ** Unless required by applicable law or agreed to in writing, software - ** distributed under the License is distributed on an "AS IS" BASIS, - ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ** See the License for the specific language governing permissions and - ** limitations under the License. - */ - -package com.android.server.accessibility.gestures; - -import android.accessibilityservice.AccessibilityGestureEvent; -import android.accessibilityservice.AccessibilityService; -import android.content.Context; -import android.gesture.GesturePoint; -import android.graphics.PointF; -import android.util.Slog; -import android.util.TypedValue; -import android.view.GestureDetector; -import android.view.MotionEvent; - -import java.util.ArrayList; - -/** - * This class handles gesture detection for the Touch Explorer. It collects - * touch events and determines when they match a gesture, as well as when they - * won't match a gesture. These state changes are then surfaced to mListener. - */ -class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListener { - - private static final boolean DEBUG = false; - - // Tag for logging received events. - private static final String LOG_TAG = "AccessibilityGestureDetector"; - - // Constants for sampling motion event points. - // We sample based on a minimum distance between points, primarily to improve accuracy by - // reducing noisy minor changes in direction. - private static final float MIN_INCHES_BETWEEN_SAMPLES = 0.1f; - private final float mMinPixelsBetweenSamplesX; - private final float mMinPixelsBetweenSamplesY; - - // Constants for separating gesture segments - private static final float ANGLE_THRESHOLD = 0.0f; - - // Constants for line segment directions - private static final int LEFT = 0; - private static final int RIGHT = 1; - private static final int UP = 2; - private static final int DOWN = 3; - private static final int[][] DIRECTIONS_TO_GESTURE_ID = { - { - AccessibilityService.GESTURE_SWIPE_LEFT, - AccessibilityService.GESTURE_SWIPE_LEFT_AND_RIGHT, - AccessibilityService.GESTURE_SWIPE_LEFT_AND_UP, - AccessibilityService.GESTURE_SWIPE_LEFT_AND_DOWN - }, - { - AccessibilityService.GESTURE_SWIPE_RIGHT_AND_LEFT, - AccessibilityService.GESTURE_SWIPE_RIGHT, - AccessibilityService.GESTURE_SWIPE_RIGHT_AND_UP, - AccessibilityService.GESTURE_SWIPE_RIGHT_AND_DOWN - }, - { - AccessibilityService.GESTURE_SWIPE_UP_AND_LEFT, - AccessibilityService.GESTURE_SWIPE_UP_AND_RIGHT, - AccessibilityService.GESTURE_SWIPE_UP, - AccessibilityService.GESTURE_SWIPE_UP_AND_DOWN - }, - { - AccessibilityService.GESTURE_SWIPE_DOWN_AND_LEFT, - AccessibilityService.GESTURE_SWIPE_DOWN_AND_RIGHT, - AccessibilityService.GESTURE_SWIPE_DOWN_AND_UP, - AccessibilityService.GESTURE_SWIPE_DOWN - } - }; - - - /** - * Listener functions are called as a result of onMoveEvent(). The current - * MotionEvent in the context of these functions is the event passed into - * onMotionEvent. - */ - public interface Listener { - /** - * Called when the user has performed a double tap and then held down - * the second tap. - * - * @param event The most recent MotionEvent received. - * @param policyFlags The policy flags of the most recent event. - */ - void onDoubleTapAndHold(MotionEvent event, int policyFlags); - - /** - * Called when the user lifts their finger on the second tap of a double - * tap. - * - * @param event The most recent MotionEvent received. - * @param policyFlags The policy flags of the most recent event. - * - * @return true if the event is consumed, else false - */ - boolean onDoubleTap(MotionEvent event, int policyFlags); - - /** - * Called when the system has decided the event stream is a gesture. - * - * @return true if the event is consumed, else false - */ - boolean onGestureStarted(); - - /** - * Called when an event stream is recognized as a gesture. - * - * @param gestureEvent Information about the gesture. - * - * @return true if the event is consumed, else false - */ - boolean onGestureCompleted(AccessibilityGestureEvent gestureEvent); - - /** - * Called when the system has decided an event stream doesn't match any - * known gesture. - * - * @param event The most recent MotionEvent received. - * @param policyFlags The policy flags of the most recent event. - * - * @return true if the event is consumed, else false - */ - public boolean onGestureCancelled(MotionEvent event, int policyFlags); - } - - private final Listener mListener; - private final Context mContext; // Retained for on-demand construction of GestureDetector. - private final GestureDetector mGestureDetector; // Double-tap detector. - - // Indicates that a single tap has occurred. - private boolean mFirstTapDetected; - - // Indicates that the down event of a double tap has occured. - private boolean mDoubleTapDetected; - - // Indicates that motion events are being collected to match a gesture. - private boolean mRecognizingGesture; - - // Indicates that we've collected enough data to be sure it could be a - // gesture. - private boolean mGestureStarted; - - // Indicates that motion events from the second pointer are being checked - // for a double tap. - private boolean mSecondFingerDoubleTap; - - // Tracks the most recent time where ACTION_POINTER_DOWN was sent for the - // second pointer. - private long mSecondPointerDownTime; - - // Policy flags of the previous event. - private int mPolicyFlags; - - // These values track the previous point that was saved to use for gesture - // detection. They are only updated when the user moves more than the - // recognition threshold. - private float mPreviousGestureX; - private float mPreviousGestureY; - - // These values track the previous point that was used to determine if there - // was a transition into or out of gesture detection. They are updated when - // the user moves more than the detection threshold. - private float mBaseX; - private float mBaseY; - private long mBaseTime; - - // This is the calculated movement threshold used track if the user is still - // moving their finger. - private final float mGestureDetectionThreshold; - - // Buffer for storing points for gesture detection. - private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100); - - // The minimal delta between moves to add a gesture point. - private static final int TOUCH_TOLERANCE = 3; - - // The minimal score for accepting a predicted gesture. - private static final float MIN_PREDICTION_SCORE = 2.0f; - - // Distance a finger must travel before we decide if it is a gesture or not. - private static final int GESTURE_CONFIRM_MM = 10; - - // Time threshold used to determine if an interaction is a gesture or not. - // If the first movement of 1cm takes longer than this value, we assume it's - // a slow movement, and therefore not a gesture. - // - // This value was determined by measuring the time for the first 1cm - // movement when gesturing, and touch exploring. Based on user testing, - // all gestures started with the initial movement taking less than 100ms. - // When touch exploring, the first movement almost always takes longer than - // 200ms. - private static final long CANCEL_ON_PAUSE_THRESHOLD_NOT_STARTED_MS = 150; - - // Time threshold used to determine if a gesture should be cancelled. If - // the finger takes more than this time to move 1cm, the ongoing gesture is - // cancelled. - private static final long CANCEL_ON_PAUSE_THRESHOLD_STARTED_MS = 300; - - /** - * Construct the gesture detector for {@link TouchExplorer}. - * - * @see #AccessibilityGestureDetector(Context, Listener, GestureDetector) - */ - AccessibilityGestureDetector(Context context, Listener listener) { - this(context, listener, null); - } - - /** - * Construct the gesture detector for {@link TouchExplorer}. - * - * @param context A context handle for accessing resources. - * @param listener A listener to callback with gesture state or information. - * @param detector The gesture detector to handle touch event. If null the default one created - * in place, or for testing purpose. - */ - AccessibilityGestureDetector(Context context, Listener listener, GestureDetector detector) { - mListener = listener; - mContext = context; - - // Break the circular dependency between constructors and let the class to be testable - if (detector == null) { - mGestureDetector = new GestureDetector(context, this); - } else { - mGestureDetector = detector; - } - mGestureDetector.setOnDoubleTapListener(this); - mGestureDetectionThreshold = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1, - context.getResources().getDisplayMetrics()) * GESTURE_CONFIRM_MM; - - // Calculate minimum gesture velocity - final float pixelsPerInchX = context.getResources().getDisplayMetrics().xdpi; - final float pixelsPerInchY = context.getResources().getDisplayMetrics().ydpi; - mMinPixelsBetweenSamplesX = MIN_INCHES_BETWEEN_SAMPLES * pixelsPerInchX; - mMinPixelsBetweenSamplesY = MIN_INCHES_BETWEEN_SAMPLES * pixelsPerInchY; - } - - /** - * Handle a motion event. If an action is completed, the appropriate - * callback on mListener is called, and the return value of the callback is - * passed to the caller. - * - * @param event The transformed motion event to be handled. - * @param rawEvent The raw motion event. It's important that this be the raw - * event, before any transformations have been applied, so that measurements - * can be made in physical units. - * @param policyFlags Policy flags for the event. - * - * @return true if the event is consumed, else false - */ - public boolean onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { - // The accessibility gesture detector is interested in the movements in physical space, - // so it uses the rawEvent to ignore magnification and other transformations. - final float x = rawEvent.getX(); - final float y = rawEvent.getY(); - final long time = rawEvent.getEventTime(); - - mPolicyFlags = policyFlags; - switch (rawEvent.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - mDoubleTapDetected = false; - mSecondFingerDoubleTap = false; - mRecognizingGesture = true; - mGestureStarted = false; - mPreviousGestureX = x; - mPreviousGestureY = y; - mStrokeBuffer.clear(); - mStrokeBuffer.add(new GesturePoint(x, y, time)); - - mBaseX = x; - mBaseY = y; - mBaseTime = time; - break; - - case MotionEvent.ACTION_MOVE: - if (mRecognizingGesture) { - final float deltaX = mBaseX - x; - final float deltaY = mBaseY - y; - final double moveDelta = Math.hypot(deltaX, deltaY); - if (moveDelta > mGestureDetectionThreshold) { - // If the pointer has moved more than the threshold, - // update the stored values. - mBaseX = x; - mBaseY = y; - mBaseTime = time; - - // Since the pointer has moved, this is not a double - // tap. - mFirstTapDetected = false; - mDoubleTapDetected = false; - - // If this hasn't been confirmed as a gesture yet, send - // the event. - if (!mGestureStarted) { - mGestureStarted = true; - return mListener.onGestureStarted(); - } - } else if (!mFirstTapDetected) { - // The finger may not move if they are double tapping. - // In that case, we shouldn't cancel the gesture. - final long timeDelta = time - mBaseTime; - final long threshold = mGestureStarted ? - CANCEL_ON_PAUSE_THRESHOLD_STARTED_MS : - CANCEL_ON_PAUSE_THRESHOLD_NOT_STARTED_MS; - - // If the pointer hasn't moved for longer than the - // timeout, cancel gesture detection. - if (timeDelta > threshold) { - cancelGesture(); - return mListener.onGestureCancelled(rawEvent, policyFlags); - } - } - - final float dX = Math.abs(x - mPreviousGestureX); - final float dY = Math.abs(y - mPreviousGestureY); - if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) { - mPreviousGestureX = x; - mPreviousGestureY = y; - mStrokeBuffer.add(new GesturePoint(x, y, time)); - } - } - break; - - case MotionEvent.ACTION_UP: - if (mDoubleTapDetected) { - return finishDoubleTap(rawEvent, policyFlags); - } - if (mGestureStarted) { - final float dX = Math.abs(x - mPreviousGestureX); - final float dY = Math.abs(y - mPreviousGestureY); - if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) { - mStrokeBuffer.add(new GesturePoint(x, y, time)); - } - return recognizeGesture(rawEvent, policyFlags); - } - break; - - case MotionEvent.ACTION_POINTER_DOWN: - // Once a second finger is used, we're definitely not - // recognizing a gesture. - cancelGesture(); - - if (rawEvent.getPointerCount() == 2) { - // If this was the second finger, attempt to recognize double - // taps on it. - mSecondFingerDoubleTap = true; - mSecondPointerDownTime = time; - } else { - // If there are more than two fingers down, stop watching - // for a double tap. - mSecondFingerDoubleTap = false; - } - break; - - case MotionEvent.ACTION_POINTER_UP: - // If we're detecting taps on the second finger, see if we - // should finish the double tap. - if (mSecondFingerDoubleTap && mDoubleTapDetected) { - return finishDoubleTap(rawEvent, policyFlags); - } - break; - - case MotionEvent.ACTION_CANCEL: - clear(); - break; - } - - // If we're detecting taps on the second finger, map events from the - // finger to the first finger. - if (mSecondFingerDoubleTap) { - MotionEvent newEvent = mapSecondPointerToFirstPointer(rawEvent); - if (newEvent == null) { - return false; - } - boolean handled = mGestureDetector.onTouchEvent(newEvent); - newEvent.recycle(); - return handled; - } - - if (!mRecognizingGesture) { - return false; - } - - // Pass the transformed event on to the standard gesture detector. - return mGestureDetector.onTouchEvent(event); - } - - public void clear() { - mFirstTapDetected = false; - mDoubleTapDetected = false; - mSecondFingerDoubleTap = false; - mGestureStarted = false; - mGestureDetector.onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_CANCEL, - 0.0f, 0.0f, 0)); - cancelGesture(); - } - - - @Override - public void onLongPress(MotionEvent e) { - maybeSendLongPress(e, mPolicyFlags); - } - - @Override - public boolean onSingleTapUp(MotionEvent event) { - mFirstTapDetected = true; - return false; - } - - @Override - public boolean onSingleTapConfirmed(MotionEvent event) { - clear(); - return false; - } - - @Override - public boolean onDoubleTap(MotionEvent event) { - // The processing of the double tap is deferred until the finger is - // lifted, so that we can detect a long press on the second tap. - mDoubleTapDetected = true; - return false; - } - - private void maybeSendLongPress(MotionEvent event, int policyFlags) { - if (!mDoubleTapDetected) { - return; - } - - clear(); - - mListener.onDoubleTapAndHold(event, policyFlags); - } - - private boolean finishDoubleTap(MotionEvent event, int policyFlags) { - clear(); - - return mListener.onDoubleTap(event, policyFlags); - } - - private void cancelGesture() { - mRecognizingGesture = false; - mGestureStarted = false; - mStrokeBuffer.clear(); - } - - /** - * Looks at the sequence of motions in mStrokeBuffer, classifies the gesture, then calls - * Listener callbacks for success or failure. - * - * @param event The raw motion event to pass to the listener callbacks. - * @param policyFlags Policy flags for the event. - * - * @return true if the event is consumed, else false - */ - private boolean recognizeGesture(MotionEvent event, int policyFlags) { - if (mStrokeBuffer.size() < 2) { - return mListener.onGestureCancelled(event, policyFlags); - } - - // Look at mStrokeBuffer and extract 2 line segments, delimited by near-perpendicular - // direction change. - // Method: for each sampled motion event, check the angle of the most recent motion vector - // versus the preceding motion vector, and segment the line if the angle is about - // 90 degrees. - - ArrayList<PointF> path = new ArrayList<>(); - PointF lastDelimiter = new PointF(mStrokeBuffer.get(0).x, mStrokeBuffer.get(0).y); - path.add(lastDelimiter); - - float dX = 0; // Sum of unit vectors from last delimiter to each following point - float dY = 0; - int count = 0; // Number of points since last delimiter - float length = 0; // Vector length from delimiter to most recent point - - PointF next = new PointF(); - for (int i = 1; i < mStrokeBuffer.size(); ++i) { - next = new PointF(mStrokeBuffer.get(i).x, mStrokeBuffer.get(i).y); - if (count > 0) { - // Average of unit vectors from delimiter to following points - float currentDX = dX / count; - float currentDY = dY / count; - - // newDelimiter is a possible new delimiter, based on a vector with length from - // the last delimiter to the previous point, but in the direction of the average - // unit vector from delimiter to previous points. - // Using the averaged vector has the effect of "squaring off the curve", - // creating a sharper angle between the last motion and the preceding motion from - // the delimiter. In turn, this sharper angle achieves the splitting threshold - // even in a gentle curve. - PointF newDelimiter = new PointF(length * currentDX + lastDelimiter.x, - length * currentDY + lastDelimiter.y); - - // Unit vector from newDelimiter to the most recent point - float nextDX = next.x - newDelimiter.x; - float nextDY = next.y - newDelimiter.y; - float nextLength = (float) Math.sqrt(nextDX * nextDX + nextDY * nextDY); - nextDX = nextDX / nextLength; - nextDY = nextDY / nextLength; - - // Compare the initial motion direction to the most recent motion direction, - // and segment the line if direction has changed by about 90 degrees. - float dot = currentDX * nextDX + currentDY * nextDY; - if (dot < ANGLE_THRESHOLD) { - path.add(newDelimiter); - lastDelimiter = newDelimiter; - dX = 0; - dY = 0; - count = 0; - } - } - - // Vector from last delimiter to most recent point - float currentDX = next.x - lastDelimiter.x; - float currentDY = next.y - lastDelimiter.y; - length = (float) Math.sqrt(currentDX * currentDX + currentDY * currentDY); - - // Increment sum of unit vectors from delimiter to each following point - count = count + 1; - dX = dX + currentDX / length; - dY = dY + currentDY / length; - } - - path.add(next); - Slog.i(LOG_TAG, "path=" + path.toString()); - - // Classify line segments, and call Listener callbacks. - return recognizeGesturePath(event, policyFlags, path); - } - - /** - * Classifies a pair of line segments, by direction. - * Calls Listener callbacks for success or failure. - * - * @param event The raw motion event to pass to the listener's onGestureCanceled method. - * @param policyFlags Policy flags for the event. - * @param path A sequence of motion line segments derived from motion points in mStrokeBuffer. - * - * @return true if the event is consumed, else false - */ - private boolean recognizeGesturePath(MotionEvent event, int policyFlags, - ArrayList<PointF> path) { - - final int displayId = event.getDisplayId(); - if (path.size() == 2) { - PointF start = path.get(0); - PointF end = path.get(1); - - float dX = end.x - start.x; - float dY = end.y - start.y; - int direction = toDirection(dX, dY); - switch (direction) { - case LEFT: - return mListener.onGestureCompleted( - new AccessibilityGestureEvent(AccessibilityService.GESTURE_SWIPE_LEFT, - displayId)); - case RIGHT: - return mListener.onGestureCompleted( - new AccessibilityGestureEvent(AccessibilityService.GESTURE_SWIPE_RIGHT, - displayId)); - case UP: - return mListener.onGestureCompleted( - new AccessibilityGestureEvent(AccessibilityService.GESTURE_SWIPE_UP, - displayId)); - case DOWN: - return mListener.onGestureCompleted( - new AccessibilityGestureEvent(AccessibilityService.GESTURE_SWIPE_DOWN, - displayId)); - default: - // Do nothing. - } - - } else if (path.size() == 3) { - PointF start = path.get(0); - PointF mid = path.get(1); - PointF end = path.get(2); - - float dX0 = mid.x - start.x; - float dY0 = mid.y - start.y; - - float dX1 = end.x - mid.x; - float dY1 = end.y - mid.y; - - int segmentDirection0 = toDirection(dX0, dY0); - int segmentDirection1 = toDirection(dX1, dY1); - int gestureId = DIRECTIONS_TO_GESTURE_ID[segmentDirection0][segmentDirection1]; - return mListener.onGestureCompleted( - new AccessibilityGestureEvent(gestureId, displayId)); - } - // else if (path.size() < 2 || 3 < path.size()) then no gesture recognized. - return mListener.onGestureCancelled(event, policyFlags); - } - - /** Maps a vector to a dominant direction in set {LEFT, RIGHT, UP, DOWN}. */ - private static int toDirection(float dX, float dY) { - if (Math.abs(dX) > Math.abs(dY)) { - // Horizontal - return (dX < 0) ? LEFT : RIGHT; - } else { - // Vertical - return (dY < 0) ? UP : DOWN; - } - } - - private MotionEvent mapSecondPointerToFirstPointer(MotionEvent event) { - // Only map basic events when two fingers are down. - if (event.getPointerCount() != 2 || - (event.getActionMasked() != MotionEvent.ACTION_POINTER_DOWN && - event.getActionMasked() != MotionEvent.ACTION_POINTER_UP && - event.getActionMasked() != MotionEvent.ACTION_MOVE)) { - return null; - } - - int action = event.getActionMasked(); - - if (action == MotionEvent.ACTION_POINTER_DOWN) { - action = MotionEvent.ACTION_DOWN; - } else if (action == MotionEvent.ACTION_POINTER_UP) { - action = MotionEvent.ACTION_UP; - } - - // Map the information from the second pointer to the first. - return MotionEvent.obtain(mSecondPointerDownTime, event.getEventTime(), action, - event.getX(1), event.getY(1), event.getPressure(1), event.getSize(1), - event.getMetaState(), event.getXPrecision(), event.getYPrecision(), - event.getDeviceId(), event.getEdgeFlags()); - } -} diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java b/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java new file mode 100644 index 000000000000..9b7adc883dee --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility.gestures; + +import static android.accessibilityservice.AccessibilityService.GESTURE_DOUBLE_TAP; +import static android.accessibilityservice.AccessibilityService.GESTURE_DOUBLE_TAP_AND_HOLD; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN_AND_LEFT; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN_AND_RIGHT; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN_AND_UP; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_LEFT; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_LEFT_AND_DOWN; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_LEFT_AND_RIGHT; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_LEFT_AND_UP; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_RIGHT; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_RIGHT_AND_DOWN; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_RIGHT_AND_LEFT; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_RIGHT_AND_UP; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_UP; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_UP_AND_DOWN; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_UP_AND_LEFT; +import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_UP_AND_RIGHT; + +import static com.android.server.accessibility.gestures.Swipe.DOWN; +import static com.android.server.accessibility.gestures.Swipe.LEFT; +import static com.android.server.accessibility.gestures.Swipe.RIGHT; +import static com.android.server.accessibility.gestures.Swipe.UP; +import static com.android.server.accessibility.gestures.TouchExplorer.DEBUG; + +import android.accessibilityservice.AccessibilityGestureEvent; +import android.content.Context; +import android.os.Handler; +import android.util.Slog; +import android.view.MotionEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class coordinates a series of individual gesture matchers to serve as a unified gesture + * detector. Gesture matchers are tied to a single gesture. It calls listener callback functions + * when a gesture starts or completes. + */ +class GestureManifold implements GestureMatcher.StateChangeListener { + + private static final String LOG_TAG = "GestureManifold"; + + private final List<GestureMatcher> mGestures = new ArrayList<>(); + private final Context mContext; + // Handler for performing asynchronous operations. + private final Handler mHandler; + // Listener to be notified of gesture start and end. + private Listener mListener; + // Shared state information. + private TouchState mState; + + GestureManifold(Context context, Listener listener, TouchState state) { + mContext = context; + mHandler = new Handler(context.getMainLooper()); + mListener = listener; + mState = state; + // Set up gestures. + // Start with double tap. + mGestures.add(new MultiTap(context, 2, GESTURE_DOUBLE_TAP, this)); + mGestures.add(new MultiTapAndHold(context, 2, GESTURE_DOUBLE_TAP_AND_HOLD, this)); + // One-direction swipes. + mGestures.add(new Swipe(context, RIGHT, GESTURE_SWIPE_RIGHT, this)); + mGestures.add(new Swipe(context, LEFT, GESTURE_SWIPE_LEFT, this)); + mGestures.add(new Swipe(context, UP, GESTURE_SWIPE_UP, this)); + mGestures.add(new Swipe(context, DOWN, GESTURE_SWIPE_DOWN, this)); + // Two-direction swipes. + mGestures.add(new Swipe(context, LEFT, RIGHT, GESTURE_SWIPE_LEFT_AND_RIGHT, this)); + mGestures.add(new Swipe(context, LEFT, UP, GESTURE_SWIPE_LEFT_AND_UP, this)); + mGestures.add(new Swipe(context, LEFT, DOWN, GESTURE_SWIPE_LEFT_AND_DOWN, this)); + mGestures.add(new Swipe(context, RIGHT, UP, GESTURE_SWIPE_RIGHT_AND_UP, this)); + mGestures.add(new Swipe(context, RIGHT, DOWN, GESTURE_SWIPE_RIGHT_AND_DOWN, this)); + mGestures.add(new Swipe(context, RIGHT, LEFT, GESTURE_SWIPE_RIGHT_AND_LEFT, this)); + mGestures.add(new Swipe(context, DOWN, UP, GESTURE_SWIPE_DOWN_AND_UP, this)); + mGestures.add(new Swipe(context, DOWN, LEFT, GESTURE_SWIPE_DOWN_AND_LEFT, this)); + mGestures.add(new Swipe(context, DOWN, RIGHT, GESTURE_SWIPE_DOWN_AND_RIGHT, this)); + mGestures.add(new Swipe(context, UP, DOWN, GESTURE_SWIPE_UP_AND_DOWN, this)); + mGestures.add(new Swipe(context, UP, LEFT, GESTURE_SWIPE_UP_AND_LEFT, this)); + mGestures.add(new Swipe(context, UP, RIGHT, GESTURE_SWIPE_UP_AND_RIGHT, this)); + } + + /** + * Processes a motion event. + * + * @param event The event as received from the previous entry in the event stream. + * @param rawEvent The event without any transformations e.g. magnification. + * @param policyFlags + * @return True if the event has been appropriately handled by the gesture manifold and related + * callback functions, false if it should be handled further by the calling function. + */ + boolean onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (mState.isClear()) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + // Sanity safeguard: if touch state is clear, then matchers should always be clear + // before processing the next down event. + clear(); + } else { + // If for some reason other events come through while in the clear state they could + // compromise the state of particular matchers, so we just ignore them. + return false; + } + } + for (GestureMatcher matcher : mGestures) { + if (matcher.getState() != GestureMatcher.STATE_GESTURE_CANCELED) { + if (DEBUG) { + Slog.d(LOG_TAG, matcher.toString()); + } + matcher.onMotionEvent(event, rawEvent, policyFlags); + if (DEBUG) { + Slog.d(LOG_TAG, matcher.toString()); + } + if (matcher.getState() == GestureMatcher.STATE_GESTURE_COMPLETED) { + // Here we just clear and return. The actual gesture dispatch is done in + // onStateChanged(). + clear(); + // No need to process this event any further. + return true; + } + } + } + return false; + } + + public void clear() { + for (GestureMatcher matcher : mGestures) { + matcher.clear(); + } + } + + /** + * Listener that receives notifications of the state of the gesture detector. Listener functions + * are called as a result of onMotionEvent(). The current MotionEvent in the context of these + * functions is the event passed into onMotionEvent. + */ + public interface Listener { + /** + * Called when the user has performed a double tap and then held down the second tap. + */ + void onDoubleTapAndHold(); + + /** + * Called when the user lifts their finger on the second tap of a double tap. + * @return true if the event is consumed, else false + */ + boolean onDoubleTap(); + + /** + * Called when the system has decided the event stream is a gesture. + * + * @return true if the event is consumed, else false + */ + boolean onGestureStarted(); + + /** + * Called when an event stream is recognized as a gesture. + * + * @param gestureEvent Information about the gesture. + * @return true if the event is consumed, else false + */ + boolean onGestureCompleted(AccessibilityGestureEvent gestureEvent); + + /** + * Called when the system has decided an event stream doesn't match any known gesture. + * + * @param event The most recent MotionEvent received. + * @param policyFlags The policy flags of the most recent event. + * @return true if the event is consumed, else false + */ + boolean onGestureCancelled(MotionEvent event, MotionEvent rawEvent, int policyFlags); + } + + @Override + public void onStateChanged( + int gestureId, int state, MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (state == GestureMatcher.STATE_GESTURE_STARTED && !mState.isGestureDetecting()) { + mListener.onGestureStarted(); + } else if (state == GestureMatcher.STATE_GESTURE_COMPLETED) { + onGestureCompleted(gestureId); + } else if (state == GestureMatcher.STATE_GESTURE_CANCELED && mState.isGestureDetecting()) { + // We only want to call the cancelation callback if there are no other pending + // detectors. + for (GestureMatcher matcher : mGestures) { + if (matcher.getState() == GestureMatcher.STATE_GESTURE_STARTED) { + return; + } + } + if (DEBUG) { + Slog.d(LOG_TAG, "Cancelling."); + } + mListener.onGestureCancelled(event, rawEvent, policyFlags); + } + } + + private void onGestureCompleted(int gestureId) { + MotionEvent event = mState.getLastReceivedEvent(); + // Note that gestures that complete immediately call clear() from onMotionEvent. + // Gestures that complete on a delay call clear() here. + switch (gestureId) { + case GESTURE_DOUBLE_TAP: + mListener.onDoubleTap(); + clear(); + break; + case GESTURE_DOUBLE_TAP_AND_HOLD: + mListener.onDoubleTapAndHold(); + clear(); + break; + default: + AccessibilityGestureEvent gestureEvent = + new AccessibilityGestureEvent(gestureId, event.getDisplayId()); + mListener.onGestureCompleted(gestureEvent); + break; + } + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/GestureMatcher.java b/services/accessibility/java/com/android/server/accessibility/gestures/GestureMatcher.java new file mode 100644 index 000000000000..0b30ff57ddde --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/gestures/GestureMatcher.java @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility.gestures; + +import static com.android.server.accessibility.gestures.TouchExplorer.DEBUG; + +import android.annotation.IntDef; +import android.os.Handler; +import android.util.Slog; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +/** + * This class describes a common base for gesture matchers. A gesture matcher checks a series of + * motion events against a single gesture. Coordinating the individual gesture matchers is done by + * the GestureManifold. To create a new Gesture, extend this class and override the onDown, onMove, + * onUp, etc methods as necessary. If you don't override a method your matcher will do nothing in + * response to that type of event. Finally, be sure to give your gesture a name by overriding + * getGestureName(). + */ +abstract class GestureMatcher { + // Potential states for this individual gesture matcher. + // In STATE_CLEAR, this matcher is accepting new motion events but has not formally signaled + // that there is enough data to judge that a gesture has started. + static final int STATE_CLEAR = 0; + // In STATE_GESTURE_STARTED, this matcher continues to accept motion events and it has signaled + // to the gesture manifold that what looks like the specified gesture has started. + static final int STATE_GESTURE_STARTED = 1; + // In STATE_GESTURE_COMPLETED, this matcher has successfully matched the specified gesture. and + // will not accept motion events until it is cleared. + static final int STATE_GESTURE_COMPLETED = 2; + // In STATE_GESTURE_CANCELED, this matcher will not accept new motion events because it is + // impossible that this set of motion events will match the specified gesture. + static final int STATE_GESTURE_CANCELED = 3; + + @IntDef({STATE_CLEAR, STATE_GESTURE_STARTED, STATE_GESTURE_COMPLETED, STATE_GESTURE_CANCELED}) + public @interface State {} + + @State private int mState = STATE_CLEAR; + // The id number of the gesture that gets passed to accessibility services. + private final int mGestureId; + // handler for asynchronous operations like timeouts + private final Handler mHandler; + + private final StateChangeListener mListener; + + // Use this to transition to new states after a delay. + // e.g. cancel or complete after some timeout. + // Convenience functions for tapTimeout and doubleTapTimeout are already defined here. + protected final DelayedTransition mDelayedTransition; + + GestureMatcher(int gestureId, Handler handler, StateChangeListener listener) { + mGestureId = gestureId; + mHandler = handler; + mDelayedTransition = new DelayedTransition(); + mListener = listener; + } + + /** + * Resets all state information for this matcher. Subclasses that include their own state + * information should override this method to reset their own state information and call + * super.clear(). + */ + protected void clear() { + mState = STATE_CLEAR; + cancelPendingTransitions(); + } + + public int getState() { + return mState; + } + + /** + * Transitions to a new state and notifies any listeners. Note that any pending transitions are + * canceled. + */ + private void setState( + @State int state, MotionEvent event, MotionEvent rawEvent, int policyFlags) { + mState = state; + cancelPendingTransitions(); + mListener.onStateChanged(mGestureId, mState, event, rawEvent, policyFlags); + } + + /** Indicates that there is evidence to suggest that this gesture has started. */ + protected final void startGesture(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + setState(STATE_GESTURE_STARTED, event, rawEvent, policyFlags); + } + + /** Indicates this stream of motion events can no longer match this gesture. */ + protected final void cancelGesture(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + setState(STATE_GESTURE_CANCELED, event, rawEvent, policyFlags); + } + + /** Indicates this gesture is completed. */ + protected final void completeGesture(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + setState(STATE_GESTURE_COMPLETED, event, rawEvent, policyFlags); + } + + public int getGestureId() { + return mGestureId; + } + + /** + * Process a motion event and attempt to match it to this gesture. + * + * @param event the event as passed in from the event stream. + * @param rawEvent the original un-modified event. Useful for calculating movements in physical + * space. + * @param policyFlags the policy flags as passed in from the event stream. + * @return the state of this matcher. + */ + public final int onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (mState == STATE_GESTURE_CANCELED || mState == STATE_GESTURE_COMPLETED) { + return mState; + } + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + onDown(event, rawEvent, policyFlags); + break; + case MotionEvent.ACTION_POINTER_DOWN: + onPointerDown(event, rawEvent, policyFlags); + break; + case MotionEvent.ACTION_MOVE: + onMove(event, rawEvent, policyFlags); + break; + case MotionEvent.ACTION_POINTER_UP: + onPointerUp(event, rawEvent, policyFlags); + break; + case MotionEvent.ACTION_UP: + onUp(event, rawEvent, policyFlags); + break; + default: + // Cancel because of invalid event. + setState(STATE_GESTURE_CANCELED, event, rawEvent, policyFlags); + break; + } + return mState; + } + + /** + * Matchers override this method to respond to ACTION_DOWN events. ACTION_DOWN events indicate + * the first finger has touched the screen. If not overridden the default response is to do + * nothing. + */ + protected void onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {} + + /** + * Matchers override this method to respond to ACTION_POINTER_DOWN events. ACTION_POINTER_DOWN + * indicates that more than one finger has touched the screen. If not overridden the default + * response is to do nothing. + * + * @param event the event as passed in from the event stream. + * @param rawEvent the original un-modified event. Useful for calculating movements in physical + * space. + * @param policyFlags the policy flags as passed in from the event stream. + */ + protected void onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {} + + /** + * Matchers override this method to respond to ACTION_MOVE events. ACTION_MOVE indicates that + * one or fingers has moved. If not overridden the default response is to do nothing. + * + * @param event the event as passed in from the event stream. + * @param rawEvent the original un-modified event. Useful for calculating movements in physical + * space. + * @param policyFlags the policy flags as passed in from the event stream. + */ + protected void onMove(MotionEvent event, MotionEvent rawEvent, int policyFlags) {} + + /** + * Matchers override this method to respond to ACTION_POINTER_UP events. ACTION_POINTER_UP + * indicates that a finger has lifted from the screen but at least one finger continues to touch + * the screen. If not overridden the default response is to do nothing. + * + * @param event the event as passed in from the event stream. + * @param rawEvent the original un-modified event. Useful for calculating movements in physical + * space. + * @param policyFlags the policy flags as passed in from the event stream. + */ + protected void onPointerUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {} + + /** + * Matchers override this method to respond to ACTION_UP events. ACTION_UP indicates that there + * are no more fingers touching the screen. If not overridden the default response is to do + * nothing. + * + * @param event the event as passed in from the event stream. + * @param rawEvent the original un-modified event. Useful for calculating movements in physical + * space. + * @param policyFlags the policy flags as passed in from the event stream. + */ + protected void onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {} + + /** Cancels this matcher after the tap timeout. Any pending state transitions are removed. */ + protected void cancelAfterTapTimeout(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + cancelAfter(ViewConfiguration.getTapTimeout(), event, rawEvent, policyFlags); + } + + /** Cancels this matcher after the double tap timeout. Any pending cancelations are removed. */ + protected final void cancelAfterDoubleTapTimeout( + MotionEvent event, MotionEvent rawEvent, int policyFlags) { + cancelAfter(ViewConfiguration.getDoubleTapTimeout(), event, rawEvent, policyFlags); + } + + /** + * Cancels this matcher after the specified timeout. Any pending cancelations are removed. Used + * to prevent this matcher from accepting motion events until it is cleared. + */ + protected final void cancelAfter( + long timeout, MotionEvent event, MotionEvent rawEvent, int policyFlags) { + mDelayedTransition.cancel(); + mDelayedTransition.post(STATE_GESTURE_CANCELED, timeout, event, rawEvent, policyFlags); + } + + /** Cancels any delayed transitions between states scheduled for this matcher. */ + protected final void cancelPendingTransitions() { + mDelayedTransition.cancel(); + } + + /** + * Signals that this gesture has been completed after the tap timeout has expired. Used to + * ensure that there is no conflict with another gesture or for gestures that explicitly require + * a hold. + */ + protected final void completeAfterLongPressTimeout( + MotionEvent event, MotionEvent rawEvent, int policyFlags) { + completeAfter(ViewConfiguration.getLongPressTimeout(), event, rawEvent, policyFlags); + } + + /** + * Signals that this gesture has been completed after the tap timeout has expired. Used to + * ensure that there is no conflict with another gesture or for gestures that explicitly require + * a hold. + */ + protected final void completeAfterTapTimeout( + MotionEvent event, MotionEvent rawEvent, int policyFlags) { + completeAfter(ViewConfiguration.getTapTimeout(), event, rawEvent, policyFlags); + } + + /** + * Signals that this gesture has been completed after the specified timeout has expired. Used to + * ensure that there is no conflict with another gesture or for gestures that explicitly require + * a hold. + */ + protected final void completeAfter( + long timeout, MotionEvent event, MotionEvent rawEvent, int policyFlags) { + mDelayedTransition.cancel(); + mDelayedTransition.post(STATE_GESTURE_COMPLETED, timeout, event, rawEvent, policyFlags); + } + + /** + * Signals that this gesture has been completed after the double-tap timeout has expired. Used + * to ensure that there is no conflict with another gesture or for gestures that explicitly + * require a hold. + */ + protected final void completeAfterDoubleTapTimeout( + MotionEvent event, MotionEvent rawEvent, int policyFlags) { + completeAfter(ViewConfiguration.getDoubleTapTimeout(), event, rawEvent, policyFlags); + } + + public static String getStateSymbolicName(@State int state) { + switch (state) { + case STATE_CLEAR: + return "STATE_CLEAR"; + case STATE_GESTURE_STARTED: + return "STATE_GESTURE_STARTED"; + case STATE_GESTURE_COMPLETED: + return "STATE_GESTURE_COMPLETED"; + case STATE_GESTURE_CANCELED: + return "STATE_GESTURE_CANCELED"; + default: + return "Unknown state: " + state; + } + } + + /** + * Returns a readable name for this matcher that can be displayed to the user and in system + * logs. + */ + abstract String getGestureName(); + + /** + * Returns a String representation of this matcher. Each matcher can override this method to add + * extra state information to the string representation. + */ + public String toString() { + return getGestureName() + ":" + getStateSymbolicName(mState); + } + + /** This class allows matchers to transition between states on a delay. */ + protected final class DelayedTransition implements Runnable { + + private static final String LOG_TAG = "GestureMatcher.DelayedTransition"; + int mTargetState; + MotionEvent mEvent; + MotionEvent mRawEvent; + int mPolicyFlags; + + public void cancel() { + // Avoid meaningless debug messages. + if (DEBUG && isPending()) { + Slog.d( + LOG_TAG, + getGestureName() + + ": canceling delayed transition to " + + getStateSymbolicName(mTargetState)); + } + mHandler.removeCallbacks(this); + } + + public void post( + int state, long delay, MotionEvent event, MotionEvent rawEvent, int policyFlags) { + mTargetState = state; + mEvent = event; + mRawEvent = rawEvent; + mPolicyFlags = policyFlags; + mHandler.postDelayed(this, delay); + if (DEBUG) { + Slog.d( + LOG_TAG, + getGestureName() + + ": posting delayed transition to " + + getStateSymbolicName(mTargetState)); + } + } + + public boolean isPending() { + return mHandler.hasCallbacks(this); + } + + public void forceSendAndRemove() { + if (isPending()) { + run(); + cancel(); + } + } + + @Override + public void run() { + if (DEBUG) { + Slog.d( + LOG_TAG, + getGestureName() + + ": executing delayed transition to " + + getStateSymbolicName(mTargetState)); + } + setState(mTargetState, mEvent, mRawEvent, mPolicyFlags); + } + } + + /** Interface to allow a class to listen for state changes in a specific gesture matcher */ + interface StateChangeListener { + + void onStateChanged( + int gestureId, int state, MotionEvent event, MotionEvent rawEvent, int policyFlags); + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/MultiTap.java b/services/accessibility/java/com/android/server/accessibility/gestures/MultiTap.java new file mode 100644 index 000000000000..2891c6c294f5 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/gestures/MultiTap.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility.gestures; + +import android.content.Context; +import android.os.Handler; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +/** + * This class matches multi-tap gestures. The number of taps for each instance is specified in the + * constructor. + */ +class MultiTap extends GestureMatcher { + + // Maximum reasonable number of taps. + public static final int MAX_TAPS = 10; + final int mTargetTaps; + // The acceptable distance between two taps + int mDoubleTapSlop; + // The acceptable distance the pointer can move and still count as a tap. + int mTouchSlop; + int mTapTimeout; + int mDoubleTapTimeout; + int mCurrentTaps; + float mBaseX; + float mBaseY; + + MultiTap(Context context, int taps, int gesture, GestureMatcher.StateChangeListener listener) { + super(gesture, new Handler(context.getMainLooper()), listener); + mTargetTaps = taps; + mDoubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mTapTimeout = ViewConfiguration.getTapTimeout(); + mDoubleTapTimeout = ViewConfiguration.getDoubleTapTimeout(); + clear(); + } + + @Override + protected void clear() { + mCurrentTaps = 0; + mBaseX = Float.NaN; + mBaseY = Float.NaN; + super.clear(); + } + + @Override + protected void onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + cancelAfterTapTimeout(event, rawEvent, policyFlags); + if (Float.isNaN(mBaseX) && Float.isNaN(mBaseY)) { + mBaseX = event.getX(); + mBaseY = event.getY(); + } + if (!isInsideSlop(rawEvent, mDoubleTapSlop)) { + cancelGesture(event, rawEvent, policyFlags); + } + mBaseX = event.getX(); + mBaseY = event.getY(); + } + + @Override + protected void onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + cancelAfterDoubleTapTimeout(event, rawEvent, policyFlags); + if (!isInsideSlop(rawEvent, mTouchSlop)) { + cancelGesture(event, rawEvent, policyFlags); + } + if (getState() == STATE_GESTURE_STARTED || getState() == STATE_CLEAR) { + mCurrentTaps++; + if (mCurrentTaps == mTargetTaps) { + // Done. + completeAfterTapTimeout(event, rawEvent, policyFlags); + return; + } + // Needs more taps. + cancelAfterDoubleTapTimeout(event, rawEvent, policyFlags); + } else { + // Either too many taps or nonsensical event stream. + cancelGesture(event, rawEvent, policyFlags); + } + } + + @Override + protected void onMove(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (!isInsideSlop(rawEvent, mTouchSlop)) { + cancelGesture(event, rawEvent, policyFlags); + } + } + + @Override + protected void onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + cancelGesture(event, rawEvent, policyFlags); + } + + @Override + protected void onPointerUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + cancelGesture(event, rawEvent, policyFlags); + } + + @Override + public String getGestureName() { + switch (mTargetTaps) { + case 2: + return "Double Tap"; + case 3: + return "Triple Tap"; + default: + return Integer.toString(mTargetTaps) + " Taps"; + } + } + + private boolean isInsideSlop(MotionEvent rawEvent, int slop) { + final float deltaX = mBaseX - rawEvent.getX(); + final float deltaY = mBaseY - rawEvent.getY(); + if (deltaX == 0 && deltaY == 0) { + return true; + } + final double moveDelta = Math.hypot(deltaX, deltaY); + return moveDelta <= slop; + } + + @Override + public String toString() { + return super.toString() + + ", Taps:" + + mCurrentTaps + + ", mBaseX: " + + Float.toString(mBaseX) + + ", mBaseY: " + + Float.toString(mBaseY); + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/MultiTapAndHold.java b/services/accessibility/java/com/android/server/accessibility/gestures/MultiTapAndHold.java new file mode 100644 index 000000000000..6a1f1a546bc2 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/gestures/MultiTapAndHold.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility.gestures; + +import android.content.Context; +import android.view.MotionEvent; + +/** + * This class matches gestures of the form multi-tap and hold. The number of taps for each instance + * is specified in the constructor. + */ +class MultiTapAndHold extends MultiTap { + MultiTapAndHold( + Context context, int taps, int gesture, GestureMatcher.StateChangeListener listener) { + super(context, taps, gesture, listener); + } + + @Override + protected void onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + super.onDown(event, rawEvent, policyFlags); + if (mCurrentTaps + 1 == mTargetTaps) { + completeAfterLongPressTimeout(event, rawEvent, policyFlags); + } + } + + @Override + public String getGestureName() { + switch (mTargetTaps) { + case 2: + return "Double Tap and Hold"; + case 3: + return "Triple Tap and Hold"; + default: + return Integer.toString(mTargetTaps) + " Taps and Hold"; + } + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/Swipe.java b/services/accessibility/java/com/android/server/accessibility/gestures/Swipe.java new file mode 100644 index 000000000000..b246c67944c7 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/gestures/Swipe.java @@ -0,0 +1,430 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility.gestures; + +import static com.android.server.accessibility.gestures.TouchExplorer.DEBUG; + +import android.content.Context; +import android.gesture.GesturePoint; +import android.graphics.PointF; +import android.os.Handler; +import android.util.DisplayMetrics; +import android.util.Slog; +import android.util.TypedValue; +import android.view.MotionEvent; + +import java.util.ArrayList; + +/** + * This class is responsible for matching one-finger swipe gestures. Each instance matches one swipe + * gesture. A swipe is specified as a series of one or more directions e.g. left, left and up, etc. + * At this time swipes with more than two directions are not supported. + */ +class Swipe extends GestureMatcher { + + // Direction constants. + public static final int LEFT = 0; + public static final int RIGHT = 1; + public static final int UP = 2; + public static final int DOWN = 3; + // This is the calculated movement threshold used track if the user is still + // moving their finger. + private final float mGestureDetectionThreshold; + + // Buffer for storing points for gesture detection. + private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100); + + // The minimal delta between moves to add a gesture point. + private static final int TOUCH_TOLERANCE_PIX = 3; + + // The minimal score for accepting a predicted gesture. + private static final float MIN_PREDICTION_SCORE = 2.0f; + + // Distance a finger must travel before we decide if it is a gesture or not. + private static final int GESTURE_CONFIRM_CM = 1; + + // Time threshold used to determine if an interaction is a gesture or not. + // If the first movement of 1cm takes longer than this value, we assume it's + // a slow movement, and therefore not a gesture. + // + // This value was determined by measuring the time for the first 1cm + // movement when gesturing, and touch exploring. Based on user testing, + // all gestures started with the initial movement taking less than 100ms. + // When touch exploring, the first movement almost always takes longer than + // 200ms. + private static final long CANCEL_ON_PAUSE_THRESHOLD_NOT_STARTED_MS = 150; + + // Time threshold used to determine if a gesture should be cancelled. If + // the finger takes more than this time to move 1cm, the ongoing gesture is + // cancelled. + private static final long CANCEL_ON_PAUSE_THRESHOLD_STARTED_MS = 300; + + private int[] mDirections; + private float mBaseX; + private float mBaseY; + private long mBaseTime; + private float mPreviousGestureX; + private float mPreviousGestureY; + // Constants for sampling motion event points. + // We sample based on a minimum distance between points, primarily to improve accuracy by + // reducing noisy minor changes in direction. + private static final float MIN_CM_BETWEEN_SAMPLES = 0.25f; + private final float mMinPixelsBetweenSamplesX; + private final float mMinPixelsBetweenSamplesY; + + // Constants for separating gesture segments + private static final float ANGLE_THRESHOLD = 0.0f; + + Swipe( + Context context, + int direction, + int gesture, + GestureMatcher.StateChangeListener listener) { + this(context, new int[] {direction}, gesture, listener); + } + + Swipe( + Context context, + int direction1, + int direction2, + int gesture, + GestureMatcher.StateChangeListener listener) { + this(context, new int[] {direction1, direction2}, gesture, listener); + } + + private Swipe( + Context context, + int[] directions, + int gesture, + GestureMatcher.StateChangeListener listener) { + super(gesture, new Handler(context.getMainLooper()), listener); + mDirections = directions; + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + mGestureDetectionThreshold = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 10, displayMetrics) + * GESTURE_CONFIRM_CM; + // Calculate minimum gesture velocity + final float pixelsPerCmX = displayMetrics.xdpi / 2.54f; + final float pixelsPerCmY = displayMetrics.ydpi / 2.54f; + mMinPixelsBetweenSamplesX = MIN_CM_BETWEEN_SAMPLES * pixelsPerCmX; + mMinPixelsBetweenSamplesY = MIN_CM_BETWEEN_SAMPLES * pixelsPerCmY; + clear(); + } + + @Override + protected void clear() { + mBaseX = Float.NaN; + mBaseY = Float.NaN; + mBaseTime = 0; + mStrokeBuffer.clear(); + super.clear(); + } + + @Override + protected void onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + cancelAfterDelay(event, rawEvent, policyFlags); + if (Float.isNaN(mBaseX) && Float.isNaN(mBaseY)) { + mBaseX = rawEvent.getX(); + mBaseY = rawEvent.getY(); + mBaseTime = event.getEventTime(); + mPreviousGestureX = mBaseX; + mPreviousGestureY = mBaseY; + } + // Otherwise do nothing because this event doesn't make sense in the middle of a gesture. + } + + @Override + protected void onMove(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + final float x = rawEvent.getX(); + final float y = rawEvent.getY(); + final long time = event.getEventTime(); + final float dX = Math.abs(x - mPreviousGestureX); + final float dY = Math.abs(y - mPreviousGestureY); + final long timeDelta = time - mBaseTime; + final double moveDelta = Math.hypot(Math.abs(x - mBaseX), Math.abs(y - mBaseY)); + if (DEBUG) { + Slog.d( + getGestureName(), + "moveDelta:" + + Double.toString(moveDelta) + + " mGestureDetectionThreshold: " + + Float.toString(mGestureDetectionThreshold)); + } + if (getState() == STATE_CLEAR) { + if (mStrokeBuffer.size() == 0) { + // First, make sure the pointer is going in the right direction. + cancelAfterDelay(event, rawEvent, policyFlags); + int direction = toDirection(x - mBaseX, y - mBaseY); + if (direction != mDirections[0]) { + cancelGesture(event, rawEvent, policyFlags); + return; + } else { + // This is confirmed to be some kind of swipe so start tracking points. + mStrokeBuffer.add(new GesturePoint(mBaseX, mBaseY, mBaseTime)); + } + } + if (moveDelta > mGestureDetectionThreshold) { + // If the pointer has moved more than the threshold, + // update the stored values. + mBaseX = x; + mBaseY = y; + mBaseTime = time; + if (getState() == STATE_CLEAR) { + startGesture(event, rawEvent, policyFlags); + cancelAfterDelay(event, rawEvent, policyFlags); + } + } + } + if (getState() == STATE_GESTURE_STARTED) { + if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) { + mPreviousGestureX = x; + mPreviousGestureY = y; + mStrokeBuffer.add(new GesturePoint(x, y, time)); + cancelAfterDelay(event, rawEvent, policyFlags); + } + } + } + + @Override + protected void onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (getState() != STATE_GESTURE_STARTED) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + + final float x = rawEvent.getX(); + final float y = rawEvent.getY(); + final long time = event.getEventTime(); + final float dX = Math.abs(x - mPreviousGestureX); + final float dY = Math.abs(y - mPreviousGestureY); + if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) { + mStrokeBuffer.add(new GesturePoint(x, y, time)); + } + recognizeGesture(event, rawEvent, policyFlags); + } + + @Override + protected void onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + cancelGesture(event, rawEvent, policyFlags); + } + + @Override + protected void onPointerUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + cancelGesture(event, rawEvent, policyFlags); + } + + /** + * queues a transition to STATE_GESTURE_CANCEL based on the current state. If we have + * transitioned to STATE_GESTURE_STARTED the delay is longer. + */ + private void cancelAfterDelay(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + cancelPendingTransitions(); + switch (getState()) { + case STATE_CLEAR: + cancelAfter(CANCEL_ON_PAUSE_THRESHOLD_NOT_STARTED_MS, event, rawEvent, policyFlags); + break; + case STATE_GESTURE_STARTED: + cancelAfter(CANCEL_ON_PAUSE_THRESHOLD_STARTED_MS, event, rawEvent, policyFlags); + break; + default: + break; + } + } + + /** + * Looks at the sequence of motions in mStrokeBuffer, classifies the gesture, then calls + * Listener callbacks for success or failure. + * + * @param event The raw motion event to pass to the listener callbacks. + * @param policyFlags Policy flags for the event. + * @return true if the event is consumed, else false + */ + private void recognizeGesture(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (mStrokeBuffer.size() < 2) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + + // Look at mStrokeBuffer and extract 2 line segments, delimited by near-perpendicular + // direction change. + // Method: for each sampled motion event, check the angle of the most recent motion vector + // versus the preceding motion vector, and segment the line if the angle is about + // 90 degrees. + + ArrayList<PointF> path = new ArrayList<>(); + PointF lastDelimiter = new PointF(mStrokeBuffer.get(0).x, mStrokeBuffer.get(0).y); + path.add(lastDelimiter); + + float dX = 0; // Sum of unit vectors from last delimiter to each following point + float dY = 0; + int count = 0; // Number of points since last delimiter + float length = 0; // Vector length from delimiter to most recent point + + PointF next = new PointF(); + for (int i = 1; i < mStrokeBuffer.size(); ++i) { + next = new PointF(mStrokeBuffer.get(i).x, mStrokeBuffer.get(i).y); + if (count > 0) { + // Average of unit vectors from delimiter to following points + float currentDX = dX / count; + float currentDY = dY / count; + + // newDelimiter is a possible new delimiter, based on a vector with length from + // the last delimiter to the previous point, but in the direction of the average + // unit vector from delimiter to previous points. + // Using the averaged vector has the effect of "squaring off the curve", + // creating a sharper angle between the last motion and the preceding motion from + // the delimiter. In turn, this sharper angle achieves the splitting threshold + // even in a gentle curve. + PointF newDelimiter = + new PointF( + length * currentDX + lastDelimiter.x, + length * currentDY + lastDelimiter.y); + + // Unit vector from newDelimiter to the most recent point + float nextDX = next.x - newDelimiter.x; + float nextDY = next.y - newDelimiter.y; + float nextLength = (float) Math.sqrt(nextDX * nextDX + nextDY * nextDY); + nextDX = nextDX / nextLength; + nextDY = nextDY / nextLength; + + // Compare the initial motion direction to the most recent motion direction, + // and segment the line if direction has changed by about 90 degrees. + float dot = currentDX * nextDX + currentDY * nextDY; + if (dot < ANGLE_THRESHOLD) { + path.add(newDelimiter); + lastDelimiter = newDelimiter; + dX = 0; + dY = 0; + count = 0; + } + } + + // Vector from last delimiter to most recent point + float currentDX = next.x - lastDelimiter.x; + float currentDY = next.y - lastDelimiter.y; + length = (float) Math.sqrt(currentDX * currentDX + currentDY * currentDY); + + // Increment sum of unit vectors from delimiter to each following point + count = count + 1; + dX = dX + currentDX / length; + dY = dY + currentDY / length; + } + + path.add(next); + if (DEBUG) { + Slog.d(getGestureName(), "path=" + path.toString()); + } + // Classify line segments, and call Listener callbacks. + recognizeGesturePath(event, rawEvent, policyFlags, path); + } + + /** + * Classifies a pair of line segments, by direction. Calls Listener callbacks for success or + * failure. + * + * @param event The raw motion event to pass to the listener's onGestureCanceled method. + * @param policyFlags Policy flags for the event. + * @param path A sequence of motion line segments derived from motion points in mStrokeBuffer. + * @return true if the event is consumed, else false + */ + private void recognizeGesturePath( + MotionEvent event, MotionEvent rawEvent, int policyFlags, ArrayList<PointF> path) { + + final int displayId = event.getDisplayId(); + if (path.size() != mDirections.length + 1) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + for (int i = 0; i < path.size() - 1; ++i) { + PointF start = path.get(i); + PointF end = path.get(i + 1); + + float dX = end.x - start.x; + float dY = end.y - start.y; + int direction = toDirection(dX, dY); + if (direction != mDirections[i]) { + if (DEBUG) { + Slog.d( + getGestureName(), + "Found direction " + + directionToString(direction) + + " when expecting " + + directionToString(mDirections[i])); + } + cancelGesture(event, rawEvent, policyFlags); + return; + } + } + if (DEBUG) { + Slog.d(getGestureName(), "Completed."); + } + completeGesture(event, rawEvent, policyFlags); + } + + private static int toDirection(float dX, float dY) { + if (Math.abs(dX) > Math.abs(dY)) { + // Horizontal + return (dX < 0) ? LEFT : RIGHT; + } else { + // Vertical + return (dY < 0) ? UP : DOWN; + } + } + + public static String directionToString(int direction) { + switch (direction) { + case LEFT: + return "left"; + case RIGHT: + return "right"; + case UP: + return "up"; + case DOWN: + return "down"; + default: + return "Unknown Direction"; + } + } + + @Override + String getGestureName() { + StringBuilder builder = new StringBuilder(); + builder.append("Swipe ").append(directionToString(mDirections[0])); + for (int i = 1; i < mDirections.length; ++i) { + builder.append(" and ").append(directionToString(mDirections[i])); + } + return builder.toString(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(super.toString()); + if (getState() != STATE_GESTURE_CANCELED) { + builder.append(", mBaseX: ") + .append(mBaseX) + .append(", mBaseY: ") + .append(mBaseY) + .append(", mGestureDetectionThreshold:") + .append(mGestureDetectionThreshold) + .append(", mMinPixelsBetweenSamplesX:") + .append(mMinPixelsBetweenSamplesX) + .append(", mMinPixelsBetweenSamplesY:") + .append(mMinPixelsBetweenSamplesY); + } + return builder.toString(); + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java index b62e260aacad..5f4163880366 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java @@ -59,7 +59,7 @@ import java.util.List; * @hide */ public class TouchExplorer extends BaseEventStreamTransformation - implements AccessibilityGestureDetector.Listener { + implements GestureManifold.Listener { static final boolean DEBUG = false; @@ -104,7 +104,7 @@ public class TouchExplorer extends BaseEventStreamTransformation private final ExitGestureDetectionModeDelayed mExitGestureDetectionModeDelayed; // Helper to detect gestures. - private final AccessibilityGestureDetector mGestureDetector; + private final GestureManifold mGestureDetector; // Helper class to track received pointers. private final TouchState.ReceivedPointerTracker mReceivedPointerTracker; @@ -142,7 +142,7 @@ public class TouchExplorer extends BaseEventStreamTransformation * one created in place, or for testing purpose. */ public TouchExplorer(Context context, AccessibilityManagerService service, - AccessibilityGestureDetector detector) { + GestureManifold detector) { mContext = context; mAms = service; mState = new TouchState(); @@ -161,7 +161,7 @@ public class TouchExplorer extends BaseEventStreamTransformation AccessibilityEvent.TYPE_TOUCH_INTERACTION_END, mDetermineUserIntentTimeout); if (detector == null) { - mGestureDetector = new AccessibilityGestureDetector(context, this); + mGestureDetector = new GestureManifold(context, this, mState); } else { mGestureDetector = detector; } @@ -285,7 +285,7 @@ public class TouchExplorer extends BaseEventStreamTransformation } @Override - public void onDoubleTapAndHold(MotionEvent event, int policyFlags) { + public void onDoubleTapAndHold() { // Ignore the event if we aren't touch interacting. if (!mState.isTouchInteracting()) { return; @@ -303,7 +303,7 @@ public class TouchExplorer extends BaseEventStreamTransformation } @Override - public boolean onDoubleTap(MotionEvent event, int policyFlags) { + public boolean onDoubleTap() { if (!mState.isTouchInteracting()) { return false; } @@ -319,7 +319,7 @@ public class TouchExplorer extends BaseEventStreamTransformation // Announce the end of a new touch interaction. mDispatcher.sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); - + mSendTouchInteractionEndDelayed.cancel(); // Try to use the standard accessibility API to click if (!mAms.performActionOnAccessibilityFocusedItem( AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK)) { @@ -356,7 +356,7 @@ public class TouchExplorer extends BaseEventStreamTransformation } @Override - public boolean onGestureCancelled(MotionEvent event, int policyFlags) { + public boolean onGestureCancelled(MotionEvent event, MotionEvent rawEvent, int policyFlags) { if (mState.isGestureDetecting()) { endGestureDetection(event.getActionMasked() == MotionEvent.ACTION_UP); return true; diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java index f463260a9d02..d23dbbefd325 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java @@ -71,7 +71,10 @@ public class TouchState { // Helper class to track received pointers. // Todo: collapse or hide this class so multiple classes don't modify it. private final ReceivedPointerTracker mReceivedPointerTracker; + // The most recently received motion event. private MotionEvent mLastReceivedEvent; + // The accompanying raw event without any transformations. + private MotionEvent mLastReceivedRawEvent; public TouchState() { mReceivedPointerTracker = new ReceivedPointerTracker(); @@ -97,6 +100,9 @@ public class TouchState { if (mLastReceivedEvent != null) { mLastReceivedEvent.recycle(); } + if (mLastReceivedRawEvent != null) { + mLastReceivedRawEvent.recycle(); + } mLastReceivedEvent = MotionEvent.obtain(rawEvent); mReceivedPointerTracker.onMotionEvent(rawEvent); } @@ -246,7 +252,6 @@ public class TouchState { // or if it goes up the next one that most recently went down. private int mPrimaryPointerId; - ReceivedPointerTracker() { clear(); } diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index d7ed2e9abde4..202f90068fc1 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -82,6 +82,7 @@ import com.android.server.autofill.AutofillManagerService.AutofillCompatState; import com.android.server.autofill.RemoteAugmentedAutofillService.RemoteAugmentedAutofillServiceCallbacks; import com.android.server.autofill.ui.AutoFillUI; import com.android.server.infra.AbstractPerUserSystemService; +import com.android.server.inputmethod.InputMethodManagerInternal; import java.io.PrintWriter; import java.util.ArrayList; @@ -168,6 +169,8 @@ final class AutofillManagerServiceImpl @Nullable private ServiceInfo mRemoteAugmentedAutofillServiceInfo; + private final InputMethodManagerInternal mInputMethodManagerInternal; + AutofillManagerServiceImpl(AutofillManagerService master, Object lock, LocalLog uiLatencyHistory, LocalLog wtfHistory, int userId, AutoFillUI ui, AutofillCompatState autofillCompatState, @@ -179,6 +182,7 @@ final class AutofillManagerServiceImpl mUi = ui; mFieldClassificationStrategy = new FieldClassificationStrategy(getContext(), userId); mAutofillCompatState = autofillCompatState; + mInputMethodManagerInternal = LocalServices.getService(InputMethodManagerInternal.class); updateLocked(disabled); } @@ -493,7 +497,7 @@ final class AutofillManagerServiceImpl sessionId, taskId, uid, activityToken, appCallbackToken, hasCallback, mUiLatencyHistory, mWtfHistory, serviceComponentName, componentName, compatMode, bindInstantServiceAllowed, forAugmentedAutofillOnly, - flags); + flags, mInputMethodManagerInternal); mSessions.put(newSession.id, newSession); return newSession; diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 3b2da911a849..67bcccd1d7de 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -42,6 +42,8 @@ import android.app.IAssistDataReceiver; import android.app.assist.AssistStructure; import android.app.assist.AssistStructure.AutofillOverlay; import android.app.assist.AssistStructure.ViewNode; +import android.app.slice.Slice; +import android.app.slice.SliceItem; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -80,6 +82,8 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.LocalLog; +import android.util.Log; +import android.util.Size; import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; @@ -90,22 +94,38 @@ import android.view.autofill.AutofillManager.SmartSuggestionMode; import android.view.autofill.AutofillValue; import android.view.autofill.IAutoFillManagerClient; import android.view.autofill.IAutofillWindowPresenter; +import android.view.inline.InlinePresentationSpec; +import android.view.inputmethod.InlineSuggestion; +import android.view.inputmethod.InlineSuggestionInfo; +import android.view.inputmethod.InlineSuggestionsRequest; +import android.view.inputmethod.InlineSuggestionsResponse; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.ArrayUtils; +import com.android.internal.view.IInlineSuggestionsRequestCallback; +import com.android.internal.view.IInlineSuggestionsResponseCallback; +import com.android.internal.view.inline.IInlineContentCallback; +import com.android.internal.view.inline.IInlineContentProvider; import com.android.server.autofill.ui.AutoFillUI; import com.android.server.autofill.ui.PendingUi; +import com.android.server.inputmethod.InputMethodManagerInternal; import java.io.PrintWriter; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; /** @@ -290,6 +310,23 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @GuardedBy("mLock") private boolean mForAugmentedAutofillOnly; + @NonNull + private final InputMethodManagerInternal mInputMethodManagerInternal; + + @GuardedBy("mLock") + @Nullable + private CompletableFuture<InlineSuggestionsRequest> mSuggestionsRequestFuture; + + @GuardedBy("mLock") + @Nullable + private CompletableFuture<IInlineSuggestionsResponseCallback> + mInlineSuggestionsResponseCallbackFuture; + + @Nullable + private InlineSuggestionsRequestCallback mInlineSuggestionsRequestCallback; + + private static final int INLINE_REQUEST_TIMEOUT_MS = 1000; + /** * Receiver of assist data from the app's {@link Activity}. */ @@ -386,7 +423,23 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState final ArrayList<FillContext> contexts = mergePreviousSessionLocked(/* forSave= */ false); - request = new FillRequest(requestId, contexts, mClientState, flags); + + InlineSuggestionsRequest suggestionsRequest = null; + if (mSuggestionsRequestFuture != null) { + try { + suggestionsRequest = mSuggestionsRequestFuture.get( + INLINE_REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + Log.w(TAG, "Exception getting inline suggestions request in time: " + e); + } catch (CancellationException e) { + Log.w(TAG, "Inline suggestions request cancelled"); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + request = new FillRequest(requestId, contexts, mClientState, flags, + suggestionsRequest); } mRemoteFillService.onFillRequest(request); @@ -569,6 +622,70 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } /** + * Returns whether inline suggestions are enabled for Autofill. + */ + // TODO(b/137800469): Implement this + private boolean isInlineSuggestionsEnabled() { + return true; + } + + /** + * Ask the IME to make an inline suggestions request if enabled. + */ + private void maybeRequestInlineSuggestionsRequestThenFillLocked(@NonNull ViewState viewState, + int newState, int flags) { + if (isInlineSuggestionsEnabled()) { + mSuggestionsRequestFuture = new CompletableFuture<>(); + mInlineSuggestionsResponseCallbackFuture = new CompletableFuture<>(); + + if (mInlineSuggestionsRequestCallback == null) { + mInlineSuggestionsRequestCallback = new InlineSuggestionsRequestCallback(this); + } + + mInputMethodManagerInternal.onCreateInlineSuggestionsRequest( + mComponentName, mCurrentViewId, mInlineSuggestionsRequestCallback); + } + + requestNewFillResponseLocked(viewState, newState, flags); + } + + private static final class InlineSuggestionsRequestCallback + extends IInlineSuggestionsRequestCallback.Stub { + private final WeakReference<Session> mSession; + + private InlineSuggestionsRequestCallback(Session session) { + mSession = new WeakReference<>(session); + } + + @Override + public void onInlineSuggestionsUnsupported() throws RemoteException { + Log.i(TAG, "inline suggestions request unsupported, " + + "falling back to regular autofill"); + final Session session = mSession.get(); + if (session != null) { + synchronized (session.mLock) { + session.mSuggestionsRequestFuture.cancel(true); + session.mInlineSuggestionsResponseCallbackFuture.cancel(true); + } + } + } + + @Override + public void onInlineSuggestionsRequest(InlineSuggestionsRequest request, + IInlineSuggestionsResponseCallback callback) throws RemoteException { + Log.i(TAG, "onInlineSuggestionsRequest() received: " + + request); + final Session session = mSession.get(); + if (session != null) { + synchronized (session.mLock) { + session.mSuggestionsRequestFuture.complete(request); + session.mInlineSuggestionsResponseCallbackFuture.complete(callback); + } + } + } + } + + /** * Reads a new structure and then request a new fill response from the fill service. */ @GuardedBy("mLock") @@ -584,6 +701,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState triggerAugmentedAutofillLocked(); return; } + viewState.setState(newState); int requestId; @@ -636,7 +754,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @NonNull IBinder client, boolean hasCallback, @NonNull LocalLog uiLatencyHistory, @NonNull LocalLog wtfHistory, @Nullable ComponentName serviceComponentName, @NonNull ComponentName componentName, boolean compatMode, - boolean bindInstantServiceAllowed, boolean forAugmentedAutofillOnly, int flags) { + boolean bindInstantServiceAllowed, boolean forAugmentedAutofillOnly, int flags, + @NonNull InputMethodManagerInternal inputMethodManagerInternal) { if (sessionId < 0) { wtf(null, "Non-positive sessionId: %s", sessionId); } @@ -661,6 +780,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mForAugmentedAutofillOnly = forAugmentedAutofillOnly; setClientLocked(client); + mInputMethodManagerInternal = inputMethodManagerInternal; + mMetricsLogger.write(newLogMaker(MetricsEvent.AUTOFILL_SESSION_STARTED) .addTaggedData(MetricsEvent.FIELD_AUTOFILL_FLAGS, flags)); } @@ -2208,7 +2329,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if ((flags & FLAG_MANUAL_REQUEST) != 0) { mForAugmentedAutofillOnly = false; if (sDebug) Slog.d(TAG, "Re-starting session on view " + id + " and flags " + flags); - requestNewFillResponseLocked(viewState, ViewState.STATE_RESTARTED_SESSION, flags); + maybeRequestInlineSuggestionsRequestThenFillLocked(viewState, + ViewState.STATE_RESTARTED_SESSION, flags); return; } @@ -2218,7 +2340,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Slog.d(TAG, "Starting partition or augmented request for view id " + id + ": " + viewState.getStateAsString()); } - requestNewFillResponseLocked(viewState, ViewState.STATE_STARTED_PARTITION, flags); + maybeRequestInlineSuggestionsRequestThenFillLocked(viewState, + ViewState.STATE_STARTED_PARTITION, flags); } else { if (sVerbose) { Slog.v(TAG, "Not starting new partition for view " + id + ": " @@ -2325,7 +2448,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // View is triggering autofill. mCurrentViewId = viewState.id; viewState.update(value, virtualBounds, flags); - requestNewFillResponseLocked(viewState, ViewState.STATE_STARTED_SESSION, flags); + maybeRequestInlineSuggestionsRequestThenFillLocked(viewState, + ViewState.STATE_STARTED_SESSION, flags); break; case ACTION_VALUE_CHANGED: if (mCompatMode && (viewState.getState() & ViewState.STATE_URL_BAR) != 0) { @@ -2527,6 +2651,16 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState wtf(null, "onFillReady(): no service label or icon"); return; } + + final List<Slice> inlineSuggestionSlices = response.getInlineSuggestionSlices(); + if (inlineSuggestionSlices != null) { + if (requestShowInlineSuggestions(inlineSuggestionSlices, response)) { + //TODO(b/137800469): Add logging instead of bypassing below logic. + return; + } + } + + getUiForShowing().showFillUi(filledId, response, filterText, mService.getServicePackageName(), mComponentName, serviceLabel, serviceIcon, this, id, mCompatMode); @@ -2558,6 +2692,109 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + /** + * Returns whether we made a request to show inline suggestions. + */ + private boolean requestShowInlineSuggestions(List<Slice> inlineSuggestionSlices, + FillResponse response) { + IInlineSuggestionsResponseCallback inlineContentCallback = null; + synchronized (mLock) { + if (mInlineSuggestionsResponseCallbackFuture != null) { + try { + inlineContentCallback = mInlineSuggestionsResponseCallbackFuture.get( + INLINE_REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + Log.w(TAG, "Exception getting inline suggestions callback in time: " + e); + } catch (CancellationException e) { + Log.w(TAG, "Inline suggestions callback cancelled"); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + } + + if (inlineContentCallback == null) { + Log.w(TAG, "Session input method callback is not set yet"); + return false; + } + + final List<Dataset> datasets = response.getDatasets(); + if (datasets == null) { + Log.w(TAG, "response returned null datasets"); + return false; + } + + final ArrayList<InlineSuggestion> inlineSuggestions = new ArrayList<>(); + final int slicesSize = inlineSuggestionSlices.size(); + if (datasets.size() < slicesSize) { + Log.w(TAG, "Too many slices provided, not enough corresponding datasets"); + return false; + } + + for (int sliceIndex = 0; sliceIndex < slicesSize; sliceIndex++) { + Log.i(TAG, "Reading slice-" + sliceIndex + " at requestshowinlinesuggestions"); + final Slice inlineSuggestionSlice = inlineSuggestionSlices.get(sliceIndex); + final List<SliceItem> sliceItems = inlineSuggestionSlice.getItems(); + + final int itemsSize = sliceItems.size(); + int minWidth = -1; + int maxWidth = -1; + int minHeight = -1; + int maxHeight = -1; + for (int itemIndex = 0; itemIndex < itemsSize; itemIndex++) { + final SliceItem item = sliceItems.get(itemIndex); + final String subtype = item.getSubType(); + switch (item.getSubType()) { + case "SUBTYPE_MIN_WIDTH": + minWidth = item.getInt(); + break; + case "SUBTYPE_MAX_WIDTH": + maxWidth = item.getInt(); + break; + case "SUBTYPE_MIN_HEIGHT": + minHeight = item.getInt(); + break; + case "SUBTYPE_MAX_HEIGHT": + maxHeight = item.getInt(); + break; + default: + Log.i(TAG, "unrecognized inline suggestions subtype: " + subtype); + } + } + + if (minWidth < 0 || maxWidth < 0 || minHeight < 0 || maxHeight < 0) { + Log.w(TAG, "missing inline suggestion requirements"); + return false; + } + + final InlinePresentationSpec spec = new InlinePresentationSpec.Builder( + new Size(minWidth, minHeight), new Size(maxWidth, maxHeight)).build(); + final InlineSuggestionInfo inlineSuggestionInfo = new InlineSuggestionInfo( + spec, InlineSuggestionInfo.SOURCE_AUTOFILL, new String[] { "" }); + final Dataset dataset = datasets.get(sliceIndex); + + inlineSuggestions.add(new InlineSuggestion(inlineSuggestionInfo, + new IInlineContentProvider.Stub() { + @Override + public void provideContent(int width, int height, + IInlineContentCallback callback) throws RemoteException { + getUiForShowing().getSuggestionSurfaceForShowing(dataset, response, + mCurrentViewId, width, height, callback); + } + })); + } + + try { + inlineContentCallback.onInlineSuggestionsResponse( + new InlineSuggestionsResponse(inlineSuggestions)); + } catch (RemoteException e) { + Log.w(TAG, "onFillReady() remote error calling onInlineSuggestionsResponse()"); + return false; + } + + return true; + } + boolean isDestroyed() { synchronized (mLock) { return mDestroyed; @@ -2831,6 +3068,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState final AutofillId focusedId = AutofillId.withoutSession(mCurrentViewId); + // TODO(b/137800469): implement inlined suggestions for augmented autofill remoteService.onRequestAutofillLocked(id, mClient, taskId, mComponentName, focusedId, currentValue); diff --git a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java index 26bb7c35b9dc..eadfd31c27bf 100644 --- a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java +++ b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java @@ -24,6 +24,8 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentSender; +import android.graphics.Color; +import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; import android.metrics.LogMaker; import android.os.Bundle; @@ -37,13 +39,19 @@ import android.service.autofill.ValueFinder; import android.text.TextUtils; import android.util.Slog; import android.view.KeyEvent; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.view.WindowlessViewRoot; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; +import android.view.autofill.AutofillValue; import android.view.autofill.IAutofillWindowPresenter; +import android.widget.TextView; import android.widget.Toast; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.view.inline.IInlineContentCallback; import com.android.server.LocalServices; import com.android.server.UiModeManagerInternal; import com.android.server.UiThread; @@ -171,6 +179,75 @@ public final class AutoFillUI { } /** + * TODO(b/137800469): Fill in javadoc. + * TODO(b/137800469): peoperly manage lifecycle of suggestions surfaces. + */ + public void getSuggestionSurfaceForShowing(@NonNull Dataset dataset, + @NonNull FillResponse response, AutofillId autofillId, int width, int height, + IInlineContentCallback cb) { + if (dataset == null) { + Slog.w(TAG, "getSuggestionSurfaceForShowing() called with null dataset"); + } + mHandler.post(() -> { + final SurfaceControl suggestionSurface = inflateInlineSuggestion(dataset, response, + autofillId, width, height); + + try { + cb.onContent(suggestionSurface); + } catch (RemoteException e) { + Slog.w(TAG, "RemoteException replying onContent(" + suggestionSurface + "): " + e); + } + }); + } + + /** + * TODO(b/137800469): Fill in javadoc, generate custom templated view for inline suggestions. + * TODO: Move to ExtServices. + * + * @return a {@link SurfaceControl} with the inflated content embedded in it. + */ + private SurfaceControl inflateInlineSuggestion(@NonNull Dataset dataset, + @NonNull FillResponse response, AutofillId autofillId, int width, int height) { + Slog.i(TAG, "inflate() called"); + final Context context = mContext; + final int index = dataset.getFieldIds().indexOf(autofillId); + if (index < 0) { + Slog.w(TAG, "inflateInlineSuggestion(): AutofillId=" + autofillId + + " not found in dataset"); + } + + final AutofillValue datasetValue = dataset.getFieldValues().get(index); + final SurfaceControl sc = new SurfaceControl.Builder() + // TODO(b/137800469): sanitize name + .setName("af suggestion") + .build(); + + //TODO(b/137800469): Pass in inputToken from IME. + final WindowlessViewRoot wvr = new WindowlessViewRoot(context, context.getDisplay(), sc, + null); + + TextView textView = new TextView(context); + textView.setText(datasetValue.getTextValue()); + textView.setBackgroundColor(Color.WHITE); + textView.setTextColor(Color.BLACK); + textView.setOnClickListener(v -> { + Slog.d(TAG, "Inline suggestion clicked"); + hideFillUiUiThread(mCallback, true); + if (mCallback != null) { + final int datasetIndex = response.getDatasets().indexOf(dataset); + mCallback.fill(response.getRequestId(), datasetIndex, dataset); + } + }); + + WindowManager.LayoutParams lp = + new WindowManager.LayoutParams(width, height, + WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.OPAQUE); + wvr.addView(textView, lp); + + return sc; + } + + /** * Shows the fill UI, removing the previous fill UI if the has changed. * * @param focusedId the currently focused field diff --git a/services/core/Android.bp b/services/core/Android.bp index 203bc61c2022..b7adfa4a3ff1 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -116,6 +116,7 @@ java_library_static { "android.hardware.oemlock-V1.0-java", "android.hardware.configstore-V1.0-java", "android.hardware.contexthub-V1.0-java", + "android.hardware.soundtrigger-V2.3-java", "android.hidl.manager-V1.2-java", "dnsresolver_aidl_interface-V2-java", "netd_event_listener_interface-java", diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java index f79b876ec0ce..49046b244bb4 100644 --- a/services/core/java/android/content/pm/PackageManagerInternal.java +++ b/services/core/java/android/content/pm/PackageManagerInternal.java @@ -341,14 +341,16 @@ public abstract class PackageManagerInternal { * @param responseObj The response of the first phase of ephemeral resolution * @param origIntent The original intent that triggered ephemeral resolution * @param resolvedType The resolved type of the intent - * @param callingPackage The name of the package requesting the ephemeral application + * @param callingPkg The app requesting the ephemeral application + * @param isRequesterInstantApp Whether or not the app requesting the ephemeral application + * is an instant app * @param verificationBundle Optional bundle to pass to the installer for additional * verification * @param userId The ID of the user that triggered ephemeral resolution */ public abstract void requestInstantAppResolutionPhaseTwo(AuxiliaryResolveInfo responseObj, - Intent origIntent, String resolvedType, String callingPackage, - Bundle verificationBundle, int userId); + Intent origIntent, String resolvedType, String callingPkg, + boolean isRequesterInstantApp, Bundle verificationBundle, int userId); /** * Grants implicit access based on an interaction between two apps. This grants the target app diff --git a/services/core/java/android/os/UserManagerInternal.java b/services/core/java/android/os/UserManagerInternal.java index a2e9341bae71..d84197c749b6 100644 --- a/services/core/java/android/os/UserManagerInternal.java +++ b/services/core/java/android/os/UserManagerInternal.java @@ -57,7 +57,7 @@ public abstract class UserManagerInternal { * Called by {@link com.android.server.devicepolicy.DevicePolicyManagerService} to set * restrictions enforced by the user. * - * @param userId target user id for the local restrictions. + * @param originatingUserId user id of the user where the restriction originated. * @param restrictions a bundle of user restrictions. * @param restrictionOwnerType determines which admin {@code userId} corresponds to. * The admin can be either @@ -70,8 +70,8 @@ public abstract class UserManagerInternal { * otherwise it will be applied just on the current user. * @see OwnerType */ - public abstract void setDevicePolicyUserRestrictions(int userId, @Nullable Bundle restrictions, - @OwnerType int restrictionOwnerType); + public abstract void setDevicePolicyUserRestrictions(int originatingUserId, + @Nullable Bundle restrictions, @OwnerType int restrictionOwnerType); /** * Returns the "base" user restrictions. diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 9b1d9e9a246c..5a78036911a8 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -118,6 +118,7 @@ import android.sysprop.VoldProperties; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.AtomicFile; import android.util.DataUnit; import android.util.FeatureFlagUtils; @@ -3161,16 +3162,20 @@ class StorageManagerService extends IStorageManager.Stub final boolean forWrite = (flags & StorageManager.FLAG_FOR_WRITE) != 0; final boolean realState = (flags & StorageManager.FLAG_REAL_STATE) != 0; final boolean includeInvisible = (flags & StorageManager.FLAG_INCLUDE_INVISIBLE) != 0; + final boolean includeRecent = (flags & StorageManager.FLAG_INCLUDE_RECENT) != 0; // Report all volumes as unmounted until we've recorded that user 0 has unlocked. There // are no guarantees that callers will see a consistent view of the volume before that // point final boolean systemUserUnlocked = isSystemUnlocked(UserHandle.USER_SYSTEM); + final boolean userIsDemo; final boolean userKeyUnlocked; final boolean storagePermission; final long token = Binder.clearCallingIdentity(); try { + userIsDemo = LocalServices.getService(UserManagerInternal.class) + .getUserInfo(userId).isDemo(); userKeyUnlocked = isUserKeyUnlocked(userId); storagePermission = mStorageManagerInternal.hasExternalStorage(uid, packageName); } finally { @@ -3180,6 +3185,7 @@ class StorageManagerService extends IStorageManager.Stub boolean foundPrimary = false; final ArrayList<StorageVolume> res = new ArrayList<>(); + final ArraySet<String> resUuids = new ArraySet<>(); synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { final VolumeInfo vol = mVolumes.valueAt(i); @@ -3222,7 +3228,43 @@ class StorageManagerService extends IStorageManager.Stub } else { res.add(userVol); } + resUuids.add(userVol.getUuid()); } + + if (includeRecent) { + final long lastWeek = System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS; + for (int i = 0; i < mRecords.size(); i++) { + final VolumeRecord rec = mRecords.valueAt(i); + + // Skip if we've already included it above + if (resUuids.contains(rec.fsUuid)) continue; + + // Treat as recent if mounted within the last week + if (rec.lastSeenMillis > 0 && rec.lastSeenMillis < lastWeek) { + final StorageVolume userVol = rec.buildStorageVolume(mContext); + res.add(userVol); + resUuids.add(userVol.getUuid()); + } + } + } + } + + // Synthesize a volume for preloaded media under demo users, so that + // it's scanned into MediaStore + if (userIsDemo) { + final String id = "demo"; + final File path = Environment.getDataPreloadsMediaDirectory(); + final boolean primary = false; + final boolean removable = false; + final boolean emulated = true; + final boolean allowMassStorage = false; + final long maxFileSize = 0; + final UserHandle user = new UserHandle(userId); + final String envState = Environment.MEDIA_MOUNTED_READ_ONLY; + final String description = mContext.getString(android.R.string.unknownName); + + res.add(new StorageVolume(id, path, path, description, primary, removable, + emulated, allowMassStorage, maxFileSize, user, id, envState)); } if (!foundPrimary) { diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index 4a0c7a33467d..0e144ed19520 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -31,7 +31,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.net.LinkProperties; -import android.net.NetworkCapabilities; import android.os.Binder; import android.os.Build; import android.os.Bundle; @@ -210,8 +209,6 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { private int[] mDataConnectionNetworkType; - private int[] mOtaspMode; - private ArrayList<List<CellInfo>> mCellInfo = null; private Map<Integer, List<EmergencyNumber>> mEmergencyNumberList; @@ -260,7 +257,9 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { private final LocalLog mListenLog = new LocalLog(100); - private PreciseDataConnectionState[] mPreciseDataConnectionState; + // Per-phoneMap of APN Type to DataConnectionState + private List<Map<String, PreciseDataConnectionState>> mPreciseDataConnectionStates = + new ArrayList<Map<String, PreciseDataConnectionState>>(); // Nothing here yet, but putting it here in case we want to add more in the future. static final int ENFORCE_COARSE_LOCATION_PERMISSION_MASK = 0; @@ -405,7 +404,6 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mCallForwarding = copyOf(mCallForwarding, mNumPhones); mCellLocation = copyOf(mCellLocation, mNumPhones); mSrvccState = copyOf(mSrvccState, mNumPhones); - mOtaspMode = copyOf(mOtaspMode, mNumPhones); mPreciseCallState = copyOf(mPreciseCallState, mNumPhones); mForegroundCallState = copyOf(mForegroundCallState, mNumPhones); mBackgroundCallState = copyOf(mBackgroundCallState, mNumPhones); @@ -415,7 +413,6 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mCallQuality = copyOf(mCallQuality, mNumPhones); mCallNetworkType = copyOf(mCallNetworkType, mNumPhones); mCallAttributes = copyOf(mCallAttributes, mNumPhones); - mPreciseDataConnectionState = copyOf(mPreciseDataConnectionState, mNumPhones); mOutgoingCallEmergencyNumber = copyOf(mOutgoingCallEmergencyNumber, mNumPhones); mOutgoingSmsEmergencyNumber = copyOf(mOutgoingSmsEmergencyNumber, mNumPhones); @@ -423,6 +420,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { if (mNumPhones < oldNumPhones) { cutListToSize(mCellInfo, mNumPhones); cutListToSize(mImsReasonInfo, mNumPhones); + cutListToSize(mPreciseDataConnectionStates, mNumPhones); return; } @@ -443,7 +441,6 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mCellInfo.add(i, null); mImsReasonInfo.add(i, null); mSrvccState[i] = TelephonyManager.SRVCC_STATE_HANDOVER_NONE; - mOtaspMode[i] = TelephonyManager.OTASP_UNKNOWN; mCallDisconnectCause[i] = DisconnectCause.NOT_VALID; mCallPreciseDisconnectCause[i] = PreciseDisconnectCause.NOT_VALID; mCallQuality[i] = createCallQuality(); @@ -454,7 +451,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mRingingCallState[i] = PreciseCallState.PRECISE_CALL_STATE_IDLE; mForegroundCallState[i] = PreciseCallState.PRECISE_CALL_STATE_IDLE; mBackgroundCallState[i] = PreciseCallState.PRECISE_CALL_STATE_IDLE; - mPreciseDataConnectionState[i] = new PreciseDataConnectionState(); + mPreciseDataConnectionStates.add(new HashMap<String, PreciseDataConnectionState>()); } // Note that location can be null for non-phone builds like @@ -506,7 +503,6 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mCallForwarding = new boolean[numPhones]; mCellLocation = new Bundle[numPhones]; mSrvccState = new int[numPhones]; - mOtaspMode = new int[numPhones]; mPreciseCallState = new PreciseCallState[numPhones]; mForegroundCallState = new int[numPhones]; mBackgroundCallState = new int[numPhones]; @@ -516,7 +512,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mCallQuality = new CallQuality[numPhones]; mCallNetworkType = new int[numPhones]; mCallAttributes = new CallAttributes[numPhones]; - mPreciseDataConnectionState = new PreciseDataConnectionState[numPhones]; + mPreciseDataConnectionStates = new ArrayList<>(); mCellInfo = new ArrayList<>(); mImsReasonInfo = new ArrayList<>(); mEmergencyNumberList = new HashMap<>(); @@ -538,7 +534,6 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mCellInfo.add(i, null); mImsReasonInfo.add(i, null); mSrvccState[i] = TelephonyManager.SRVCC_STATE_HANDOVER_NONE; - mOtaspMode[i] = TelephonyManager.OTASP_UNKNOWN; mCallDisconnectCause[i] = DisconnectCause.NOT_VALID; mCallPreciseDisconnectCause[i] = PreciseDisconnectCause.NOT_VALID; mCallQuality[i] = createCallQuality(); @@ -549,7 +544,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mRingingCallState[i] = PreciseCallState.PRECISE_CALL_STATE_IDLE; mForegroundCallState[i] = PreciseCallState.PRECISE_CALL_STATE_IDLE; mBackgroundCallState[i] = PreciseCallState.PRECISE_CALL_STATE_IDLE; - mPreciseDataConnectionState[i] = new PreciseDataConnectionState(); + mPreciseDataConnectionStates.add(new HashMap<String, PreciseDataConnectionState>()); } // Note that location can be null for non-phone builds like @@ -880,13 +875,6 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { remove(r.binder); } } - if ((events & PhoneStateListener.LISTEN_OTASP_CHANGED) != 0) { - try { - r.callback.onOtaspChanged(mOtaspMode[phoneId]); - } catch (RemoteException ex) { - remove(r.binder); - } - } if (validateEventsAndUserLocked(r, PhoneStateListener.LISTEN_CELL_INFO)) { try { if (DBG_LOC) log("listen: mCellInfo[" + phoneId + "] = " @@ -922,8 +910,10 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } if ((events & PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE) != 0) { try { - r.callback.onPreciseDataConnectionStateChanged( - mPreciseDataConnectionState[phoneId]); + for (PreciseDataConnectionState pdcs + : mPreciseDataConnectionStates.get(phoneId).values()) { + r.callback.onPreciseDataConnectionStateChanged(pdcs); + } } catch (RemoteException ex) { remove(r.binder); } @@ -937,7 +927,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } if ((events & PhoneStateListener.LISTEN_VOICE_ACTIVATION_STATE) !=0) { try { - r.callback.onVoiceActivationStateChanged(mVoiceActivationState[phoneId]); + r.callback.onVoiceActivationStateChanged( + mVoiceActivationState[phoneId]); } catch (RemoteException ex) { remove(r.binder); } @@ -1496,20 +1487,38 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } } - public void notifyDataConnectionForSubscriber(int phoneId, int subId, int state, - boolean isDataAllowed, - String apn, String apnType, - LinkProperties linkProperties, NetworkCapabilities networkCapabilities, - int networkType, boolean roaming) { + /** + * Send a notification to registrants that the data connection state has changed. + * + * @param phoneId the phoneId carrying the data connection + * @param subId the subscriptionId for the data connection + * @param apnType the APN type that triggered a change in the data connection + * @param preciseState a PreciseDataConnectionState that has info about the data connection + */ + public void notifyDataConnectionForSubscriber( + int phoneId, int subId, String apnType, PreciseDataConnectionState preciseState) { if (!checkNotifyPermission("notifyDataConnection()" )) { return; } + + String apn = ""; + int state = TelephonyManager.DATA_UNKNOWN; + int networkType = TelephonyManager.NETWORK_TYPE_UNKNOWN; + LinkProperties linkProps = null; + + if (preciseState != null) { + apn = preciseState.getDataConnectionApn(); + state = preciseState.getState(); + networkType = preciseState.getNetworkType(); + linkProps = preciseState.getDataConnectionLinkProperties(); + } if (VDBG) { log("notifyDataConnectionForSubscriber: subId=" + subId - + " state=" + state + " isDataAllowed=" + isDataAllowed - + "' apn='" + apn + "' apnType=" + apnType + " networkType=" + networkType - + " mRecords.size()=" + mRecords.size()); + + " state=" + state + "' apn='" + apn + + "' apnType=" + apnType + " networkType=" + networkType + + "' preciseState=" + preciseState); } + synchronized (mRecords) { if (validatePhoneId(phoneId)) { // We only call the callback when the change is for default APN type. @@ -1541,30 +1550,48 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mDataConnectionState[phoneId] = state; mDataConnectionNetworkType[phoneId] = networkType; } - mPreciseDataConnectionState[phoneId] = new PreciseDataConnectionState( - state, networkType, - ApnSetting.getApnTypesBitmaskFromString(apnType), apn, - linkProperties, DataFailCause.NONE); - for (Record r : mRecords) { - if (r.matchPhoneStateListenerEvent( - PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE) - && idMatch(r.subId, subId, phoneId)) { - try { - r.callback.onPreciseDataConnectionStateChanged( - mPreciseDataConnectionState[phoneId]); - } catch (RemoteException ex) { - mRemoveList.add(r.binder); + + boolean needsNotify = false; + // State has been cleared for this APN Type + if (preciseState == null) { + // We try clear the state and check if the state was previously not cleared + needsNotify = mPreciseDataConnectionStates.get(phoneId).remove(apnType) != null; + } else { + // We need to check to see if the state actually changed + PreciseDataConnectionState oldPreciseState = + mPreciseDataConnectionStates.get(phoneId).put(apnType, preciseState); + needsNotify = !preciseState.equals(oldPreciseState); + } + + if (needsNotify) { + for (Record r : mRecords) { + if (r.matchPhoneStateListenerEvent( + PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE) + && idMatch(r.subId, subId, phoneId)) { + try { + r.callback.onPreciseDataConnectionStateChanged(preciseState); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } } } } } handleRemoveListLocked(); } - broadcastDataConnectionStateChanged(state, isDataAllowed, apn, apnType, linkProperties, - networkCapabilities, roaming, subId); + + broadcastDataConnectionStateChanged(state, apn, apnType, subId); + } + + /** + * Stub to satisfy the ITelephonyRegistry aidl interface; do not use this function. + * @see #notifyDataConnectionFailedForSubscriber + */ + public void notifyDataConnectionFailed(String apnType) { + loge("This function should not be invoked"); } - public void notifyDataConnectionFailedForSubscriber(int phoneId, int subId, String apnType) { + private void notifyDataConnectionFailedForSubscriber(int phoneId, int subId, String apnType) { if (!checkNotifyPermission("notifyDataConnectionFailed()")) { return; } @@ -1574,17 +1601,20 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } synchronized (mRecords) { if (validatePhoneId(phoneId)) { - mPreciseDataConnectionState[phoneId] = new PreciseDataConnectionState( - TelephonyManager.DATA_UNKNOWN,TelephonyManager.NETWORK_TYPE_UNKNOWN, - ApnSetting.getApnTypesBitmaskFromString(apnType), null, null, - DataFailCause.NONE); + mPreciseDataConnectionStates.get(phoneId).put( + apnType, + new PreciseDataConnectionState( + TelephonyManager.DATA_UNKNOWN, + TelephonyManager.NETWORK_TYPE_UNKNOWN, + ApnSetting.getApnTypesBitmaskFromString(apnType), null, null, + DataFailCause.NONE)); for (Record r : mRecords) { if (r.matchPhoneStateListenerEvent( PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE) && idMatch(r.subId, subId, phoneId)) { try { r.callback.onPreciseDataConnectionStateChanged( - mPreciseDataConnectionState[phoneId]); + mPreciseDataConnectionStates.get(phoneId).get(apnType)); } catch (RemoteException ex) { mRemoveList.add(r.binder); } @@ -1635,29 +1665,6 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } } - public void notifyOtaspChanged(int subId, int otaspMode) { - if (!checkNotifyPermission("notifyOtaspChanged()" )) { - return; - } - int phoneId = SubscriptionManager.getPhoneId(subId); - synchronized (mRecords) { - if (validatePhoneId(phoneId)) { - mOtaspMode[phoneId] = otaspMode; - for (Record r : mRecords) { - if (r.matchPhoneStateListenerEvent(PhoneStateListener.LISTEN_OTASP_CHANGED) - && idMatch(r.subId, subId, phoneId)) { - try { - r.callback.onOtaspChanged(otaspMode); - } catch (RemoteException ex) { - mRemoveList.add(r.binder); - } - } - } - } - handleRemoveListLocked(); - } - } - public void notifyPreciseCallState(int phoneId, int subId, int ringingCallState, int foregroundCallState, int backgroundCallState) { if (!checkNotifyPermission("notifyPreciseCallState()")) { @@ -1772,25 +1779,32 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { if (!checkNotifyPermission("notifyPreciseDataConnectionFailed()")) { return; } + + // precise notify invokes imprecise notify + notifyDataConnectionFailedForSubscriber(phoneId, subId, apnType); + synchronized (mRecords) { if (validatePhoneId(phoneId)) { - mPreciseDataConnectionState[phoneId] = new PreciseDataConnectionState( - TelephonyManager.DATA_UNKNOWN, TelephonyManager.NETWORK_TYPE_UNKNOWN, - ApnSetting.getApnTypesBitmaskFromString(apnType), apn, null, failCause); + mPreciseDataConnectionStates.get(phoneId).put( + apnType, + new PreciseDataConnectionState( + TelephonyManager.DATA_UNKNOWN, + TelephonyManager.NETWORK_TYPE_UNKNOWN, + ApnSetting.getApnTypesBitmaskFromString(apnType), null, null, + failCause)); for (Record r : mRecords) { if (r.matchPhoneStateListenerEvent( PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE) && idMatch(r.subId, subId, phoneId)) { try { r.callback.onPreciseDataConnectionStateChanged( - mPreciseDataConnectionState[phoneId]); + mPreciseDataConnectionStates.get(phoneId).get(apnType)); } catch (RemoteException ex) { mRemoveList.add(r.binder); } } } } - handleRemoveListLocked(); } } @@ -2048,7 +2062,6 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } } - @Override public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); @@ -2082,12 +2095,11 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { pw.println("mCellInfo=" + mCellInfo.get(i)); pw.println("mImsCallDisconnectCause=" + mImsReasonInfo.get(i)); pw.println("mSrvccState=" + mSrvccState[i]); - pw.println("mOtaspMode=" + mOtaspMode[i]); pw.println("mCallPreciseDisconnectCause=" + mCallPreciseDisconnectCause[i]); pw.println("mCallQuality=" + mCallQuality[i]); pw.println("mCallAttributes=" + mCallAttributes[i]); pw.println("mCallNetworkType=" + mCallNetworkType[i]); - pw.println("mPreciseDataConnectionState=" + mPreciseDataConnectionState[i]); + pw.println("mPreciseDataConnectionStates=" + mPreciseDataConnectionStates.get(i)); pw.println("mOutgoingCallEmergencyNumber=" + mOutgoingCallEmergencyNumber[i]); pw.println("mOutgoingSmsEmergencyNumber=" + mOutgoingSmsEmergencyNumber[i]); pw.decreaseIndent(); @@ -2247,29 +2259,13 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } } - private void broadcastDataConnectionStateChanged(int state, boolean isDataAllowed, String apn, - String apnType, LinkProperties linkProperties, - NetworkCapabilities networkCapabilities, - boolean roaming, int subId) { + private void broadcastDataConnectionStateChanged(int state, String apn, + String apnType, int subId) { // Note: not reporting to the battery stats service here, because the // status bar takes care of that after taking into account all of the // required info. Intent intent = new Intent(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED); intent.putExtra(TelephonyManager.EXTRA_STATE, dataStateToString(state)); - if (!isDataAllowed) { - intent.putExtra(PhoneConstants.NETWORK_UNAVAILABLE_KEY, true); - } - if (linkProperties != null) { - intent.putExtra(PhoneConstants.DATA_LINK_PROPERTIES_KEY, linkProperties); - String iface = linkProperties.getInterfaceName(); - if (iface != null) { - intent.putExtra(PhoneConstants.DATA_IFACE_NAME_KEY, iface); - } - } - if (networkCapabilities != null) { - intent.putExtra(PhoneConstants.DATA_NETWORK_CAPABILITIES_KEY, networkCapabilities); - } - if (roaming) intent.putExtra(PhoneConstants.DATA_NETWORK_ROAMING_KEY, true); intent.putExtra(PhoneConstants.DATA_APN_KEY, apn); intent.putExtra(PhoneConstants.DATA_APN_TYPE_KEY, apnType); diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java index f6c11cd1d0d6..76a8f92312ae 100644 --- a/services/core/java/com/android/server/VibratorService.java +++ b/services/core/java/com/android/server/VibratorService.java @@ -61,6 +61,7 @@ import android.provider.DeviceConfig; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; import android.util.DebugUtils; +import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.util.StatsLog; @@ -162,6 +163,8 @@ public class VibratorService extends IVibratorService.Stub private int mHapticFeedbackIntensity; private int mNotificationIntensity; private int mRingIntensity; + private SparseArray<Pair<VibrationEffect, AudioAttributes>> mAlwaysOnEffects = + new SparseArray<>(); static native boolean vibratorExists(); static native void vibratorInit(); @@ -173,6 +176,8 @@ public class VibratorService extends IVibratorService.Stub static native boolean vibratorSupportsExternalControl(); static native void vibratorSetExternalControl(boolean enabled); static native long vibratorGetCapabilities(); + static native void vibratorAlwaysOnEnable(long id, long effect, long strength); + static native void vibratorAlwaysOnDisable(long id); private final IUidObserver mUidObserver = new IUidObserver.Stub() { @Override public void onUidStateChanged(int uid, int procState, long procStateSeq, @@ -522,6 +527,41 @@ public class VibratorService extends IVibratorService.Stub } } + @Override // Binder call + public boolean setAlwaysOnEffect(int id, VibrationEffect effect, AudioAttributes attrs) { + if (!hasPermission(android.Manifest.permission.VIBRATE_ALWAYS_ON)) { + throw new SecurityException("Requires VIBRATE_ALWAYS_ON permission"); + } + if ((mCapabilities & IVibrator.CAP_ALWAYS_ON_CONTROL) == 0) { + Slog.e(TAG, "Always-on effects not supported."); + return false; + } + if (effect == null) { + synchronized (mLock) { + mAlwaysOnEffects.delete(id); + vibratorAlwaysOnDisable(id); + } + } else { + if (!verifyVibrationEffect(effect)) { + return false; + } + if (!(effect instanceof VibrationEffect.Prebaked)) { + Slog.e(TAG, "Only prebaked effects supported for always-on."); + return false; + } + if (attrs == null) { + attrs = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_UNKNOWN) + .build(); + } + synchronized (mLock) { + mAlwaysOnEffects.put(id, Pair.create(effect, attrs)); + updateAlwaysOnLocked(id, effect, attrs); + } + } + return true; + } + private void verifyIncomingUid(int uid) { if (uid == Binder.getCallingUid()) { return; @@ -992,6 +1032,8 @@ public class VibratorService extends IVibratorService.Stub // If the state changes out from under us then just reset. doCancelVibrateLocked(); } + + updateAlwaysOnLocked(); } } @@ -1058,6 +1100,27 @@ public class VibratorService extends IVibratorService.Stub mVibrator.getDefaultRingVibrationIntensity(), UserHandle.USER_CURRENT); } + private void updateAlwaysOnLocked(int id, VibrationEffect effect, AudioAttributes attrs) { + // TODO: Check DND and LowPower settings + final Vibration vib = new Vibration(null, effect, attrs, 0, null, null); + final int intensity = getCurrentIntensityLocked(vib); + if (intensity == Vibrator.VIBRATION_INTENSITY_OFF) { + vibratorAlwaysOnDisable(id); + } else { + final VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) effect; + final int strength = intensityToEffectStrength(intensity); + vibratorAlwaysOnEnable(id, prebaked.getId(), strength); + } + } + + private void updateAlwaysOnLocked() { + for (int i = 0; i < mAlwaysOnEffects.size(); i++) { + int id = mAlwaysOnEffects.keyAt(i); + Pair<VibrationEffect, AudioAttributes> pair = mAlwaysOnEffects.valueAt(i); + updateAlwaysOnLocked(id, pair.first, pair.second); + } + } + @Override public void onInputDeviceAdded(int deviceId) { updateVibrators(); diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index e101fe0e192c..5996b7d848ea 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -4409,7 +4409,6 @@ public class AccountManagerService return true; } - @Override public boolean renameSharedAccountAsUser(Account account, String newName, int userId) { userId = handleIncomingUser(userId); UserAccounts accounts = getUserAccounts(userId); @@ -4425,7 +4424,6 @@ public class AccountManagerService return r > 0; } - @Override public boolean removeSharedAccountAsUser(Account account, int userId) { return removeSharedAccountAsUser(account, userId, getCallingUid()); } @@ -4443,7 +4441,6 @@ public class AccountManagerService return deleted; } - @Override public Account[] getSharedAccountsAsUser(int userId) { userId = handleIncomingUser(userId); UserAccounts accounts = getUserAccounts(userId); diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 5378f438fea5..ee064191f97a 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -461,9 +461,7 @@ public final class ProcessList { @GuardedBy("ProcessList.this.mService") void freeIsolatedUidLocked(int uid) { - // Strip out userId - final int appId = UserHandle.getAppId(uid); - mUidUsed.delete(appId); + mUidUsed.delete(uid); } }; diff --git a/services/core/java/com/android/server/attention/AttentionManagerService.java b/services/core/java/com/android/server/attention/AttentionManagerService.java index 232bc08e5b5c..fc67e24684bb 100644 --- a/services/core/java/com/android/server/attention/AttentionManagerService.java +++ b/services/core/java/com/android/server/attention/AttentionManagerService.java @@ -77,21 +77,28 @@ public class AttentionManagerService extends SystemService { private static final String LOG_TAG = "AttentionManagerService"; private static final boolean DEBUG = false; + /** Service will unbind if connection is not used for that amount of time. */ + private static final long CONNECTION_TTL_MILLIS = 60_000; + + /** DeviceConfig flag name, if {@code true}, enables AttentionManagerService features. */ + private static final String KEY_SERVICE_ENABLED = "service_enabled"; + /** Default value in absence of {@link DeviceConfig} override. */ private static final boolean DEFAULT_SERVICE_ENABLED = true; - /** Service will unbind if connection is not used for that amount of time. */ - private static final long CONNECTION_TTL_MILLIS = 60_000; + /** + * DeviceConfig flag name, describes how much time we consider a result fresh; if the check + * attention called within that period - cached value will be returned. + */ + @VisibleForTesting static final String KEY_STALE_AFTER_MILLIS = "stale_after_millis"; - /** If the check attention called within that period - cached value will be returned. */ - private static final long STALE_AFTER_MILLIS = 5_000; + /** Default value in absence of {@link DeviceConfig} override. */ + @VisibleForTesting static final long DEFAULT_STALE_AFTER_MILLIS = 1_000; /** The size of the buffer that stores recent attention check results. */ @VisibleForTesting protected static final int ATTENTION_CACHE_BUFFER_SIZE = 5; - /** DeviceConfig flag name, if {@code true}, enables AttentionManagerService features. */ - private static final String SERVICE_ENABLED = "service_enabled"; private static String sTestAttentionServicePackage; private final Context mContext; private final PowerManager mPowerManager; @@ -160,11 +167,29 @@ public class AttentionManagerService extends SystemService { @VisibleForTesting protected boolean isServiceEnabled() { - return DeviceConfig.getBoolean(NAMESPACE_ATTENTION_MANAGER_SERVICE, SERVICE_ENABLED, + return DeviceConfig.getBoolean(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_SERVICE_ENABLED, DEFAULT_SERVICE_ENABLED); } /** + * How much time we consider a result fresh; if the check attention called within that period - + * cached value will be returned. + */ + @VisibleForTesting + protected long getStaleAfterMillis() { + final long millis = DeviceConfig.getLong(NAMESPACE_ATTENTION_MANAGER_SERVICE, + KEY_STALE_AFTER_MILLIS, + DEFAULT_STALE_AFTER_MILLIS); + + if (millis < 0 || millis > 10_000) { + Slog.w(LOG_TAG, "Bad flag value supplied for: " + KEY_STALE_AFTER_MILLIS); + return DEFAULT_STALE_AFTER_MILLIS; + } + + return millis; + } + + /** * Checks whether user attention is at the screen and calls in the provided callback. * * Calling this multiple times quickly in a row will result in either a) returning a cached @@ -199,7 +224,7 @@ public class AttentionManagerService extends SystemService { // throttle frequent requests final AttentionCheckCache cache = userState.mAttentionCheckCacheBuffer == null ? null : userState.mAttentionCheckCacheBuffer.getLast(); - if (cache != null && now < cache.mLastComputed + STALE_AFTER_MILLIS) { + if (cache != null && now < cache.mLastComputed + getStaleAfterMillis()) { callbackInternal.onSuccess(cache.mResult, cache.mTimestamp); return true; } diff --git a/services/core/java/com/android/server/biometrics/BiometricServiceBase.java b/services/core/java/com/android/server/biometrics/BiometricServiceBase.java index 2de18c391d4a..60f0e8e6c63d 100644 --- a/services/core/java/com/android/server/biometrics/BiometricServiceBase.java +++ b/services/core/java/com/android/server/biometrics/BiometricServiceBase.java @@ -16,6 +16,7 @@ package com.android.server.biometrics; +import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE; import android.app.ActivityManager; @@ -1013,8 +1014,13 @@ public abstract class BiometricServiceBase extends SystemService private boolean isForegroundActivity(int uid, int pid) { try { - List<ActivityManager.RunningAppProcessInfo> procs = + final List<ActivityManager.RunningAppProcessInfo> procs = ActivityManager.getService().getRunningAppProcesses(); + if (procs == null) { + Slog.e(getTag(), "Processes null, defaulting to true"); + return true; + } + int N = procs.size(); for (int i = 0; i < N; i++) { ActivityManager.RunningAppProcessInfo proc = procs.get(i); @@ -1206,6 +1212,11 @@ public abstract class BiometricServiceBase extends SystemService * @return authenticator id for the calling user */ protected long getAuthenticatorId(String opPackageName) { + if (isKeyguard(opPackageName)) { + // If an app tells us it's keyguard, check that it actually is. + checkPermission(USE_BIOMETRIC_INTERNAL); + } + final int userId = getUserOrWorkProfileId(opPackageName, UserHandle.getCallingUserId()); return mAuthenticatorIds.getOrDefault(userId, 0L); } diff --git a/services/core/java/com/android/server/biometrics/OWNERS b/services/core/java/com/android/server/biometrics/OWNERS new file mode 100644 index 000000000000..8765c9a64b77 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/OWNERS @@ -0,0 +1,7 @@ +set noparent + +kchyn@google.com +jaggies@google.com +curtislb@google.com +ilyamaty@google.com +joshmccloskey@google.com diff --git a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java index 9bae902eb7b1..af8a3666e9b7 100644 --- a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java +++ b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java @@ -39,11 +39,11 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.net.ISocketKeepaliveCallback; +import android.net.InvalidPacketException; import android.net.KeepalivePacketData; import android.net.NattKeepalivePacketData; import android.net.NetworkAgent; import android.net.NetworkUtils; -import android.net.SocketKeepalive.InvalidPacketException; import android.net.SocketKeepalive.InvalidSocketException; import android.net.TcpKeepalivePacketData; import android.net.util.IpUtils; @@ -657,7 +657,10 @@ public class KeepaliveTracker { final TcpKeepalivePacketData packet; try { packet = TcpKeepaliveController.getTcpKeepalivePacket(fd); - } catch (InvalidPacketException | InvalidSocketException e) { + } catch (InvalidSocketException e) { + notifyErrorCallback(cb, e.error); + return; + } catch (InvalidPacketException e) { notifyErrorCallback(cb, e.error); return; } diff --git a/services/core/java/com/android/server/connectivity/TcpKeepaliveController.java b/services/core/java/com/android/server/connectivity/TcpKeepaliveController.java index e570ef1e9bf2..1129899ee3ff 100644 --- a/services/core/java/com/android/server/connectivity/TcpKeepaliveController.java +++ b/services/core/java/com/android/server/connectivity/TcpKeepaliveController.java @@ -30,8 +30,8 @@ import static android.system.OsConstants.IP_TTL; import static android.system.OsConstants.TIOCOUTQ; import android.annotation.NonNull; +import android.net.InvalidPacketException; import android.net.NetworkUtils; -import android.net.SocketKeepalive.InvalidPacketException; import android.net.SocketKeepalive.InvalidSocketException; import android.net.TcpKeepalivePacketData; import android.net.TcpKeepalivePacketDataParcelable; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java index 944a95dda99b..44c8971ba4c2 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java @@ -18,8 +18,14 @@ package com.android.server.inputmethod; import android.annotation.NonNull; import android.annotation.UserIdInt; +import android.content.ComponentName; +import android.os.RemoteException; +import android.util.Log; +import android.view.autofill.AutofillId; +import android.view.inputmethod.InlineSuggestionsRequest; import android.view.inputmethod.InputMethodInfo; +import com.android.internal.view.IInlineSuggestionsRequestCallback; import com.android.server.LocalServices; import java.util.Collections; @@ -57,6 +63,17 @@ public abstract class InputMethodManagerInternal { public abstract List<InputMethodInfo> getEnabledInputMethodListAsUser(@UserIdInt int userId); /** + * Called by the Autofill Frameworks to request an {@link InlineSuggestionsRequest} from + * the input method. + * + * @param componentName {@link ComponentName} of current app/activity. + * @param autofillId {@link AutofillId} of currently focused field. + * @param cb {@link IInlineSuggestionsRequestCallback} used to pass back the request object. + */ + public abstract void onCreateInlineSuggestionsRequest(ComponentName componentName, + AutofillId autofillId, IInlineSuggestionsRequestCallback cb); + + /** * Fake implementation of {@link InputMethodManagerInternal}. All the methods do nothing. */ private static final InputMethodManagerInternal NOP = @@ -78,6 +95,17 @@ public abstract class InputMethodManagerInternal { public List<InputMethodInfo> getEnabledInputMethodListAsUser(int userId) { return Collections.emptyList(); } + + @Override + public void onCreateInlineSuggestionsRequest(ComponentName componentName, + AutofillId autofillId, IInlineSuggestionsRequestCallback cb) { + try { + cb.onInlineSuggestionsUnsupported(); + } catch (RemoteException e) { + Log.w("IMManagerInternal", "RemoteException calling" + + " onInlineSuggestionsUnsupported: " + e); + } + } }; /** diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 471fa72b1957..5865dc471b30 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -109,6 +109,7 @@ import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager.LayoutParams; import android.view.WindowManager.LayoutParams.SoftInputModeFlags; +import android.view.autofill.AutofillId; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputBinding; import android.view.inputmethod.InputConnection; @@ -140,6 +141,7 @@ import com.android.internal.os.SomeArgs; import com.android.internal.os.TransferPipe; import com.android.internal.util.DumpUtils; import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.view.IInlineSuggestionsRequestCallback; import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethod; import com.android.internal.view.IInputMethodClient; @@ -211,6 +213,8 @@ public class InputMethodManagerService extends IInputMethodManager.Stub static final int MSG_SYSTEM_UNLOCK_USER = 5000; + static final int MSG_INLINE_SUGGESTIONS_REQUEST = 6000; + static final long TIME_TO_RECONNECT = 3 * 1000; static final int SECURE_SUGGESTION_SPANS_MAX_SIZE = 20; @@ -1785,6 +1789,16 @@ public class InputMethodManagerService extends IInputMethodManager.Stub return settings.getEnabledInputMethodListLocked(); } + @GuardedBy("mMethodMap") + private void onCreateInlineSuggestionsRequestLocked(ComponentName componentName, + AutofillId autofillId, IInlineSuggestionsRequestCallback callback) { + if (mCurMethod != null) { + executeOrSendMessage(mCurMethod, + mCaller.obtainMessageOOOO(MSG_INLINE_SUGGESTIONS_REQUEST, mCurMethod, + componentName, autofillId, callback)); + } + } + /** * @param imiId if null, returns enabled subtypes for the current imi * @return enabled subtypes of the specified imi @@ -3874,6 +3888,21 @@ public class InputMethodManagerService extends IInputMethodManager.Stub final int userId = msg.arg1; onUnlockUser(userId); return true; + + // --------------------------------------------------------------- + case MSG_INLINE_SUGGESTIONS_REQUEST: + args = (SomeArgs) msg.obj; + final ComponentName componentName = (ComponentName) args.arg2; + final AutofillId autofillId = (AutofillId) args.arg3; + final IInlineSuggestionsRequestCallback callback = + (IInlineSuggestionsRequestCallback) args.arg4; + try { + ((IInputMethod) args.arg1).onCreateInlineSuggestionsRequest(componentName, + autofillId, callback); + } catch (RemoteException e) { + Slog.w(TAG, "RemoteException calling onCreateInlineSuggestionsRequest(): " + e); + } + return true; } return false; } @@ -4434,6 +4463,13 @@ public class InputMethodManagerService extends IInputMethodManager.Stub } } + private void onCreateInlineSuggestionsRequest(ComponentName componentName, + AutofillId autofillId, IInlineSuggestionsRequestCallback callback) { + synchronized (mMethodMap) { + onCreateInlineSuggestionsRequestLocked(componentName, autofillId, callback); + } + } + private static final class LocalServiceImpl extends InputMethodManagerInternal { @NonNull private final InputMethodManagerService mService; @@ -4464,6 +4500,12 @@ public class InputMethodManagerService extends IInputMethodManager.Stub public List<InputMethodInfo> getEnabledInputMethodListAsUser(int userId) { return mService.getEnabledInputMethodListAsUser(userId); } + + @Override + public void onCreateInlineSuggestionsRequest(ComponentName componentName, + AutofillId autofillId, IInlineSuggestionsRequestCallback cb) { + mService.onCreateInlineSuggestionsRequest(componentName, autofillId, cb); + } } @BinderThread diff --git a/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java index 02e29e0b2ab5..c13d55adda08 100644 --- a/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/MultiClientInputMethodManagerService.java @@ -59,10 +59,12 @@ import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.Log; import android.util.Slog; import android.util.SparseArray; import android.view.InputChannel; import android.view.WindowManager.LayoutParams.SoftInputModeFlags; +import android.view.autofill.AutofillId; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags; import android.view.inputmethod.InputMethodInfo; @@ -82,6 +84,7 @@ import com.android.internal.os.TransferPipe; import com.android.internal.util.DumpUtils; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.function.pooled.PooledLambda; +import com.android.internal.view.IInlineSuggestionsRequestCallback; import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethodClient; import com.android.internal.view.IInputMethodManager; @@ -187,6 +190,18 @@ public final class MultiClientInputMethodManagerService { @UserIdInt int userId) { return userIdToInputMethodInfoMapper.getAsList(userId); } + + @Override + public void onCreateInlineSuggestionsRequest(ComponentName componentName, + AutofillId autofillId, IInlineSuggestionsRequestCallback cb) { + try { + //TODO(b/137800469): support multi client IMEs. + cb.onInlineSuggestionsUnsupported(); + } catch (RemoteException e) { + Log.w("MultiClientIMManager", "RemoteException calling" + + " onInlineSuggestionsUnsupported: " + e); + } + } }); } diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 2cb7e3b62479..069aeefd7a73 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -47,7 +47,8 @@ import android.media.MediaController2; import android.media.Session2CommandGroup; import android.media.Session2Token; import android.media.session.IActiveSessionsListener; -import android.media.session.ICallback; +import android.media.session.IOnMediaKeyEventDispatchedListener; +import android.media.session.IOnMediaKeyEventSessionChangedListener; import android.media.session.IOnMediaKeyListener; import android.media.session.IOnVolumeKeyLongPressListener; import android.media.session.ISession; @@ -750,7 +751,10 @@ public class MediaSessionService extends SystemService implements Monitor { private final int mFullUserId; private final MediaSessionStack mPriorityStack; - private final HashMap<IBinder, CallbackRecord> mCallbacks = new HashMap<>(); + private final HashMap<IBinder, OnMediaKeyEventDispatchedListenerRecord> + mOnMediaKeyEventDispatchedListeners = new HashMap<>(); + private final HashMap<IBinder, OnMediaKeyEventSessionChangedListenerRecord> + mOnMediaKeyEventSessionChangedListeners = new HashMap<>(); private PendingIntent mLastMediaButtonReceiver; private ComponentName mRestoredMediaButtonReceiver; @@ -796,21 +800,47 @@ public class MediaSessionService extends SystemService implements Monitor { } } - public void registerCallbackLocked(ICallback callback, int uid) { - IBinder cbBinder = callback.asBinder(); - CallbackRecord cr = new CallbackRecord(callback, uid); - mCallbacks.put(cbBinder, cr); + public void addOnMediaKeyEventDispatchedListenerLocked( + IOnMediaKeyEventDispatchedListener listener, int uid) { + IBinder cbBinder = listener.asBinder(); + OnMediaKeyEventDispatchedListenerRecord cr = + new OnMediaKeyEventDispatchedListenerRecord(listener, uid); + mOnMediaKeyEventDispatchedListeners.put(cbBinder, cr); try { cbBinder.linkToDeath(cr, 0); } catch (RemoteException e) { - Log.w(TAG, "Failed to register callback", e); - mCallbacks.remove(cbBinder); + Log.w(TAG, "Failed to add listener", e); + mOnMediaKeyEventDispatchedListeners.remove(cbBinder); } } - public void unregisterCallbackLocked(ICallback callback) { - IBinder cbBinder = callback.asBinder(); - CallbackRecord cr = mCallbacks.remove(cbBinder); + public void removeOnMediaKeyEventDispatchedListenerLocked( + IOnMediaKeyEventDispatchedListener listener) { + IBinder cbBinder = listener.asBinder(); + OnMediaKeyEventDispatchedListenerRecord cr = + mOnMediaKeyEventDispatchedListeners.remove(cbBinder); + cbBinder.unlinkToDeath(cr, 0); + } + + public void addOnMediaKeyEventSessionChangedListenerLocked( + IOnMediaKeyEventSessionChangedListener listener, int uid) { + IBinder cbBinder = listener.asBinder(); + OnMediaKeyEventSessionChangedListenerRecord cr = + new OnMediaKeyEventSessionChangedListenerRecord(listener, uid); + mOnMediaKeyEventSessionChangedListeners.put(cbBinder, cr); + try { + cbBinder.linkToDeath(cr, 0); + } catch (RemoteException e) { + Log.w(TAG, "Failed to add listener", e); + mOnMediaKeyEventSessionChangedListeners.remove(cbBinder); + } + } + + public void removeOnMediaKeyEventSessionChangedListener( + IOnMediaKeyEventSessionChangedListener listener) { + IBinder cbBinder = listener.asBinder(); + OnMediaKeyEventSessionChangedListenerRecord cr = + mOnMediaKeyEventSessionChangedListeners.remove(cbBinder); cbBinder.unlinkToDeath(cr, 0); } @@ -832,8 +862,16 @@ public class MediaSessionService extends SystemService implements Monitor { pw.println(indent + "Media key listener: " + mOnMediaKeyListener); pw.println(indent + "Media key listener package: " + getCallingPackageName(mOnMediaKeyListenerUid)); - pw.println(indent + "Callbacks: registered " + mCallbacks.size() + " callback(s)"); - for (CallbackRecord cr : mCallbacks.values()) { + pw.println(indent + "OnMediaKeyEventDispatchedListener: added " + + mOnMediaKeyEventDispatchedListeners.size() + " listener(s)"); + for (OnMediaKeyEventDispatchedListenerRecord cr + : mOnMediaKeyEventDispatchedListeners.values()) { + pw.println(indent + " from " + getCallingPackageName(cr.uid)); + } + pw.println(indent + "OnMediaKeyEventSessionChangedListener: added " + + mOnMediaKeyEventSessionChangedListeners.size() + " listener(s)"); + for (OnMediaKeyEventSessionChangedListenerRecord cr + : mOnMediaKeyEventSessionChangedListeners.values()) { pw.println(indent + " from " + getCallingPackageName(cr.uid)); } pw.println(indent + "Last MediaButtonReceiver: " + mLastMediaButtonReceiver); @@ -895,19 +933,22 @@ public class MediaSessionService extends SystemService implements Monitor { mFullUserId); } - private void pushAddressedPlayerChangedLocked(ICallback callback) { + private void pushAddressedPlayerChangedLocked( + IOnMediaKeyEventSessionChangedListener callback) { try { MediaSessionRecord mediaButtonSession = getMediaButtonSessionLocked(); if (mediaButtonSession != null) { - callback.onAddressedPlayerChangedToMediaSession( + callback.onMediaKeyEventSessionChanged(mediaButtonSession.getPackageName(), mediaButtonSession.getSessionToken()); } else if (mCurrentFullUserRecord.mLastMediaButtonReceiver != null) { - callback.onAddressedPlayerChangedToMediaButtonReceiver( + callback.onMediaKeyEventSessionChanged( mCurrentFullUserRecord.mLastMediaButtonReceiver - .getIntent().getComponent()); + .getIntent().getComponent().getPackageName(), + null); } else if (mCurrentFullUserRecord.mRestoredMediaButtonReceiver != null) { - callback.onAddressedPlayerChangedToMediaButtonReceiver( - mCurrentFullUserRecord.mRestoredMediaButtonReceiver); + callback.onMediaKeyEventSessionChanged( + mCurrentFullUserRecord.mRestoredMediaButtonReceiver.getPackageName(), + null); } } catch (RemoteException e) { Log.w(TAG, "Failed to pushAddressedPlayerChangedLocked", e); @@ -915,7 +956,8 @@ public class MediaSessionService extends SystemService implements Monitor { } private void pushAddressedPlayerChangedLocked() { - for (CallbackRecord cr : mCallbacks.values()) { + for (OnMediaKeyEventSessionChangedListenerRecord cr + : mOnMediaKeyEventSessionChangedListeners.values()) { pushAddressedPlayerChangedLocked(cr.callback); } } @@ -954,11 +996,12 @@ public class MediaSessionService extends SystemService implements Monitor { return COMPONENT_TYPE_BROADCAST; } - final class CallbackRecord implements IBinder.DeathRecipient { - public final ICallback callback; + final class OnMediaKeyEventDispatchedListenerRecord implements IBinder.DeathRecipient { + public final IOnMediaKeyEventDispatchedListener callback; public final int uid; - CallbackRecord(ICallback callback, int uid) { + OnMediaKeyEventDispatchedListenerRecord(IOnMediaKeyEventDispatchedListener callback, + int uid) { this.callback = callback; this.uid = uid; } @@ -966,7 +1009,25 @@ public class MediaSessionService extends SystemService implements Monitor { @Override public void binderDied() { synchronized (mLock) { - mCallbacks.remove(callback.asBinder()); + mOnMediaKeyEventDispatchedListeners.remove(callback.asBinder()); + } + } + } + + final class OnMediaKeyEventSessionChangedListenerRecord implements IBinder.DeathRecipient { + public final IOnMediaKeyEventSessionChangedListener callback; + public final int uid; + + OnMediaKeyEventSessionChangedListenerRecord( + IOnMediaKeyEventSessionChangedListener callback, int uid) { + this.callback = callback; + this.uid = uid; + } + + @Override + public void binderDied() { + synchronized (mLock) { + mOnMediaKeyEventSessionChangedListeners.remove(callback.asBinder()); } } } @@ -1355,7 +1416,64 @@ public class MediaSessionService extends SystemService implements Monitor { } @Override - public void registerCallback(final ICallback callback) { + public void addOnMediaKeyEventDispatchedListener( + final IOnMediaKeyEventDispatchedListener listener) { + final int pid = Binder.getCallingPid(); + final int uid = Binder.getCallingUid(); + final int userId = UserHandle.getUserId(uid); + final long token = Binder.clearCallingIdentity(); + try { + if (!hasMediaControlPermission(pid, uid)) { + throw new SecurityException("MEDIA_CONTENT_CONTROL permission is required to" + + " add MediaKeyEventDispatchedListener"); + } + synchronized (mLock) { + FullUserRecord user = getFullUserRecordLocked(userId); + if (user == null || user.mFullUserId != userId) { + Log.w(TAG, "Only the full user can add the listener" + + ", userId=" + userId); + return; + } + user.addOnMediaKeyEventDispatchedListenerLocked(listener, uid); + Log.d(TAG, "The MediaKeyEventDispatchedListener (" + listener.asBinder() + + ") is added by " + getCallingPackageName(uid)); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void removeOnMediaKeyEventDispatchedListener( + final IOnMediaKeyEventDispatchedListener listener) { + final int pid = Binder.getCallingPid(); + final int uid = Binder.getCallingUid(); + final int userId = UserHandle.getUserId(uid); + final long token = Binder.clearCallingIdentity(); + try { + if (!hasMediaControlPermission(pid, uid)) { + throw new SecurityException("MEDIA_CONTENT_CONTROL permission is required to" + + " remove MediaKeyEventDispatchedListener"); + } + synchronized (mLock) { + FullUserRecord user = getFullUserRecordLocked(userId); + if (user == null || user.mFullUserId != userId) { + Log.w(TAG, "Only the full user can remove the listener" + + ", userId=" + userId); + return; + } + user.removeOnMediaKeyEventDispatchedListenerLocked(listener); + Log.d(TAG, "The MediaKeyEventDispatchedListener (" + listener.asBinder() + + ") is removed by " + getCallingPackageName(uid)); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void addOnMediaKeyEventSessionChangedListener( + final IOnMediaKeyEventSessionChangedListener listener) { final int pid = Binder.getCallingPid(); final int uid = Binder.getCallingUid(); final int userId = UserHandle.getUserId(uid); @@ -1363,18 +1481,18 @@ public class MediaSessionService extends SystemService implements Monitor { try { if (!hasMediaControlPermission(pid, uid)) { throw new SecurityException("MEDIA_CONTENT_CONTROL permission is required to" - + " register Callback"); + + " add MediaKeyEventSessionChangedListener"); } synchronized (mLock) { FullUserRecord user = getFullUserRecordLocked(userId); if (user == null || user.mFullUserId != userId) { - Log.w(TAG, "Only the full user can register the callback" + Log.w(TAG, "Only the full user can add the listener" + ", userId=" + userId); return; } - user.registerCallbackLocked(callback, uid); - Log.d(TAG, "The callback (" + callback.asBinder() - + ") is registered by " + getCallingPackageName(uid)); + user.addOnMediaKeyEventSessionChangedListenerLocked(listener, uid); + Log.d(TAG, "The MediaKeyEventSessionChangedListener (" + listener.asBinder() + + ") is added by " + getCallingPackageName(uid)); } } finally { Binder.restoreCallingIdentity(token); @@ -1382,7 +1500,8 @@ public class MediaSessionService extends SystemService implements Monitor { } @Override - public void unregisterCallback(final ICallback callback) { + public void removeOnMediaKeyEventSessionChangedListener( + final IOnMediaKeyEventSessionChangedListener listener) { final int pid = Binder.getCallingPid(); final int uid = Binder.getCallingUid(); final int userId = UserHandle.getUserId(uid); @@ -1390,18 +1509,18 @@ public class MediaSessionService extends SystemService implements Monitor { try { if (!hasMediaControlPermission(pid, uid)) { throw new SecurityException("MEDIA_CONTENT_CONTROL permission is required to" - + " unregister Callback"); + + " remove MediaKeyEventSessionChangedListener"); } synchronized (mLock) { FullUserRecord user = getFullUserRecordLocked(userId); if (user == null || user.mFullUserId != userId) { - Log.w(TAG, "Only the full user can unregister the callback" + Log.w(TAG, "Only the full user can remove the listener" + ", userId=" + userId); return; } - user.unregisterCallbackLocked(callback); - Log.d(TAG, "The callback (" + callback.asBinder() - + ") is unregistered by " + getCallingPackageName(uid)); + user.removeOnMediaKeyEventSessionChangedListener(listener); + Log.d(TAG, "The MediaKeyEventSessionChangedListener (" + listener.asBinder() + + ") is removed by " + getCallingPackageName(uid)); } } finally { Binder.restoreCallingIdentity(token); @@ -2015,10 +2134,10 @@ public class MediaSessionService extends SystemService implements Monitor { needWakeLock ? mKeyEventReceiver.mLastTimeoutId : -1, mKeyEventReceiver); try { - for (FullUserRecord.CallbackRecord cr - : mCurrentFullUserRecord.mCallbacks.values()) { - cr.callback.onMediaKeyEventDispatchedToMediaSession( - keyEvent, session.getSessionToken()); + for (FullUserRecord.OnMediaKeyEventDispatchedListenerRecord cr + : mCurrentFullUserRecord.mOnMediaKeyEventDispatchedListeners.values()) { + cr.callback.onMediaKeyEventDispatched( + keyEvent, session.getPackageName(), session.getSessionToken()); } } catch (RemoteException e) { Log.w(TAG, "Failed to send callback", e); @@ -2048,10 +2167,11 @@ public class MediaSessionService extends SystemService implements Monitor { ComponentName componentName = mCurrentFullUserRecord .mLastMediaButtonReceiver.getIntent().getComponent(); if (componentName != null) { - for (FullUserRecord.CallbackRecord cr - : mCurrentFullUserRecord.mCallbacks.values()) { - cr.callback.onMediaKeyEventDispatchedToMediaButtonReceiver( - keyEvent, componentName); + for (FullUserRecord.OnMediaKeyEventDispatchedListenerRecord cr + : mCurrentFullUserRecord + .mOnMediaKeyEventDispatchedListeners.values()) { + cr.callback.onMediaKeyEventDispatched(keyEvent, + componentName.getPackageName(), null); } } } else { @@ -2083,10 +2203,11 @@ public class MediaSessionService extends SystemService implements Monitor { Log.w(TAG, "Error sending media button to the restored intent " + receiver + ", type=" + componentType, e); } - for (FullUserRecord.CallbackRecord cr - : mCurrentFullUserRecord.mCallbacks.values()) { - cr.callback.onMediaKeyEventDispatchedToMediaButtonReceiver( - keyEvent, receiver); + for (FullUserRecord.OnMediaKeyEventDispatchedListenerRecord cr + : mCurrentFullUserRecord + .mOnMediaKeyEventDispatchedListeners.values()) { + cr.callback.onMediaKeyEventDispatched(keyEvent, + receiver.getPackageName(), null); } } } catch (CanceledException e) { diff --git a/services/core/java/com/android/server/notification/NotificationComparator.java b/services/core/java/com/android/server/notification/NotificationComparator.java index 7098435ca299..a7e40cbc5b66 100644 --- a/services/core/java/com/android/server/notification/NotificationComparator.java +++ b/services/core/java/com/android/server/notification/NotificationComparator.java @@ -59,6 +59,12 @@ public class NotificationComparator return -1 * Boolean.compare(isLeftHighImportance, isRightHighImportance); } + // If a score has been assigned by notification assistant service, use this service + // rank results within each bucket instead of this comparator implementation. + if (left.getRankingScore() != right.getRankingScore()) { + return -1 * Float.compare(left.getRankingScore(), right.getRankingScore()); + } + // first all colorized notifications boolean leftImportantColorized = isImportantColorized(left); boolean rightImportantColorized = isImportantColorized(right); diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index e968fb700ca7..c8afcc97d5b1 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -144,6 +144,7 @@ public final class NotificationRecord { private int mSystemImportance = IMPORTANCE_UNSPECIFIED; private int mAssistantImportance = IMPORTANCE_UNSPECIFIED; private int mImportance = IMPORTANCE_UNSPECIFIED; + private float mRankingScore = 0f; // Field used in global sort key to bypass normal notifications private int mCriticality = CriticalNotificationExtractor.NORMAL; // A MetricsEvent.NotificationImportanceExplanation, tracking source of mImportance. @@ -655,6 +656,9 @@ public final class NotificationRecord { importance = Math.min(IMPORTANCE_HIGH, importance); setAssistantImportance(importance); } + if (signals.containsKey(Adjustment.KEY_RANKING_SCORE)) { + mRankingScore = signals.getFloat(Adjustment.KEY_RANKING_SCORE); + } if (!signals.isEmpty() && adjustment.getIssuer() != null) { mAdjustmentIssuer = adjustment.getIssuer(); } @@ -772,6 +776,10 @@ public final class NotificationRecord { return mImportance; } + public float getRankingScore() { + return mRankingScore; + } + public CharSequence getImportanceExplanation() { switch (mImportanceExplanationCode) { case MetricsEvent.IMPORTANCE_EXPLANATION_UNKNOWN: diff --git a/services/core/java/com/android/server/om/OverlayActorEnforcer.java b/services/core/java/com/android/server/om/OverlayActorEnforcer.java index e05511681ba8..ac3bf9ad5d8a 100644 --- a/services/core/java/com/android/server/om/OverlayActorEnforcer.java +++ b/services/core/java/com/android/server/om/OverlayActorEnforcer.java @@ -26,6 +26,7 @@ import android.net.Uri; import android.os.Process; import android.os.RemoteException; import android.text.TextUtils; +import android.util.Pair; import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; @@ -44,6 +45,38 @@ public class OverlayActorEnforcer { private final VerifyCallback mVerifyCallback; + /** + * @return nullable actor result with {@link ActorState} failure status + */ + static Pair<String, ActorState> getPackageNameForActor(String actorUriString, + Map<String, Map<String, String>> namedActors) { + if (namedActors.isEmpty()) { + return Pair.create(null, ActorState.NO_NAMED_ACTORS); + } + + Uri actorUri = Uri.parse(actorUriString); + + String actorScheme = actorUri.getScheme(); + List<String> actorPathSegments = actorUri.getPathSegments(); + if (!"overlay".equals(actorScheme) || CollectionUtils.size(actorPathSegments) != 1) { + return Pair.create(null, ActorState.INVALID_OVERLAYABLE_ACTOR_NAME); + } + + String actorNamespace = actorUri.getAuthority(); + Map<String, String> namespace = namedActors.get(actorNamespace); + if (namespace == null) { + return Pair.create(null, ActorState.MISSING_NAMESPACE); + } + + String actorName = actorPathSegments.get(0); + String packageName = namespace.get(actorName); + if (TextUtils.isEmpty(packageName)) { + return Pair.create(null, ActorState.MISSING_ACTOR_NAME); + } + + return Pair.create(packageName, ActorState.ALLOWED); + } + public OverlayActorEnforcer(@NonNull VerifyCallback verifyCallback) { mVerifyCallback = verifyCallback; } @@ -141,31 +174,14 @@ public class OverlayActorEnforcer { } } - Map<String, ? extends Map<String, String>> namedActors = mVerifyCallback.getNamedActors(); - if (namedActors.isEmpty()) { - return ActorState.NO_NAMED_ACTORS; - } - - Uri actorUri = Uri.parse(actor); - - String actorScheme = actorUri.getScheme(); - List<String> actorPathSegments = actorUri.getPathSegments(); - if (!"overlay".equals(actorScheme) || CollectionUtils.size(actorPathSegments) != 1) { - return ActorState.INVALID_OVERLAYABLE_ACTOR_NAME; - } - - String actorNamespace = actorUri.getAuthority(); - Map<String, String> namespace = namedActors.get(actorNamespace); - if (namespace == null) { - return ActorState.MISSING_NAMESPACE; - } - - String actorName = actorPathSegments.get(0); - String packageName = namespace.get(actorName); - if (TextUtils.isEmpty(packageName)) { - return ActorState.MISSING_ACTOR_NAME; + Map<String, Map<String, String>> namedActors = mVerifyCallback.getNamedActors(); + Pair<String, ActorState> actorUriPair = getPackageNameForActor(actor, namedActors); + ActorState actorUriState = actorUriPair.second; + if (actorUriState != ActorState.ALLOWED) { + return actorUriState; } + String packageName = actorUriPair.first; PackageInfo packageInfo = mVerifyCallback.getPackageInfo(packageName, userId); if (packageInfo == null) { return ActorState.MISSING_APP_INFO; @@ -192,7 +208,7 @@ public class OverlayActorEnforcer { * For easier logging/debugging, a set of all possible failure/success states when running * enforcement. */ - private enum ActorState { + enum ActorState { ALLOWED, INVALID_ACTOR, MISSING_NAMESPACE, @@ -244,7 +260,7 @@ public class OverlayActorEnforcer { * value maps actor name to package name */ @NonNull - Map<String, ? extends Map<String, String>> getNamedActors(); + Map<String, Map<String, String>> getNamedActors(); /** * @return true if the target package has declared an overlayable diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java index 8b69946873d8..f1947ac15645 100644 --- a/services/core/java/com/android/server/om/OverlayManagerService.java +++ b/services/core/java/com/android/server/om/OverlayManagerService.java @@ -1073,8 +1073,6 @@ public final class OverlayManagerService extends SystemService { mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); } - // TODO(b/143096091): Remove PackageInfo cache so that PackageManager is always queried - // to enforce visibility/other permission checks public PackageInfo getPackageInfo(@NonNull final String packageName, final int userId, final boolean useCache) { if (useCache) { @@ -1097,18 +1095,12 @@ public final class OverlayManagerService extends SystemService { @Override public PackageInfo getPackageInfo(@NonNull final String packageName, final int userId) { - // TODO(b/143096091): Remove clearing calling ID - long callingIdentity = Binder.clearCallingIdentity(); - try { - return getPackageInfo(packageName, userId, true); - } finally { - Binder.restoreCallingIdentity(callingIdentity); - } + return getPackageInfo(packageName, userId, true); } @NonNull @Override - public Map<String, ? extends Map<String, String>> getNamedActors() { + public Map<String, Map<String, String>> getNamedActors() { return SystemConfig.getInstance().getNamedActors(); } @@ -1136,57 +1128,45 @@ public final class OverlayManagerService extends SystemService { public OverlayableInfo getOverlayableForTarget(@NonNull String packageName, @Nullable String targetOverlayableName, int userId) throws IOException { - // TODO(b/143096091): Remove clearing calling ID - long callingIdentity = Binder.clearCallingIdentity(); - try { - PackageInfo packageInfo = getPackageInfo(packageName, userId); - if (packageInfo == null) { - throw new IOException("Unable to get target package"); - } + PackageInfo packageInfo = getPackageInfo(packageName, userId); + if (packageInfo == null) { + throw new IOException("Unable to get target package"); + } - String baseCodePath = packageInfo.applicationInfo.getBaseCodePath(); + String baseCodePath = packageInfo.applicationInfo.getBaseCodePath(); - ApkAssets apkAssets = null; - try { - apkAssets = ApkAssets.loadFromPath(baseCodePath); - return apkAssets.getOverlayableInfo(targetOverlayableName); - } finally { - if (apkAssets != null) { - try { - apkAssets.close(); - } catch (Throwable ignored) { - } + ApkAssets apkAssets = null; + try { + apkAssets = ApkAssets.loadFromPath(baseCodePath); + return apkAssets.getOverlayableInfo(targetOverlayableName); + } finally { + if (apkAssets != null) { + try { + apkAssets.close(); + } catch (Throwable ignored) { } } - } finally { - Binder.restoreCallingIdentity(callingIdentity); } } @Override public boolean doesTargetDefineOverlayable(String targetPackageName, int userId) throws RemoteException, IOException { - // TODO(b/143096091): Remove clearing calling ID - long callingIdentity = Binder.clearCallingIdentity(); - try { - PackageInfo packageInfo = mPackageManager.getPackageInfo(targetPackageName, 0, - userId); - String baseCodePath = packageInfo.applicationInfo.getBaseCodePath(); + PackageInfo packageInfo = mPackageManager.getPackageInfo(targetPackageName, 0, + userId); + String baseCodePath = packageInfo.applicationInfo.getBaseCodePath(); - ApkAssets apkAssets = null; - try { - apkAssets = ApkAssets.loadFromPath(baseCodePath); - return apkAssets.definesOverlayable(); - } finally { - if (apkAssets != null) { - try { - apkAssets.close(); - } catch (Throwable ignored) { - } + ApkAssets apkAssets = null; + try { + apkAssets = ApkAssets.loadFromPath(baseCodePath); + return apkAssets.definesOverlayable(); + } finally { + if (apkAssets != null) { + try { + apkAssets.close(); + } catch (Throwable ignored) { } } - } finally { - Binder.restoreCallingIdentity(callingIdentity); } } @@ -1229,16 +1209,10 @@ public final class OverlayManagerService extends SystemService { @Nullable @Override public String[] getPackagesForUid(int uid) { - // TODO(b/143096091): Remove clearing calling ID - long callingIdentity = Binder.clearCallingIdentity(); try { - try { - return mPackageManager.getPackagesForUid(uid); - } catch (RemoteException ignored) { - return null; - } - } finally { - Binder.restoreCallingIdentity(callingIdentity); + return mPackageManager.getPackagesForUid(uid); + } catch (RemoteException ignored) { + return null; } } diff --git a/services/core/java/com/android/server/om/OverlayReferenceMapper.java b/services/core/java/com/android/server/om/OverlayReferenceMapper.java new file mode 100644 index 000000000000..8bea119f3490 --- /dev/null +++ b/services/core/java/com/android/server/om/OverlayReferenceMapper.java @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.om; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.pm.parsing.AndroidPackage; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Pair; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.CollectionUtils; +import com.android.server.SystemConfig; +import com.android.server.pm.PackageSetting; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * Track visibility of a targets and overlays to actors. + * + * 4 cases to handle: + * <ol> + * <li>Target adds/changes an overlayable to add a reference to an actor + * <ul> + * <li>Must expose target to actor</li> + * <li>Must expose any overlays that pointed to that overlayable name to the actor</li> + * </ul> + * </li> + * <li>Target removes/changes an overlayable to remove a reference to an actor + * <ul> + * <li>If this target has no other overlayables referencing the actor, hide the + * target</li> + * <li>For all overlays targeting this overlayable, if the overlay is only visible to + * the actor through this overlayable, hide the overlay</li> + * </ul> + * </li> + * <li>Overlay adds/changes an overlay tag to add a reference to an overlayable name + * <ul> + * <li>Expose this overlay to the actor defined by the target overlayable</li> + * </ul> + * </li> + * <li>Overlay removes/changes an overlay tag to remove a reference to an overlayable name + * <ul> + * <li>If this overlay is only visible to an actor through this overlayable name's + * target's actor</li> + * </ul> + * </li> + * </ol> + * + * In this class, the names "actor", "target", and "overlay" all refer to the ID representations. + * All other use cases are named appropriate. "actor" is actor name, "target" is target package + * name, and "overlay" is overlay package name. + */ +public class OverlayReferenceMapper { + + private final Object mLock = new Object(); + + /** + * Keys are actors, values are maps which map target to a set of overlays targeting it. + * The presence of a target in the value map means the actor and targets are connected, even + * if the corresponding target's set is empty. + * See class comment for specific types. + */ + @GuardedBy("mLock") + private final Map<String, Map<String, Set<String>>> mActorToTargetToOverlays = new HashMap<>(); + + /** + * Keys are actor package names, values are generic package names the actor should be able + * to see. + */ + @GuardedBy("mLock") + private final Map<String, Set<String>> mActorPkgToPkgs = new HashMap<>(); + + @GuardedBy("mLock") + private boolean mDeferRebuild; + + @NonNull + private final Provider mProvider; + + /** + * @param deferRebuild whether or not to defer rebuild calls on add/remove until first get call; + * useful during boot when multiple packages are added in rapid succession + * and queries in-between are not expected + */ + public OverlayReferenceMapper(boolean deferRebuild, @Nullable Provider provider) { + this.mDeferRebuild = deferRebuild; + this.mProvider = provider != null ? provider : new Provider() { + @Nullable + @Override + public String getActorPkg(String actor) { + Map<String, Map<String, String>> namedActors = SystemConfig.getInstance() + .getNamedActors(); + + Pair<String, OverlayActorEnforcer.ActorState> actorPair = + OverlayActorEnforcer.getPackageNameForActor(actor, namedActors); + return actorPair.first; + } + + @NonNull + @Override + public Map<String, Set<String>> getTargetToOverlayables(@NonNull AndroidPackage pkg) { + String target = pkg.getOverlayTarget(); + if (TextUtils.isEmpty(target)) { + return Collections.emptyMap(); + } + + String overlayable = pkg.getOverlayTargetName(); + Map<String, Set<String>> targetToOverlayables = new HashMap<>(); + Set<String> overlayables = new HashSet<>(); + overlayables.add(overlayable); + targetToOverlayables.put(target, overlayables); + return targetToOverlayables; + } + }; + } + + /** + * @return mapping of actor package to a set of packages it can view + */ + @NonNull + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public Map<String, Set<String>> getActorPkgToPkgs() { + return mActorPkgToPkgs; + } + + public boolean isValidActor(@NonNull String targetName, @NonNull String actorPackageName) { + synchronized (mLock) { + assertMapBuilt(); + Set<String> validSet = mActorPkgToPkgs.get(actorPackageName); + return validSet != null && validSet.contains(targetName); + } + } + + /** + * Add a package to be considered for visibility. Currently supports adding as a target and/or + * an overlay. Adding an actor is not supported. Those are configured as part of + * {@link SystemConfig#getNamedActors()}. + * + * @param pkg the package to add + * @param otherPkgs map of other packages to consider, excluding {@param pkg} + */ + public void addPkg(AndroidPackage pkg, Map<String, AndroidPackage> otherPkgs) { + synchronized (mLock) { + if (!pkg.getOverlayables().isEmpty()) { + addTarget(pkg, otherPkgs); + } + + // TODO(b/135203078): Replace with isOverlay boolean flag check; fix test mocks + if (!mProvider.getTargetToOverlayables(pkg).isEmpty()) { + addOverlay(pkg, otherPkgs); + } + + if (!mDeferRebuild) { + rebuild(); + } + } + } + + /** + * Removes a package to be considered for visibility. Currently supports removing as a target + * and/or an overlay. Removing an actor is not supported. Those are staticly configured as part + * of {@link SystemConfig#getNamedActors()}. + * + * @param pkgName name to remove, as was added through {@link #addPkg(AndroidPackage, Map)} + */ + public void removePkg(String pkgName) { + synchronized (mLock) { + removeTarget(pkgName); + removeOverlay(pkgName); + + if (!mDeferRebuild) { + rebuild(); + } + } + } + + private void removeTarget(String target) { + synchronized (mLock) { + Iterator<Map<String, Set<String>>> iterator = + mActorToTargetToOverlays.values().iterator(); + while (iterator.hasNext()) { + Map<String, Set<String>> next = iterator.next(); + next.remove(target); + if (next.isEmpty()) { + iterator.remove(); + } + } + } + } + + /** + * Associate an actor with an association of a new target to overlays for that target. + * + * If a target overlays itself, it will not be associated with itself, as only one half of the + * relationship needs to exist for visibility purposes. + */ + private void addTarget(AndroidPackage targetPkg, Map<String, AndroidPackage> otherPkgs) { + synchronized (mLock) { + String target = targetPkg.getPackageName(); + removeTarget(target); + + Map<String, String> overlayablesToActors = targetPkg.getOverlayables(); + for (String overlayable : overlayablesToActors.keySet()) { + String actor = overlayablesToActors.get(overlayable); + addTargetToMap(actor, target); + + for (AndroidPackage overlayPkg : otherPkgs.values()) { + Map<String, Set<String>> targetToOverlayables = + mProvider.getTargetToOverlayables(overlayPkg); + Set<String> overlayables = targetToOverlayables.get(target); + if (CollectionUtils.isEmpty(overlayables)) { + continue; + } + + if (overlayables.contains(overlayable)) { + addOverlayToMap(actor, target, overlayPkg.getPackageName()); + } + } + } + } + } + + private void removeOverlay(String overlay) { + synchronized (mLock) { + for (Map<String, Set<String>> targetToOverlays : mActorToTargetToOverlays.values()) { + for (Set<String> overlays : targetToOverlays.values()) { + overlays.remove(overlay); + } + } + } + } + + /** + * Associate an actor with an association of targets to overlays for a new overlay. + * + * If an overlay targets itself, it will not be associated with itself, as only one half of the + * relationship needs to exist for visibility purposes. + */ + private void addOverlay(AndroidPackage overlayPkg, Map<String, AndroidPackage> otherPkgs) { + synchronized (mLock) { + String overlay = overlayPkg.getPackageName(); + removeOverlay(overlay); + + Map<String, Set<String>> targetToOverlayables = + mProvider.getTargetToOverlayables(overlayPkg); + for (Map.Entry<String, Set<String>> entry : targetToOverlayables.entrySet()) { + String target = entry.getKey(); + Set<String> overlayables = entry.getValue(); + AndroidPackage targetPkg = otherPkgs.get(target); + if (targetPkg == null) { + continue; + } + + String targetPkgName = targetPkg.getPackageName(); + Map<String, String> overlayableToActor = targetPkg.getOverlayables(); + for (String overlayable : overlayables) { + String actor = overlayableToActor.get(overlayable); + if (TextUtils.isEmpty(actor)) { + continue; + } + addOverlayToMap(actor, targetPkgName, overlay); + } + } + } + } + + public void rebuildIfDeferred() { + synchronized (mLock) { + if (mDeferRebuild) { + rebuild(); + mDeferRebuild = false; + } + } + } + + private void assertMapBuilt() { + if (mDeferRebuild) { + throw new IllegalStateException("The actor map must be built by calling " + + "rebuildIfDeferred before it is queried"); + } + } + + private void rebuild() { + synchronized (mLock) { + mActorPkgToPkgs.clear(); + for (String actor : mActorToTargetToOverlays.keySet()) { + String actorPkg = mProvider.getActorPkg(actor); + if (TextUtils.isEmpty(actorPkg)) { + continue; + } + + Map<String, Set<String>> targetToOverlays = mActorToTargetToOverlays.get(actor); + Set<String> pkgs = new HashSet<>(); + + for (String target : targetToOverlays.keySet()) { + Set<String> overlays = targetToOverlays.get(target); + pkgs.add(target); + pkgs.addAll(overlays); + } + + mActorPkgToPkgs.put(actorPkg, pkgs); + } + } + } + + private void addTargetToMap(String actor, String target) { + Map<String, Set<String>> targetToOverlays = mActorToTargetToOverlays.get(actor); + if (targetToOverlays == null) { + targetToOverlays = new HashMap<>(); + mActorToTargetToOverlays.put(actor, targetToOverlays); + } + + Set<String> overlays = targetToOverlays.get(target); + if (overlays == null) { + overlays = new HashSet<>(); + targetToOverlays.put(target, overlays); + } + } + + private void addOverlayToMap(String actor, String target, String overlay) { + synchronized (mLock) { + Map<String, Set<String>> targetToOverlays = mActorToTargetToOverlays.get(actor); + if (targetToOverlays == null) { + targetToOverlays = new HashMap<>(); + mActorToTargetToOverlays.put(actor, targetToOverlays); + } + + Set<String> overlays = targetToOverlays.get(target); + if (overlays == null) { + overlays = new HashSet<>(); + targetToOverlays.put(target, overlays); + } + + overlays.add(overlay); + } + } + + public interface Provider { + + /** + * Given the actor string from an overlayable definition, return the actor's package name. + */ + @Nullable + String getActorPkg(String actor); + + /** + * Mock response of multiple overlay tags. + * + * TODO(b/119899133): Replace with actual implementation; fix OverlayReferenceMapperTests + */ + @NonNull + Map<String, Set<String>> getTargetToOverlayables(@NonNull AndroidPackage pkg); + } +} diff --git a/services/core/java/com/android/server/pm/AppsFilter.java b/services/core/java/com/android/server/pm/AppsFilter.java index 8374ee63e07e..c4bcf809a67d 100644 --- a/services/core/java/com/android/server/pm/AppsFilter.java +++ b/services/core/java/com/android/server/pm/AppsFilter.java @@ -16,8 +16,6 @@ package com.android.server.pm; -import static android.content.pm.PackageParser.Component; -import static android.content.pm.PackageParser.IntentInfo; import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER; import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE; @@ -26,7 +24,6 @@ import android.annotation.Nullable; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; -import android.content.pm.PackageParser; import android.content.pm.parsing.AndroidPackage; import android.content.pm.parsing.ComponentParseUtils; import android.content.pm.parsing.ComponentParseUtils.ParsedActivity; @@ -50,9 +47,9 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.server.FgThread; +import com.android.server.om.OverlayReferenceMapper; import java.io.PrintWriter; -import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; @@ -109,11 +106,16 @@ public class AppsFilter { private final FeatureConfig mFeatureConfig; + private final OverlayReferenceMapper mOverlayReferenceMapper; + AppsFilter(FeatureConfig featureConfig, String[] forceQueryableWhitelist, - boolean systemAppsQueryable) { + boolean systemAppsQueryable, + @Nullable OverlayReferenceMapper.Provider overlayProvider) { mFeatureConfig = featureConfig; mForceQueryableByDevicePackageNames = forceQueryableWhitelist; mSystemAppsQueryable = systemAppsQueryable; + mOverlayReferenceMapper = new OverlayReferenceMapper(true /*deferRebuild*/, + overlayProvider); } public interface FeatureConfig { @@ -193,7 +195,7 @@ public class AppsFilter { } } return new AppsFilter(featureConfig, forcedQueryablePackageNames, - forceSystemAppsQueryable); + forceSystemAppsQueryable, null); } /** Returns true if the querying package may query for the potential target package */ @@ -282,6 +284,7 @@ public class AppsFilter { public void onSystemReady() { mFeatureConfig.onSystemReady(); + mOverlayReferenceMapper.rebuildIfDeferred(); } /** @@ -338,6 +341,16 @@ public class AppsFilter { } } } + + int existingSize = existingSettings.size(); + ArrayMap<String, AndroidPackage> existingPkgs = new ArrayMap<>(existingSize); + for (int index = 0; index < existingSize; index++) { + PackageSetting pkgSetting = existingSettings.valueAt(index); + if (pkgSetting.pkg != null) { + existingPkgs.put(pkgSetting.name, pkgSetting.pkg); + } + } + mOverlayReferenceMapper.addPkg(newPkgSetting.pkg, existingPkgs); } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } @@ -381,6 +394,8 @@ public class AppsFilter { addPackage(setting.sharedUser.packages.valueAt(i), existingSettings); } } + + mOverlayReferenceMapper.removePkg(setting.name); } /** @@ -397,8 +412,7 @@ public class AppsFilter { PackageSetting targetPkgSetting, int userId) { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "shouldFilterApplication"); try { - if (!shouldFilterApplicationInternal(callingUid, callingSetting, - targetPkgSetting, + if (!shouldFilterApplicationInternal(callingUid, callingSetting, targetPkgSetting, userId)) { return false; } @@ -412,8 +426,8 @@ public class AppsFilter { } } - private boolean shouldFilterApplicationInternal(int callingUid, - SettingBase callingSetting, PackageSetting targetPkgSetting, int userId) { + private boolean shouldFilterApplicationInternal(int callingUid, SettingBase callingSetting, + PackageSetting targetPkgSetting, int userId) { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "shouldFilterApplicationInternal"); try { final boolean featureEnabled = mFeatureConfig.isGloballyEnabled(); @@ -530,6 +544,29 @@ public class AppsFilter { } } } + + if (callingSharedPkgSettings != null) { + int size = callingSharedPkgSettings.size(); + for (int index = 0; index < size; index++) { + PackageSetting pkgSetting = callingSharedPkgSettings.valueAt(index); + if (mOverlayReferenceMapper.isValidActor(targetName, pkgSetting.name)) { + if (DEBUG_LOGGING) { + log(callingPkgSetting, targetPkgSetting, + "matches shared user of package that acts on target of " + + "overlay"); + } + return false; + } + } + } else { + if (mOverlayReferenceMapper.isValidActor(targetName, callingPkgSetting.name)) { + if (DEBUG_LOGGING) { + log(callingPkgSetting, targetPkgSetting, "acts on target of overlay"); + } + return false; + } + } + return true; } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); diff --git a/services/core/java/com/android/server/pm/InstantAppResolver.java b/services/core/java/com/android/server/pm/InstantAppResolver.java index 30b2c9d21a6a..8333ae5cbbad 100644 --- a/services/core/java/com/android/server/pm/InstantAppResolver.java +++ b/services/core/java/com/android/server/pm/InstantAppResolver.java @@ -39,6 +39,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.AuxiliaryResolveInfo; import android.content.pm.InstantAppIntentFilter; import android.content.pm.InstantAppRequest; +import android.content.pm.InstantAppRequestInfo; import android.content.pm.InstantAppResolveInfo; import android.content.pm.InstantAppResolveInfo.InstantAppDigest; import android.metrics.LogMaker; @@ -47,6 +48,8 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; +import android.os.UserHandle; +import android.text.TextUtils; import android.util.Log; import android.util.Slog; @@ -63,7 +66,6 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; -import java.util.UUID; /** @hide */ public abstract class InstantAppResolver { @@ -117,26 +119,40 @@ public abstract class InstantAppResolver { return sanitizedIntent; } + /** + * Generate an {@link InstantAppDigest} from an {@link Intent} which contains hashes of the + * host. The object contains both secure and insecure hash array variants, and the secure + * version must be passed along to ensure the random data is consistent. + */ + @NonNull + public static InstantAppDigest parseDigest(@NonNull Intent origIntent) { + if (origIntent.getData() != null && !TextUtils.isEmpty(origIntent.getData().getHost())) { + return new InstantAppResolveInfo.InstantAppDigest(origIntent.getData().getHost(), + 5 /*maxDigests*/); + } else { + return InstantAppResolveInfo.InstantAppDigest.UNDEFINED; + } + } + public static AuxiliaryResolveInfo doInstantAppResolutionPhaseOne( InstantAppResolverConnection connection, InstantAppRequest requestObj) { final long startTime = System.currentTimeMillis(); - final String token = UUID.randomUUID().toString(); + final String token = requestObj.token; if (DEBUG_INSTANT) { Log.d(TAG, "[" + token + "] Phase1; resolving"); } - final Intent origIntent = requestObj.origIntent; - final Intent sanitizedIntent = sanitizeIntent(origIntent); AuxiliaryResolveInfo resolveInfo = null; @ResolutionStatus int resolutionStatus = RESOLUTION_SUCCESS; + Intent origIntent = requestObj.origIntent; try { final List<InstantAppResolveInfo> instantAppResolveInfoList = - connection.getInstantAppResolveInfoList(sanitizedIntent, - requestObj.digest.getDigestPrefixSecure(), requestObj.userId, token); + connection.getInstantAppResolveInfoList(buildRequestInfo(requestObj)); if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) { resolveInfo = InstantAppResolver.filterInstantAppIntent( instantAppResolveInfoList, origIntent, requestObj.resolvedType, - requestObj.userId, origIntent.getPackage(), requestObj.digest, token); + requestObj.userId, origIntent.getPackage(), token, + requestObj.hostDigestPrefixSecure); } } catch (ConnectionException e) { if (e.failure == ConnectionException.FAILURE_BIND) { @@ -166,7 +182,7 @@ public abstract class InstantAppResolver { // if the match external flag is set, return an empty resolve info instead of a null result. if (resolveInfo == null && (origIntent.getFlags() & FLAG_ACTIVITY_MATCH_EXTERNAL) != 0) { return new AuxiliaryResolveInfo(token, false, createFailureIntent(origIntent, token), - null /* filters */); + null /* filters */, requestObj.hostDigestPrefixSecure); } return resolveInfo; } @@ -175,7 +191,7 @@ public abstract class InstantAppResolver { InstantAppResolverConnection connection, InstantAppRequest requestObj, ActivityInfo instantAppInstaller, Handler callbackHandler) { final long startTime = System.currentTimeMillis(); - final String token = requestObj.responseObj.token; + final String token = requestObj.token; if (DEBUG_INSTANT) { Log.d(TAG, "[" + token + "] Phase2; resolving"); } @@ -191,8 +207,8 @@ public abstract class InstantAppResolver { final AuxiliaryResolveInfo instantAppIntentInfo = InstantAppResolver.filterInstantAppIntent( instantAppResolveInfoList, origIntent, null /*resolvedType*/, - 0 /*userId*/, origIntent.getPackage(), requestObj.digest, - token); + 0 /*userId*/, origIntent.getPackage(), + token, requestObj.hostDigestPrefixSecure); if (instantAppIntentInfo != null) { failureIntent = instantAppIntentInfo.failureIntent; } else { @@ -223,8 +239,7 @@ public abstract class InstantAppResolver { } }; try { - connection.getInstantAppIntentFilterList(sanitizedIntent, - requestObj.digest.getDigestPrefixSecure(), requestObj.userId, token, callback, + connection.getInstantAppIntentFilterList(buildRequestInfo(requestObj), callback, callbackHandler, startTime); } catch (ConnectionException e) { @ResolutionStatus int resolutionStatus = RESOLUTION_FAILURE; @@ -356,10 +371,22 @@ public abstract class InstantAppResolver { return intent; } + private static InstantAppRequestInfo buildRequestInfo(InstantAppRequest request) { + return new InstantAppRequestInfo( + sanitizeIntent(request.origIntent), + // This must only expose the secured version of the host + request.hostDigestPrefixSecure, + UserHandle.getUserHandleForUid(request.userId), + request.isRequesterInstantApp, + request.token + ); + } + private static AuxiliaryResolveInfo filterInstantAppIntent( - List<InstantAppResolveInfo> instantAppResolveInfoList, - Intent origIntent, String resolvedType, int userId, String packageName, - InstantAppDigest digest, String token) { + List<InstantAppResolveInfo> instantAppResolveInfoList, Intent origIntent, + String resolvedType, int userId, String packageName, String token, + int[] hostDigestPrefixSecure) { + InstantAppDigest digest = InstantAppResolver.parseDigest(origIntent); final int[] shaPrefix = digest.getDigestPrefix(); final byte[][] digestBytes = digest.getDigestBytes(); boolean requiresSecondPhase = false; @@ -404,7 +431,7 @@ public abstract class InstantAppResolver { } if (filters != null && !filters.isEmpty()) { return new AuxiliaryResolveInfo(token, requiresSecondPhase, - createFailureIntent(origIntent, token), filters); + createFailureIntent(origIntent, token), filters, hostDigestPrefixSecure); } // Hash or filter mis-match; no instant apps for this domain. return null; diff --git a/services/core/java/com/android/server/pm/InstantAppResolverConnection.java b/services/core/java/com/android/server/pm/InstantAppResolverConnection.java index c0e9f58f3b8e..0fe2eb1e6f4b 100644 --- a/services/core/java/com/android/server/pm/InstantAppResolverConnection.java +++ b/services/core/java/com/android/server/pm/InstantAppResolverConnection.java @@ -24,6 +24,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.content.pm.InstantAppRequestInfo; import android.content.pm.InstantAppResolveInfo; import android.os.Binder; import android.os.Build; @@ -85,13 +86,13 @@ final class InstantAppResolverConnection implements DeathRecipient { mBgHandler = BackgroundThread.getHandler(); } - public List<InstantAppResolveInfo> getInstantAppResolveInfoList(Intent sanitizedIntent, - int[] hashPrefix, int userId, String token) throws ConnectionException { + public List<InstantAppResolveInfo> getInstantAppResolveInfoList(InstantAppRequestInfo request) + throws ConnectionException { throwIfCalledOnMainThread(); IInstantAppResolver target = null; try { try { - target = getRemoteInstanceLazy(token); + target = getRemoteInstanceLazy(request.token); } catch (TimeoutException e) { throw new ConnectionException(ConnectionException.FAILURE_BIND); } catch (InterruptedException e) { @@ -99,8 +100,7 @@ final class InstantAppResolverConnection implements DeathRecipient { } try { return mGetInstantAppResolveInfoCaller - .getInstantAppResolveInfoList(target, sanitizedIntent, hashPrefix, userId, - token); + .getInstantAppResolveInfoList(target, request); } catch (TimeoutException e) { throw new ConnectionException(ConnectionException.FAILURE_CALL); } catch (RemoteException ignore) { @@ -113,8 +113,8 @@ final class InstantAppResolverConnection implements DeathRecipient { return null; } - public void getInstantAppIntentFilterList(Intent sanitizedIntent, int[] hashPrefix, int userId, - String token, PhaseTwoCallback callback, Handler callbackHandler, final long startTime) + public void getInstantAppIntentFilterList(InstantAppRequestInfo request, + PhaseTwoCallback callback, Handler callbackHandler, final long startTime) throws ConnectionException { final IRemoteCallback remoteCallback = new IRemoteCallback.Stub() { @Override @@ -126,9 +126,8 @@ final class InstantAppResolverConnection implements DeathRecipient { } }; try { - getRemoteInstanceLazy(token) - .getInstantAppIntentFilterList(sanitizedIntent, hashPrefix, userId, token, - remoteCallback); + getRemoteInstanceLazy(request.token) + .getInstantAppIntentFilterList(request, remoteCallback); } catch (TimeoutException e) { throw new ConnectionException(ConnectionException.FAILURE_BIND); } catch (InterruptedException e) { @@ -352,12 +351,10 @@ final class InstantAppResolverConnection implements DeathRecipient { }; } - public List<InstantAppResolveInfo> getInstantAppResolveInfoList( - IInstantAppResolver target, Intent sanitizedIntent, int[] hashPrefix, int userId, - String token) throws RemoteException, TimeoutException { + public List<InstantAppResolveInfo> getInstantAppResolveInfoList(IInstantAppResolver target, + InstantAppRequestInfo request) throws RemoteException, TimeoutException { final int sequence = onBeforeRemoteCall(); - target.getInstantAppResolveInfoList(sanitizedIntent, hashPrefix, userId, token, - sequence, mCallback); + target.getInstantAppResolveInfoList(request, sequence, mCallback); return getResultTimed(sequence); } } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 2d7bcd09caf6..99d5e4a21df4 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -155,6 +155,7 @@ import android.content.pm.IPackageStatsObserver; import android.content.pm.InstallSourceInfo; import android.content.pm.InstantAppInfo; import android.content.pm.InstantAppRequest; +import android.content.pm.InstantAppResolveInfo.InstantAppDigest; import android.content.pm.InstrumentationInfo; import android.content.pm.IntentFilterVerificationInfo; import android.content.pm.KeySet; @@ -376,6 +377,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -6143,10 +6145,12 @@ public class PackageManagerService extends IPackageManager.Stub private void requestInstantAppResolutionPhaseTwo(AuxiliaryResolveInfo responseObj, Intent origIntent, String resolvedType, String callingPackage, - Bundle verificationBundle, int userId) { + boolean isRequesterInstantApp, Bundle verificationBundle, int userId) { final Message msg = mHandler.obtainMessage(INSTANT_APP_RESOLUTION_PHASE_TWO, new InstantAppRequest(responseObj, origIntent, resolvedType, - callingPackage, userId, verificationBundle, false /*resolveForStart*/)); + callingPackage, isRequesterInstantApp, userId, verificationBundle, + false /*resolveForStart*/, responseObj.hostDigestPrefixSecure, + responseObj.token)); mHandler.sendMessage(msg); } @@ -6765,8 +6769,10 @@ public class PackageManagerService extends IPackageManager.Stub } } if (addInstant) { - result = maybeAddInstantAppInstaller( - result, intent, resolvedType, flags, userId, resolveForStart); + String callingPkgName = getInstantAppPackageName(filterCallingUid); + boolean isRequesterInstantApp = isInstantApp(callingPkgName, userId); + result = maybeAddInstantAppInstaller(result, intent, resolvedType, flags, userId, + resolveForStart, isRequesterInstantApp); } if (sortResult) { Collections.sort(result, RESOLVE_PRIORITY_SORTER); @@ -6777,7 +6783,8 @@ public class PackageManagerService extends IPackageManager.Stub } private List<ResolveInfo> maybeAddInstantAppInstaller(List<ResolveInfo> result, Intent intent, - String resolvedType, int flags, int userId, boolean resolveForStart) { + String resolvedType, int flags, int userId, boolean resolveForStart, + boolean isRequesterInstantApp) { // first, check to see if we've got an instant app already installed final boolean alreadyResolvedLocally = (flags & PackageManager.MATCH_INSTANT) != 0; ResolveInfo localInstantApp = null; @@ -6825,10 +6832,13 @@ public class PackageManagerService extends IPackageManager.Stub if (localInstantApp == null) { // we don't have an instant app locally, resolve externally Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "resolveEphemeral"); + String token = UUID.randomUUID().toString(); + InstantAppDigest digest = InstantAppResolver.parseDigest(intent); final InstantAppRequest requestObject = new InstantAppRequest( null /*responseObj*/, intent /*origIntent*/, resolvedType, - null /*callingPackage*/, userId, null /*verificationBundle*/, - resolveForStart); + null /*callingPackage*/, isRequesterInstantApp, + userId, null /*verificationBundle*/, resolveForStart, + digest.getDigestPrefixSecure(), token); auxiliaryResponse = InstantAppResolver.doInstantAppResolutionPhaseOne( mInstantAppResolverConnection, requestObject); Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); @@ -17952,14 +17962,6 @@ public class PackageManagerService extends IPackageManager.Stub } } mPermissionManager.resetRuntimePermissions(pkg, nextUserId); - // Also delete contributed media, when requested - if ((flags & PackageManager.DELETE_CONTRIBUTED_MEDIA) != 0) { - try { - MediaStore.deleteContributedMedia(mContext, ps.name, UserHandle.of(nextUserId)); - } catch (IOException e) { - Slog.w(TAG, "Failed to delete contributed media for " + ps.name, e); - } - } } if (outInfo != null) { @@ -22833,10 +22835,10 @@ public class PackageManagerService extends IPackageManager.Stub @Override public void requestInstantAppResolutionPhaseTwo(AuxiliaryResolveInfo responseObj, Intent origIntent, String resolvedType, String callingPackage, - Bundle verificationBundle, int userId) { + boolean isRequesterInstantApp, Bundle verificationBundle, int userId) { PackageManagerService.this.requestInstantAppResolutionPhaseTwo( - responseObj, origIntent, resolvedType, callingPackage, verificationBundle, - userId); + responseObj, origIntent, resolvedType, callingPackage, isRequesterInstantApp, + verificationBundle, userId); } @Override diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING index e8798ffa3720..59a58042011e 100644 --- a/services/core/java/com/android/server/pm/TEST_MAPPING +++ b/services/core/java/com/android/server/pm/TEST_MAPPING @@ -24,7 +24,20 @@ ] }, { - "name": "PackageManagerShellCommandTest" + "name": "CtsContentTestCases", + "options": [ + { + "include-filter": "android.content.pm.cts.PackageManagerShellCommandTest" + } + ] + }, + { + "name": "GtsSecurityHostTestCases", + "options": [ + { + "include-filter": "com.google.android.security.gts.PackageVerifierTest" + } + ] } ], "postsubmit": [ diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 4d436c05c3a5..5fabdb618f44 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -351,6 +351,7 @@ public class UserManagerService extends IUserManager.Stub { * User restrictions set by {@link com.android.server.devicepolicy.DevicePolicyManagerService} * that should be applied to all users, including guests. Only non-empty restriction bundles are * stored. + * The key is the user id of the user whom the restriction originated from. */ @GuardedBy("mRestrictionsLock") private final SparseArray<Bundle> mDevicePolicyGlobalUserRestrictions = new SparseArray<>(); @@ -364,6 +365,7 @@ public class UserManagerService extends IUserManager.Stub { /** * User restrictions set by {@link com.android.server.devicepolicy.DevicePolicyManagerService} * for each user. Only non-empty restriction bundles are stored. + * The key is the user id of the user whom the restriction originated from. */ @GuardedBy("mRestrictionsLock") private final SparseArray<Bundle> mDevicePolicyLocalUserRestrictions = new SparseArray<>(); @@ -1621,7 +1623,7 @@ public class UserManagerService extends IUserManager.Stub { /** * See {@link UserManagerInternal#setDevicePolicyUserRestrictions} */ - private void setDevicePolicyUserRestrictionsInner(@UserIdInt int userId, + private void setDevicePolicyUserRestrictionsInner(@UserIdInt int originatingUserId, @Nullable Bundle restrictions, @UserManagerInternal.OwnerType int restrictionOwnerType) { final Bundle global = new Bundle(); @@ -1635,16 +1637,16 @@ public class UserManagerService extends IUserManager.Stub { synchronized (mRestrictionsLock) { // Update global and local restrictions if they were changed. globalChanged = updateRestrictionsIfNeededLR( - userId, global, mDevicePolicyGlobalUserRestrictions); + originatingUserId, global, mDevicePolicyGlobalUserRestrictions); localChanged = updateRestrictionsIfNeededLR( - userId, local, mDevicePolicyLocalUserRestrictions); + originatingUserId, local, mDevicePolicyLocalUserRestrictions); if (restrictionOwnerType == UserManagerInternal.OWNER_TYPE_DEVICE_OWNER) { // Remember the global restriction owner userId to be able to make a distinction // in getUserRestrictionSource on who set local policies. - mDeviceOwnerUserId = userId; + mDeviceOwnerUserId = originatingUserId; } else { - if (mDeviceOwnerUserId == userId) { + if (mDeviceOwnerUserId == originatingUserId) { // When profile owner sets restrictions it passes null global bundle and we // reset global restriction owner userId. // This means this user used to have DO, but now the DO is gone and the user @@ -1654,15 +1656,16 @@ public class UserManagerService extends IUserManager.Stub { } } if (DBG) { - Log.d(LOG_TAG, "setDevicePolicyUserRestrictions: userId=" + userId - + " global=" + global + (globalChanged ? " (changed)" : "") - + " local=" + local + (localChanged ? " (changed)" : "") + Log.d(LOG_TAG, "setDevicePolicyUserRestrictions: " + + " originatingUserId=" + originatingUserId + + " global=" + global + (globalChanged ? " (changed)" : "") + + " local=" + local + (localChanged ? " (changed)" : "") ); } // Don't call them within the mRestrictionsLock. synchronized (mPackagesLock) { if (localChanged || globalChanged) { - writeUserLP(getUserDataNoChecks(userId)); + writeUserLP(getUserDataNoChecks(originatingUserId)); } } @@ -1670,7 +1673,7 @@ public class UserManagerService extends IUserManager.Stub { if (globalChanged) { applyUserRestrictionsForAllUsersLR(); } else if (localChanged) { - applyUserRestrictionsLR(userId); + applyUserRestrictionsLR(originatingUserId); } } } @@ -4507,9 +4510,10 @@ public class UserManagerService extends IUserManager.Stub { private class LocalService extends UserManagerInternal { @Override - public void setDevicePolicyUserRestrictions(@UserIdInt int userId, - @Nullable Bundle restrictions, @OwnerType int restrictionOwnerType) { - UserManagerService.this.setDevicePolicyUserRestrictionsInner(userId, + public void setDevicePolicyUserRestrictions(@UserIdInt int originatingUserId, + @Nullable Bundle restrictions, + @OwnerType int restrictionOwnerType) { + UserManagerService.this.setDevicePolicyUserRestrictionsInner(originatingUserId, restrictions, restrictionOwnerType); } diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java index 0beff7a32bc1..e0bd0b400940 100644 --- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java +++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java @@ -411,6 +411,13 @@ public class UserRestrictionsUtils { } /** + * @return true if a restriction is settable by profile owner of an organization owned device. + */ + public static boolean canProfileOwnerOfOrganizationOwnedDeviceChange(String restriction) { + return PROFILE_OWNER_ORGANIZATION_OWNED_GLOBAL_RESTRICTIONS.contains(restriction); + } + + /** * Returns the user restrictions that default to {@code true} for device owners. * These user restrictions are local, though. ie only for the device owner's user id. */ diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java index 9324870904d9..6cdfcff61415 100644 --- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java +++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java @@ -58,6 +58,7 @@ import com.android.internal.util.DumpUtils; import com.android.internal.util.IndentingPrintWriter; import com.android.server.LocalServices; import com.android.server.PackageWatchdog; +import com.android.server.SystemConfig; import com.android.server.Watchdog; import com.android.server.pm.Installer; @@ -1008,11 +1009,19 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { installerPackageName) == PackageManager.PERMISSION_GRANTED; // For now only allow rollbacks for modules or for testing. - return (isModule(packageName) && manageRollbacksGranted) + return (isRollbackWhitelisted(packageName) && manageRollbacksGranted) || testManageRollbacksGranted; } /** + * Returns true is this package is eligible for enabling rollback. + */ + private boolean isRollbackWhitelisted(String packageName) { + // TODO: Remove #isModule when the white list is ready. + return SystemConfig.getInstance().getRollbackWhitelistedPackages().contains(packageName) + || isModule(packageName); + } + /** * Returns true if the package name is the name of a module. */ private boolean isModule(String packageName) { diff --git a/services/core/java/com/android/server/soundtrigger_middleware/AudioSessionProviderImpl.java b/services/core/java/com/android/server/soundtrigger_middleware/AudioSessionProviderImpl.java new file mode 100644 index 000000000000..3fa52301d9a0 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/AudioSessionProviderImpl.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +/** + * An implementation of SoundTriggerMiddlewareImpl.AudioSessionProvider that ties to native + * AudioSystem module via JNI. + */ +class AudioSessionProviderImpl extends SoundTriggerMiddlewareImpl.AudioSessionProvider { + @Override + public native AudioSession acquireSession(); + + @Override + public native void releaseSession(int sessionHandle); +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java b/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java new file mode 100644 index 000000000000..9b22f33a20b0 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.hardware.audio.common.V2_0.Uuid; +import android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback; +import android.hardware.soundtrigger.V2_3.ISoundTriggerHw; +import android.media.audio.common.AudioConfig; +import android.media.audio.common.AudioOffloadInfo; +import android.media.soundtrigger_middleware.ConfidenceLevel; +import android.media.soundtrigger_middleware.ModelParameter; +import android.media.soundtrigger_middleware.ModelParameterRange; +import android.media.soundtrigger_middleware.Phrase; +import android.media.soundtrigger_middleware.PhraseRecognitionEvent; +import android.media.soundtrigger_middleware.PhraseRecognitionExtra; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionConfig; +import android.media.soundtrigger_middleware.RecognitionEvent; +import android.media.soundtrigger_middleware.RecognitionMode; +import android.media.soundtrigger_middleware.RecognitionStatus; +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.SoundModelType; +import android.media.soundtrigger_middleware.SoundTriggerModuleProperties; +import android.os.HidlMemoryUtil; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utilities for type conversion between SoundTrigger HAL types and SoundTriggerMiddleware service + * types. + * + * @hide + */ +class ConversionUtil { + static @NonNull + SoundTriggerModuleProperties hidl2aidlProperties( + @NonNull ISoundTriggerHw.Properties hidlProperties) { + SoundTriggerModuleProperties aidlProperties = new SoundTriggerModuleProperties(); + aidlProperties.implementor = hidlProperties.implementor; + aidlProperties.description = hidlProperties.description; + aidlProperties.version = hidlProperties.version; + aidlProperties.uuid = hidl2aidlUuid(hidlProperties.uuid); + aidlProperties.maxSoundModels = hidlProperties.maxSoundModels; + aidlProperties.maxKeyPhrases = hidlProperties.maxKeyPhrases; + aidlProperties.maxUsers = hidlProperties.maxUsers; + aidlProperties.recognitionModes = hidlProperties.recognitionModes; + aidlProperties.captureTransition = hidlProperties.captureTransition; + aidlProperties.maxBufferMs = hidlProperties.maxBufferMs; + aidlProperties.concurrentCapture = hidlProperties.concurrentCapture; + aidlProperties.triggerInEvent = hidlProperties.triggerInEvent; + aidlProperties.powerConsumptionMw = hidlProperties.powerConsumptionMw; + return aidlProperties; + } + + static @NonNull + String hidl2aidlUuid(@NonNull Uuid hidlUuid) { + if (hidlUuid.node == null || hidlUuid.node.length != 6) { + throw new IllegalArgumentException("UUID.node must be of length 6."); + } + return String.format(UuidUtil.FORMAT, + hidlUuid.timeLow, + hidlUuid.timeMid, + hidlUuid.versionAndTimeHigh, + hidlUuid.variantAndClockSeqHigh, + hidlUuid.node[0], + hidlUuid.node[1], + hidlUuid.node[2], + hidlUuid.node[3], + hidlUuid.node[4], + hidlUuid.node[5]); + } + + static @NonNull + Uuid aidl2hidlUuid(@NonNull String aidlUuid) { + Matcher matcher = UuidUtil.PATTERN.matcher(aidlUuid); + if (!matcher.matches()) { + throw new IllegalArgumentException("Illegal format for UUID: " + aidlUuid); + } + Uuid hidlUuid = new Uuid(); + hidlUuid.timeLow = Integer.parseUnsignedInt(matcher.group(1), 16); + hidlUuid.timeMid = (short) Integer.parseUnsignedInt(matcher.group(2), 16); + hidlUuid.versionAndTimeHigh = (short) Integer.parseUnsignedInt(matcher.group(3), 16); + hidlUuid.variantAndClockSeqHigh = (short) Integer.parseUnsignedInt(matcher.group(4), 16); + hidlUuid.node = new byte[]{(byte) Integer.parseUnsignedInt(matcher.group(5), 16), + (byte) Integer.parseUnsignedInt(matcher.group(6), 16), + (byte) Integer.parseUnsignedInt(matcher.group(7), 16), + (byte) Integer.parseUnsignedInt(matcher.group(8), 16), + (byte) Integer.parseUnsignedInt(matcher.group(9), 16), + (byte) Integer.parseUnsignedInt(matcher.group(10), 16)}; + return hidlUuid; + } + + static int aidl2hidlSoundModelType(int aidlType) { + switch (aidlType) { + case SoundModelType.GENERIC: + return android.hardware.soundtrigger.V2_0.SoundModelType.GENERIC; + case SoundModelType.KEYPHRASE: + return android.hardware.soundtrigger.V2_0.SoundModelType.KEYPHRASE; + default: + throw new IllegalArgumentException("Unknown sound model type: " + aidlType); + } + } + + static int hidl2aidlSoundModelType(int hidlType) { + switch (hidlType) { + case android.hardware.soundtrigger.V2_0.SoundModelType.GENERIC: + return SoundModelType.GENERIC; + case android.hardware.soundtrigger.V2_0.SoundModelType.KEYPHRASE: + return SoundModelType.KEYPHRASE; + default: + throw new IllegalArgumentException("Unknown sound model type: " + hidlType); + } + } + + static @NonNull + ISoundTriggerHw.Phrase aidl2hidlPhrase(@NonNull Phrase aidlPhrase) { + ISoundTriggerHw.Phrase hidlPhrase = new ISoundTriggerHw.Phrase(); + hidlPhrase.id = aidlPhrase.id; + hidlPhrase.recognitionModes = aidl2hidlRecognitionModes(aidlPhrase.recognitionModes); + for (int aidlUser : aidlPhrase.users) { + hidlPhrase.users.add(aidlUser); + } + hidlPhrase.locale = aidlPhrase.locale; + hidlPhrase.text = aidlPhrase.text; + return hidlPhrase; + } + + static int aidl2hidlRecognitionModes(int aidlModes) { + int hidlModes = 0; + + if ((aidlModes & RecognitionMode.VOICE_TRIGGER) != 0) { + hidlModes |= android.hardware.soundtrigger.V2_0.RecognitionMode.VOICE_TRIGGER; + } + if ((aidlModes & RecognitionMode.USER_IDENTIFICATION) != 0) { + hidlModes |= android.hardware.soundtrigger.V2_0.RecognitionMode.USER_IDENTIFICATION; + } + if ((aidlModes & RecognitionMode.USER_AUTHENTICATION) != 0) { + hidlModes |= android.hardware.soundtrigger.V2_0.RecognitionMode.USER_AUTHENTICATION; + } + if ((aidlModes & RecognitionMode.GENERIC_TRIGGER) != 0) { + hidlModes |= android.hardware.soundtrigger.V2_0.RecognitionMode.GENERIC_TRIGGER; + } + return hidlModes; + } + + static int hidl2aidlRecognitionModes(int hidlModes) { + int aidlModes = 0; + if ((hidlModes & android.hardware.soundtrigger.V2_0.RecognitionMode.VOICE_TRIGGER) != 0) { + aidlModes |= RecognitionMode.VOICE_TRIGGER; + } + if ((hidlModes & android.hardware.soundtrigger.V2_0.RecognitionMode.USER_IDENTIFICATION) + != 0) { + aidlModes |= RecognitionMode.USER_IDENTIFICATION; + } + if ((hidlModes & android.hardware.soundtrigger.V2_0.RecognitionMode.USER_AUTHENTICATION) + != 0) { + aidlModes |= RecognitionMode.USER_AUTHENTICATION; + } + if ((hidlModes & android.hardware.soundtrigger.V2_0.RecognitionMode.GENERIC_TRIGGER) != 0) { + aidlModes |= RecognitionMode.GENERIC_TRIGGER; + } + return aidlModes; + } + + static @NonNull + ISoundTriggerHw.SoundModel aidl2hidlSoundModel(@NonNull SoundModel aidlModel) { + ISoundTriggerHw.SoundModel hidlModel = new ISoundTriggerHw.SoundModel(); + hidlModel.header.type = aidl2hidlSoundModelType(aidlModel.type); + hidlModel.header.uuid = aidl2hidlUuid(aidlModel.uuid); + hidlModel.header.vendorUuid = aidl2hidlUuid(aidlModel.vendorUuid); + hidlModel.data = HidlMemoryUtil.byteArrayToHidlMemory(aidlModel.data, + "SoundTrigger SoundModel"); + return hidlModel; + } + + static @NonNull + ISoundTriggerHw.PhraseSoundModel aidl2hidlPhraseSoundModel( + @NonNull PhraseSoundModel aidlModel) { + ISoundTriggerHw.PhraseSoundModel hidlModel = new ISoundTriggerHw.PhraseSoundModel(); + hidlModel.common = aidl2hidlSoundModel(aidlModel.common); + for (Phrase aidlPhrase : aidlModel.phrases) { + hidlModel.phrases.add(aidl2hidlPhrase(aidlPhrase)); + } + return hidlModel; + } + + static @NonNull + ISoundTriggerHw.RecognitionConfig aidl2hidlRecognitionConfig( + @NonNull RecognitionConfig aidlConfig) { + ISoundTriggerHw.RecognitionConfig hidlConfig = new ISoundTriggerHw.RecognitionConfig(); + hidlConfig.header.captureRequested = aidlConfig.captureRequested; + for (PhraseRecognitionExtra aidlPhraseExtra : aidlConfig.phraseRecognitionExtras) { + hidlConfig.header.phrases.add(aidl2hidlPhraseRecognitionExtra(aidlPhraseExtra)); + } + hidlConfig.data = HidlMemoryUtil.byteArrayToHidlMemory(aidlConfig.data, + "SoundTrigger RecognitionConfig"); + return hidlConfig; + } + + static @NonNull + android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra aidl2hidlPhraseRecognitionExtra( + @NonNull PhraseRecognitionExtra aidlExtra) { + android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra hidlExtra = + new android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra(); + hidlExtra.id = aidlExtra.id; + hidlExtra.recognitionModes = aidl2hidlRecognitionModes(aidlExtra.recognitionModes); + hidlExtra.confidenceLevel = aidlExtra.confidenceLevel; + hidlExtra.levels.ensureCapacity(aidlExtra.levels.length); + for (ConfidenceLevel aidlLevel : aidlExtra.levels) { + hidlExtra.levels.add(aidl2hidlConfidenceLevel(aidlLevel)); + } + return hidlExtra; + } + + static @NonNull + PhraseRecognitionExtra hidl2aidlPhraseRecognitionExtra( + @NonNull android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra hidlExtra) { + PhraseRecognitionExtra aidlExtra = new PhraseRecognitionExtra(); + aidlExtra.id = hidlExtra.id; + aidlExtra.recognitionModes = hidl2aidlRecognitionModes(hidlExtra.recognitionModes); + aidlExtra.confidenceLevel = hidlExtra.confidenceLevel; + aidlExtra.levels = new ConfidenceLevel[hidlExtra.levels.size()]; + for (int i = 0; i < hidlExtra.levels.size(); ++i) { + aidlExtra.levels[i] = hidl2aidlConfidenceLevel(hidlExtra.levels.get(i)); + } + return aidlExtra; + } + + static @NonNull + android.hardware.soundtrigger.V2_0.ConfidenceLevel aidl2hidlConfidenceLevel( + @NonNull ConfidenceLevel aidlLevel) { + android.hardware.soundtrigger.V2_0.ConfidenceLevel hidlLevel = + new android.hardware.soundtrigger.V2_0.ConfidenceLevel(); + hidlLevel.userId = aidlLevel.userId; + hidlLevel.levelPercent = aidlLevel.levelPercent; + return hidlLevel; + } + + static @NonNull + ConfidenceLevel hidl2aidlConfidenceLevel( + @NonNull android.hardware.soundtrigger.V2_0.ConfidenceLevel hidlLevel) { + ConfidenceLevel aidlLevel = new ConfidenceLevel(); + aidlLevel.userId = hidlLevel.userId; + aidlLevel.levelPercent = hidlLevel.levelPercent; + return aidlLevel; + } + + static int hidl2aidlRecognitionStatus(int hidlStatus) { + switch (hidlStatus) { + case ISoundTriggerHwCallback.RecognitionStatus.SUCCESS: + return RecognitionStatus.SUCCESS; + case ISoundTriggerHwCallback.RecognitionStatus.ABORT: + return RecognitionStatus.ABORTED; + case ISoundTriggerHwCallback.RecognitionStatus.FAILURE: + return RecognitionStatus.FAILURE; + case 3: // This doesn't have a constant in HIDL. + return RecognitionStatus.FORCED; + default: + throw new IllegalArgumentException("Unknown recognition status: " + hidlStatus); + } + } + + static @NonNull + RecognitionEvent hidl2aidlRecognitionEvent(@NonNull + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent hidlEvent) { + RecognitionEvent aidlEvent = new RecognitionEvent(); + aidlEvent.status = hidl2aidlRecognitionStatus(hidlEvent.status); + aidlEvent.type = hidl2aidlSoundModelType(hidlEvent.type); + aidlEvent.captureAvailable = hidlEvent.captureAvailable; + // hidlEvent.captureSession is never a valid field. + aidlEvent.captureSession = -1; + aidlEvent.captureDelayMs = hidlEvent.captureDelayMs; + aidlEvent.capturePreambleMs = hidlEvent.capturePreambleMs; + aidlEvent.triggerInData = hidlEvent.triggerInData; + aidlEvent.audioConfig = hidl2aidlAudioConfig(hidlEvent.audioConfig); + aidlEvent.data = new byte[hidlEvent.data.size()]; + for (int i = 0; i < aidlEvent.data.length; ++i) { + aidlEvent.data[i] = hidlEvent.data.get(i); + } + return aidlEvent; + } + + static @NonNull + RecognitionEvent hidl2aidlRecognitionEvent( + @NonNull ISoundTriggerHwCallback.RecognitionEvent hidlEvent) { + RecognitionEvent aidlEvent = hidl2aidlRecognitionEvent(hidlEvent.header); + // Data needs to get overridden with 2.1 data. + aidlEvent.data = HidlMemoryUtil.hidlMemoryToByteArray(hidlEvent.data); + return aidlEvent; + } + + static @NonNull + PhraseRecognitionEvent hidl2aidlPhraseRecognitionEvent(@NonNull + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent hidlEvent) { + PhraseRecognitionEvent aidlEvent = new PhraseRecognitionEvent(); + aidlEvent.common = hidl2aidlRecognitionEvent(hidlEvent.common); + aidlEvent.phraseExtras = new PhraseRecognitionExtra[hidlEvent.phraseExtras.size()]; + for (int i = 0; i < hidlEvent.phraseExtras.size(); ++i) { + aidlEvent.phraseExtras[i] = hidl2aidlPhraseRecognitionExtra( + hidlEvent.phraseExtras.get(i)); + } + return aidlEvent; + } + + static @NonNull + PhraseRecognitionEvent hidl2aidlPhraseRecognitionEvent( + @NonNull ISoundTriggerHwCallback.PhraseRecognitionEvent hidlEvent) { + PhraseRecognitionEvent aidlEvent = new PhraseRecognitionEvent(); + aidlEvent.common = hidl2aidlRecognitionEvent(hidlEvent.common); + aidlEvent.phraseExtras = new PhraseRecognitionExtra[hidlEvent.phraseExtras.size()]; + for (int i = 0; i < hidlEvent.phraseExtras.size(); ++i) { + aidlEvent.phraseExtras[i] = hidl2aidlPhraseRecognitionExtra( + hidlEvent.phraseExtras.get(i)); + } + return aidlEvent; + } + + static @NonNull + AudioConfig hidl2aidlAudioConfig( + @NonNull android.hardware.audio.common.V2_0.AudioConfig hidlConfig) { + AudioConfig aidlConfig = new AudioConfig(); + // TODO(ytai): channelMask and format might need a more careful conversion to make sure the + // constants match. + aidlConfig.sampleRateHz = hidlConfig.sampleRateHz; + aidlConfig.channelMask = hidlConfig.channelMask; + aidlConfig.format = hidlConfig.format; + aidlConfig.offloadInfo = hidl2aidlOffloadInfo(hidlConfig.offloadInfo); + aidlConfig.frameCount = hidlConfig.frameCount; + return aidlConfig; + } + + static @NonNull + AudioOffloadInfo hidl2aidlOffloadInfo( + @NonNull android.hardware.audio.common.V2_0.AudioOffloadInfo hidlInfo) { + AudioOffloadInfo aidlInfo = new AudioOffloadInfo(); + // TODO(ytai): channelMask, format, streamType and usage might need a more careful + // conversion to make sure the constants match. + aidlInfo.sampleRateHz = hidlInfo.sampleRateHz; + aidlInfo.channelMask = hidlInfo.channelMask; + aidlInfo.format = hidlInfo.format; + aidlInfo.streamType = hidlInfo.streamType; + aidlInfo.bitRatePerSecond = hidlInfo.bitRatePerSecond; + aidlInfo.durationMicroseconds = hidlInfo.durationMicroseconds; + aidlInfo.hasVideo = hidlInfo.hasVideo; + aidlInfo.isStreaming = hidlInfo.isStreaming; + aidlInfo.bitWidth = hidlInfo.bitWidth; + aidlInfo.bufferSize = hidlInfo.bufferSize; + aidlInfo.usage = hidlInfo.usage; + return aidlInfo; + } + + @Nullable + static ModelParameterRange hidl2aidlModelParameterRange( + android.hardware.soundtrigger.V2_3.ModelParameterRange hidlRange) { + if (hidlRange == null) { + return null; + } + ModelParameterRange aidlRange = new ModelParameterRange(); + aidlRange.minInclusive = hidlRange.start; + aidlRange.maxInclusive = hidlRange.end; + return aidlRange; + } + + static int aidl2hidlModelParameter(int aidlParam) { + switch (aidlParam) { + case ModelParameter.THRESHOLD_FACTOR: + return android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR; + + default: + return android.hardware.soundtrigger.V2_3.ModelParameter.INVALID; + } + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/HalException.java b/services/core/java/com/android/server/soundtrigger_middleware/HalException.java new file mode 100644 index 000000000000..8b3e70875183 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/HalException.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.NonNull; + +/** + * This exception represents a non-zero status code returned by a HAL invocation. + * Depending on the operation that threw the error, the integrity of the HAL implementation and the + * client's tolerance to error, this error may or may not be recoverable. The HAL itself is expected + * to retain the state it had prior to the invocation (so, unless the error is a result of a HAL + * bug, normal operation may resume). + * <p> + * The reason why this is a RuntimeException, even though the HAL interface allows returning them + * is because we expect none of them to actually occur as part of correct usage of the HAL. + * + * @hide + */ +public class HalException extends RuntimeException { + public final int errorCode; + + public HalException(int errorCode, @NonNull String message) { + super(message); + this.errorCode = errorCode; + } + + public HalException(int errorCode) { + this.errorCode = errorCode; + } + + @Override + public @NonNull String toString() { + return super.toString() + " (code " + errorCode + ")"; + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/Hw2CompatUtil.java b/services/core/java/com/android/server/soundtrigger_middleware/Hw2CompatUtil.java new file mode 100644 index 000000000000..f0a0d8305bc6 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/Hw2CompatUtil.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.os.HidlMemoryUtil; + +import java.util.ArrayList; + +/** + * Utilities for maintaining data compatibility between different minor versions of soundtrigger@2.x + * HAL. + * Note that some of these conversion utilities are destructive, i.e. mutate their input (for the + * sake of simplifying code and reducing copies). + */ +class Hw2CompatUtil { + static android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel convertSoundModel_2_1_to_2_0( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel) { + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel model_2_0 = soundModel.header; + // Note: this mutates the input! + model_2_0.data = HidlMemoryUtil.hidlMemoryToByteList(soundModel.data); + return model_2_0; + } + + static android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent convertRecognitionEvent_2_0_to_2_1( + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent event) { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event_2_1 = + new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent(); + event_2_1.header = event; + event_2_1.data = HidlMemoryUtil.byteListToHidlMemory(event_2_1.header.data, + "SoundTrigger RecognitionEvent"); + // Note: this mutates the input! + event_2_1.header.data = new ArrayList<>(); + return event_2_1; + } + + static android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent convertPhraseRecognitionEvent_2_0_to_2_1( + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent event) { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent + event_2_1 = + new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent(); + event_2_1.common = convertRecognitionEvent_2_0_to_2_1(event.common); + event_2_1.phraseExtras = event.phraseExtras; + return event_2_1; + } + + static android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel convertPhraseSoundModel_2_1_to_2_0( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel) { + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel model_2_0 = + new android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel(); + model_2_0.common = convertSoundModel_2_1_to_2_0(soundModel.common); + model_2_0.phrases = soundModel.phrases; + return model_2_0; + } + + static android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig convertRecognitionConfig_2_1_to_2_0( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config) { + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig config_2_0 = + config.header; + // Note: this mutates the input! + config_2_0.data = HidlMemoryUtil.hidlMemoryToByteList(config.data); + return config_2_0; + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/ISoundTriggerHw2.java b/services/core/java/com/android/server/soundtrigger_middleware/ISoundTriggerHw2.java new file mode 100644 index 000000000000..81252c9a8c14 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/ISoundTriggerHw2.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.hardware.soundtrigger.V2_3.ISoundTriggerHw; +import android.hardware.soundtrigger.V2_3.ModelParameterRange; +import android.hidl.base.V1_0.IBase; +import android.os.IHwBinder; + +/** + * This interface mimics android.hardware.soundtrigger.V2_x.ISoundTriggerHw and + * android.hardware.soundtrigger.V2_x.ISoundTriggerHwCallback, with a few key differences: + * <ul> + * <li>Methods in the original interface generally have a status return value and potentially a + * second return value which is the actual return value. This is reflected via a synchronous + * callback, which is not very pleasant to work with. This interface replaces that pattern with + * the convention that a HalException is thrown for non-OK status, and then we can use the + * return value for the actual return value. + * <li>This interface will always include all the methods from the latest 2.x version (and thus + * from every 2.x version) interface, with the convention that unsupported methods throw a + * {@link RecoverableException} with a + * {@link android.media.soundtrigger_middleware.Status#OPERATION_NOT_SUPPORTED} + * code. + * <li>Cases where the original interface had multiple versions of a method representing the exact + * thing, or there exists a trivial conversion between the new and old version, this interface + * represents only the latest version, without any _version suffixes. + * <li>Removes some of the obscure IBinder methods. + * <li>No RemoteExceptions are specified. Some implementations of this interface may rethrow + * RemoteExceptions as RuntimeExceptions, some can guarantee handling them somehow and never throw + * them. + * <li>soundModelCallback has been removed, since nobody cares about it. Implementations are free + * to silently discard it. + * </ul> + * For cases where the client wants to explicitly handle specific versions of the underlying driver + * interface, they may call {@link #interfaceDescriptor()}. + * <p> + * <b>Note to maintainers</b>: This class must always be kept in sync with the latest 2.x version, + * so that clients have access to the entire functionality without having to burden themselves with + * compatibility, as much as possible. + */ +public interface ISoundTriggerHw2 { + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#getProperties(android.hardware.soundtrigger.V2_0.ISoundTriggerHw.getPropertiesCallback + */ + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.Properties getProperties(); + + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#loadSoundModel_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel, + * android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback, int, + * android.hardware.soundtrigger.V2_1.ISoundTriggerHw.loadSoundModel_2_1Callback) + */ + int loadSoundModel( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel, + SoundTriggerHw2Compat.Callback callback, int cookie); + + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#loadPhraseSoundModel_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel, + * android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback, int, + * android.hardware.soundtrigger.V2_1.ISoundTriggerHw.loadPhraseSoundModel_2_1Callback) + */ + int loadPhraseSoundModel( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel, + SoundTriggerHw2Compat.Callback callback, int cookie); + + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#unloadSoundModel(int) + */ + void unloadSoundModel(int modelHandle); + + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#stopRecognition(int) + */ + void stopRecognition(int modelHandle); + + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#stopAllRecognitions() + */ + void stopAllRecognitions(); + + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#startRecognition_2_1(int, + * android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig, + * android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback, int) + */ + void startRecognition(int modelHandle, + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config, + SoundTriggerHw2Compat.Callback callback, int cookie); + + /** + * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#getModelState(int) + */ + void getModelState(int modelHandle); + + /** + * @see android.hardware.soundtrigger.V2_3.ISoundTriggerHw#getParameter(int, int, + * ISoundTriggerHw.getParameterCallback) + */ + int getModelParameter(int modelHandle, int param); + + /** + * @see android.hardware.soundtrigger.V2_3.ISoundTriggerHw#setParameter(int, int, int) + */ + void setModelParameter(int modelHandle, int param, int value); + + /** + * @return null if not supported. + * @see android.hardware.soundtrigger.V2_3.ISoundTriggerHw#queryParameter(int, int, + * ISoundTriggerHw.queryParameterCallback) + */ + ModelParameterRange queryParameter(int modelHandle, int param); + + /** + * @see IHwBinder#linkToDeath(IHwBinder.DeathRecipient, long) + */ + boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie); + + /** + * @see IHwBinder#unlinkToDeath(IHwBinder.DeathRecipient) + */ + boolean unlinkToDeath(IHwBinder.DeathRecipient recipient); + + /** + * @see IBase#interfaceDescriptor() + */ + String interfaceDescriptor() throws android.os.RemoteException; + + interface Callback { + /** + * @see android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback#recognitionCallback_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent, + * int) + */ + void recognitionCallback( + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event, + int cookie); + + /** + * @see android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback#phraseRecognitionCallback_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent, + * int) + */ + void phraseRecognitionCallback( + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent event, + int cookie); + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/InternalServerError.java b/services/core/java/com/android/server/soundtrigger_middleware/InternalServerError.java new file mode 100644 index 000000000000..e1fb2266b7c6 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/InternalServerError.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.NonNull; + +/** + * An internal server error. + * <p> + * This exception wraps any exception thrown from a service implementation, which is a result of a + * bug in the server implementation (or any of its dependencies). + * <p> + * Specifically, this type is excluded from the set of whitelisted exceptions that binder would + * tunnel to the client process, since these exceptions are ambiguous regarding whether the client + * had done something wrong or the server is buggy. For example, a client getting an + * IllegalArgumentException cannot easily determine whether they had provided illegal arguments to + * the method they were calling, or whether the method implementation provided illegal arguments to + * some method it was calling due to a bug. + * + * @hide + */ +public class InternalServerError extends RuntimeException { + public InternalServerError(@NonNull Throwable cause) { + super(cause); + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/RecoverableException.java b/services/core/java/com/android/server/soundtrigger_middleware/RecoverableException.java new file mode 100644 index 000000000000..83618505814e --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/RecoverableException.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.NonNull; + +/** + * This exception represents a fault which: + * <ul> + * <li>Could not have been anticipated by a caller (i.e. is not a violation of any preconditions). + * <li>Is guaranteed to not have been caused any meaningful state change in the callee. The caller + * may continue operation as if the call has never been made. + * </ul> + * <p> + * Some recoverable faults are permanent and some are transient / circumstantial, the specific error + * code can provide more information about the possible recovery options. + * <p> + * The reason why this is a RuntimeException is to allow it to go through interfaces defined by + * AIDL, which we have no control over. + * + * @hide + */ +public class RecoverableException extends RuntimeException { + public final int errorCode; + + public RecoverableException(int errorCode, @NonNull String message) { + super(message); + this.errorCode = errorCode; + } + + public RecoverableException(int errorCode) { + this.errorCode = errorCode; + } + + @Override + public @NonNull String toString() { + return super.toString() + " (code " + errorCode + ")"; + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java new file mode 100644 index 000000000000..4a852c4b68e8 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java @@ -0,0 +1,470 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.media.soundtrigger_middleware.Status; +import android.os.IHwBinder; +import android.os.RemoteException; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * An implementation of {@link ISoundTriggerHw2}, on top of any + * android.hardware.soundtrigger.V2_x.ISoundTriggerHw implementation. This class hides away some of + * the details involved with retaining backward compatibility and adapts to the more pleasant syntax + * exposed by {@link ISoundTriggerHw2}, compared to the bare driver interface. + * <p> + * Exception handling: + * <ul> + * <li>All {@link RemoteException}s get rethrown as {@link RuntimeException}. + * <li>All HAL malfunctions get thrown as {@link HalException}. + * <li>All unsupported operations get thrown as {@link RecoverableException} with a + * {@link android.media.soundtrigger_middleware.Status#OPERATION_NOT_SUPPORTED} + * code. + * </ul> + */ +final class SoundTriggerHw2Compat implements ISoundTriggerHw2 { + private final @NonNull + IHwBinder mBinder; + private final @NonNull + android.hardware.soundtrigger.V2_0.ISoundTriggerHw mUnderlying_2_0; + private final @Nullable + android.hardware.soundtrigger.V2_1.ISoundTriggerHw mUnderlying_2_1; + private final @Nullable + android.hardware.soundtrigger.V2_2.ISoundTriggerHw mUnderlying_2_2; + private final @Nullable + android.hardware.soundtrigger.V2_3.ISoundTriggerHw mUnderlying_2_3; + + public SoundTriggerHw2Compat( + @NonNull android.hardware.soundtrigger.V2_0.ISoundTriggerHw underlying) { + this(underlying.asBinder()); + } + + public SoundTriggerHw2Compat(IHwBinder binder) { + Objects.requireNonNull(binder); + + mBinder = binder; + + // We want to share the proxy instances rather than create a separate proxy for every + // version, so we go down the versions in descending order to find the latest one supported, + // and then simply up-cast it to obtain all the versions that are earlier. + + // Attempt 2.3 + android.hardware.soundtrigger.V2_3.ISoundTriggerHw as2_3 = + android.hardware.soundtrigger.V2_3.ISoundTriggerHw.asInterface(binder); + if (as2_3 != null) { + mUnderlying_2_0 = mUnderlying_2_1 = mUnderlying_2_2 = mUnderlying_2_3 = as2_3; + return; + } + + // Attempt 2.2 + android.hardware.soundtrigger.V2_2.ISoundTriggerHw as2_2 = + android.hardware.soundtrigger.V2_2.ISoundTriggerHw.asInterface(binder); + if (as2_2 != null) { + mUnderlying_2_0 = mUnderlying_2_1 = mUnderlying_2_2 = as2_2; + mUnderlying_2_3 = null; + return; + } + + // Attempt 2.1 + android.hardware.soundtrigger.V2_1.ISoundTriggerHw as2_1 = + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.asInterface(binder); + if (as2_1 != null) { + mUnderlying_2_0 = mUnderlying_2_1 = as2_1; + mUnderlying_2_2 = mUnderlying_2_3 = null; + return; + } + + // Attempt 2.0 + android.hardware.soundtrigger.V2_0.ISoundTriggerHw as2_0 = + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.asInterface(binder); + if (as2_0 != null) { + mUnderlying_2_0 = as2_0; + mUnderlying_2_1 = mUnderlying_2_2 = mUnderlying_2_3 = null; + return; + } + + throw new RuntimeException("Binder doesn't support ISoundTriggerHw@2.0"); + } + + private static void handleHalStatus(int status, String methodName) { + if (status != 0) { + throw new HalException(status, methodName); + } + } + + @Override + public android.hardware.soundtrigger.V2_1.ISoundTriggerHw.Properties getProperties() { + try { + AtomicInteger retval = new AtomicInteger(-1); + AtomicReference<android.hardware.soundtrigger.V2_1.ISoundTriggerHw.Properties> + properties = + new AtomicReference<>(); + as2_0().getProperties( + (r, p) -> { + retval.set(r); + properties.set(p); + }); + handleHalStatus(retval.get(), "getProperties"); + return properties.get(); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + @Override + public int loadSoundModel( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel, + Callback callback, int cookie) { + try { + AtomicInteger retval = new AtomicInteger(-1); + AtomicInteger handle = new AtomicInteger(0); + try { + as2_1().loadSoundModel_2_1(soundModel, new SoundTriggerCallback(callback), cookie, + (r, h) -> { + retval.set(r); + handle.set(h); + }); + } catch (NotSupported e) { + // Fall-back to the 2.0 version: + return loadSoundModel_2_0(soundModel, callback, cookie); + } + handleHalStatus(retval.get(), "loadSoundModel_2_1"); + return handle.get(); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + @Override + public int loadPhraseSoundModel( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel, + Callback callback, int cookie) { + try { + AtomicInteger retval = new AtomicInteger(-1); + AtomicInteger handle = new AtomicInteger(0); + try { + as2_1().loadPhraseSoundModel_2_1(soundModel, new SoundTriggerCallback(callback), + cookie, + (r, h) -> { + retval.set(r); + handle.set(h); + }); + } catch (NotSupported e) { + // Fall-back to the 2.0 version: + return loadPhraseSoundModel_2_0(soundModel, callback, cookie); + } + handleHalStatus(retval.get(), "loadSoundModel_2_1"); + return handle.get(); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + @Override + public void unloadSoundModel(int modelHandle) { + try { + int retval = as2_0().unloadSoundModel(modelHandle); + handleHalStatus(retval, "unloadSoundModel"); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + @Override + public void stopRecognition(int modelHandle) { + try { + int retval = as2_0().stopRecognition(modelHandle); + handleHalStatus(retval, "stopRecognition"); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + + } + + @Override + public void stopAllRecognitions() { + try { + int retval = as2_0().stopAllRecognitions(); + handleHalStatus(retval, "stopAllRecognitions"); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + @Override + public void startRecognition(int modelHandle, + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config, + Callback callback, int cookie) { + try { + try { + int retval = as2_1().startRecognition_2_1(modelHandle, config, + new SoundTriggerCallback(callback), cookie); + handleHalStatus(retval, "startRecognition_2_1"); + } catch (NotSupported e) { + // Fall-back to the 2.0 version: + startRecognition_2_0(modelHandle, config, callback, cookie); + } + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + @Override + public void getModelState(int modelHandle) { + try { + int retval = as2_2().getModelState(modelHandle); + handleHalStatus(retval, "getModelState"); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } catch (NotSupported e) { + throw e.throwAsRecoverableException(); + } + } + + @Override + public int getModelParameter(int modelHandle, int param) { + AtomicInteger status = new AtomicInteger(-1); + AtomicInteger value = new AtomicInteger(0); + try { + as2_3().getParameter(modelHandle, param, + (s, v) -> { + status.set(s); + value.set(v); + }); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } catch (NotSupported e) { + throw e.throwAsRecoverableException(); + } + handleHalStatus(status.get(), "getParameter"); + return value.get(); + } + + @Override + public void setModelParameter(int modelHandle, int param, int value) { + try { + int retval = as2_3().setParameter(modelHandle, param, value); + handleHalStatus(retval, "setParameter"); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } catch (NotSupported e) { + throw e.throwAsRecoverableException(); + } + } + + @Override + public android.hardware.soundtrigger.V2_3.ModelParameterRange queryParameter(int modelHandle, + int param) { + AtomicInteger status = new AtomicInteger(-1); + AtomicReference<android.hardware.soundtrigger.V2_3.OptionalModelParameterRange> + optionalRange = + new AtomicReference<>(); + try { + as2_3().queryParameter(modelHandle, param, + (s, r) -> { + status.set(s); + optionalRange.set(r); + }); + } catch (NotSupported e) { + // For older drivers, we consider no model parameter to be supported. + return null; + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + handleHalStatus(status.get(), "queryParameter"); + return (optionalRange.get().getDiscriminator() + == android.hardware.soundtrigger.V2_3.OptionalModelParameterRange.hidl_discriminator.range) + ? + optionalRange.get().range() : null; + } + + @Override + public boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie) { + return mBinder.linkToDeath(recipient, cookie); + } + + @Override + public boolean unlinkToDeath(IHwBinder.DeathRecipient recipient) { + return mBinder.unlinkToDeath(recipient); + } + + @Override + public String interfaceDescriptor() throws RemoteException { + return as2_0().interfaceDescriptor(); + } + + private int loadSoundModel_2_0( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel, + Callback callback, int cookie) + throws RemoteException { + // Convert the soundModel to V2.0. + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel model_2_0 = + Hw2CompatUtil.convertSoundModel_2_1_to_2_0(soundModel); + + AtomicInteger retval = new AtomicInteger(-1); + AtomicInteger handle = new AtomicInteger(0); + as2_0().loadSoundModel(model_2_0, new SoundTriggerCallback(callback), cookie, (r, h) -> { + retval.set(r); + handle.set(h); + }); + handleHalStatus(retval.get(), "loadSoundModel"); + return handle.get(); + } + + private int loadPhraseSoundModel_2_0( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel, + Callback callback, int cookie) + throws RemoteException { + // Convert the soundModel to V2.0. + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel model_2_0 = + Hw2CompatUtil.convertPhraseSoundModel_2_1_to_2_0(soundModel); + + AtomicInteger retval = new AtomicInteger(-1); + AtomicInteger handle = new AtomicInteger(0); + as2_0().loadPhraseSoundModel(model_2_0, new SoundTriggerCallback(callback), cookie, + (r, h) -> { + retval.set(r); + handle.set(h); + }); + handleHalStatus(retval.get(), "loadSoundModel"); + return handle.get(); + } + + private void startRecognition_2_0(int modelHandle, + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config, + Callback callback, int cookie) + throws RemoteException { + + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig config_2_0 = + Hw2CompatUtil.convertRecognitionConfig_2_1_to_2_0(config); + int retval = as2_0().startRecognition(modelHandle, config_2_0, + new SoundTriggerCallback(callback), cookie); + handleHalStatus(retval, "startRecognition"); + } + + private @NonNull + android.hardware.soundtrigger.V2_0.ISoundTriggerHw as2_0() { + return mUnderlying_2_0; + } + + private @NonNull + android.hardware.soundtrigger.V2_1.ISoundTriggerHw as2_1() throws NotSupported { + if (mUnderlying_2_1 == null) { + throw new NotSupported("Underlying driver version < 2.1"); + } + return mUnderlying_2_1; + } + + private @NonNull + android.hardware.soundtrigger.V2_2.ISoundTriggerHw as2_2() throws NotSupported { + if (mUnderlying_2_2 == null) { + throw new NotSupported("Underlying driver version < 2.2"); + } + return mUnderlying_2_2; + } + + private @NonNull + android.hardware.soundtrigger.V2_3.ISoundTriggerHw as2_3() throws NotSupported { + if (mUnderlying_2_3 == null) { + throw new NotSupported("Underlying driver version < 2.3"); + } + return mUnderlying_2_3; + } + + /** + * A checked exception representing the requested interface version not being supported. + * At the public interface layer, use {@link #throwAsRecoverableException()} to propagate it to + * the caller if the request cannot be fulfilled. + */ + private static class NotSupported extends Exception { + NotSupported(String message) { + super(message); + } + + /** + * Throw this as a recoverable exception. + * + * @return Never actually returns anything. Always throws. Used so that caller can write + * throw e.throwAsRecoverableException(). + */ + RecoverableException throwAsRecoverableException() { + throw new RecoverableException(Status.OPERATION_NOT_SUPPORTED, getMessage()); + } + } + + private static class SoundTriggerCallback extends + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.Stub { + private final @NonNull + Callback mDelegate; + + private SoundTriggerCallback( + @NonNull Callback delegate) { + mDelegate = Objects.requireNonNull(delegate); + } + + @Override + public void recognitionCallback_2_1( + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event, + int cookie) { + mDelegate.recognitionCallback(event, cookie); + } + + @Override + public void phraseRecognitionCallback_2_1( + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent event, + int cookie) { + mDelegate.phraseRecognitionCallback(event, cookie); + } + + @Override + public void soundModelCallback_2_1( + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent event, + int cookie) { + // Nobody cares. + } + + @Override + public void recognitionCallback( + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent event, + int cookie) { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event_2_1 = + Hw2CompatUtil.convertRecognitionEvent_2_0_to_2_1(event); + mDelegate.recognitionCallback(event_2_1, cookie); + } + + @Override + public void phraseRecognitionCallback( + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent event, + int cookie) { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent + event_2_1 = Hw2CompatUtil.convertPhraseRecognitionEvent_2_0_to_2_1(event); + mDelegate.phraseRecognitionCallback(event_2_1, cookie); + } + + @Override + public void soundModelCallback( + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent event, + int cookie) { + // Nobody cares. + } + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImpl.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImpl.java new file mode 100644 index 000000000000..9d51b65ea152 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImpl.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.NonNull; +import android.hardware.soundtrigger.V2_0.ISoundTriggerHw; +import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; +import android.media.soundtrigger_middleware.ISoundTriggerModule; +import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; +import android.os.IBinder; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * This is an implementation of the ISoundTriggerMiddlewareService interface. + * <p> + * <b>Important conventions:</b> + * <ul> + * <li>Correct usage is assumed. This implementation does not attempt to gracefully handle invalid + * usage, and such usage will result in undefined behavior. If this service is to be offered to an + * untrusted client, it must be wrapped with input and state validation. + * <li>There is no binder instance associated with this implementation. Do not call asBinder(). + * <li>The implementation may throw a {@link RecoverableException} to indicate non-fatal, + * recoverable faults. The error code would one of the + * {@link android.media.soundtrigger_middleware.Status} + * constants. Any other exception thrown should be regarded as a bug in the implementation or one + * of its dependencies (assuming correct usage). + * <li>The implementation is designed for testibility by featuring dependency injection (the + * underlying HAL driver instances are passed to the ctor) and by minimizing dependencies on + * Android runtime. + * <li>The implementation is thread-safe. + * </ul> + * + * @hide + */ +public class SoundTriggerMiddlewareImpl implements ISoundTriggerMiddlewareService { + static private final String TAG = "SoundTriggerMiddlewareImpl"; + private final SoundTriggerModule[] mModules; + + /** + * Interface to the audio system, which can allocate capture session handles. + * SoundTrigger uses those sessions in order to associate a recognition session with an optional + * capture from the same device that triggered the recognition. + */ + public static abstract class AudioSessionProvider { + public static final class AudioSession { + final int mSessionHandle; + final int mIoHandle; + final int mDeviceHandle; + + AudioSession(int sessionHandle, int ioHandle, int deviceHandle) { + mSessionHandle = sessionHandle; + mIoHandle = ioHandle; + mDeviceHandle = deviceHandle; + } + } + + public abstract AudioSession acquireSession(); + + public abstract void releaseSession(int sessionHandle); + } + + /** + * Most generic constructor - gets an array of HAL driver instances. + */ + public SoundTriggerMiddlewareImpl(@NonNull ISoundTriggerHw[] halServices, + @NonNull AudioSessionProvider audioSessionProvider) { + List<SoundTriggerModule> modules = new ArrayList<>(halServices.length); + + for (int i = 0; i < halServices.length; ++i) { + ISoundTriggerHw service = halServices[i]; + try { + modules.add(new SoundTriggerModule(service, audioSessionProvider)); + } catch (Exception e) { + Log.e(TAG, "Failed to a SoundTriggerModule instance", e); + } + } + + mModules = modules.toArray(new SoundTriggerModule[modules.size()]); + } + + /** + * Convenience constructor - gets a single HAL driver instance. + */ + public SoundTriggerMiddlewareImpl(@NonNull ISoundTriggerHw halService, + @NonNull AudioSessionProvider audioSessionProvider) { + this(new ISoundTriggerHw[]{halService}, audioSessionProvider); + } + + @Override + public @NonNull + SoundTriggerModuleDescriptor[] listModules() { + SoundTriggerModuleDescriptor[] result = new SoundTriggerModuleDescriptor[mModules.length]; + + for (int i = 0; i < mModules.length; ++i) { + SoundTriggerModuleDescriptor desc = new SoundTriggerModuleDescriptor(); + desc.handle = i; + desc.properties = mModules[i].getProperties(); + result[i] = desc; + } + return result; + } + + @Override + public @NonNull + ISoundTriggerModule attach(int handle, @NonNull ISoundTriggerCallback callback) { + return mModules[handle].attach(callback); + } + + @Override + public void setExternalCaptureState(boolean active) { + for (SoundTriggerModule module : mModules) { + module.setExternalCaptureState(active); + } + } + + @Override + public @NonNull + IBinder asBinder() { + throw new UnsupportedOperationException( + "This implementation is not inteded to be used directly with Binder."); + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java new file mode 100644 index 000000000000..a7cfe1037f11 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java @@ -0,0 +1,709 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.soundtrigger.V2_0.ISoundTriggerHw; +import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; +import android.media.soundtrigger_middleware.ISoundTriggerModule; +import android.media.soundtrigger_middleware.ModelParameterRange; +import android.media.soundtrigger_middleware.PhraseRecognitionEvent; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionConfig; +import android.media.soundtrigger_middleware.RecognitionEvent; +import android.media.soundtrigger_middleware.RecognitionStatus; +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.util.Log; + +import com.android.internal.util.Preconditions; +import com.android.server.SystemService; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * This is a wrapper around an {@link ISoundTriggerMiddlewareService} implementation, which exposes + * it as a Binder service and enforces permissions and correct usage by the client, as well as makes + * sure that exceptions representing a server malfunction do not get sent to the client. + * <p> + * This is intended to extract the non-business logic out of the underlying implementation and thus + * make it easier to maintain each one of those separate aspects. A design trade-off is being made + * here, in that this class would need to essentially eavesdrop on all the client-server + * communication and retain all state known to the client, while the client doesn't necessarily care + * about all of it, and while the server has its own representation of this information. However, + * in this case, this is a small amount of data, and the benefits in code elegance seem worth it. + * There is also some additional cost in employing a simplistic locking mechanism here, but + * following the same line of reasoning, the benefits in code simplicity outweigh it. + * <p> + * Every public method in this class, overriding an interface method, must follow the following + * pattern: + * <code><pre> + * @Override public T method(S arg) { + * // Permission check. + * checkPermissions(); + * // Input validation. + * ValidationUtil.validateS(arg); + * synchronized (this) { + * // State validation. + * if (...state is not valid for this call...) { + * throw new IllegalStateException("State is invalid because..."); + * } + * // From here on, every exception isn't client's fault. + * try { + * T result = mDelegate.method(arg); + * // Update state.; + * ... + * return result; + * } catch (Exception e) { + * throw handleException(e); + * } + * } + * } + * </pre></code> + * Following this patterns ensures a consistent and rigorous handling of all aspects associated + * with client-server separation. + * <p> + * <b>Exception handling approach:</b><br> + * We make sure all client faults (permissions, argument and state validation) happen first, and + * would throw {@link SecurityException}, {@link IllegalArgumentException}/ + * {@link NullPointerException} or {@link + * IllegalStateException}, respectively. All those exceptions are treated specially by Binder and + * will get sent back to the client.<br> + * Once this is done, any subsequent fault is considered a server fault. Only {@link + * RecoverableException}s thrown by the implementation are special-cased: they would get sent back + * to the caller as a {@link ServiceSpecificException}, which is the behavior of Binder. Any other + * exception gets wrapped with a {@link InternalServerError}, which is specifically chosen as a type + * that <b>does NOT</b> get forwarded by binder. Those exceptions would be handled by a high-level + * exception handler on the server side, typically resulting in rebooting the server. + * <p> + * <b>Exposing this service as a System Service:</b><br> + * Insert this line into {@link com.android.server.SystemServer}: + * <code><pre> + * mSystemServiceManager.startService(SoundTriggerMiddlewareService.Lifecycle.class); + * </pre></code> + * + * {@hide} + */ +public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareService.Stub { + static private final String TAG = "SoundTriggerMiddlewareService"; + + final ISoundTriggerMiddlewareService mDelegate; + final Context mContext; + Set<Integer> mModuleHandles; + + /** + * Constructor for internal use only. Could be exposed for testing purposes in the future. + * Users should access this class via {@link Lifecycle}. + */ + private SoundTriggerMiddlewareService( + @NonNull ISoundTriggerMiddlewareService delegate, @NonNull Context context) { + mDelegate = delegate; + mContext = context; + } + + /** + * Generic exception handling for exceptions thrown by the underlying implementation. + * + * Would throw any {@link RecoverableException} as a {@link ServiceSpecificException} (passed + * by Binder to the caller) and <i>any other</i> exception as {@link InternalServerError} + * (<b>not</b> passed by Binder to the caller). + * <p> + * Typical usage: + * <code><pre> + * try { + * ... Do server operations ... + * } catch (Exception e) { + * throw handleException(e); + * } + * </pre></code> + */ + private static @NonNull + RuntimeException handleException(@NonNull Exception e) { + if (e instanceof RecoverableException) { + throw new ServiceSpecificException(((RecoverableException) e).errorCode, + e.getMessage()); + } + throw new InternalServerError(e); + } + + @Override + public @NonNull + SoundTriggerModuleDescriptor[] listModules() { + // Permission check. + checkPermissions(); + // Input validation (always valid). + + synchronized (this) { + // State validation (always valid). + + // From here on, every exception isn't client's fault. + try { + SoundTriggerModuleDescriptor[] result = mDelegate.listModules(); + mModuleHandles = new HashSet<>(result.length); + for (SoundTriggerModuleDescriptor desc : result) { + mModuleHandles.add(desc.handle); + } + return result; + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public @NonNull + ISoundTriggerModule attach(int handle, @NonNull ISoundTriggerCallback callback) { + // Permission check. + checkPermissions(); + // Input validation. + Preconditions.checkNotNull(callback); + Preconditions.checkNotNull(callback.asBinder()); + + synchronized (this) { + // State validation. + if (mModuleHandles == null) { + throw new IllegalStateException( + "Client must call listModules() prior to attaching."); + } + if (!mModuleHandles.contains(handle)) { + throw new IllegalArgumentException("Invalid handle: " + handle); + } + + // From here on, every exception isn't client's fault. + try { + ModuleService moduleService = new ModuleService(callback); + moduleService.attach(mDelegate.attach(handle, moduleService)); + return moduleService; + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public void setExternalCaptureState(boolean active) { + // Permission check. + checkPreemptPermissions(); + // Input validation (always valid). + + synchronized (this) { + // State validation (always valid). + + // From here on, every exception isn't client's fault. + try { + mDelegate.setExternalCaptureState(active); + } catch (Exception e) { + throw handleException(e); + } + } + } + + /** + * Throws a {@link SecurityException} if caller doesn't have the right permissions to use this + * service. + */ + private void checkPermissions() { + mContext.enforceCallingOrSelfPermission(Manifest.permission.RECORD_AUDIO, + "Caller must have the android.permission.RECORD_AUDIO permission."); + mContext.enforceCallingOrSelfPermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD, + "Caller must have the android.permission.CAPTURE_AUDIO_HOTWORD permission."); + } + + /** + * Throws a {@link SecurityException} if caller doesn't have the right permissions to preempt + * active sound trigger sessions. + */ + private void checkPreemptPermissions() { + mContext.enforceCallingOrSelfPermission(Manifest.permission.PREEMPT_SOUND_TRIGGER, + "Caller must have the android.permission.PREEMPT_SOUND_TRIGGER permission."); + } + + /** State of a sound model. */ + static class ModelState { + /** Activity state of a sound model. */ + enum Activity { + /** Model is loaded, recognition is inactive. */ + LOADED, + /** Model is loaded, recognition is active. */ + ACTIVE + } + + /** Activity state. */ + public Activity activityState = Activity.LOADED; + + /** + * A map of known parameter support. A missing key means we don't know yet whether the + * parameter is supported. A null value means it is known to not be supported. A non-null + * value indicates the valid value range. + */ + private Map<Integer, ModelParameterRange> parameterSupport = new HashMap<>(); + + /** + * Check that the given parameter is known to be supported for this model. + * + * @param modelParam The parameter key. + */ + public void checkSupported(int modelParam) { + if (!parameterSupport.containsKey(modelParam)) { + throw new IllegalStateException("Parameter has not been checked for support."); + } + ModelParameterRange range = parameterSupport.get(modelParam); + if (range == null) { + throw new IllegalArgumentException("Paramater is not supported."); + } + } + + /** + * Check that the given parameter is known to be supported for this model and that the given + * value is a valid value for it. + * + * @param modelParam The parameter key. + * @param value The value. + */ + public void checkSupported(int modelParam, int value) { + if (!parameterSupport.containsKey(modelParam)) { + throw new IllegalStateException("Parameter has not been checked for support."); + } + ModelParameterRange range = parameterSupport.get(modelParam); + if (range == null) { + throw new IllegalArgumentException("Paramater is not supported."); + } + Preconditions.checkArgumentInRange(value, range.minInclusive, range.maxInclusive, + "value"); + } + + /** + * Update support state for the given parameter for this model. + * + * @param modelParam The parameter key. + * @param range The parameter value range, or null if not supported. + */ + public void updateParameterSupport(int modelParam, @Nullable ModelParameterRange range) { + parameterSupport.put(modelParam, range); + } + } + + /** + * Entry-point to this module: exposes the module as a {@link SystemService}. + */ + public static final class Lifecycle extends SystemService { + private SoundTriggerMiddlewareService mService; + + public Lifecycle(Context context) { + super(context); + } + + @Override + public void onStart() { + ISoundTriggerHw[] services; + try { + services = new ISoundTriggerHw[]{ISoundTriggerHw.getService(true)}; + Log.d(TAG, "Connected to default ISoundTriggerHw"); + } catch (Exception e) { + Log.e(TAG, "Failed to connect to default ISoundTriggerHw", e); + services = new ISoundTriggerHw[0]; + } + + mService = new SoundTriggerMiddlewareService( + new SoundTriggerMiddlewareImpl(services, new AudioSessionProviderImpl()), + getContext()); + publishBinderService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE, mService); + } + } + + /** + * A wrapper around an {@link ISoundTriggerModule} implementation, to address the same aspects + * mentioned in {@link SoundTriggerModule} above. This class follows the same conventions. + */ + private class ModuleService extends ISoundTriggerModule.Stub implements ISoundTriggerCallback, + DeathRecipient { + private final ISoundTriggerCallback mCallback; + private ISoundTriggerModule mDelegate; + private Map<Integer, ModelState> mLoadedModels = new HashMap<>(); + + ModuleService(@NonNull ISoundTriggerCallback callback) { + mCallback = callback; + try { + mCallback.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + void attach(@NonNull ISoundTriggerModule delegate) { + mDelegate = delegate; + } + + @Override + public int loadModel(@NonNull SoundModel model) { + // Permission check. + checkPermissions(); + // Input validation. + ValidationUtil.validateGenericModel(model); + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + + // From here on, every exception isn't client's fault. + try { + int handle = mDelegate.loadModel(model); + mLoadedModels.put(handle, new ModelState()); + return handle; + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public int loadPhraseModel(@NonNull PhraseSoundModel model) { + // Permission check. + checkPermissions(); + // Input validation. + ValidationUtil.validatePhraseModel(model); + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + + // From here on, every exception isn't client's fault. + try { + int handle = mDelegate.loadPhraseModel(model); + mLoadedModels.put(handle, new ModelState()); + return handle; + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public void unloadModel(int modelHandle) { + // Permission check. + checkPermissions(); + // Input validation (always valid). + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState == null) { + throw new IllegalStateException("Invalid handle: " + modelHandle); + } + if (modelState.activityState != ModelState.Activity.LOADED) { + throw new IllegalStateException("Model with handle: " + modelHandle + + " has invalid state for unloading: " + modelState.activityState); + } + + // From here on, every exception isn't client's fault. + try { + mDelegate.unloadModel(modelHandle); + mLoadedModels.remove(modelHandle); + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) { + // Permission check. + checkPermissions(); + // Input validation. + ValidationUtil.validateRecognitionConfig(config); + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState == null) { + throw new IllegalStateException("Invalid handle: " + modelHandle); + } + if (modelState.activityState != ModelState.Activity.LOADED) { + throw new IllegalStateException("Model with handle: " + modelHandle + + " has invalid state for starting recognition: " + + modelState.activityState); + } + + // From here on, every exception isn't client's fault. + try { + mDelegate.startRecognition(modelHandle, config); + modelState.activityState = ModelState.Activity.ACTIVE; + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public void stopRecognition(int modelHandle) { + // Permission check. + checkPermissions(); + // Input validation (always valid). + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState == null) { + throw new IllegalStateException("Invalid handle: " + modelHandle); + } + // stopRecognition is idempotent - no need to check model state. + + // From here on, every exception isn't client's fault. + try { + mDelegate.stopRecognition(modelHandle); + modelState.activityState = ModelState.Activity.LOADED; + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public void forceRecognitionEvent(int modelHandle) { + // Permission check. + checkPermissions(); + // Input validation (always valid). + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState == null) { + throw new IllegalStateException("Invalid handle: " + modelHandle); + } + // forceRecognitionEvent is idempotent - no need to check model state. + + // From here on, every exception isn't client's fault. + try { + mDelegate.forceRecognitionEvent(modelHandle); + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public void setModelParameter(int modelHandle, int modelParam, int value) { + // Permission check. + checkPermissions(); + // Input validation. + ValidationUtil.validateModelParameter(modelParam); + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState == null) { + throw new IllegalStateException("Invalid handle: " + modelHandle); + } + modelState.checkSupported(modelParam, value); + + // From here on, every exception isn't client's fault. + try { + mDelegate.setModelParameter(modelHandle, modelParam, value); + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public int getModelParameter(int modelHandle, int modelParam) { + // Permission check. + checkPermissions(); + // Input validation. + ValidationUtil.validateModelParameter(modelParam); + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState == null) { + throw new IllegalStateException("Invalid handle: " + modelHandle); + } + modelState.checkSupported(modelParam); + + // From here on, every exception isn't client's fault. + try { + return mDelegate.getModelParameter(modelHandle, modelParam); + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + @Nullable + public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) { + // Permission check. + checkPermissions(); + // Input validation. + ValidationUtil.validateModelParameter(modelParam); + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has been detached."); + } + ModelState modelState = mLoadedModels.get(modelHandle); + if (modelState == null) { + throw new IllegalStateException("Invalid handle: " + modelHandle); + } + + // From here on, every exception isn't client's fault. + try { + ModelParameterRange result = mDelegate.queryModelParameterSupport(modelHandle, + modelParam); + modelState.updateParameterSupport(modelParam, result); + return result; + } catch (Exception e) { + throw handleException(e); + } + } + } + + @Override + public void detach() { + // Permission check. + checkPermissions(); + // Input validation (always valid). + + synchronized (this) { + // State validation. + if (mDelegate == null) { + throw new IllegalStateException("Module has already been detached."); + } + if (!mLoadedModels.isEmpty()) { + throw new IllegalStateException("Cannot detach while models are loaded."); + } + + // From here on, every exception isn't client's fault. + try { + detachInternal(); + } catch (Exception e) { + throw handleException(e); + } + } + } + + private void detachInternal() { + try { + mDelegate.detach(); + mDelegate = null; + mCallback.asBinder().unlinkToDeath(this, 0); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////// + // Callbacks + + @Override + public void onRecognition(int modelHandle, @NonNull RecognitionEvent event) { + synchronized (this) { + if (event.status != RecognitionStatus.FORCED) { + mLoadedModels.get(modelHandle).activityState = ModelState.Activity.LOADED; + } + try { + mCallback.onRecognition(modelHandle, event); + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback execption.", e); + } + } + } + + @Override + public void onPhraseRecognition(int modelHandle, @NonNull PhraseRecognitionEvent event) { + synchronized (this) { + if (event.common.status != RecognitionStatus.FORCED) { + mLoadedModels.get(modelHandle).activityState = ModelState.Activity.LOADED; + } + try { + mCallback.onPhraseRecognition(modelHandle, event); + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback execption.", e); + } + } + } + + @Override + public void onRecognitionAvailabilityChange(boolean available) throws RemoteException { + synchronized (this) { + try { + mCallback.onRecognitionAvailabilityChange(available); + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback execption.", e); + } + } + } + + @Override + public void binderDied() { + // This is called whenever our client process dies. + synchronized (this) { + try { + // Gracefully stop all active recognitions and unload the models. + for (Map.Entry<Integer, ModelState> entry : mLoadedModels.entrySet()) { + if (entry.getValue().activityState == ModelState.Activity.ACTIVE) { + mDelegate.stopRecognition(entry.getKey()); + } + mDelegate.unloadModel(entry.getKey()); + } + // Detach. + detachInternal(); + } catch (Exception e) { + throw handleException(e); + } + } + } + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java new file mode 100644 index 000000000000..81789e1362c0 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java @@ -0,0 +1,545 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback; +import android.hardware.soundtrigger.V2_2.ISoundTriggerHw; +import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.ISoundTriggerModule; +import android.media.soundtrigger_middleware.ModelParameterRange; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionConfig; +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.SoundModelType; +import android.media.soundtrigger_middleware.SoundTriggerModuleProperties; +import android.media.soundtrigger_middleware.Status; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * This is an implementation of a single module of the ISoundTriggerMiddlewareService interface, + * exposing itself through the {@link ISoundTriggerModule} interface, possibly to multiple separate + * clients. + * <p> + * Typical usage is to query the module capabilities using {@link #getProperties()} and then to use + * the module through an {@link ISoundTriggerModule} instance, obtained via {@link + * #attach(ISoundTriggerCallback)}. Every such interface is its own session and state is not shared + * between sessions (i.e. cannot use a handle obtained from one session through another). + * <p> + * <b>Important conventions:</b> + * <ul> + * <li>Correct usage is assumed. This implementation does not attempt to gracefully handle + * invalid usage, and such usage will result in undefined behavior. If this service is to be + * offered to an untrusted client, it must be wrapped with input and state validation. + * <li>The underlying driver is assumed to be correct. This implementation does not attempt to + * gracefully handle driver malfunction and such behavior will result in undefined behavior. If this + * service is to used with an untrusted driver, the driver must be wrapped with validation / error + * recovery code. + * <li>RemoteExceptions thrown by the driver are treated as RuntimeExceptions - they are not + * considered recoverable faults and should not occur in a properly functioning system. + * <li>There is no binder instance associated with this implementation. Do not call asBinder(). + * <li>The implementation may throw a {@link RecoverableException} to indicate non-fatal, + * recoverable faults. The error code would one of the + * {@link android.media.soundtrigger_middleware.Status} constants. Any other exception + * thrown should be regarded as a bug in the implementation or one of its dependencies + * (assuming correct usage). + * <li>The implementation is designed for testability by featuring dependency injection (the + * underlying HAL driver instances are passed to the ctor) and by minimizing dependencies + * on Android runtime. + * <li>The implementation is thread-safe. This is achieved by a simplistic model, where all entry- + * points (both client API and driver callbacks) obtain a lock on the SoundTriggerModule instance + * for their entire scope. Any other method can be assumed to be running with the lock already + * obtained, so no further locking should be done. While this is not necessarily the most efficient + * synchronization strategy, it is very easy to reason about and this code is likely not on any + * performance-critical + * path. + * </ul> + * + * @hide + */ +class SoundTriggerModule { + static private final String TAG = "SoundTriggerModule"; + @NonNull private final ISoundTriggerHw2 mHalService; + @NonNull private final SoundTriggerMiddlewareImpl.AudioSessionProvider mAudioSessionProvider; + private final Set<Session> mActiveSessions = new HashSet<>(); + private int mNumLoadedModels = 0; + private SoundTriggerModuleProperties mProperties = null; + private boolean mRecognitionAvailable; + + /** + * Ctor. + * + * @param halService The underlying HAL driver. + */ + SoundTriggerModule(@NonNull android.hardware.soundtrigger.V2_0.ISoundTriggerHw halService, + @NonNull SoundTriggerMiddlewareImpl.AudioSessionProvider audioSessionProvider) { + assert halService != null; + mHalService = new SoundTriggerHw2Compat(halService); + mAudioSessionProvider = audioSessionProvider; + mProperties = ConversionUtil.hidl2aidlProperties(mHalService.getProperties()); + + // We conservatively assume that external capture is active until explicitly told otherwise. + mRecognitionAvailable = mProperties.concurrentCapture; + } + + /** + * Establish a client session with this module. + * + * This module may be shared by multiple clients, each will get its own session. While resources + * are shared between the clients, each session has its own state and data should not be shared + * across sessions. + * + * @param callback The client callback, which will be used for all messages. This is a oneway + * callback, so will never block, throw an unchecked exception or return a + * value. + * @return The interface through which this module can be controlled. + */ + synchronized @NonNull + Session attach(@NonNull ISoundTriggerCallback callback) { + Log.d(TAG, "attach()"); + Session session = new Session(callback); + mActiveSessions.add(session); + return session; + } + + /** + * Query the module's properties. + * + * @return The properties structure. + */ + synchronized @NonNull + SoundTriggerModuleProperties getProperties() { + return mProperties; + } + + /** + * Notify the module that external capture has started / finished, using the same input device + * used for recognition. + * If the underlying driver does not support recognition while capturing, capture will be + * aborted, and the recognition callback will receive and abort event. In addition, all active + * clients will be notified of the change in state. + * + * @param active true iff external capture is active. + */ + synchronized void setExternalCaptureState(boolean active) { + Log.d(TAG, String.format("setExternalCaptureState(active=%b)", active)); + if (mProperties.concurrentCapture) { + // If we support concurrent capture, we don't care about any of this. + return; + } + mRecognitionAvailable = !active; + if (!mRecognitionAvailable) { + // Our module does not support recognition while a capture is active - + // need to abort all active recognitions. + for (Session session : mActiveSessions) { + session.abortActiveRecognitions(); + } + } + for (Session session : mActiveSessions) { + session.notifyRecognitionAvailability(); + } + } + + /** + * Remove session from the list of active sessions. + * + * @param session The session to remove. + */ + private void removeSession(@NonNull Session session) { + mActiveSessions.remove(session); + } + + /** State of a single sound model. */ + private enum ModelState { + /** Initial state, until load() is called. */ + INIT, + /** Model is loaded, but recognition is not active. */ + LOADED, + /** Model is loaded and recognition is active. */ + ACTIVE + } + + /** + * A single client session with this module. + * + * This is the main interface used to interact with this module. + */ + private class Session implements ISoundTriggerModule { + private ISoundTriggerCallback mCallback; + private Map<Integer, Model> mLoadedModels = new HashMap<>(); + + /** + * Ctor. + * + * @param callback The client callback interface. + */ + private Session(@NonNull ISoundTriggerCallback callback) { + mCallback = callback; + notifyRecognitionAvailability(); + } + + @Override + public void detach() { + Log.d(TAG, "detach()"); + synchronized (SoundTriggerModule.this) { + removeSession(this); + } + } + + @Override + public int loadModel(@NonNull SoundModel model) { + Log.d(TAG, String.format("loadModel(model=%s)", model)); + synchronized (SoundTriggerModule.this) { + if (mNumLoadedModels == mProperties.maxSoundModels) { + throw new RecoverableException(Status.RESOURCE_CONTENTION, + "Maximum number of models loaded."); + } + Model loadedModel = new Model(); + int result = loadedModel.load(model); + ++mNumLoadedModels; + return result; + } + } + + @Override + public int loadPhraseModel(@NonNull PhraseSoundModel model) { + Log.d(TAG, String.format("loadPhraseModel(model=%s)", model)); + synchronized (SoundTriggerModule.this) { + if (mNumLoadedModels == mProperties.maxSoundModels) { + throw new RecoverableException(Status.RESOURCE_CONTENTION, + "Maximum number of models loaded."); + } + Model loadedModel = new Model(); + int result = loadedModel.load(model); + ++mNumLoadedModels; + Log.d(TAG, String.format("loadPhraseModel()->%d", result)); + return result; + } + } + + @Override + public void unloadModel(int modelHandle) { + Log.d(TAG, String.format("unloadModel(handle=%d)", modelHandle)); + synchronized (SoundTriggerModule.this) { + mLoadedModels.get(modelHandle).unload(); + --mNumLoadedModels; + } + } + + @Override + public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) { + Log.d(TAG, + String.format("startRecognition(handle=%d, config=%s)", modelHandle, config)); + synchronized (SoundTriggerModule.this) { + mLoadedModels.get(modelHandle).startRecognition(config); + } + } + + @Override + public void stopRecognition(int modelHandle) { + Log.d(TAG, String.format("stopRecognition(handle=%d)", modelHandle)); + synchronized (SoundTriggerModule.this) { + mLoadedModels.get(modelHandle).stopRecognition(); + } + } + + @Override + public void forceRecognitionEvent(int modelHandle) { + Log.d(TAG, String.format("forceRecognitionEvent(handle=%d)", modelHandle)); + synchronized (SoundTriggerModule.this) { + mLoadedModels.get(modelHandle).forceRecognitionEvent(); + } + } + + @Override + public void setModelParameter(int modelHandle, int modelParam, int value) + throws RemoteException { + Log.d(TAG, + String.format("setModelParameter(handle=%d, param=%d, value=%d)", modelHandle, + modelParam, value)); + synchronized (SoundTriggerModule.this) { + mLoadedModels.get(modelHandle).setParameter(modelParam, value); + } + } + + @Override + public int getModelParameter(int modelHandle, int modelParam) throws RemoteException { + Log.d(TAG, String.format("getModelParameter(handle=%d, param=%d)", modelHandle, + modelParam)); + synchronized (SoundTriggerModule.this) { + return mLoadedModels.get(modelHandle).getParameter(modelParam); + } + } + + @Override + @Nullable + public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) { + Log.d(TAG, String.format("queryModelParameterSupport(handle=%d, param=%d)", modelHandle, + modelParam)); + synchronized (SoundTriggerModule.this) { + return mLoadedModels.get(modelHandle).queryModelParameterSupport(modelParam); + } + } + + /** + * Abort all currently active recognitions. + */ + private void abortActiveRecognitions() { + for (Model model : mLoadedModels.values()) { + model.abortActiveRecognition(); + } + } + + private void notifyRecognitionAvailability() { + try { + mCallback.onRecognitionAvailabilityChange(mRecognitionAvailable); + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback execption.", e); + } + } + + @Override + public @NonNull + IBinder asBinder() { + throw new UnsupportedOperationException( + "This implementation is not intended to be used directly with Binder."); + } + + /** + * A single sound model in the system. + * + * All model-based operations are delegated to this class and implemented here. + */ + private class Model implements ISoundTriggerHw2.Callback { + public int mHandle; + private ModelState mState = ModelState.INIT; + private int mModelType = SoundModelType.UNKNOWN; + private SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession mSession; + + private @NonNull + ModelState getState() { + return mState; + } + + private void setState(@NonNull ModelState state) { + mState = state; + SoundTriggerModule.this.notifyAll(); + } + + private void waitStateChange() throws InterruptedException { + SoundTriggerModule.this.wait(); + } + + private int load(@NonNull SoundModel model) { + mModelType = model.type; + ISoundTriggerHw.SoundModel hidlModel = ConversionUtil.aidl2hidlSoundModel(model); + + mSession = mAudioSessionProvider.acquireSession(); + try { + mHandle = mHalService.loadSoundModel(hidlModel, this, 0); + } catch (Exception e) { + mAudioSessionProvider.releaseSession(mSession.mSessionHandle); + throw e; + } + + setState(ModelState.LOADED); + mLoadedModels.put(mHandle, this); + return mHandle; + } + + private int load(@NonNull PhraseSoundModel model) { + mModelType = model.common.type; + ISoundTriggerHw.PhraseSoundModel hidlModel = + ConversionUtil.aidl2hidlPhraseSoundModel(model); + + mSession = mAudioSessionProvider.acquireSession(); + try { + mHandle = mHalService.loadPhraseSoundModel(hidlModel, this, 0); + } catch (Exception e) { + mAudioSessionProvider.releaseSession(mSession.mSessionHandle); + throw e; + } + + setState(ModelState.LOADED); + mLoadedModels.put(mHandle, this); + return mHandle; + } + + private void unload() { + mAudioSessionProvider.releaseSession(mSession.mSessionHandle); + mHalService.unloadSoundModel(mHandle); + mLoadedModels.remove(mHandle); + } + + private void startRecognition(@NonNull RecognitionConfig config) { + if (!mRecognitionAvailable) { + // Recognition is unavailable - send an abort event immediately. + notifyAbort(); + return; + } + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig hidlConfig = + ConversionUtil.aidl2hidlRecognitionConfig(config); + hidlConfig.header.captureDevice = mSession.mDeviceHandle; + hidlConfig.header.captureHandle = mSession.mIoHandle; + mHalService.startRecognition(mHandle, hidlConfig, this, 0); + setState(ModelState.ACTIVE); + } + + private void stopRecognition() { + if (getState() == ModelState.LOADED) { + // This call is idempotent in order to avoid races. + return; + } + mHalService.stopRecognition(mHandle); + setState(ModelState.LOADED); + } + + /** Request a forced recognition event. Will do nothing if recognition is inactive. */ + private void forceRecognitionEvent() { + if (getState() != ModelState.ACTIVE) { + // This call is idempotent in order to avoid races. + return; + } + mHalService.getModelState(mHandle); + } + + + private void setParameter(int modelParam, int value) { + mHalService.setModelParameter(mHandle, + ConversionUtil.aidl2hidlModelParameter(modelParam), value); + } + + private int getParameter(int modelParam) { + return mHalService.getModelParameter(mHandle, + ConversionUtil.aidl2hidlModelParameter(modelParam)); + } + + @Nullable + private ModelParameterRange queryModelParameterSupport(int modelParam) { + return ConversionUtil.hidl2aidlModelParameterRange( + mHalService.queryParameter(mHandle, + ConversionUtil.aidl2hidlModelParameter(modelParam))); + } + + /** Abort the recognition, if active. */ + private void abortActiveRecognition() { + // If we're inactive, do nothing. + if (getState() != ModelState.ACTIVE) { + return; + } + // Stop recognition. + stopRecognition(); + + // Notify the client that recognition has been aborted. + notifyAbort(); + } + + /** Notify the client that recognition has been aborted. */ + private void notifyAbort() { + try { + switch (mModelType) { + case SoundModelType.GENERIC: { + android.media.soundtrigger_middleware.RecognitionEvent event = + new android.media.soundtrigger_middleware.RecognitionEvent(); + event.status = + android.media.soundtrigger_middleware.RecognitionStatus.ABORTED; + mCallback.onRecognition(mHandle, event); + } + break; + + case SoundModelType.KEYPHRASE: { + android.media.soundtrigger_middleware.PhraseRecognitionEvent event = + new android.media.soundtrigger_middleware.PhraseRecognitionEvent(); + event.common = + new android.media.soundtrigger_middleware.RecognitionEvent(); + event.common.status = + android.media.soundtrigger_middleware.RecognitionStatus.ABORTED; + mCallback.onPhraseRecognition(mHandle, event); + } + break; + + default: + Log.e(TAG, "Unknown model type: " + mModelType); + + } + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback execption.", e); + } + } + + @Override + public void recognitionCallback( + @NonNull ISoundTriggerHwCallback.RecognitionEvent recognitionEvent, + int cookie) { + Log.d(TAG, String.format("recognitionCallback_2_1(event=%s, cookie=%d)", + recognitionEvent, cookie)); + synchronized (SoundTriggerModule.this) { + android.media.soundtrigger_middleware.RecognitionEvent aidlEvent = + ConversionUtil.hidl2aidlRecognitionEvent(recognitionEvent); + aidlEvent.captureSession = mSession.mSessionHandle; + try { + mCallback.onRecognition(mHandle, aidlEvent); + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback execption.", e); + } + if (aidlEvent.status + != android.media.soundtrigger_middleware.RecognitionStatus.FORCED) { + setState(ModelState.LOADED); + } + } + } + + @Override + public void phraseRecognitionCallback( + @NonNull ISoundTriggerHwCallback.PhraseRecognitionEvent phraseRecognitionEvent, + int cookie) { + Log.d(TAG, String.format("phraseRecognitionCallback_2_1(event=%s, cookie=%d)", + phraseRecognitionEvent, cookie)); + synchronized (SoundTriggerModule.this) { + android.media.soundtrigger_middleware.PhraseRecognitionEvent aidlEvent = + ConversionUtil.hidl2aidlPhraseRecognitionEvent(phraseRecognitionEvent); + aidlEvent.common.captureSession = mSession.mSessionHandle; + try { + mCallback.onPhraseRecognition(mHandle, aidlEvent); + } catch (RemoteException e) { + // Dead client will be handled by binderDied() - no need to handle here. + // In any case, client callbacks are considered best effort. + Log.e(TAG, "Client callback execption.", e); + } + if (aidlEvent.common.status + != android.media.soundtrigger_middleware.RecognitionStatus.FORCED) { + setState(ModelState.LOADED); + } + } + } + } + } +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/TEST_MAPPING b/services/core/java/com/android/server/soundtrigger_middleware/TEST_MAPPING new file mode 100644 index 000000000000..9ed894bc1ca9 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/TEST_MAPPING @@ -0,0 +1,12 @@ +{ + "presubmit": [ + { + "name": "FrameworksServicesTests", + "options": [ + { + "include-filter": "com.android.server.soundtrigger_middleware" + } + ] + } + ] +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/UuidUtil.java b/services/core/java/com/android/server/soundtrigger_middleware/UuidUtil.java new file mode 100644 index 000000000000..80f69d08c089 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/UuidUtil.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import java.util.regex.Pattern; + +/** + * Utilities for representing UUIDs as strings. + * + * @hide + */ +public class UuidUtil { + /** + * Regex pattern that can be used to validate / extract the various fields of a string-formatted + * UUID. + */ + static final Pattern PATTERN = Pattern.compile("^([a-fA-F0-9]{8})-" + + "([a-fA-F0-9]{4})-" + + "([a-fA-F0-9]{4})-" + + "([a-fA-F0-9]{4})-" + + "([a-fA-F0-9]{2})" + + "([a-fA-F0-9]{2})" + + "([a-fA-F0-9]{2})" + + "([a-fA-F0-9]{2})" + + "([a-fA-F0-9]{2})" + + "([a-fA-F0-9]{2})$"); + + /** Printf-style pattern for formatting the various fields of a UUID as a string. */ + static final String FORMAT = "%08x-%04x-%04x-%04x-%02x%02x%02x%02x%02x%02x"; +} diff --git a/services/core/java/com/android/server/soundtrigger_middleware/ValidationUtil.java b/services/core/java/com/android/server/soundtrigger_middleware/ValidationUtil.java new file mode 100644 index 000000000000..4898e6b59ab2 --- /dev/null +++ b/services/core/java/com/android/server/soundtrigger_middleware/ValidationUtil.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.Nullable; +import android.media.soundtrigger_middleware.ConfidenceLevel; +import android.media.soundtrigger_middleware.ModelParameter; +import android.media.soundtrigger_middleware.Phrase; +import android.media.soundtrigger_middleware.PhraseRecognitionExtra; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionConfig; +import android.media.soundtrigger_middleware.RecognitionMode; +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.SoundModelType; + +import com.android.internal.util.Preconditions; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utilities for asserting the validity of various data types used by this module. + * Each of the methods below would throw an {@link IllegalArgumentException} if its input is + * invalid. The input's validity is determined irrespective of any context. In cases where the valid + * value space is further limited by state, it is the caller's responsibility to assert. + * + * @hide + */ +public class ValidationUtil { + static void validateUuid(@Nullable String uuid) { + Preconditions.checkNotNull(uuid); + Matcher matcher = UuidUtil.PATTERN.matcher(uuid); + if (!matcher.matches()) { + throw new IllegalArgumentException( + "Illegal format for UUID: " + uuid); + } + } + + static void validateGenericModel(@Nullable SoundModel model) { + validateModel(model, SoundModelType.GENERIC); + } + + static void validateModel(@Nullable SoundModel model, int expectedType) { + Preconditions.checkNotNull(model); + if (model.type != expectedType) { + throw new IllegalArgumentException("Invalid type"); + } + validateUuid(model.uuid); + validateUuid(model.vendorUuid); + Preconditions.checkNotNull(model.data); + } + + static void validatePhraseModel(@Nullable PhraseSoundModel model) { + Preconditions.checkNotNull(model); + validateModel(model.common, SoundModelType.KEYPHRASE); + Preconditions.checkNotNull(model.phrases); + for (Phrase phrase : model.phrases) { + Preconditions.checkNotNull(phrase); + if ((phrase.recognitionModes & ~(RecognitionMode.VOICE_TRIGGER + | RecognitionMode.USER_IDENTIFICATION | RecognitionMode.USER_AUTHENTICATION + | RecognitionMode.GENERIC_TRIGGER)) != 0) { + throw new IllegalArgumentException("Invalid recognitionModes"); + } + Preconditions.checkNotNull(phrase.users); + Preconditions.checkNotNull(phrase.locale); + Preconditions.checkNotNull(phrase.text); + } + } + + static void validateRecognitionConfig(@Nullable RecognitionConfig config) { + Preconditions.checkNotNull(config); + Preconditions.checkNotNull(config.phraseRecognitionExtras); + for (PhraseRecognitionExtra extra : config.phraseRecognitionExtras) { + Preconditions.checkNotNull(extra); + if ((extra.recognitionModes & ~(RecognitionMode.VOICE_TRIGGER + | RecognitionMode.USER_IDENTIFICATION | RecognitionMode.USER_AUTHENTICATION + | RecognitionMode.GENERIC_TRIGGER)) != 0) { + throw new IllegalArgumentException("Invalid recognitionModes"); + } + if (extra.confidenceLevel < 0 || extra.confidenceLevel > 100) { + throw new IllegalArgumentException("Invalid confidenceLevel"); + } + Preconditions.checkNotNull(extra.levels); + for (ConfidenceLevel level : extra.levels) { + Preconditions.checkNotNull(level); + if (level.levelPercent < 0 || level.levelPercent > 100) { + throw new IllegalArgumentException("Invalid confidenceLevel"); + } + } + } + Preconditions.checkNotNull(config.data); + } + + static void validateModelParameter(int modelParam) { + switch (modelParam) { + case ModelParameter.THRESHOLD_FACTOR: + return; + + default: + throw new IllegalArgumentException("Invalid model parameter"); + } + } +} diff --git a/services/core/java/com/android/server/utils/TEST_MAPPING b/services/core/java/com/android/server/utils/TEST_MAPPING new file mode 100644 index 000000000000..bb7cea98eda0 --- /dev/null +++ b/services/core/java/com/android/server/utils/TEST_MAPPING @@ -0,0 +1,12 @@ +{ + "presubmit": [ + { + "name": "FrameworksMockingServicesTests", + "options": [ + { + "include-filter": "com.android.server.utils" + } + ] + } + ] +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/utils/quota/UptcMap.java b/services/core/java/com/android/server/utils/quota/UptcMap.java new file mode 100644 index 000000000000..7b499139aa7c --- /dev/null +++ b/services/core/java/com/android/server/utils/quota/UptcMap.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.utils.quota; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArrayMap; +import android.util.SparseArrayMap; + +import java.util.function.Consumer; + +/** + * A SparseArrayMap of ArrayMaps, which is suitable for holding userId-packageName-tag combination + * (UPTC)->object associations. Tags are any desired String. + * + * @see Uptc + */ +class UptcMap<T> { + private final SparseArrayMap<ArrayMap<String, T>> mData = new SparseArrayMap<>(); + + public void add(int userId, @NonNull String packageName, @Nullable String tag, + @Nullable T obj) { + ArrayMap<String, T> data = mData.get(userId, packageName); + if (data == null) { + data = new ArrayMap<>(); + mData.add(userId, packageName, data); + } + data.put(tag, obj); + } + + public void clear() { + mData.clear(); + } + + public boolean contains(int userId, @NonNull String packageName) { + return mData.contains(userId, packageName); + } + + public boolean contains(int userId, @NonNull String packageName, @Nullable String tag) { + // This structure never inserts a null ArrayMap, so if get(userId, packageName) returns + // null, the UPTC was never inserted. + ArrayMap<String, T> data = mData.get(userId, packageName); + return data != null && data.containsKey(tag); + } + + /** Removes all the data for the user, if there was any. */ + public void delete(int userId) { + mData.delete(userId); + } + + /** Removes the data for the user, package, and tag, if there was any. */ + public void delete(int userId, @NonNull String packageName, @Nullable String tag) { + final ArrayMap<String, T> data = mData.get(userId, packageName); + if (data != null) { + data.remove(tag); + if (data.size() == 0) { + mData.delete(userId, packageName); + } + } + } + + /** Removes the data for the user and package, if there was any. */ + public ArrayMap<String, T> delete(int userId, @NonNull String packageName) { + return mData.delete(userId, packageName); + } + + /** + * Returns the set of tag -> object mappings for the given userId and packageName + * combination. + */ + @Nullable + public ArrayMap<String, T> get(int userId, @NonNull String packageName) { + return mData.get(userId, packageName); + } + + /** Returns the saved object for the given UPTC. */ + @Nullable + public T get(int userId, @NonNull String packageName, @Nullable String tag) { + final ArrayMap<String, T> data = mData.get(userId, packageName); + return data != null ? data.get(tag) : null; + } + + /** + * Returns the index for which {@link #getUserIdAtIndex(int)} would return the specified userId, + * or a negative number if the specified userId is not mapped. + */ + public int indexOfUserId(int userId) { + return mData.indexOfKey(userId); + } + + /** + * Returns the index for which {@link #getPackageNameAtIndex(int, int)} would return the + * specified userId, or a negative number if the specified userId and packageName are not mapped + * together. + */ + public int indexOfUserIdAndPackage(int userId, @NonNull String packageName) { + return mData.indexOfKey(userId, packageName); + } + + /** Returns the userId at the given index. */ + public int getUserIdAtIndex(int index) { + return mData.keyAt(index); + } + + /** Returns the package name at the given index. */ + @NonNull + public String getPackageNameAtIndex(int userIndex, int packageIndex) { + return mData.keyAt(userIndex, packageIndex); + } + + /** Returns the tag at the given index. */ + @NonNull + public String getTagAtIndex(int userIndex, int packageIndex, int tagIndex) { + // This structure never inserts a null ArrayMap, so if the indices are valid, valueAt() + // won't return null. + return mData.valueAt(userIndex, packageIndex).keyAt(tagIndex); + } + + /** Returns the size of the outer (userId) array. */ + public int userCount() { + return mData.numMaps(); + } + + /** Returns the number of packages saved for a given userId. */ + public int packageCountForUser(int userId) { + return mData.numElementsForKey(userId); + } + + /** Returns the number of tags saved for a given userId-packageName combination. */ + public int tagCountForUserAndPackage(int userId, @NonNull String packageName) { + final ArrayMap data = mData.get(userId, packageName); + return data != null ? data.size() : 0; + } + + /** Returns the value T at the given user, package, and tag indices. */ + @Nullable + public T valueAt(int userIndex, int packageIndex, int tagIndex) { + final ArrayMap<String, T> data = mData.valueAt(userIndex, packageIndex); + return data != null ? data.valueAt(tagIndex) : null; + } + + public void forEach(Consumer<T> consumer) { + mData.forEach((tagMap) -> { + for (int i = tagMap.size() - 1; i >= 0; --i) { + consumer.accept(tagMap.valueAt(i)); + } + }); + } +} diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 23083c9fb7f7..674955e20859 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -98,6 +98,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.AuxiliaryResolveInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.content.res.Configuration; @@ -1309,9 +1310,11 @@ class ActivityStarter { String resolvedType, int userId) { if (auxiliaryResponse != null && auxiliaryResponse.needsPhaseTwo) { // request phase two resolution - mService.getPackageManagerInternalLocked().requestInstantAppResolutionPhaseTwo( + PackageManagerInternal packageManager = mService.getPackageManagerInternalLocked(); + boolean isRequesterInstantApp = packageManager.isInstantApp(callingPackage, userId); + packageManager.requestInstantAppResolutionPhaseTwo( auxiliaryResponse, originalIntent, resolvedType, callingPackage, - verificationBundle, userId); + isRequesterInstantApp, verificationBundle, userId); } return InstantAppResolver.buildEphemeralInstallerIntent( originalIntent, diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index e3ea2c5f1ac4..4667eab1f55b 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -4955,7 +4955,11 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo // Re-parent IME's SurfaceControl when MagnificationSpec changed. updateImeParent(); - applyMagnificationSpec(getPendingTransaction(), spec); + if (spec.scale != 1.0) { + applyMagnificationSpec(getPendingTransaction(), spec); + } else { + clearMagnificationSpec(getPendingTransaction()); + } getPendingTransaction().apply(); } diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index d73cb50fe107..06cea3741aa1 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -244,6 +244,8 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< protected final Rect mTmpRect = new Rect(); final Rect mTmpPrevBounds = new Rect(); + private MagnificationSpec mLastMagnificationSpec; + WindowContainer(WindowManagerService wms) { mWmService = wms; mPendingTransaction = wms.mTransactionFactory.get(); @@ -1726,6 +1728,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< if (shouldMagnify()) { t.setMatrix(mSurfaceControl, spec.scale, 0, 0, spec.scale) .setPosition(mSurfaceControl, spec.offsetX, spec.offsetY); + mLastMagnificationSpec = spec; } else { for (int i = 0; i < mChildren.size(); i++) { mChildren.get(i).applyMagnificationSpec(t, spec); @@ -1733,6 +1736,17 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } } + void clearMagnificationSpec(Transaction t) { + if (mLastMagnificationSpec != null) { + t.setMatrix(mSurfaceControl, 1, 0, 0, 1) + .setPosition(mSurfaceControl, 0, 0); + } + mLastMagnificationSpec = null; + for (int i = 0; i < mChildren.size(); i++) { + mChildren.get(i).clearMagnificationSpec(t); + } + } + void prepareSurfaces() { // If a leash has been set when the transaction was committed, then the leash reparent has // been committed. diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index fd8094cc43dd..a34b7fdb911c 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -35,6 +35,7 @@ cc_library_static { "com_android_server_power_PowerManagerService.cpp", "com_android_server_security_VerityUtils.cpp", "com_android_server_SerialService.cpp", + "com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp", "com_android_server_storage_AppFuseBridge.cpp", "com_android_server_SystemServer.cpp", "com_android_server_TestNetworkService.cpp", diff --git a/services/core/jni/com_android_server_VibratorService.cpp b/services/core/jni/com_android_server_VibratorService.cpp index dcff5a11aca0..6811e6d0e6f2 100644 --- a/services/core/jni/com_android_server_VibratorService.cpp +++ b/services/core/jni/com_android_server_VibratorService.cpp @@ -410,6 +410,21 @@ static jlong vibratorGetCapabilities(JNIEnv*, jclass) { return 0; } +static void vibratorAlwaysOnEnable(JNIEnv* env, jclass, jlong id, jlong effect, jlong strength) { + auto status = halCall(&aidl::IVibrator::alwaysOnEnable, id, + static_cast<aidl::Effect>(effect), static_cast<aidl::EffectStrength>(strength)); + if (!status.isOk()) { + ALOGE("vibratortAlwaysOnEnable command failed (%s).", status.toString8().string()); + } +} + +static void vibratorAlwaysOnDisable(JNIEnv* env, jclass, jlong id) { + auto status = halCall(&aidl::IVibrator::alwaysOnDisable, id); + if (!status.isOk()) { + ALOGE("vibratorAlwaysOnDisable command failed (%s).", status.toString8().string()); + } +} + static const JNINativeMethod method_table[] = { { "vibratorExists", "()Z", (void*)vibratorExists }, { "vibratorInit", "()V", (void*)vibratorInit }, @@ -422,6 +437,8 @@ static const JNINativeMethod method_table[] = { { "vibratorSupportsExternalControl", "()Z", (void*)vibratorSupportsExternalControl}, { "vibratorSetExternalControl", "(Z)V", (void*)vibratorSetExternalControl}, { "vibratorGetCapabilities", "()J", (void*)vibratorGetCapabilities}, + { "vibratorAlwaysOnEnable", "(JJJ)V", (void*)vibratorAlwaysOnEnable}, + { "vibratorAlwaysOnDisable", "(J)V", (void*)vibratorAlwaysOnDisable}, }; int register_android_server_VibratorService(JNIEnv *env) diff --git a/services/core/jni/com_android_server_security_VerityUtils.cpp b/services/core/jni/com_android_server_security_VerityUtils.cpp index 906b5688d51f..be11b8620bda 100644 --- a/services/core/jni/com_android_server_security_VerityUtils.cpp +++ b/services/core/jni/com_android_server_security_VerityUtils.cpp @@ -24,6 +24,7 @@ #include <errno.h> #include <fcntl.h> +#include <linux/fsverity.h> #include <string.h> #include <sys/ioctl.h> #include <sys/stat.h> @@ -33,40 +34,6 @@ #include <android-base/unique_fd.h> -// TODO(112037636): Always include once fsverity.h is upstreamed. -#if __has_include(<linux/fsverity.h>) -#include <linux/fsverity.h> -#else - -#include <linux/limits.h> -#include <linux/ioctl.h> -#include <linux/types.h> - -#define FS_VERITY_HASH_ALG_SHA256 1 - -struct fsverity_enable_arg { - __u32 version; - __u32 hash_algorithm; - __u32 block_size; - __u32 salt_size; - __u64 salt_ptr; - __u32 sig_size; - __u32 __reserved1; - __u64 sig_ptr; - __u64 __reserved2[11]; -}; - -struct fsverity_digest { - __u16 digest_algorithm; - __u16 digest_size; /* input/output */ - __u8 digest[]; -}; - -#define FS_IOC_ENABLE_VERITY _IOW('f', 133, struct fsverity_enable_arg) -#define FS_IOC_MEASURE_VERITY _IOWR('f', 134, struct fsverity_digest) - -#endif - const int kSha256Bytes = 32; namespace android { diff --git a/services/core/jni/com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp b/services/core/jni/com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp new file mode 100644 index 000000000000..774534f23b8c --- /dev/null +++ b/services/core/jni/com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2019 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. + */ + +#include <sstream> + +#include "core_jni_helpers.h" +#include <media/AudioSystem.h> + +namespace android { + +namespace { + +#define PACKAGE "com/android/server/soundtrigger_middleware" +#define CLASSNAME PACKAGE "/AudioSessionProviderImpl" +#define SESSION_CLASSNAME PACKAGE "/SoundTriggerMiddlewareImpl$AudioSessionProvider$AudioSession" + +jobject acquireAudioSession( + JNIEnv* env, + jobject clazz) { + + audio_session_t session; + audio_io_handle_t ioHandle; + audio_devices_t device; + + status_t status = AudioSystem::acquireSoundTriggerSession(&session, + &ioHandle, + &device); + if (status != 0) { + std::ostringstream message; + message + << "AudioSystem::acquireSoundTriggerSession returned an error code: " + << status; + env->ThrowNew(FindClassOrDie(env, "java/lang/RuntimeException"), + message.str().c_str()); + return nullptr; + } + + jclass cls = FindClassOrDie(env, SESSION_CLASSNAME); + jmethodID ctor = GetMethodIDOrDie(env, cls, "<init>", "(III)V"); + return env->NewObject(cls, + ctor, + static_cast<int>(session), + static_cast<int>(ioHandle), + static_cast<int>(device)); +} + +void releaseAudioSession(JNIEnv* env, jobject clazz, jint handle) { + status_t status = + AudioSystem::releaseSoundTriggerSession(static_cast<audio_session_t>(handle)); + + if (status != 0) { + std::ostringstream message; + message + << "AudioSystem::releaseAudioSystemSession returned an error code: " + << status; + env->ThrowNew(FindClassOrDie(env, "java/lang/RuntimeException"), + message.str().c_str()); + } +} + +const JNINativeMethod g_methods[] = { + {"acquireSession", "()L" SESSION_CLASSNAME ";", + reinterpret_cast<void*>(acquireAudioSession)}, + {"releaseSession", "(I)V", + reinterpret_cast<void*>(releaseAudioSession)}, +}; + +} // namespace + +int register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl( + JNIEnv* env) { + return RegisterMethodsOrDie(env, + CLASSNAME, + g_methods, + NELEM(g_methods)); +} + +} // namespace android diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index 692c9d25baa9..165edf15ca23 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -56,6 +56,8 @@ int register_android_server_net_NetworkStatsService(JNIEnv* env); int register_android_server_security_VerityUtils(JNIEnv* env); int register_android_server_am_AppCompactor(JNIEnv* env); int register_android_server_am_LowMemDetector(JNIEnv* env); +int register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl( + JNIEnv* env); }; using namespace android; @@ -105,5 +107,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_security_VerityUtils(env); register_android_server_am_AppCompactor(env); register_android_server_am_LowMemDetector(env); + register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl( + env); return JNI_VERSION_1_4; } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index aa7bf5b25f9b..d91ec42e89a3 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -2447,7 +2447,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Settings.Secure.UNKNOWN_SOURCES_DEFAULT_REVERSED, 0, userId) != 0) { profileOwner.ensureUserRestrictions().putBoolean( UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, true); - saveUserRestrictionsLocked(userId); + saveUserRestrictionsLocked(userId, /* parent = */ false); mInjector.settingsSecurePutIntForUser( Settings.Secure.UNKNOWN_SOURCES_DEFAULT_REVERSED, 0, userId); } @@ -2478,7 +2478,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } admin.defaultEnabledRestrictionsAlreadySet.addAll(restrictionsToSet); Slog.i(LOG_TAG, "Enabled the following restrictions by default: " + restrictionsToSet); - saveUserRestrictionsLocked(userId); + saveUserRestrictionsLocked(userId, /* parent = */ false); } } @@ -8039,7 +8039,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { activeAdmin.defaultEnabledRestrictionsAlreadySet.addAll(restrictions); Slog.i(LOG_TAG, "Enabled the following restrictions by default: " + restrictions); - saveUserRestrictionsLocked(userId); + saveUserRestrictionsLocked(userId, /* parent = */ false); } long ident = mInjector.binderClearCallingIdentity(); @@ -10310,24 +10310,33 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } @Override - public void setUserRestriction(ComponentName who, String key, boolean enabledFromThisOwner) { + public void setUserRestriction(ComponentName who, String key, boolean enabledFromThisOwner, + boolean parent) { Preconditions.checkNotNull(who, "ComponentName is null"); if (!UserRestrictionsUtils.isValidRestriction(key)) { return; } - final int userHandle = mInjector.userHandleGetCallingUserId(); + int userHandle = mInjector.userHandleGetCallingUserId(); synchronized (getLockObject()) { final ActiveAdmin activeAdmin = getActiveAdminForCallerLocked(who, - DeviceAdminInfo.USES_POLICY_PROFILE_OWNER); + DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, parent); final boolean isDeviceOwner = isDeviceOwner(who, userHandle); + if (isDeviceOwner) { if (!UserRestrictionsUtils.canDeviceOwnerChange(key)) { throw new SecurityException("Device owner cannot set user restriction " + key); } - } else { // profile owner - if (!UserRestrictionsUtils.canProfileOwnerChange(key, userHandle)) { + if (parent) { + throw new IllegalArgumentException( + "Cannot use the parent instance in Device Owner mode"); + } + } else { + if (!(UserRestrictionsUtils.canProfileOwnerChange(key, userHandle) || ( + isProfileOwnerOfOrganizationOwnedDevice(activeAdmin) && parent + && UserRestrictionsUtils.canProfileOwnerOfOrganizationOwnedDeviceChange( + key)))) { throw new SecurityException("Profile owner cannot set user restriction " + key); } } @@ -10339,7 +10348,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } else { restrictions.remove(key); } - saveUserRestrictionsLocked(userHandle); + saveUserRestrictionsLocked(userHandle, parent); } final int eventId = enabledFromThisOwner ? DevicePolicyEnums.ADD_USER_RESTRICTION @@ -10357,9 +10366,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } - private void saveUserRestrictionsLocked(int userId) { + private void saveUserRestrictionsLocked(int userId, boolean parent) { saveSettingsLocked(userId); - pushUserRestrictions(userId); + pushUserRestrictions(parent ? getProfileParentId(userId) : userId); sendChangedNotification(userId); } @@ -10368,6 +10377,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { final boolean isDeviceOwner = mOwners.isDeviceOwnerUserId(userId); Bundle userRestrictions = null; final int restrictionOwnerType; + final int originatingUserId; if (isDeviceOwner) { final ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked(); @@ -10377,6 +10387,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { userRestrictions = deviceOwner.userRestrictions; addOrRemoveDisableCameraRestriction(userRestrictions, deviceOwner); restrictionOwnerType = UserManagerInternal.OWNER_TYPE_DEVICE_OWNER; + originatingUserId = deviceOwner.getUserHandle().getIdentifier(); } else { final ActiveAdmin profileOwnerOfOrganizationOwnedDevice = getProfileOwnerOfOrganizationOwnedDeviceLocked(userId); @@ -10391,21 +10402,25 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { userRestrictions = parent.userRestrictions; userRestrictions = addOrRemoveDisableCameraRestriction(userRestrictions, parent); + originatingUserId = + profileOwnerOfOrganizationOwnedDevice.getUserHandle().getIdentifier(); } else { final ActiveAdmin profileOwner = getProfileOwnerAdminLocked(userId); if (profileOwner != null) { userRestrictions = profileOwner.userRestrictions; restrictionOwnerType = UserManagerInternal.OWNER_TYPE_PROFILE_OWNER; + originatingUserId = profileOwner.getUserHandle().getIdentifier(); } else { restrictionOwnerType = UserManagerInternal.OWNER_TYPE_NO_OWNER; + originatingUserId = userId; } userRestrictions = addOrRemoveDisableCameraRestriction( userRestrictions, userId); } } - mUserManagerInternal.setDevicePolicyUserRestrictions(userId, userRestrictions, - restrictionOwnerType); + mUserManagerInternal.setDevicePolicyUserRestrictions(originatingUserId, + userRestrictions, restrictionOwnerType); } } @@ -10435,14 +10450,18 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } @Override - public Bundle getUserRestrictions(ComponentName who) { + public Bundle getUserRestrictions(ComponentName who, boolean parent) { if (!mHasFeature) { return null; } Preconditions.checkNotNull(who, "ComponentName is null"); + synchronized (getLockObject()) { final ActiveAdmin activeAdmin = getActiveAdminForCallerLocked(who, - DeviceAdminInfo.USES_POLICY_PROFILE_OWNER); + DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, parent); + if (parent) { + enforceProfileOwnerOfOrganizationOwnedDevice(activeAdmin); + } return activeAdmin.userRestrictions; } } @@ -11241,7 +11260,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } else { try { setUserRestriction(who, UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, - (Integer.parseInt(value) == 0) ? true : false); + (Integer.parseInt(value) == 0) ? true : false, /* parent */ false); DevicePolicyEventLogger .createEvent(DevicePolicyEnums.SET_SECURE_SETTING) .setAdmin(who) @@ -11286,7 +11305,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Preconditions.checkNotNull(who, "ComponentName is null"); synchronized (getLockObject()) { getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER); - setUserRestriction(who, UserManager.DISALLOW_UNMUTE_DEVICE, on); + setUserRestriction(who, UserManager.DISALLOW_UNMUTE_DEVICE, on, /* parent */ false); DevicePolicyEventLogger .createEvent(DevicePolicyEnums.SET_MASTER_VOLUME_MUTED) .setAdmin(who) diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index f3ac7d6d3cc9..a1e02370aa51 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -105,6 +105,7 @@ import com.android.server.emergency.EmergencyAffordanceService; import com.android.server.gpu.GpuService; import com.android.server.hdmi.HdmiControlService; import com.android.server.incident.IncidentCompanionService; +import com.android.server.incremental.IncrementalManagerService; import com.android.server.input.InputManagerService; import com.android.server.inputmethod.InputMethodManagerService; import com.android.server.inputmethod.InputMethodSystemProperty; @@ -148,6 +149,7 @@ import com.android.server.security.KeyAttestationApplicationIdProviderService; import com.android.server.security.KeyChainSystemService; import com.android.server.signedconfig.SignedConfigService; import com.android.server.soundtrigger.SoundTriggerService; +import com.android.server.soundtrigger_middleware.SoundTriggerMiddlewareService; import com.android.server.statusbar.StatusBarManagerService; import com.android.server.storage.DeviceStorageMonitorService; import com.android.server.telecom.TelecomLoaderService; @@ -323,6 +325,7 @@ public final class SystemServer { private ContentResolver mContentResolver; private EntropyMixer mEntropyMixer; private DataLoaderManagerService mDataLoaderManagerService; + private IncrementalManagerService mIncrementalManagerService; private boolean mOnlyCore; private boolean mFirstBoot; @@ -705,6 +708,11 @@ public final class SystemServer { DataLoaderManagerService.class); t.traceEnd(); + // Incremental service needs to be started before package manager + t.traceBegin("StartIncrementalManagerService"); + mIncrementalManagerService = IncrementalManagerService.start(mSystemContext); + t.traceEnd(); + // Power manager needs to be started early because other services need it. // Native daemons may be watching for it to be registered so it must be ready // to handle incoming binder calls immediately (including being able to verify @@ -1555,6 +1563,10 @@ public final class SystemServer { } t.traceEnd(); + t.traceBegin("StartSoundTriggerMiddlewareService"); + mSystemServiceManager.startService(SoundTriggerMiddlewareService.Lifecycle.class); + t.traceEnd(); + if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_BROADCAST_RADIO)) { t.traceBegin("StartBroadcastRadioService"); mSystemServiceManager.startService(BroadcastRadioService.class); @@ -2061,6 +2073,12 @@ public final class SystemServer { mPackageManagerService.systemReady(); t.traceEnd(); + if (mIncrementalManagerService != null) { + t.traceBegin("MakeIncrementalManagerServiceReady"); + mIncrementalManagerService.systemReady(); + t.traceEnd(); + } + t.traceBegin("MakeDisplayManagerServiceReady"); try { // TODO: use boot phase and communicate these flags some other way diff --git a/services/net/java/android/net/TcpKeepalivePacketData.java b/services/net/java/android/net/TcpKeepalivePacketData.java index 7f2f499ab21f..aad75ae16aa9 100644 --- a/services/net/java/android/net/TcpKeepalivePacketData.java +++ b/services/net/java/android/net/TcpKeepalivePacketData.java @@ -19,7 +19,6 @@ import static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS; import android.annotation.NonNull; import android.annotation.Nullable; -import android.net.SocketKeepalive.InvalidPacketException; import android.net.util.IpUtils; import android.os.Parcel; import android.os.Parcelable; diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index cf7919b38f2e..3910993d9c27 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -40,7 +40,6 @@ android_test { "platformprotosnano", "hamcrest-library", "servicestests-utils", - "xml-writer-device-lib", "service-appsearch", "service-jobscheduler", ], diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/AccessibilityGestureDetectorTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/GestureManifoldTest.java index b7079124fb79..538e2d51e88f 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/AccessibilityGestureDetectorTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/GestureManifoldTest.java @@ -24,22 +24,21 @@ import static org.mockito.Mockito.when; import android.accessibilityservice.AccessibilityService; import android.content.Context; -import android.content.res.Resources; import android.graphics.Point; import android.graphics.PointF; -import android.util.DisplayMetrics; -import android.view.GestureDetector; import android.view.MotionEvent; +import androidx.test.InstrumentationRegistry; + import org.junit.Before; import org.junit.Test; import java.util.ArrayList; /** - * Tests for AccessibilityGestureDetector + * Tests for GestureManifold */ -public class AccessibilityGestureDetectorTest { +public class GestureManifoldTest { // Constants for testRecognizeGesturePath() private static final PointF PATH_START = new PointF(300f, 300f); @@ -47,24 +46,21 @@ public class AccessibilityGestureDetectorTest { private static final long PATH_STEP_MILLISEC = 100; // Data used by all tests - private AccessibilityGestureDetector mDetector; - private AccessibilityGestureDetector.Listener mResultListener; + private GestureManifold mManifold; + private TouchState mState; + private GestureManifold.Listener mResultListener; @Before public void setUp() { - // Construct a mock Context. - DisplayMetrics displayMetricsMock = mock(DisplayMetrics.class); - displayMetricsMock.xdpi = 500; - displayMetricsMock.ydpi = 500; - Resources mockResources = mock(Resources.class); - when(mockResources.getDisplayMetrics()).thenReturn(displayMetricsMock); - Context contextMock = mock(Context.class); - when(contextMock.getResources()).thenReturn(mockResources); - - // Construct a testable AccessibilityGestureDetector. - mResultListener = mock(AccessibilityGestureDetector.Listener.class); - GestureDetector doubleTapDetectorMock = mock(GestureDetector.class); - mDetector = new AccessibilityGestureDetector(contextMock, mResultListener, doubleTapDetectorMock); + Context context = InstrumentationRegistry.getContext(); + // Construct a testable GestureManifold. + mResultListener = mock(GestureManifold.Listener.class); + mState = new TouchState(); + mManifold = new GestureManifold(context, mResultListener, mState); + // Play the role of touch explorer in updating the shared state. + when(mResultListener.onGestureStarted()).thenReturn(onGestureStarted()); + + } @@ -141,8 +137,8 @@ public class AccessibilityGestureDetectorTest { // For each path step from start (non-inclusive) to end ... add a motion point. for (int step = 1; step < numSteps; ++step) { path.add(new PointF( - (start.x + (stepX * (float) step)), - (start.y + (stepY * (float) step)))); + (start.x + (stepX * (float) step)), + (start.y + (stepY * (float) step)))); } } @@ -170,12 +166,22 @@ public class AccessibilityGestureDetectorTest { point.x, point.y, 0); // Send event. - mDetector.onMotionEvent(event, event, policyFlags); + mState.onReceivedMotionEvent(event); + mManifold.onMotionEvent(event, event, policyFlags); eventTimeMs += PATH_STEP_MILLISEC; + if (mState.isClear()) { + mState.startTouchInteracting(); + } } + mState.clear(); // Check that correct gesture was recognized. verify(mResultListener).onGestureCompleted( argThat(gestureEvent -> gestureEvent.getGestureId() == gestureId)); } + + private boolean onGestureStarted() { + mState.startGestureDetecting(); + return false; + } } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java index 4b1ec6fe032b..a4ceadb3028b 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java @@ -74,7 +74,7 @@ public class TouchExplorerTest { private TouchExplorer mTouchExplorer; private long mLastDownTime = Integer.MIN_VALUE; - // mock package-private AccessibilityGestureDetector class + // mock package-private GestureManifold class @Rule public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule = new DexmakerShareClassLoaderRule(); @@ -108,7 +108,7 @@ public class TouchExplorerTest { public void setUp() { Context context = InstrumentationRegistry.getContext(); AccessibilityManagerService ams = new AccessibilityManagerService(context); - AccessibilityGestureDetector detector = mock(AccessibilityGestureDetector.class); + GestureManifold detector = mock(GestureManifold.class); mCaptor = new EventCaptor(); mTouchExplorer = new TouchExplorer(context, ams, detector); mTouchExplorer.setNext(mCaptor); diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java index 49412bc0c559..2ce17a1d0cea 100644 --- a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java @@ -349,10 +349,21 @@ public class ActivityManagerServiceTest { verifyUidRangesNoOverlap(range, range2); verifyIsolatedUidAllocator(range2); - // Free both, then try to allocate the maximum number of UID ranges + // Free both allocator.freeUidRangeLocked(appInfo); allocator.freeUidRangeLocked(appInfo2); + // Verify for a secondary user + ApplicationInfo appInfo3 = new ApplicationInfo(); + appInfo3.processName = "com.android.test.app"; + appInfo3.uid = 1010000; + final IsolatedUidRange range3 = allocator.getOrCreateIsolatedUidRangeLocked( + appInfo3.processName, appInfo3.uid); + validateAppZygoteIsolatedUidRange(range3); + verifyIsolatedUidAllocator(range3); + + allocator.freeUidRangeLocked(appInfo3); + // Try to allocate the maximum number of UID ranges int maxNumUidRanges = (Process.LAST_APP_ZYGOTE_ISOLATED_UID - Process.FIRST_APP_ZYGOTE_ISOLATED_UID + 1) / Process.NUM_UIDS_PER_APP_ZYGOTE; for (int i = 0; i < maxNumUidRanges; i++) { diff --git a/services/tests/servicestests/src/com/android/server/attention/AttentionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/attention/AttentionManagerServiceTest.java index a47a5671ccb3..e90cb4641752 100644 --- a/services/tests/servicestests/src/com/android/server/attention/AttentionManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/attention/AttentionManagerServiceTest.java @@ -16,7 +16,11 @@ package com.android.server.attention; +import static android.provider.DeviceConfig.NAMESPACE_ATTENTION_MANAGER_SERVICE; + import static com.android.server.attention.AttentionManagerService.ATTENTION_CACHE_BUFFER_SIZE; +import static com.android.server.attention.AttentionManagerService.DEFAULT_STALE_AFTER_MILLIS; +import static com.android.server.attention.AttentionManagerService.KEY_STALE_AFTER_MILLIS; import static com.google.common.truth.Truth.assertThat; @@ -35,6 +39,7 @@ import android.os.IBinder; import android.os.IPowerManager; import android.os.PowerManager; import android.os.RemoteException; +import android.provider.DeviceConfig; import android.service.attention.IAttentionCallback; import android.service.attention.IAttentionService; @@ -180,6 +185,45 @@ public class AttentionManagerServiceTest { assertThat(buffer.get(0)).isEqualTo(cache); } + @Test + public void testGetStaleAfterMillis_handlesGoodFlagValue() { + DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE, + KEY_STALE_AFTER_MILLIS, "123", false); + assertThat(mSpyAttentionManager.getStaleAfterMillis()).isEqualTo(123); + } + + @Test + public void testGetStaleAfterMillis_handlesBadFlagValue_1() { + DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE, + KEY_STALE_AFTER_MILLIS, "-123", false); + assertThat(mSpyAttentionManager.getStaleAfterMillis()).isEqualTo( + DEFAULT_STALE_AFTER_MILLIS); + } + + @Test + public void testGetStaleAfterMillis_handlesBadFlagValue_2() { + DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE, + KEY_STALE_AFTER_MILLIS, "15000", false); + assertThat(mSpyAttentionManager.getStaleAfterMillis()).isEqualTo( + DEFAULT_STALE_AFTER_MILLIS); + } + + @Test + public void testGetStaleAfterMillis_handlesBadFlagValue_3() { + DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE, + KEY_STALE_AFTER_MILLIS, "abracadabra", false); + assertThat(mSpyAttentionManager.getStaleAfterMillis()).isEqualTo( + DEFAULT_STALE_AFTER_MILLIS); + } + + @Test + public void testGetStaleAfterMillis_handlesBadFlagValue_4() { + DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE, + KEY_STALE_AFTER_MILLIS, "15_000L", false); + assertThat(mSpyAttentionManager.getStaleAfterMillis()).isEqualTo( + DEFAULT_STALE_AFTER_MILLIS); + } + private class MockIAttentionService implements IAttentionService { public void checkAttention(IAttentionCallback callback) throws RemoteException { callback.onSuccess(0, 0); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/OWNERS b/services/tests/servicestests/src/com/android/server/biometrics/OWNERS new file mode 100644 index 000000000000..8765c9a64b77 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/OWNERS @@ -0,0 +1,7 @@ +set noparent + +kchyn@google.com +jaggies@google.com +curtislb@google.com +ilyamaty@google.com +joshmccloskey@google.com diff --git a/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java b/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java index 72679769be8e..cb99c118a407 100644 --- a/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/compat/CompatConfigTest.java @@ -22,15 +22,13 @@ import android.content.pm.ApplicationInfo; import androidx.test.runner.AndroidJUnit4; -import com.android.compat.annotation.Change; -import com.android.compat.annotation.XmlWriter; - import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.util.UUID; @RunWith(AndroidJUnit4.class) @@ -50,18 +48,10 @@ public class CompatConfigTest { return dir; } - private void writeChangesToFile(Change[] changes, File f) { - XmlWriter writer = new XmlWriter(); - for (Change change: changes) { - writer.addChange(change); - } - try { - f.createNewFile(); - writer.write(new FileOutputStream(f)); - } catch (IOException e) { - throw new RuntimeException( - "Encountered an error while writing compat config file", e); - } + private void writeToFile(File dir, String filename, String content) throws IOException { + OutputStream os = new FileOutputStream(new File(dir, filename)); + os.write(content.getBytes()); + os.close(); } @Test @@ -173,13 +163,15 @@ public class CompatConfigTest { } @Test - public void testReadConfig() { - Change[] changes = {new Change(1234L, "MY_CHANGE1", false, 2, null), new Change(1235L, - "MY_CHANGE2", true, null, "description"), new Change(1236L, "MY_CHANGE3", false, - null, "")}; + public void testReadConfig() throws IOException { + String configXml = "<config>" + + "<compat-change id=\"1234\" name=\"MY_CHANGE1\" enableAfterTargetSdk=\"2\" />" + + "<compat-change id=\"1235\" name=\"MY_CHANGE2\" disabled=\"true\" />" + + "<compat-change id=\"1236\" name=\"MY_CHANGE3\" />" + + "</config>"; File dir = createTempDir(); - writeChangesToFile(changes, new File(dir.getPath() + "/platform_compat_config.xml")); + writeToFile(dir, "platform_compat_config.xml", configXml); CompatConfig pc = new CompatConfig(); pc.initConfigFromLib(dir); @@ -191,17 +183,18 @@ public class CompatConfigTest { } @Test - public void testReadConfigMultipleFiles() { - Change[] changes1 = {new Change(1234L, "MY_CHANGE1", false, 2, null)}; - Change[] changes2 = {new Change(1235L, "MY_CHANGE2", true, null, ""), new Change(1236L, - "MY_CHANGE3", false, null, null)}; + public void testReadConfigMultipleFiles() throws IOException { + String configXml1 = "<config>" + + "<compat-change id=\"1234\" name=\"MY_CHANGE1\" enableAfterTargetSdk=\"2\" />" + + "</config>"; + String configXml2 = "<config>" + + "<compat-change id=\"1235\" name=\"MY_CHANGE2\" disabled=\"true\" />" + + "<compat-change id=\"1236\" name=\"MY_CHANGE3\" />" + + "</config>"; File dir = createTempDir(); - writeChangesToFile(changes1, - new File(dir.getPath() + "/libcore_platform_compat_config.xml")); - writeChangesToFile(changes2, - new File(dir.getPath() + "/frameworks_platform_compat_config.xml")); - + writeToFile(dir, "libcore_platform_compat_config.xml", configXml1); + writeToFile(dir, "frameworks_platform_compat_config.xml", configXml2); CompatConfig pc = new CompatConfig(); pc.initConfigFromLib(dir); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index f54f88553751..f97c887c2894 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -1162,7 +1162,8 @@ public class DevicePolicyManagerTest extends DpmTestBase { MockUtils.checkUserHandle(UserHandle.USER_SYSTEM)); verify(getServices().userManagerInternal).setDevicePolicyUserRestrictions( - eq(UserHandle.USER_SYSTEM), eq(null), + eq(UserHandle.USER_SYSTEM), + eq(null), eq(UserManagerInternal.OWNER_TYPE_DEVICE_OWNER)); verify(getServices().usageStatsManagerInternal).setActiveAdminApps( @@ -1966,7 +1967,6 @@ public class DevicePolicyManagerTest extends DpmTestBase { // TODO Make sure restrictions are written to the file. } - // TODO: (b/138709470) test addUserRestriction as PO of an organization-owned device public void testSetUserRestriction_asPoOfOrgOwnedDevice() throws Exception { final int MANAGED_PROFILE_USER_ID = DpmMockContext.CALLER_USER_HANDLE; final int MANAGED_PROFILE_ADMIN_UID = @@ -1979,16 +1979,26 @@ public class DevicePolicyManagerTest extends DpmTestBase { when(getServices().userManager.getProfileParent(MANAGED_PROFILE_USER_ID)) .thenReturn(new UserInfo(UserHandle.USER_SYSTEM, "user system", 0)); + parentDpm.addUserRestriction(admin1, UserManager.DISALLOW_CONFIG_DATE_TIME); + verify(getServices().userManagerInternal).setDevicePolicyUserRestrictions( + eq(MANAGED_PROFILE_USER_ID), + MockUtils.checkUserRestrictions(UserManager.DISALLOW_CONFIG_DATE_TIME), + eq(UserManagerInternal.OWNER_TYPE_PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE)); + reset(getServices().userManagerInternal); + + parentDpm.clearUserRestriction(admin1, UserManager.DISALLOW_CONFIG_DATE_TIME); + reset(getServices().userManagerInternal); + parentDpm.setCameraDisabled(admin1, true); verify(getServices().userManagerInternal).setDevicePolicyUserRestrictions( - eq(UserHandle.USER_SYSTEM), + eq(MANAGED_PROFILE_USER_ID), MockUtils.checkUserRestrictions(UserManager.DISALLOW_CAMERA), eq(UserManagerInternal.OWNER_TYPE_PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE)); reset(getServices().userManagerInternal); parentDpm.setCameraDisabled(admin1, false); verify(getServices().userManagerInternal).setDevicePolicyUserRestrictions( - eq(UserHandle.USER_SYSTEM), + eq(MANAGED_PROFILE_USER_ID), MockUtils.checkUserRestrictions(), eq(UserManagerInternal.OWNER_TYPE_PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE)); reset(getServices().userManagerInternal); diff --git a/services/tests/servicestests/src/com/android/server/om/MockitoUtils.kt b/services/tests/servicestests/src/com/android/server/om/MockitoUtils.kt new file mode 100644 index 000000000000..0f915dbdcf6f --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/om/MockitoUtils.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.om + +import org.mockito.Answers +import org.mockito.Mockito +import org.mockito.invocation.InvocationOnMock +import org.mockito.stubbing.Answer +import org.mockito.stubbing.Stubber + +// TODO(chiuwinson): Move this entire file to a shared utility module +// TODO(b/135203078): De-dupe utils added for overlays vs package refactor +object MockitoUtils { + val ANSWER_THROWS = Answer<Any?> { + when (val name = it.method.name) { + "toString" -> return@Answer Answers.CALLS_REAL_METHODS.answer(it) + else -> { + val arguments = it.arguments + ?.takeUnless { it.isEmpty() } + ?.joinToString() + ?.let { + "with $it" + } + .orEmpty() + + throw UnsupportedOperationException("${it.mock::class.java.simpleName}#$name " + + "$arguments should not be called") + } + } + } +} + +inline fun <reified T> mock(block: T.() -> Unit = {}) = Mockito.mock(T::class.java).apply(block) + +fun <Type> Stubber.whenever(mock: Type) = Mockito.`when`(mock) +fun <Type : Any?> whenever(mock: Type) = Mockito.`when`(mock) + +@Suppress("UNCHECKED_CAST") +fun <Type : Any?> whenever(mock: Type, block: InvocationOnMock.() -> Any?) = + Mockito.`when`(mock).thenAnswer { block(it) } + +fun whenever(mock: Unit) = Mockito.`when`(mock).thenAnswer { } + +inline fun <reified T> mockThrowOnUnmocked(block: T.() -> Unit): T { + val swappingAnswer = object : Answer<Any?> { + var delegate: Answer<*> = Answers.RETURNS_DEFAULTS + + override fun answer(invocation: InvocationOnMock?): Any? { + return delegate.answer(invocation) + } + } + + return Mockito.mock(T::class.java, swappingAnswer).apply(block) + .also { + // To allow when() usage inside block, only swap to throwing afterwards + swappingAnswer.delegate = MockitoUtils.ANSWER_THROWS + } +} diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayReferenceMapperTests.kt b/services/tests/servicestests/src/com/android/server/om/OverlayReferenceMapperTests.kt new file mode 100644 index 000000000000..ef1294819f34 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/om/OverlayReferenceMapperTests.kt @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.om + +import android.content.pm.parsing.AndroidPackage +import android.net.Uri +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.testng.Assert.assertThrows + +@RunWith(Parameterized::class) +class OverlayReferenceMapperTests { + + companion object { + private const val TARGET_PACKAGE_NAME = "com.test.target" + private const val OVERLAY_PACKAGE_NAME = "com.test.overlay" + private const val ACTOR_PACKAGE_NAME = "com.test.actor" + private const val ACTOR_NAME = "overlay://test/actorName" + + @JvmStatic + @Parameterized.Parameters(name = "deferRebuild {0}") + fun parameters() = arrayOf(true, false) + } + + private lateinit var mapper: OverlayReferenceMapper + + @JvmField + @Parameterized.Parameter(0) + var deferRebuild = false + + @Before + fun initMapper() { + mapper = mapper() + } + + @Test + fun targetWithOverlay() { + val target = mockTarget() + val overlay = mockOverlay() + val existing = mapper.addInOrder(overlay) + assertEmpty() + mapper.addInOrder(target, existing = existing) + assertMapping(ACTOR_PACKAGE_NAME to setOf(target, overlay)) + mapper.remove(target) + assertEmpty() + } + + @Test + fun targetWithMultipleOverlays() { + val target = mockTarget() + val overlay0 = mockOverlay(0) + val overlay1 = mockOverlay(1) + mapper = mapper( + overlayToTargetToOverlayables = mapOf( + overlay0.packageName to mapOf( + target.packageName to target.overlayables.keys + ), + overlay1.packageName to mapOf( + target.packageName to target.overlayables.keys + ) + ) + ) + val existing = mapper.addInOrder(overlay0, overlay1) + assertEmpty() + mapper.addInOrder(target, existing = existing) + assertMapping(ACTOR_PACKAGE_NAME to setOf(target, overlay0, overlay1)) + mapper.remove(overlay0) + assertMapping(ACTOR_PACKAGE_NAME to setOf(target, overlay1)) + mapper.remove(target) + assertEmpty() + } + + @Test + fun targetWithoutOverlay() { + val target = mockTarget() + mapper.addInOrder(target) + assertMapping(ACTOR_PACKAGE_NAME to setOf(target)) + mapper.remove(target) + assertEmpty() + } + + @Test + fun overlayWithTarget() { + val target = mockTarget() + val overlay = mockOverlay() + val existing = mapper.addInOrder(target) + assertMapping(ACTOR_PACKAGE_NAME to setOf(target)) + mapper.addInOrder(overlay, existing = existing) + assertMapping(ACTOR_PACKAGE_NAME to setOf(target, overlay)) + mapper.remove(overlay) + assertMapping(ACTOR_PACKAGE_NAME to setOf(target)) + } + + @Test + fun overlayWithMultipleTargets() { + val target0 = mockTarget(0) + val target1 = mockTarget(1) + val overlay = mockOverlay() + mapper = mapper( + overlayToTargetToOverlayables = mapOf( + overlay.packageName to mapOf( + target0.packageName to target0.overlayables.keys, + target1.packageName to target1.overlayables.keys + ) + ) + ) + mapper.addInOrder(target0, target1, overlay) + assertMapping(ACTOR_PACKAGE_NAME to setOf(target0, target1, overlay)) + mapper.remove(target0) + assertMapping(ACTOR_PACKAGE_NAME to setOf(target1, overlay)) + mapper.remove(target1) + assertEmpty() + } + + @Test + fun overlayWithoutTarget() { + val overlay = mockOverlay() + mapper.addInOrder(overlay) + // An overlay can only have visibility exposed through its target + assertEmpty() + mapper.remove(overlay) + assertEmpty() + } + + private fun OverlayReferenceMapper.addInOrder( + vararg pkgs: AndroidPackage, + existing: MutableMap<String, AndroidPackage> = mutableMapOf() + ) = pkgs.fold(existing) { map, pkg -> + addPkg(pkg, map) + map[pkg.packageName] = pkg + return@fold map + } + + private fun OverlayReferenceMapper.remove(pkg: AndroidPackage) = removePkg(pkg.packageName) + + private fun assertMapping(vararg pairs: Pair<String, Set<AndroidPackage>>) { + val expected = pairs.associate { it } + .mapValues { pair -> pair.value.map { it.packageName }.toSet() } + + // This validates the API exposed for querying the relationships + expected.forEach { (actorPkgName, expectedPkgNames) -> + expectedPkgNames.forEach { expectedPkgName -> + if (deferRebuild) { + assertThrows(IllegalStateException::class.java) { + mapper.isValidActor(expectedPkgName, actorPkgName) + } + mapper.rebuildIfDeferred() + deferRebuild = false + } + + assertThat(mapper.isValidActor(expectedPkgName, actorPkgName)).isTrue() + } + } + + // This asserts no other relationships are defined besides those tested above + assertThat(mapper.actorPkgToPkgs).containsExactlyEntriesIn(expected) + } + + private fun assertEmpty() = assertMapping() + + private fun mapper( + namedActors: Map<String, Map<String, String>> = Uri.parse(ACTOR_NAME).run { + mapOf(authority!! to mapOf(pathSegments.first() to ACTOR_PACKAGE_NAME)) + }, + overlayToTargetToOverlayables: Map<String, Map<String, Set<String>>> = mapOf( + mockOverlay().packageName to mapOf( + mockTarget().run { packageName to overlayables.keys } + ) + ) + ) = OverlayReferenceMapper(deferRebuild, object : OverlayReferenceMapper.Provider { + override fun getActorPkg(actor: String?) = + OverlayActorEnforcer.getPackageNameForActor(actor, namedActors).first + + override fun getTargetToOverlayables(pkg: AndroidPackage) = + overlayToTargetToOverlayables[pkg.packageName] ?: emptyMap() + }) + + private fun mockTarget(increment: Int = 0) = mockThrowOnUnmocked<AndroidPackage> { + whenever(packageName) { "$TARGET_PACKAGE_NAME$increment" } + whenever(overlayables) { mapOf("overlayableName$increment" to ACTOR_NAME) } + whenever(toString()) { "Package{$packageName}" } + } + + private fun mockOverlay(increment: Int = 0) = mockThrowOnUnmocked<AndroidPackage> { + whenever(packageName) { "$OVERLAY_PACKAGE_NAME$increment" } + whenever(overlayables) { emptyMap<String, String>() } + whenever(toString()) { "Package{$packageName}" } + } +} diff --git a/services/tests/servicestests/src/com/android/server/pm/AppsFilterTest.java b/services/tests/servicestests/src/com/android/server/pm/AppsFilterTest.java index 4fc625a1e1fb..82bbdcba5bc1 100644 --- a/services/tests/servicestests/src/com/android/server/pm/AppsFilterTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/AppsFilterTest.java @@ -35,6 +35,11 @@ import android.content.pm.parsing.ParsingPackage; import android.os.Build; import android.os.Process; import android.util.ArrayMap; +import android.util.ArraySet; + +import androidx.annotation.NonNull; + +import com.android.server.om.OverlayReferenceMapper; import org.junit.Before; import org.junit.Test; @@ -43,11 +48,18 @@ import org.junit.runners.JUnit4; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + @RunWith(JUnit4.class) public class AppsFilterTest { private static final int DUMMY_CALLING_UID = 10345; private static final int DUMMY_TARGET_UID = 10556; + private static final int DUMMY_ACTOR_UID = 10656; + private static final int DUMMY_OVERLAY_UID = 10756; + private static final int DUMMY_ACTOR_TWO_UID = 10856; @Mock AppsFilter.FeatureConfig mFeatureConfigMock; @@ -117,7 +129,7 @@ public class AppsFilterTest { @Test public void testSystemReadyPropogates() throws Exception { final AppsFilter appsFilter = - new AppsFilter(mFeatureConfigMock, new String[]{}, false); + new AppsFilter(mFeatureConfigMock, new String[]{}, false, null); appsFilter.onSystemReady(); verify(mFeatureConfigMock).onSystemReady(); } @@ -125,7 +137,8 @@ public class AppsFilterTest { @Test public void testQueriesAction_FilterMatches() { final AppsFilter appsFilter = - new AppsFilter(mFeatureConfigMock, new String[]{}, false); + new AppsFilter(mFeatureConfigMock, new String[]{}, false, null); + appsFilter.onSystemReady(); PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package", new IntentFilter("TEST_ACTION")), DUMMY_TARGET_UID); @@ -138,7 +151,8 @@ public class AppsFilterTest { @Test public void testQueriesAction_NoMatchingAction_Filters() { final AppsFilter appsFilter = - new AppsFilter(mFeatureConfigMock, new String[]{}, false); + new AppsFilter(mFeatureConfigMock, new String[]{}, false, null); + appsFilter.onSystemReady(); PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package"), DUMMY_TARGET_UID); @@ -151,7 +165,8 @@ public class AppsFilterTest { @Test public void testQueriesAction_NoMatchingActionFilterLowSdk_DoesntFilter() { final AppsFilter appsFilter = - new AppsFilter(mFeatureConfigMock, new String[]{}, false); + new AppsFilter(mFeatureConfigMock, new String[]{}, false, null); + appsFilter.onSystemReady(); PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package"), DUMMY_TARGET_UID); @@ -169,7 +184,8 @@ public class AppsFilterTest { @Test public void testNoQueries_Filters() { final AppsFilter appsFilter = - new AppsFilter(mFeatureConfigMock, new String[]{}, false); + new AppsFilter(mFeatureConfigMock, new String[]{}, false, null); + appsFilter.onSystemReady(); PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package"), DUMMY_TARGET_UID); @@ -182,7 +198,8 @@ public class AppsFilterTest { @Test public void testForceQueryable_DoesntFilter() { final AppsFilter appsFilter = - new AppsFilter(mFeatureConfigMock, new String[]{}, false); + new AppsFilter(mFeatureConfigMock, new String[]{}, false, null); + appsFilter.onSystemReady(); PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package").setForceQueryable(true), DUMMY_TARGET_UID); @@ -195,7 +212,8 @@ public class AppsFilterTest { @Test public void testForceQueryableByDevice_SystemCaller_DoesntFilter() { final AppsFilter appsFilter = - new AppsFilter(mFeatureConfigMock, new String[]{"com.some.package"}, false); + new AppsFilter(mFeatureConfigMock, new String[]{"com.some.package"}, false, null); + appsFilter.onSystemReady(); PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package"), DUMMY_TARGET_UID, @@ -209,7 +227,8 @@ public class AppsFilterTest { @Test public void testForceQueryableByDevice_NonSystemCaller_Filters() { final AppsFilter appsFilter = - new AppsFilter(mFeatureConfigMock, new String[]{"com.some.package"}, false); + new AppsFilter(mFeatureConfigMock, new String[]{"com.some.package"}, false, null); + appsFilter.onSystemReady(); PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package"), DUMMY_TARGET_UID); @@ -224,7 +243,8 @@ public class AppsFilterTest { public void testSystemQueryable_DoesntFilter() { final AppsFilter appsFilter = new AppsFilter(mFeatureConfigMock, new String[]{}, - true /* system force queryable */); + true /* system force queryable */, null); + appsFilter.onSystemReady(); PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package"), DUMMY_TARGET_UID, @@ -238,7 +258,8 @@ public class AppsFilterTest { @Test public void testQueriesPackage_DoesntFilter() { final AppsFilter appsFilter = - new AppsFilter(mFeatureConfigMock, new String[]{}, false); + new AppsFilter(mFeatureConfigMock, new String[]{}, false, null); + appsFilter.onSystemReady(); PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package"), DUMMY_TARGET_UID); @@ -253,7 +274,8 @@ public class AppsFilterTest { when(mFeatureConfigMock.packageIsEnabled(any(AndroidPackage.class))) .thenReturn(false); final AppsFilter appsFilter = - new AppsFilter(mFeatureConfigMock, new String[]{}, false); + new AppsFilter(mFeatureConfigMock, new String[]{}, false, null); + appsFilter.onSystemReady(); PackageSetting target = simulateAddPackage( appsFilter, pkg("com.some.package"), DUMMY_TARGET_UID); @@ -266,20 +288,22 @@ public class AppsFilterTest { @Test public void testSystemUid_DoesntFilter() { final AppsFilter appsFilter = - new AppsFilter(mFeatureConfigMock, new String[]{}, false); + new AppsFilter(mFeatureConfigMock, new String[]{}, false, null); + appsFilter.onSystemReady(); PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package"), DUMMY_TARGET_UID); assertFalse(appsFilter.shouldFilterApplication(0, null, target, 0)); - assertFalse(appsFilter.shouldFilterApplication( - Process.FIRST_APPLICATION_UID - 1, null, target, 0)); + assertFalse(appsFilter.shouldFilterApplication(Process.FIRST_APPLICATION_UID - 1, + null, target, 0)); } @Test public void testNonSystemUid_NoCallingSetting_Filters() { final AppsFilter appsFilter = - new AppsFilter(mFeatureConfigMock, new String[]{}, false); + new AppsFilter(mFeatureConfigMock, new String[]{}, false, null); + appsFilter.onSystemReady(); PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package"), DUMMY_TARGET_UID); @@ -290,7 +314,8 @@ public class AppsFilterTest { @Test public void testNoTargetPackage_filters() { final AppsFilter appsFilter = - new AppsFilter(mFeatureConfigMock, new String[]{}, false); + new AppsFilter(mFeatureConfigMock, new String[]{}, false, null); + appsFilter.onSystemReady(); PackageSetting target = new PackageSettingBuilder() .setName("com.some.package") @@ -304,6 +329,127 @@ public class AppsFilterTest { assertTrue(appsFilter.shouldFilterApplication(DUMMY_CALLING_UID, calling, target, 0)); } + @Test + public void testActsOnTargetOfOverlay() { + final String actorName = "overlay://test/actorName"; + + ParsingPackage target = pkg("com.some.package.target") + .addOverlayable("overlayableName", actorName); + ParsingPackage overlay = pkg("com.some.package.overlay") + .setIsOverlay(true) + .setOverlayTarget(target.getPackageName()) + .setOverlayTargetName("overlayableName"); + ParsingPackage actor = pkg("com.some.package.actor"); + + final AppsFilter appsFilter = new AppsFilter(mFeatureConfigMock, new String[]{}, false, + new OverlayReferenceMapper.Provider() { + @Nullable + @Override + public String getActorPkg(String actorString) { + if (actorName.equals(actorString)) { + return actor.getPackageName(); + } + return null; + } + + @NonNull + @Override + public Map<String, Set<String>> getTargetToOverlayables( + @NonNull AndroidPackage pkg) { + if (overlay.getPackageName().equals(pkg.getPackageName())) { + Map<String, Set<String>> map = new ArrayMap<>(); + Set<String> set = new ArraySet<>(); + set.add(overlay.getOverlayTargetName()); + map.put(overlay.getOverlayTarget(), set); + return map; + } + return Collections.emptyMap(); + } + }); + appsFilter.onSystemReady(); + + PackageSetting targetSetting = simulateAddPackage(appsFilter, target, DUMMY_TARGET_UID); + PackageSetting overlaySetting = simulateAddPackage(appsFilter, overlay, DUMMY_OVERLAY_UID); + PackageSetting actorSetting = simulateAddPackage(appsFilter, actor, DUMMY_ACTOR_UID); + + // Actor can see both target and overlay + assertFalse(appsFilter.shouldFilterApplication(DUMMY_ACTOR_UID, actorSetting, + targetSetting, 0)); + assertFalse(appsFilter.shouldFilterApplication(DUMMY_ACTOR_UID, actorSetting, + overlaySetting, 0)); + + // But target/overlay can't see each other + assertTrue(appsFilter.shouldFilterApplication(DUMMY_TARGET_UID, targetSetting, + overlaySetting, 0)); + assertTrue(appsFilter.shouldFilterApplication(DUMMY_OVERLAY_UID, overlaySetting, + targetSetting, 0)); + + // And can't see the actor + assertTrue(appsFilter.shouldFilterApplication(DUMMY_TARGET_UID, targetSetting, + actorSetting, 0)); + assertTrue(appsFilter.shouldFilterApplication(DUMMY_OVERLAY_UID, overlaySetting, + actorSetting, 0)); + } + + @Test + public void testActsOnTargetOfOverlayThroughSharedUser() { + final String actorName = "overlay://test/actorName"; + + ParsingPackage target = pkg("com.some.package.target") + .addOverlayable("overlayableName", actorName); + ParsingPackage overlay = pkg("com.some.package.overlay") + .setIsOverlay(true) + .setOverlayTarget(target.getPackageName()) + .setOverlayTargetName("overlayableName"); + ParsingPackage actorOne = pkg("com.some.package.actor.one"); + ParsingPackage actorTwo = pkg("com.some.package.actor.two"); + + final AppsFilter appsFilter = new AppsFilter(mFeatureConfigMock, new String[]{}, false, + new OverlayReferenceMapper.Provider() { + @Nullable + @Override + public String getActorPkg(String actorString) { + // Only actorOne is mapped as a valid actor + if (actorName.equals(actorString)) { + return actorOne.getPackageName(); + } + return null; + } + + @NonNull + @Override + public Map<String, Set<String>> getTargetToOverlayables( + @NonNull AndroidPackage pkg) { + if (overlay.getPackageName().equals(pkg.getPackageName())) { + Map<String, Set<String>> map = new ArrayMap<>(); + Set<String> set = new ArraySet<>(); + set.add(overlay.getOverlayTargetName()); + map.put(overlay.getOverlayTarget(), set); + return map; + } + return Collections.emptyMap(); + } + }); + appsFilter.onSystemReady(); + + PackageSetting targetSetting = simulateAddPackage(appsFilter, target, DUMMY_TARGET_UID); + PackageSetting overlaySetting = simulateAddPackage(appsFilter, overlay, DUMMY_OVERLAY_UID); + PackageSetting actorOneSetting = simulateAddPackage(appsFilter, actorOne, DUMMY_ACTOR_UID); + PackageSetting actorTwoSetting = simulateAddPackage(appsFilter, actorTwo, + DUMMY_ACTOR_TWO_UID); + + SharedUserSetting actorSharedSetting = new SharedUserSetting("actorSharedUser", + actorOneSetting.pkgFlags, actorOneSetting.pkgPrivateFlags); + actorSharedSetting.addPackage(actorOneSetting); + actorSharedSetting.addPackage(actorTwoSetting); + + // actorTwo can see both target and overlay + assertFalse(appsFilter.shouldFilterApplication(DUMMY_ACTOR_TWO_UID, actorSharedSetting, + targetSetting, 0)); + assertFalse(appsFilter.shouldFilterApplication(DUMMY_ACTOR_TWO_UID, actorSharedSetting, + overlaySetting, 0)); + } + private interface WithSettingBuilder { PackageSettingBuilder withBuilder(PackageSettingBuilder builder); } diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageSettingBuilder.java b/services/tests/servicestests/src/com/android/server/pm/PackageSettingBuilder.java index 5baeedefef05..2473997a61c9 100644 --- a/services/tests/servicestests/src/com/android/server/pm/PackageSettingBuilder.java +++ b/services/tests/servicestests/src/com/android/server/pm/PackageSettingBuilder.java @@ -22,7 +22,7 @@ import android.util.SparseArray; import java.io.File; -class PackageSettingBuilder { +public class PackageSettingBuilder { private String mName; private String mRealName; private String mCodePath; diff --git a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/ConversionUtilTest.java b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/ConversionUtilTest.java new file mode 100644 index 000000000000..5a2ce4540b82 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/ConversionUtilTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import static org.junit.Assert.assertEquals; + +import android.hardware.audio.common.V2_0.Uuid; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class ConversionUtilTest { + private static final String TAG = "ConversionUtilTest"; + + @Test + public void testUuidRoundTrip() { + Uuid hidl = new Uuid(); + hidl.timeLow = 0xFEDCBA98; + hidl.timeMid = (short) 0xEDCB; + hidl.versionAndTimeHigh = (short) 0xDCBA; + hidl.variantAndClockSeqHigh = (short) 0xCBA9; + hidl.node = new byte[] { 0x11, 0x12, 0x13, 0x14, 0x15, 0x16 }; + + String aidl = ConversionUtil.hidl2aidlUuid(hidl); + assertEquals("fedcba98-edcb-dcba-cba9-111213141516", aidl); + + Uuid reconstructed = ConversionUtil.aidl2hidlUuid(aidl); + assertEquals(hidl, reconstructed); + } +} diff --git a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImplTest.java b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImplTest.java new file mode 100644 index 000000000000..82f32f88d3a2 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImplTest.java @@ -0,0 +1,1306 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.audio.common.V2_0.AudioConfig; +import android.hardware.audio.common.V2_0.Uuid; +import android.hardware.soundtrigger.V2_3.OptionalModelParameterRange; +import android.media.audio.common.AudioChannelMask; +import android.media.audio.common.AudioFormat; +import android.media.soundtrigger_middleware.ConfidenceLevel; +import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.ISoundTriggerModule; +import android.media.soundtrigger_middleware.ModelParameter; +import android.media.soundtrigger_middleware.ModelParameterRange; +import android.media.soundtrigger_middleware.Phrase; +import android.media.soundtrigger_middleware.PhraseRecognitionEvent; +import android.media.soundtrigger_middleware.PhraseRecognitionExtra; +import android.media.soundtrigger_middleware.PhraseSoundModel; +import android.media.soundtrigger_middleware.RecognitionConfig; +import android.media.soundtrigger_middleware.RecognitionEvent; +import android.media.soundtrigger_middleware.RecognitionMode; +import android.media.soundtrigger_middleware.RecognitionStatus; +import android.media.soundtrigger_middleware.SoundModel; +import android.media.soundtrigger_middleware.SoundModelType; +import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; +import android.media.soundtrigger_middleware.SoundTriggerModuleProperties; +import android.os.HidlMemoryUtil; +import android.os.HwParcel; +import android.os.IHwBinder; +import android.os.IHwInterface; +import android.os.RemoteException; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +@RunWith(Parameterized.class) +public class SoundTriggerMiddlewareImplTest { + private static final String TAG = "SoundTriggerMiddlewareImplTest"; + + // We run the test once for every version of the underlying driver. + @Parameterized.Parameters + public static Object[] data() { + return new Object[]{ + mock(android.hardware.soundtrigger.V2_0.ISoundTriggerHw.class), + mock(android.hardware.soundtrigger.V2_1.ISoundTriggerHw.class), + mock(android.hardware.soundtrigger.V2_2.ISoundTriggerHw.class), + mock(android.hardware.soundtrigger.V2_3.ISoundTriggerHw.class), + }; + } + + @Mock + @Parameterized.Parameter + public android.hardware.soundtrigger.V2_0.ISoundTriggerHw mHalDriver; + + @Mock + private SoundTriggerMiddlewareImpl.AudioSessionProvider mAudioSessionProvider = mock( + SoundTriggerMiddlewareImpl.AudioSessionProvider.class); + + private SoundTriggerMiddlewareImpl mService; + + private static ISoundTriggerCallback createCallbackMock() { + return mock(ISoundTriggerCallback.Stub.class, Mockito.CALLS_REAL_METHODS); + } + + private static SoundModel createGenericSoundModel() { + return createSoundModel(SoundModelType.GENERIC); + } + + private static SoundModel createSoundModel(int type) { + SoundModel model = new SoundModel(); + model.type = type; + model.uuid = "12345678-2345-3456-4567-abcdef987654"; + model.vendorUuid = "87654321-5432-6543-7654-456789fedcba"; + model.data = new byte[]{91, 92, 93, 94, 95}; + return model; + } + + private static PhraseSoundModel createPhraseSoundModel() { + PhraseSoundModel model = new PhraseSoundModel(); + model.common = createSoundModel(SoundModelType.KEYPHRASE); + model.phrases = new Phrase[1]; + model.phrases[0] = new Phrase(); + model.phrases[0].id = 123; + model.phrases[0].users = new int[]{5, 6, 7}; + model.phrases[0].locale = "locale"; + model.phrases[0].text = "text"; + model.phrases[0].recognitionModes = + RecognitionMode.USER_AUTHENTICATION | RecognitionMode.USER_IDENTIFICATION; + return model; + } + + private static android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties createDefaultProperties( + boolean supportConcurrentCapture) { + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties properties = + new android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties(); + properties.implementor = "implementor"; + properties.description = "description"; + properties.version = 123; + properties.uuid = new Uuid(); + properties.uuid.timeLow = 1; + properties.uuid.timeMid = 2; + properties.uuid.versionAndTimeHigh = 3; + properties.uuid.variantAndClockSeqHigh = 4; + properties.uuid.node = new byte[]{5, 6, 7, 8, 9, 10}; + + properties.maxSoundModels = 456; + properties.maxKeyPhrases = 567; + properties.maxUsers = 678; + properties.recognitionModes = 789; + properties.captureTransition = true; + properties.maxBufferMs = 321; + properties.concurrentCapture = supportConcurrentCapture; + properties.triggerInEvent = true; + properties.powerConsumptionMw = 432; + return properties; + } + + private static void validateDefaultProperties(SoundTriggerModuleProperties properties, + boolean supportConcurrentCapture) { + assertEquals("implementor", properties.implementor); + assertEquals("description", properties.description); + assertEquals(123, properties.version); + assertEquals("00000001-0002-0003-0004-05060708090a", properties.uuid); + assertEquals(456, properties.maxSoundModels); + assertEquals(567, properties.maxKeyPhrases); + assertEquals(678, properties.maxUsers); + assertEquals(789, properties.recognitionModes); + assertTrue(properties.captureTransition); + assertEquals(321, properties.maxBufferMs); + assertEquals(supportConcurrentCapture, properties.concurrentCapture); + assertTrue(properties.triggerInEvent); + assertEquals(432, properties.powerConsumptionMw); + } + + + private static android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent createRecognitionEvent_2_0( + int hwHandle, + int status) { + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent halEvent = + new android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent(); + halEvent.status = status; + halEvent.type = SoundModelType.GENERIC; + halEvent.model = hwHandle; + halEvent.captureAvailable = true; + // This field is ignored. + halEvent.captureSession = 123; + halEvent.captureDelayMs = 234; + halEvent.capturePreambleMs = 345; + halEvent.triggerInData = true; + halEvent.audioConfig = new AudioConfig(); + halEvent.audioConfig.sampleRateHz = 456; + halEvent.audioConfig.channelMask = AudioChannelMask.IN_LEFT; + halEvent.audioConfig.format = AudioFormat.MP3; + // hwEvent.audioConfig.offloadInfo is irrelevant. + halEvent.data.add((byte) 31); + halEvent.data.add((byte) 32); + halEvent.data.add((byte) 33); + return halEvent; + } + + private static android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent createRecognitionEvent_2_1( + int hwHandle, + int status) { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent halEvent = + new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent(); + halEvent.header = createRecognitionEvent_2_0(hwHandle, status); + halEvent.header.data.clear(); + halEvent.data = HidlMemoryUtil.byteArrayToHidlMemory(new byte[]{31, 32, 33}); + return halEvent; + } + + private static void validateRecognitionEvent(RecognitionEvent event, int status) { + assertEquals(status, event.status); + assertEquals(SoundModelType.GENERIC, event.type); + assertTrue(event.captureAvailable); + assertEquals(101, event.captureSession); + assertEquals(234, event.captureDelayMs); + assertEquals(345, event.capturePreambleMs); + assertTrue(event.triggerInData); + assertEquals(456, event.audioConfig.sampleRateHz); + assertEquals(AudioChannelMask.IN_LEFT, event.audioConfig.channelMask); + assertEquals(AudioFormat.MP3, event.audioConfig.format); + } + + private static android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent createPhraseRecognitionEvent_2_0( + int hwHandle, int status) { + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent halEvent = + new android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent(); + halEvent.common = createRecognitionEvent_2_0(hwHandle, status); + + android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra halExtra = + new android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra(); + halExtra.id = 123; + halExtra.confidenceLevel = 52; + halExtra.recognitionModes = android.hardware.soundtrigger.V2_0.RecognitionMode.VOICE_TRIGGER + | android.hardware.soundtrigger.V2_0.RecognitionMode.GENERIC_TRIGGER; + android.hardware.soundtrigger.V2_0.ConfidenceLevel halLevel = + new android.hardware.soundtrigger.V2_0.ConfidenceLevel(); + halLevel.userId = 31; + halLevel.levelPercent = 43; + halExtra.levels.add(halLevel); + halEvent.phraseExtras.add(halExtra); + return halEvent; + } + + private static android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent createPhraseRecognitionEvent_2_1( + int hwHandle, int status) { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent halEvent = + new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent(); + halEvent.common = createRecognitionEvent_2_1(hwHandle, status); + + android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra halExtra = + new android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra(); + halExtra.id = 123; + halExtra.confidenceLevel = 52; + halExtra.recognitionModes = android.hardware.soundtrigger.V2_0.RecognitionMode.VOICE_TRIGGER + | android.hardware.soundtrigger.V2_0.RecognitionMode.GENERIC_TRIGGER; + android.hardware.soundtrigger.V2_0.ConfidenceLevel halLevel = + new android.hardware.soundtrigger.V2_0.ConfidenceLevel(); + halLevel.userId = 31; + halLevel.levelPercent = 43; + halExtra.levels.add(halLevel); + halEvent.phraseExtras.add(halExtra); + return halEvent; + } + + private static void validatePhraseRecognitionEvent(PhraseRecognitionEvent event, int status) { + validateRecognitionEvent(event.common, status); + + assertEquals(1, event.phraseExtras.length); + assertEquals(123, event.phraseExtras[0].id); + assertEquals(52, event.phraseExtras[0].confidenceLevel); + assertEquals(RecognitionMode.VOICE_TRIGGER | RecognitionMode.GENERIC_TRIGGER, + event.phraseExtras[0].recognitionModes); + assertEquals(1, event.phraseExtras[0].levels.length); + assertEquals(31, event.phraseExtras[0].levels[0].userId); + assertEquals(43, event.phraseExtras[0].levels[0].levelPercent); + } + + private void initService(boolean supportConcurrentCapture) throws RemoteException { + doAnswer(invocation -> { + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties properties = + createDefaultProperties( + supportConcurrentCapture); + ((android.hardware.soundtrigger.V2_0.ISoundTriggerHw.getPropertiesCallback) invocation.getArgument( + 0)).onValues(0, + properties); + return null; + }).when(mHalDriver).getProperties(any()); + mService = new SoundTriggerMiddlewareImpl(mHalDriver, mAudioSessionProvider); + } + + private int loadGenericModel_2_0(ISoundTriggerModule module, int hwHandle) + throws RemoteException { + SoundModel model = createGenericSoundModel(); + ArgumentCaptor<android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel> modelCaptor = + ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel.class); + doAnswer(invocation -> { + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback callback = + invocation.getArgument(1); + int callbackCookie = invocation.getArgument(2); + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.loadSoundModelCallback + resultCallback = invocation.getArgument(3); + + // This is the return of this method. + resultCallback.onValues(0, hwHandle); + + // This is the async mCallback that comes after. + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent modelEvent = + new android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent(); + modelEvent.status = + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.SoundModelStatus.UPDATED; + modelEvent.model = hwHandle; + callback.soundModelCallback(modelEvent, callbackCookie); + return null; + }).when(mHalDriver).loadSoundModel(modelCaptor.capture(), any(), anyInt(), any()); + + when(mAudioSessionProvider.acquireSession()).thenReturn( + new SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession(101, 102, 103)); + + int handle = module.loadModel(model); + verify(mHalDriver).loadSoundModel(any(), any(), anyInt(), any()); + verify(mAudioSessionProvider).acquireSession(); + + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel hidlModel = + modelCaptor.getValue(); + assertEquals(android.hardware.soundtrigger.V2_0.SoundModelType.GENERIC, + hidlModel.type); + assertEquals(model.uuid, ConversionUtil.hidl2aidlUuid(hidlModel.uuid)); + assertEquals(model.vendorUuid, ConversionUtil.hidl2aidlUuid(hidlModel.vendorUuid)); + assertArrayEquals(new Byte[]{91, 92, 93, 94, 95}, hidlModel.data.toArray()); + + return handle; + } + + private int loadGenericModel_2_1(ISoundTriggerModule module, int hwHandle) + throws RemoteException { + android.hardware.soundtrigger.V2_1.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_1.ISoundTriggerHw) mHalDriver; + SoundModel model = createGenericSoundModel(); + ArgumentCaptor<android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel> modelCaptor = + ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel.class); + doAnswer(invocation -> { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback callback = + invocation.getArgument(1); + int callbackCookie = invocation.getArgument(2); + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.loadSoundModel_2_1Callback + resultCallback = invocation.getArgument(3); + + // This is the return of this method. + resultCallback.onValues(0, hwHandle); + + // This is the async mCallback that comes after. + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent modelEvent = + new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent(); + modelEvent.header.status = + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.SoundModelStatus.UPDATED; + modelEvent.header.model = hwHandle; + callback.soundModelCallback_2_1(modelEvent, callbackCookie); + return null; + }).when(driver).loadSoundModel_2_1(modelCaptor.capture(), any(), anyInt(), any()); + + when(mAudioSessionProvider.acquireSession()).thenReturn( + new SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession(101, 102, 103)); + + int handle = module.loadModel(model); + verify(driver).loadSoundModel_2_1(any(), any(), anyInt(), any()); + verify(mAudioSessionProvider).acquireSession(); + + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel hidlModel = + modelCaptor.getValue(); + assertEquals(android.hardware.soundtrigger.V2_0.SoundModelType.GENERIC, + hidlModel.header.type); + assertEquals(model.uuid, ConversionUtil.hidl2aidlUuid(hidlModel.header.uuid)); + assertEquals(model.vendorUuid, ConversionUtil.hidl2aidlUuid(hidlModel.header.vendorUuid)); + assertArrayEquals(new byte[]{91, 92, 93, 94, 95}, + HidlMemoryUtil.hidlMemoryToByteArray(hidlModel.data)); + + return handle; + } + + private int loadGenericModel(ISoundTriggerModule module, int hwHandle) throws RemoteException { + if (mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw) { + return loadGenericModel_2_1(module, hwHandle); + } else { + return loadGenericModel_2_0(module, hwHandle); + } + } + + private int loadPhraseModel_2_0(ISoundTriggerModule module, int hwHandle) + throws RemoteException { + PhraseSoundModel model = createPhraseSoundModel(); + ArgumentCaptor<android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel> + modelCaptor = ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel.class); + doAnswer(invocation -> { + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback callback = + invocation.getArgument( + 1); + int callbackCookie = invocation.getArgument(2); + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.loadPhraseSoundModelCallback + resultCallback = + invocation.getArgument( + 3); + + // This is the return of this method. + resultCallback.onValues(0, hwHandle); + + // This is the async mCallback that comes after. + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent modelEvent = + new android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent(); + modelEvent.status = + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.SoundModelStatus.UPDATED; + modelEvent.model = hwHandle; + callback.soundModelCallback(modelEvent, callbackCookie); + return null; + }).when(mHalDriver).loadPhraseSoundModel(modelCaptor.capture(), any(), anyInt(), any()); + + when(mAudioSessionProvider.acquireSession()).thenReturn( + new SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession(101, 102, 103)); + + int handle = module.loadPhraseModel(model); + verify(mHalDriver).loadPhraseSoundModel(any(), any(), anyInt(), any()); + verify(mAudioSessionProvider).acquireSession(); + + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel hidlModel = + modelCaptor.getValue(); + + // Validate common part. + assertEquals(android.hardware.soundtrigger.V2_0.SoundModelType.KEYPHRASE, + hidlModel.common.type); + assertEquals(model.common.uuid, ConversionUtil.hidl2aidlUuid(hidlModel.common.uuid)); + assertEquals(model.common.vendorUuid, + ConversionUtil.hidl2aidlUuid(hidlModel.common.vendorUuid)); + assertArrayEquals(new Byte[]{91, 92, 93, 94, 95}, hidlModel.common.data.toArray()); + + // Validate phrase part. + assertEquals(1, hidlModel.phrases.size()); + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Phrase hidlPhrase = + hidlModel.phrases.get(0); + assertEquals(123, hidlPhrase.id); + assertArrayEquals(new Integer[]{5, 6, 7}, hidlPhrase.users.toArray()); + assertEquals("locale", hidlPhrase.locale); + assertEquals("text", hidlPhrase.text); + assertEquals(android.hardware.soundtrigger.V2_0.RecognitionMode.USER_AUTHENTICATION + | android.hardware.soundtrigger.V2_0.RecognitionMode.USER_IDENTIFICATION, + hidlPhrase.recognitionModes); + + return handle; + } + + private int loadPhraseModel_2_1(ISoundTriggerModule module, int hwHandle) + throws RemoteException { + android.hardware.soundtrigger.V2_1.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_1.ISoundTriggerHw) mHalDriver; + + PhraseSoundModel model = createPhraseSoundModel(); + ArgumentCaptor<android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel> + modelCaptor = ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel.class); + doAnswer(invocation -> { + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback callback = + invocation.getArgument( + 1); + int callbackCookie = invocation.getArgument(2); + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.loadPhraseSoundModel_2_1Callback + resultCallback = + invocation.getArgument( + 3); + + // This is the return of this method. + resultCallback.onValues(0, hwHandle); + + // This is the async mCallback that comes after. + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent modelEvent = + new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent(); + modelEvent.header.status = + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.SoundModelStatus.UPDATED; + modelEvent.header.model = hwHandle; + callback.soundModelCallback_2_1(modelEvent, callbackCookie); + return null; + }).when(driver).loadPhraseSoundModel_2_1(modelCaptor.capture(), any(), anyInt(), any()); + + when(mAudioSessionProvider.acquireSession()).thenReturn( + new SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession(101, 102, 103)); + + int handle = module.loadPhraseModel(model); + verify(driver).loadPhraseSoundModel_2_1(any(), any(), anyInt(), any()); + verify(mAudioSessionProvider).acquireSession(); + + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel hidlModel = + modelCaptor.getValue(); + + // Validate common part. + assertEquals(android.hardware.soundtrigger.V2_0.SoundModelType.KEYPHRASE, + hidlModel.common.header.type); + assertEquals(model.common.uuid, ConversionUtil.hidl2aidlUuid(hidlModel.common.header.uuid)); + assertEquals(model.common.vendorUuid, + ConversionUtil.hidl2aidlUuid(hidlModel.common.header.vendorUuid)); + assertArrayEquals(new byte[]{91, 92, 93, 94, 95}, + HidlMemoryUtil.hidlMemoryToByteArray(hidlModel.common.data)); + + // Validate phrase part. + assertEquals(1, hidlModel.phrases.size()); + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.Phrase hidlPhrase = + hidlModel.phrases.get(0); + assertEquals(123, hidlPhrase.id); + assertArrayEquals(new Integer[]{5, 6, 7}, hidlPhrase.users.toArray()); + assertEquals("locale", hidlPhrase.locale); + assertEquals("text", hidlPhrase.text); + assertEquals(android.hardware.soundtrigger.V2_0.RecognitionMode.USER_AUTHENTICATION + | android.hardware.soundtrigger.V2_0.RecognitionMode.USER_IDENTIFICATION, + hidlPhrase.recognitionModes); + + return handle; + } + + private int loadPhraseModel(ISoundTriggerModule module, int hwHandle) throws RemoteException { + if (mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw) { + return loadPhraseModel_2_1(module, hwHandle); + } else { + return loadPhraseModel_2_0(module, hwHandle); + } + } + + private void unloadModel(ISoundTriggerModule module, int handle, int hwHandle) + throws RemoteException { + module.unloadModel(handle); + verify(mHalDriver).unloadSoundModel(hwHandle); + verify(mAudioSessionProvider).releaseSession(101); + } + + private SoundTriggerHwCallback startRecognition_2_0(ISoundTriggerModule module, int handle, + int hwHandle) throws RemoteException { + ArgumentCaptor<android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig> + configCaptor = ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig.class); + ArgumentCaptor<android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback> callbackCaptor = + ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.class); + ArgumentCaptor<Integer> cookieCaptor = ArgumentCaptor.forClass(Integer.class); + + when(mHalDriver.startRecognition(eq(hwHandle), configCaptor.capture(), + callbackCaptor.capture(), cookieCaptor.capture())).thenReturn(0); + + RecognitionConfig config = createRecognitionConfig(); + + module.startRecognition(handle, config); + verify(mHalDriver).startRecognition(eq(hwHandle), any(), any(), anyInt()); + + android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig halConfig = + configCaptor.getValue(); + assertTrue(halConfig.captureRequested); + assertEquals(102, halConfig.captureHandle); + assertEquals(103, halConfig.captureDevice); + assertEquals(1, halConfig.phrases.size()); + android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra halPhraseExtra = + halConfig.phrases.get(0); + assertEquals(123, halPhraseExtra.id); + assertEquals(4, halPhraseExtra.confidenceLevel); + assertEquals(5, halPhraseExtra.recognitionModes); + assertEquals(1, halPhraseExtra.levels.size()); + android.hardware.soundtrigger.V2_0.ConfidenceLevel halLevel = halPhraseExtra.levels.get(0); + assertEquals(234, halLevel.userId); + assertEquals(34, halLevel.levelPercent); + assertArrayEquals(new Byte[]{5, 4, 3, 2, 1}, halConfig.data.toArray()); + + return new SoundTriggerHwCallback(callbackCaptor.getValue(), cookieCaptor.getValue()); + } + + private SoundTriggerHwCallback startRecognition_2_1(ISoundTriggerModule module, int handle, + int hwHandle) throws RemoteException { + android.hardware.soundtrigger.V2_1.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_1.ISoundTriggerHw) mHalDriver; + + ArgumentCaptor<android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig> + configCaptor = ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig.class); + ArgumentCaptor<android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback> callbackCaptor = + ArgumentCaptor.forClass( + android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.class); + ArgumentCaptor<Integer> cookieCaptor = ArgumentCaptor.forClass(Integer.class); + + when(driver.startRecognition_2_1(eq(hwHandle), configCaptor.capture(), + callbackCaptor.capture(), cookieCaptor.capture())).thenReturn(0); + + RecognitionConfig config = createRecognitionConfig(); + + module.startRecognition(handle, config); + verify(driver).startRecognition_2_1(eq(hwHandle), any(), any(), anyInt()); + + android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig halConfig = + configCaptor.getValue(); + assertTrue(halConfig.header.captureRequested); + assertEquals(102, halConfig.header.captureHandle); + assertEquals(103, halConfig.header.captureDevice); + assertEquals(1, halConfig.header.phrases.size()); + android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra halPhraseExtra = + halConfig.header.phrases.get(0); + assertEquals(123, halPhraseExtra.id); + assertEquals(4, halPhraseExtra.confidenceLevel); + assertEquals(5, halPhraseExtra.recognitionModes); + assertEquals(1, halPhraseExtra.levels.size()); + android.hardware.soundtrigger.V2_0.ConfidenceLevel halLevel = halPhraseExtra.levels.get(0); + assertEquals(234, halLevel.userId); + assertEquals(34, halLevel.levelPercent); + assertArrayEquals(new byte[]{5, 4, 3, 2, 1}, + HidlMemoryUtil.hidlMemoryToByteArray(halConfig.data)); + + return new SoundTriggerHwCallback(callbackCaptor.getValue(), cookieCaptor.getValue()); + } + + private SoundTriggerHwCallback startRecognition(ISoundTriggerModule module, int handle, + int hwHandle) throws RemoteException { + if (mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw) { + return startRecognition_2_1(module, handle, hwHandle); + } else { + return startRecognition_2_0(module, handle, hwHandle); + } + } + + private RecognitionConfig createRecognitionConfig() { + RecognitionConfig config = new RecognitionConfig(); + config.captureRequested = true; + config.phraseRecognitionExtras = new PhraseRecognitionExtra[]{new PhraseRecognitionExtra()}; + config.phraseRecognitionExtras[0].id = 123; + config.phraseRecognitionExtras[0].confidenceLevel = 4; + config.phraseRecognitionExtras[0].recognitionModes = 5; + config.phraseRecognitionExtras[0].levels = new ConfidenceLevel[]{new ConfidenceLevel()}; + config.phraseRecognitionExtras[0].levels[0].userId = 234; + config.phraseRecognitionExtras[0].levels[0].levelPercent = 34; + config.data = new byte[]{5, 4, 3, 2, 1}; + return config; + } + + private void stopRecognition(ISoundTriggerModule module, int handle, int hwHandle) + throws RemoteException { + when(mHalDriver.stopRecognition(hwHandle)).thenReturn(0); + module.stopRecognition(handle); + verify(mHalDriver).stopRecognition(hwHandle); + } + + private void verifyNotStartRecognition() throws RemoteException { + verify(mHalDriver, never()).startRecognition(anyInt(), any(), any(), anyInt()); + if (mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw) { + verify((android.hardware.soundtrigger.V2_1.ISoundTriggerHw) mHalDriver, + never()).startRecognition_2_1(anyInt(), any(), any(), anyInt()); + } + } + + + @Before + public void setUp() throws Exception { + clearInvocations(mHalDriver); + clearInvocations(mAudioSessionProvider); + + // This binder is associated with the mock, so it can be cast to either version of the + // HAL interface. + final IHwBinder binder = new IHwBinder() { + @Override + public void transact(int code, HwParcel request, HwParcel reply, int flags) + throws RemoteException { + // This is a little hacky, but a very easy way to gracefully reject a request for + // an unsupported interface (after queryLocalInterface() returns null, the client + // will attempt a remote transaction to obtain the interface. RemoteException will + // cause it to give up). + throw new RemoteException(); + } + + @Override + public IHwInterface queryLocalInterface(String descriptor) { + if (descriptor.equals("android.hardware.soundtrigger@2.0::ISoundTriggerHw") + || descriptor.equals("android.hardware.soundtrigger@2.1::ISoundTriggerHw") + && mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw + || descriptor.equals("android.hardware.soundtrigger@2.2::ISoundTriggerHw") + && mHalDriver instanceof android.hardware.soundtrigger.V2_2.ISoundTriggerHw + || descriptor.equals("android.hardware.soundtrigger@2.3::ISoundTriggerHw") + && mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw) { + return mHalDriver; + } + return null; + } + + @Override + public boolean linkToDeath(DeathRecipient recipient, long cookie) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean unlinkToDeath(DeathRecipient recipient) { + throw new UnsupportedOperationException(); + } + }; + + when(mHalDriver.asBinder()).thenReturn(binder); + } + + @Test + public void testSetUpAndTearDown() { + } + + @Test + public void testListModules() throws Exception { + initService(true); + // Note: input and output properties are NOT the same type, even though they are in any way + // equivalent. One is a type that's exposed by the HAL and one is a type that's exposed by + // the service. The service actually performs a (trivial) conversion between the two. + SoundTriggerModuleDescriptor[] allDescriptors = mService.listModules(); + assertEquals(1, allDescriptors.length); + + SoundTriggerModuleProperties properties = allDescriptors[0].properties; + + validateDefaultProperties(properties, true); + } + + @Test + public void testAttachDetach() throws Exception { + // Normal attachment / detachment. + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + verify(callback).onRecognitionAvailabilityChange(true); + assertNotNull(module); + module.detach(); + } + + @Test + public void testAttachDetachNotAvailable() throws Exception { + // Attachment / detachment during external capture, with a module not supporting concurrent + // capture. + initService(false); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + verify(callback).onRecognitionAvailabilityChange(false); + assertNotNull(module); + module.detach(); + } + + @Test + public void testAttachDetachAvailable() throws Exception { + // Attachment / detachment during external capture, with a module supporting concurrent + // capture. + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + verify(callback).onRecognitionAvailabilityChange(true); + assertNotNull(module); + module.detach(); + } + + @Test + public void testLoadUnloadModel() throws Exception { + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + final int hwHandle = 7; + int handle = loadGenericModel(module, hwHandle); + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testLoadUnloadPhraseModel() throws Exception { + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + final int hwHandle = 73; + int handle = loadPhraseModel(module, hwHandle); + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testStartStopRecognition() throws Exception { + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + // Load the model. + final int hwHandle = 7; + int handle = loadGenericModel(module, hwHandle); + + // Initiate a recognition. + startRecognition(module, handle, hwHandle); + + // Stop the recognition. + stopRecognition(module, handle, hwHandle); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testStartStopPhraseRecognition() throws Exception { + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + // Load the model. + final int hwHandle = 67; + int handle = loadPhraseModel(module, hwHandle); + + // Initiate a recognition. + startRecognition(module, handle, hwHandle); + + // Stop the recognition. + stopRecognition(module, handle, hwHandle); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testRecognition() throws Exception { + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + // Load the model. + final int hwHandle = 7; + int handle = loadGenericModel(module, hwHandle); + + // Initiate a recognition. + SoundTriggerHwCallback hwCallback = startRecognition(module, handle, hwHandle); + + // Signal a capture from the driver. + hwCallback.sendRecognitionEvent(hwHandle, + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionStatus.SUCCESS); + + ArgumentCaptor<RecognitionEvent> eventCaptor = ArgumentCaptor.forClass( + RecognitionEvent.class); + verify(callback).onRecognition(eq(handle), eventCaptor.capture()); + + // Validate the event. + validateRecognitionEvent(eventCaptor.getValue(), RecognitionStatus.SUCCESS); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testPhraseRecognition() throws Exception { + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + // Load the model. + final int hwHandle = 7; + int handle = loadPhraseModel(module, hwHandle); + + // Initiate a recognition. + SoundTriggerHwCallback hwCallback = startRecognition(module, handle, hwHandle); + + // Signal a capture from the driver. + hwCallback.sendPhraseRecognitionEvent(hwHandle, + android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionStatus.SUCCESS); + + ArgumentCaptor<PhraseRecognitionEvent> eventCaptor = ArgumentCaptor.forClass( + PhraseRecognitionEvent.class); + verify(callback).onPhraseRecognition(eq(handle), eventCaptor.capture()); + + // Validate the event. + validatePhraseRecognitionEvent(eventCaptor.getValue(), RecognitionStatus.SUCCESS); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testForceRecognition() throws Exception { + if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_2.ISoundTriggerHw)) { + return; + } + + android.hardware.soundtrigger.V2_2.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_2.ISoundTriggerHw) mHalDriver; + + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + // Load the model. + final int hwHandle = 17; + int handle = loadGenericModel(module, hwHandle); + + // Initiate a recognition. + SoundTriggerHwCallback hwCallback = startRecognition(module, handle, hwHandle); + + // Force a trigger. + module.forceRecognitionEvent(handle); + verify(driver).getModelState(hwHandle); + + // Signal a capture from the driver. + // '3' means 'forced', there's no constant for that in the HAL. + hwCallback.sendRecognitionEvent(hwHandle, 3); + + ArgumentCaptor<RecognitionEvent> eventCaptor = ArgumentCaptor.forClass( + RecognitionEvent.class); + verify(callback).onRecognition(eq(handle), eventCaptor.capture()); + + // Validate the event. + validateRecognitionEvent(eventCaptor.getValue(), RecognitionStatus.FORCED); + + // Stop the recognition. + stopRecognition(module, handle, hwHandle); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testForcePhraseRecognition() throws Exception { + if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_2.ISoundTriggerHw)) { + return; + } + + android.hardware.soundtrigger.V2_2.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_2.ISoundTriggerHw) mHalDriver; + + initService(true); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + + // Load the model. + final int hwHandle = 17; + int handle = loadPhraseModel(module, hwHandle); + + // Initiate a recognition. + SoundTriggerHwCallback hwCallback = startRecognition(module, handle, hwHandle); + + // Force a trigger. + module.forceRecognitionEvent(handle); + verify(driver).getModelState(hwHandle); + + // Signal a capture from the driver. + // '3' means 'forced', there's no constant for that in the HAL. + hwCallback.sendPhraseRecognitionEvent(hwHandle, 3); + + ArgumentCaptor<PhraseRecognitionEvent> eventCaptor = ArgumentCaptor.forClass( + PhraseRecognitionEvent.class); + verify(callback).onPhraseRecognition(eq(handle), eventCaptor.capture()); + + // Validate the event. + validatePhraseRecognitionEvent(eventCaptor.getValue(), RecognitionStatus.FORCED); + + // Stop the recognition. + stopRecognition(module, handle, hwHandle); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testAbortRecognition() throws Exception { + // Make sure the HAL doesn't support concurrent capture. + initService(false); + mService.setExternalCaptureState(false); + + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + verify(callback).onRecognitionAvailabilityChange(true); + + // Load the model. + final int hwHandle = 11; + int handle = loadGenericModel(module, hwHandle); + + // Initiate a recognition. + startRecognition(module, handle, hwHandle); + + // Abort. + mService.setExternalCaptureState(true); + + ArgumentCaptor<RecognitionEvent> eventCaptor = ArgumentCaptor.forClass( + RecognitionEvent.class); + verify(callback).onRecognition(eq(handle), eventCaptor.capture()); + + // Validate the event. + assertEquals(RecognitionStatus.ABORTED, eventCaptor.getValue().status); + + // Make sure we are notified of the lost availability. + verify(callback).onRecognitionAvailabilityChange(false); + + // Attempt to start a new recognition - should get an abort event immediately, without + // involving the HAL. + clearInvocations(callback); + clearInvocations(mHalDriver); + module.startRecognition(handle, createRecognitionConfig()); + verify(callback).onRecognition(eq(handle), eventCaptor.capture()); + assertEquals(RecognitionStatus.ABORTED, eventCaptor.getValue().status); + verifyNotStartRecognition(); + + // Now enable it and make sure we are notified. + mService.setExternalCaptureState(false); + verify(callback).onRecognitionAvailabilityChange(true); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testAbortPhraseRecognition() throws Exception { + // Make sure the HAL doesn't support concurrent capture. + initService(false); + mService.setExternalCaptureState(false); + + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + verify(callback).onRecognitionAvailabilityChange(true); + + // Load the model. + final int hwHandle = 11; + int handle = loadPhraseModel(module, hwHandle); + + // Initiate a recognition. + startRecognition(module, handle, hwHandle); + + // Abort. + mService.setExternalCaptureState(true); + + ArgumentCaptor<PhraseRecognitionEvent> eventCaptor = ArgumentCaptor.forClass( + PhraseRecognitionEvent.class); + verify(callback).onPhraseRecognition(eq(handle), eventCaptor.capture()); + + // Validate the event. + assertEquals(RecognitionStatus.ABORTED, eventCaptor.getValue().common.status); + + // Make sure we are notified of the lost availability. + verify(callback).onRecognitionAvailabilityChange(false); + + // Attempt to start a new recognition - should get an abort event immediately, without + // involving the HAL. + clearInvocations(callback); + clearInvocations(mHalDriver); + module.startRecognition(handle, createRecognitionConfig()); + verify(callback).onPhraseRecognition(eq(handle), eventCaptor.capture()); + assertEquals(RecognitionStatus.ABORTED, eventCaptor.getValue().common.status); + verifyNotStartRecognition(); + + // Now enable it and make sure we are notified. + mService.setExternalCaptureState(false); + verify(callback).onRecognitionAvailabilityChange(true); + + // Unload the model. + unloadModel(module, handle, hwHandle); + module.detach(); + } + + @Test + public void testNotAbortRecognitionConcurrent() throws Exception { + // Make sure the HAL supports concurrent capture. + initService(true); + + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + verify(callback).onRecognitionAvailabilityChange(true); + clearInvocations(callback); + + // Load the model. + final int hwHandle = 13; + int handle = loadGenericModel(module, hwHandle); + + // Initiate a recognition. + startRecognition(module, handle, hwHandle); + + // Signal concurrent capture. Shouldn't abort. + mService.setExternalCaptureState(true); + verify(callback, never()).onRecognition(anyInt(), any()); + verify(callback, never()).onRecognitionAvailabilityChange(anyBoolean()); + + // Stop the recognition. + stopRecognition(module, handle, hwHandle); + + // Initiating a new one should work fine. + clearInvocations(mHalDriver); + startRecognition(module, handle, hwHandle); + verify(callback, never()).onRecognition(anyInt(), any()); + stopRecognition(module, handle, hwHandle); + + // Unload the model. + module.unloadModel(handle); + module.detach(); + } + + @Test + public void testNotAbortPhraseRecognitionConcurrent() throws Exception { + // Make sure the HAL supports concurrent capture. + initService(true); + + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + verify(callback).onRecognitionAvailabilityChange(true); + clearInvocations(callback); + + // Load the model. + final int hwHandle = 13; + int handle = loadPhraseModel(module, hwHandle); + + // Initiate a recognition. + startRecognition(module, handle, hwHandle); + + // Signal concurrent capture. Shouldn't abort. + mService.setExternalCaptureState(true); + verify(callback, never()).onPhraseRecognition(anyInt(), any()); + verify(callback, never()).onRecognitionAvailabilityChange(anyBoolean()); + + // Stop the recognition. + stopRecognition(module, handle, hwHandle); + + // Initiating a new one should work fine. + clearInvocations(mHalDriver); + startRecognition(module, handle, hwHandle); + verify(callback, never()).onRecognition(anyInt(), any()); + stopRecognition(module, handle, hwHandle); + + // Unload the model. + module.unloadModel(handle); + module.detach(); + } + + @Test + public void testParameterSupported() throws Exception { + if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw)) { + return; + } + + android.hardware.soundtrigger.V2_3.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_3.ISoundTriggerHw) mHalDriver; + + initService(false); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + final int hwHandle = 12; + int modelHandle = loadGenericModel(module, hwHandle); + + doAnswer((Answer<Void>) invocation -> { + android.hardware.soundtrigger.V2_3.ISoundTriggerHw.queryParameterCallback + resultCallback = invocation.getArgument(2); + android.hardware.soundtrigger.V2_3.ModelParameterRange range = + new android.hardware.soundtrigger.V2_3.ModelParameterRange(); + range.start = 23; + range.end = 45; + OptionalModelParameterRange optionalRange = new OptionalModelParameterRange(); + optionalRange.range(range); + resultCallback.onValues(0, optionalRange); + return null; + }).when(driver).queryParameter(eq(hwHandle), + eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any()); + + ModelParameterRange range = module.queryModelParameterSupport(modelHandle, + ModelParameter.THRESHOLD_FACTOR); + + verify(driver).queryParameter(eq(hwHandle), + eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any()); + + assertEquals(23, range.minInclusive); + assertEquals(45, range.maxInclusive); + } + + @Test + public void testParameterNotSupportedOld() throws Exception { + if (mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw) { + return; + } + + initService(false); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + final int hwHandle = 13; + int modelHandle = loadGenericModel(module, hwHandle); + + ModelParameterRange range = module.queryModelParameterSupport(modelHandle, + ModelParameter.THRESHOLD_FACTOR); + + assertNull(range); + } + + @Test + public void testParameterNotSupported() throws Exception { + if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw)) { + return; + } + + android.hardware.soundtrigger.V2_3.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_3.ISoundTriggerHw) mHalDriver; + + initService(false); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + final int hwHandle = 13; + int modelHandle = loadGenericModel(module, hwHandle); + + doAnswer(invocation -> { + android.hardware.soundtrigger.V2_3.ISoundTriggerHw.queryParameterCallback + resultCallback = invocation.getArgument(2); + // This is the return of this method. + resultCallback.onValues(0, new OptionalModelParameterRange()); + return null; + }).when(driver).queryParameter(eq(hwHandle), + eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any()); + + ModelParameterRange range = module.queryModelParameterSupport(modelHandle, + ModelParameter.THRESHOLD_FACTOR); + + verify(driver).queryParameter(eq(hwHandle), + eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any()); + + assertNull(range); + } + + @Test + public void testGetParameter() throws Exception { + if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw)) { + return; + } + + android.hardware.soundtrigger.V2_3.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_3.ISoundTriggerHw) mHalDriver; + + initService(false); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + final int hwHandle = 14; + int modelHandle = loadGenericModel(module, hwHandle); + + doAnswer(invocation -> { + android.hardware.soundtrigger.V2_3.ISoundTriggerHw.getParameterCallback + resultCallback = invocation.getArgument(2); + // This is the return of this method. + resultCallback.onValues(0, 234); + return null; + }).when(driver).getParameter(eq(hwHandle), + eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any()); + + int value = module.getModelParameter(modelHandle, ModelParameter.THRESHOLD_FACTOR); + + verify(driver).getParameter(eq(hwHandle), + eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any()); + + assertEquals(234, value); + } + + @Test + public void testSetParameter() throws Exception { + if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw)) { + return; + } + + android.hardware.soundtrigger.V2_3.ISoundTriggerHw driver = + (android.hardware.soundtrigger.V2_3.ISoundTriggerHw) mHalDriver; + + initService(false); + ISoundTriggerCallback callback = createCallbackMock(); + ISoundTriggerModule module = mService.attach(0, callback); + final int hwHandle = 17; + int modelHandle = loadGenericModel(module, hwHandle); + + when(driver.setParameter(hwHandle, + android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR, + 456)).thenReturn(0); + + module.setModelParameter(modelHandle, ModelParameter.THRESHOLD_FACTOR, 456); + + verify(driver).setParameter(hwHandle, + android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR, 456); + } + + private static class SoundTriggerHwCallback { + private final android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback mCallback; + private final int mCookie; + + SoundTriggerHwCallback(android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback callback, + int cookie) { + mCallback = callback; + mCookie = cookie; + } + + private void sendRecognitionEvent(int hwHandle, int status) throws RemoteException { + if (mCallback instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback) { + ((android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback) mCallback).recognitionCallback_2_1( + createRecognitionEvent_2_1(hwHandle, status), mCookie); + } else { + mCallback.recognitionCallback(createRecognitionEvent_2_0(hwHandle, status), + mCookie); + } + } + + private void sendPhraseRecognitionEvent(int hwHandle, int status) throws RemoteException { + if (mCallback instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback) { + ((android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback) mCallback).phraseRecognitionCallback_2_1( + createPhraseRecognitionEvent_2_1(hwHandle, status), mCookie); + } else { + mCallback.phraseRecognitionCallback( + createPhraseRecognitionEvent_2_0(hwHandle, status), mCookie); + } + } + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java index 0b4760d89686..a328568da7ed 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java @@ -23,6 +23,7 @@ import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.spy; import android.app.Notification; import android.app.NotificationChannel; @@ -271,6 +272,18 @@ public class NotificationComparatorTest extends UiServiceTestCase { } @Test + public void testRankingScoreOverrides() { + NotificationComparator comp = new NotificationComparator(mContext); + NotificationRecord recordMinCallNonInterruptive = spy(mRecordMinCallNonInterruptive); + assertTrue(comp.compare(mRecordMinCall, recordMinCallNonInterruptive) < 0); + + when(recordMinCallNonInterruptive.getRankingScore()).thenReturn(1f); + assertTrue(comp.compare(mRecordMinCall, recordMinCallNonInterruptive) > 0); + assertTrue(comp.compare(mRecordCheater, recordMinCallNonInterruptive) > 0); + assertTrue(comp.compare(mRecordColorizedCall, recordMinCallNonInterruptive) < 0); + } + + @Test public void testMessaging() { NotificationComparator comp = new NotificationComparator(mContext); assertTrue(comp.isImportantMessaging(mRecordInlineReply)); diff --git a/services/usb/java/com/android/server/usb/UsbSerialReader.java b/services/usb/java/com/android/server/usb/UsbSerialReader.java index 86016bb6036f..095e8e9b7b5b 100644 --- a/services/usb/java/com/android/server/usb/UsbSerialReader.java +++ b/services/usb/java/com/android/server/usb/UsbSerialReader.java @@ -75,12 +75,14 @@ class UsbSerialReader extends IUsbSerialReader.Stub { if (uid != Process.SYSTEM_UID) { enforcePackageBelongsToUid(uid, packageName); + UserHandle user = Binder.getCallingUserHandle(); int packageTargetSdkVersion; long token = Binder.clearCallingIdentity(); try { PackageInfo pkg; try { - pkg = mContext.getPackageManager().getPackageInfo(packageName, 0); + pkg = mContext.getPackageManager() + .getPackageInfoAsUser(packageName, 0, user.getIdentifier()); } catch (PackageManager.NameNotFoundException e) { throw new RemoteException("package " + packageName + " cannot be found"); } diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java index c06327995bc0..0becaf237a73 100644 --- a/telecomm/java/android/telecom/Connection.java +++ b/telecomm/java/android/telecom/Connection.java @@ -695,6 +695,15 @@ public abstract class Connection extends Conferenceable { public static final String EVENT_CALL_HOLD_FAILED = "android.telecom.event.CALL_HOLD_FAILED"; /** + * Connection event used to inform Telecom when a switch operation on a call has failed. + * <p> + * Sent via {@link #sendConnectionEvent(String, Bundle)}. The {@link Bundle} parameter is + * expected to be null when this connection event is used. + */ + public static final String EVENT_CALL_SWITCH_FAILED = + "android.telecom.event.CALL_SWITCH_FAILED"; + + /** * Connection event used to inform {@link InCallService}s when the process of merging a * Connection into a conference has begun. * <p> diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 001888c6c117..7381acb36812 100755 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -419,12 +419,33 @@ public class CarrierConfigManager { KEY_GSM_NONROAMING_NETWORKS_STRING_ARRAY = "gsm_nonroaming_networks_string_array"; /** - * Override the device's configuration for the ImsService to use for this SIM card. + * The package name containing the ImsService that will be bound to the telephony framework to + * support both IMS MMTEL and RCS feature functionality instead of the device default + * ImsService for this subscription. + * @deprecated Use {@link #KEY_CONFIG_IMS_MMTEL_PACKAGE_OVERRIDE_STRING} and + * {@link #KEY_CONFIG_IMS_RCS_PACKAGE_OVERRIDE_STRING} instead to configure these values + * separately. If any of those values are not empty, they will override this value. */ public static final String KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING = "config_ims_package_override_string"; /** + * The package name containing the ImsService that will be bound to the telephony framework to + * support IMS MMTEL feature functionality instead of the device default ImsService for this + * subscription. + */ + public static final String KEY_CONFIG_IMS_MMTEL_PACKAGE_OVERRIDE_STRING = + "config_ims_mmtel_package_override_string"; + + /** + * The package name containing the ImsService that will be bound to the telephony framework to + * support IMS RCS feature functionality instead of the device default ImsService for this + * subscription. + */ + public static final String KEY_CONFIG_IMS_RCS_PACKAGE_OVERRIDE_STRING = + "config_ims_rcs_package_override_string"; + + /** * Override the package that will manage {@link SubscriptionPlan} * information instead of the {@link CarrierService} that defines this * value. @@ -1946,6 +1967,12 @@ public class CarrierConfigManager { "allow_add_call_during_video_call"; /** + * When false, indicates that holding a video call is disabled + */ + public static final String KEY_ALLOW_HOLDING_VIDEO_CALL_BOOL = + "allow_holding_video_call"; + + /** * When true, indicates that the HD audio icon in the in-call screen should not be shown for * VoWifi calls. * @hide @@ -2176,7 +2203,7 @@ public class CarrierConfigManager { * the start of the next month. * <p> * This setting may be still overridden by explicit user choice. By default, - * the platform value will be used. + * {@link #DATA_CYCLE_USE_PLATFORM_DEFAULT} will be used. */ public static final String KEY_MONTHLY_DATA_CYCLE_DAY_INT = "monthly_data_cycle_day_int"; @@ -2185,10 +2212,7 @@ public class CarrierConfigManager { * When {@link #KEY_MONTHLY_DATA_CYCLE_DAY_INT}, {@link #KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG}, * or {@link #KEY_DATA_WARNING_THRESHOLD_BYTES_LONG} are set to this value, the platform default * value will be used for that key. - * - * @hide */ - @Deprecated public static final int DATA_CYCLE_USE_PLATFORM_DEFAULT = -1; /** @@ -2212,8 +2236,8 @@ public class CarrierConfigManager { * If the value is set to {@link #DATA_CYCLE_THRESHOLD_DISABLED}, the data usage warning will * be disabled. * <p> - * This setting may be overridden by explicit user choice. By default, the platform value - * will be used. + * This setting may be overridden by explicit user choice. By default, + * {@link #DATA_CYCLE_USE_PLATFORM_DEFAULT} will be used. */ public static final String KEY_DATA_WARNING_THRESHOLD_BYTES_LONG = "data_warning_threshold_bytes_long"; @@ -2221,8 +2245,7 @@ public class CarrierConfigManager { /** * 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. - * @hide + * expected to have implemented their own notification mechanism. {@code true} by default. */ public static final String KEY_DATA_WARNING_NOTIFICATION_BOOL = "data_warning_notification_bool"; @@ -2244,8 +2267,8 @@ public class CarrierConfigManager { * phone. If the value is set to {@link #DATA_CYCLE_THRESHOLD_DISABLED}, the data limit will be * disabled. * <p> - * This setting may be overridden by explicit user choice. By default, the platform value - * will be used. + * This setting may be overridden by explicit user choice. By default, + * {@link #DATA_CYCLE_USE_PLATFORM_DEFAULT} will be used. */ public static final String KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG = "data_limit_threshold_bytes_long"; @@ -2253,8 +2276,7 @@ public class CarrierConfigManager { /** * 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. - * @hide + * expected to have implemented their own notification mechanism. {@code true} by default. */ public static final String KEY_DATA_LIMIT_NOTIFICATION_BOOL = "data_limit_notification_bool"; @@ -2262,8 +2284,7 @@ public class CarrierConfigManager { /** * 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. - * @hide + * expected to have implemented their own notification mechanism. {@code true} by default. */ public static final String KEY_DATA_RAPID_NOTIFICATION_BOOL = "data_rapid_notification_bool"; @@ -3486,6 +3507,8 @@ public class CarrierConfigManager { sDefaults.putStringArray(KEY_GSM_ROAMING_NETWORKS_STRING_ARRAY, null); sDefaults.putStringArray(KEY_GSM_NONROAMING_NETWORKS_STRING_ARRAY, null); sDefaults.putString(KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING, null); + sDefaults.putString(KEY_CONFIG_IMS_MMTEL_PACKAGE_OVERRIDE_STRING, null); + sDefaults.putString(KEY_CONFIG_IMS_RCS_PACKAGE_OVERRIDE_STRING, null); sDefaults.putStringArray(KEY_CDMA_ROAMING_NETWORKS_STRING_ARRAY, null); sDefaults.putStringArray(KEY_CDMA_NONROAMING_NETWORKS_STRING_ARRAY, null); sDefaults.putStringArray(KEY_DIAL_STRING_REPLACE_STRING_ARRAY, null); @@ -3629,6 +3652,7 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_DROP_VIDEO_CALL_WHEN_ANSWERING_AUDIO_CALL_BOOL, false); sDefaults.putBoolean(KEY_ALLOW_MERGE_WIFI_CALLS_WHEN_VOWIFI_OFF_BOOL, true); sDefaults.putBoolean(KEY_ALLOW_ADD_CALL_DURING_VIDEO_CALL_BOOL, true); + sDefaults.putBoolean(KEY_ALLOW_HOLDING_VIDEO_CALL_BOOL, true); sDefaults.putBoolean(KEY_WIFI_CALLS_CAN_BE_HD_AUDIO, true); sDefaults.putBoolean(KEY_VIDEO_CALLS_CAN_BE_HD_AUDIO, true); sDefaults.putBoolean(KEY_GSM_CDMA_CALLS_CAN_BE_HD_AUDIO, false); diff --git a/telephony/java/android/telephony/ModemActivityInfo.java b/telephony/java/android/telephony/ModemActivityInfo.java index 16fbae25199b..aebe78031ba2 100644 --- a/telephony/java/android/telephony/ModemActivityInfo.java +++ b/telephony/java/android/telephony/ModemActivityInfo.java @@ -205,10 +205,10 @@ public final class ModemActivityInfo implements Parcelable { } /** + * Indicate if the ModemActivityInfo is invalid due to modem's invalid reporting. + * * @return {@code true} if this {@link ModemActivityInfo} record is valid, * {@code false} otherwise. - * - * @hide */ public boolean isValid() { for (TransmitPower powerInfo : getTransmitPowerInfo()) { diff --git a/telephony/java/android/telephony/PreciseDataConnectionState.java b/telephony/java/android/telephony/PreciseDataConnectionState.java index 257d634f1577..78ad5c58423c 100644 --- a/telephony/java/android/telephony/PreciseDataConnectionState.java +++ b/telephony/java/android/telephony/PreciseDataConnectionState.java @@ -19,8 +19,10 @@ package android.telephony; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.annotation.UnsupportedAppUsage; import android.net.LinkProperties; +import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.telephony.Annotation.ApnType; @@ -29,6 +31,8 @@ import android.telephony.Annotation.DataState; import android.telephony.Annotation.NetworkType; import android.telephony.data.ApnSetting; +import dalvik.system.VMRuntime; + import java.util.Objects; @@ -46,35 +50,62 @@ import java.util.Objects; * <li>Data connection fail cause. * </ul> * - * @hide */ -@SystemApi public final class PreciseDataConnectionState implements Parcelable { private @DataState int mState = TelephonyManager.DATA_UNKNOWN; private @NetworkType int mNetworkType = TelephonyManager.NETWORK_TYPE_UNKNOWN; private @DataFailureCause int mFailCause = DataFailCause.NONE; - private @ApnType int mAPNTypes = ApnSetting.TYPE_NONE; - private String mAPN = ""; + private @ApnType int mApnTypes = ApnSetting.TYPE_NONE; + private String mApn = ""; private LinkProperties mLinkProperties = null; + private ApnSetting mApnSetting = null; /** * Constructor * + * @deprecated this constructor has been superseded and should not be used. * @hide */ - @UnsupportedAppUsage + @TestApi + @Deprecated + @UnsupportedAppUsage // (maxTargetSdk = Build.VERSION_CODES.Q) + // FIXME: figure out how to remove the UnsupportedAppUsage and delete this constructor public PreciseDataConnectionState(@DataState int state, @NetworkType int networkType, - @ApnType int apnTypes, String apn, - LinkProperties linkProperties, + @ApnType int apnTypes, @NonNull String apn, + @Nullable LinkProperties linkProperties, @DataFailureCause int failCause) { + this(state, networkType, apnTypes, apn, linkProperties, failCause, null); + } + + + /** + * Constructor + * + * @param state the state of the data connection + * @param networkType the access network that is/would carry this data connection + * @param apnTypes the APN types that this data connection carries + * @param apnSetting if there is a valid APN for this Data Connection, then the APN Settings; + * if there is no valid APN setting for the specific type, then this will be null + * @param linkProperties if the data connection is connected, the properties of the connection + * @param failCause in case a procedure related to this data connection fails, a non-zero error + * code indicating the cause of the failure. + * @hide + */ + public PreciseDataConnectionState(@DataState int state, + @NetworkType int networkType, + @ApnType int apnTypes, @NonNull String apn, + @Nullable LinkProperties linkProperties, + @DataFailureCause int failCause, + @Nullable ApnSetting apnSetting) { mState = state; mNetworkType = networkType; - mAPNTypes = apnTypes; - mAPN = apn; + mApnTypes = apnTypes; + mApn = apn; mLinkProperties = linkProperties; mFailCause = failCause; + mApnSetting = apnSetting; } /** @@ -93,76 +124,160 @@ public final class PreciseDataConnectionState implements Parcelable { private PreciseDataConnectionState(Parcel in) { mState = in.readInt(); mNetworkType = in.readInt(); - mAPNTypes = in.readInt(); - mAPN = in.readString(); - mLinkProperties = (LinkProperties)in.readParcelable(null); + mApnTypes = in.readInt(); + mApn = in.readString(); + mLinkProperties = (LinkProperties) in.readParcelable(null); mFailCause = in.readInt(); + mApnSetting = (ApnSetting) in.readParcelable(null); } /** * Returns the state of data connection that supported the apn types returned by * {@link #getDataConnectionApnTypeBitMask()} + * + * @deprecated use {@link #getState()} + * @hide */ + @Deprecated + @SystemApi public @DataState int getDataConnectionState() { + if (mState == TelephonyManager.DATA_DISCONNECTING + && VMRuntime.getRuntime().getTargetSdkVersion() < Build.VERSION_CODES.R) { + return TelephonyManager.DATA_CONNECTED; + } + + return mState; + } + + /** + * Returns the high-level state of this data connection. + */ + public @DataState int getState() { return mState; } /** * Returns the network type associated with this data connection. + * + * @deprecated use {@link getNetworkType()} * @hide */ + @Deprecated + @SystemApi public @NetworkType int getDataConnectionNetworkType() { return mNetworkType; } /** - * Returns the data connection APN types supported by this connection and triggers - * {@link PreciseDataConnectionState} change. + * Returns the network type associated with this data connection. + * + * Return the current/latest (radio) bearer technology that carries this data connection. + * For a variety of reasons, the network type can change during the life of the data + * connection, and this information is not reliable unless the physical link is currently + * active; (there is currently no mechanism to know whether the physical link is active at + * any given moment). Thus, this value is generally correct but may not be relied-upon to + * represent the status of the radio bearer at any given moment. + */ + public @NetworkType int getNetworkType() { + return mNetworkType; + } + + /** + * Returns the APN types mapped to this data connection. + * + * @deprecated use {@link #getApnSetting()} + * @hide */ + @Deprecated + @SystemApi public @ApnType int getDataConnectionApnTypeBitMask() { - return mAPNTypes; + return mApnTypes; } /** - * Returns APN {@link ApnSetting} of this data connection. + * Returns APN of this data connection. + * + * @deprecated use {@link #getApnSetting()} + * @hide */ - @Nullable + @NonNull + @SystemApi + @Deprecated public String getDataConnectionApn() { - return mAPN; + return mApn; } /** * Get the properties of the network link {@link LinkProperties}. + * + * @deprecated use {@link #getLinkProperties()} * @hide */ - @UnsupportedAppUsage + @Deprecated + @SystemApi + @Nullable public LinkProperties getDataConnectionLinkProperties() { return mLinkProperties; } /** - * Returns data connection fail cause, in case there was a failure. + * Get the properties of the network link {@link LinkProperties}. + */ + @Nullable + public LinkProperties getLinkProperties() { + return mLinkProperties; + } + + /** + * Returns the cause code generated by the most recent state change. + * + * @deprecated use {@link #getLastCauseCode()} + * @hide + */ + @Deprecated + @SystemApi + public int getDataConnectionFailCause() { + return mFailCause; + } + + /** + * Returns the cause code generated by the most recent state change. + * + * Return the cause code for the most recent change in {@link #getState}. In the event of an + * error, this cause code will be non-zero. */ - public @Annotation.DataFailureCause int getDataConnectionFailCause() { + // FIXME(b144774287): some of these cause codes should have a prescribed meaning. + public int getLastCauseCode() { return mFailCause; } + /** + * Return the APN Settings for this data connection. + * + * Returns the ApnSetting that was used to configure this data connection. + */ + // FIXME: This shouldn't be nullable; update once the ApnSetting is supplied correctly + @Nullable ApnSetting getApnSetting() { + return mApnSetting; + } + @Override public int describeContents() { return 0; } @Override - public void writeToParcel(Parcel out, int flags) { + public void writeToParcel(@NonNull Parcel out, int flags) { out.writeInt(mState); out.writeInt(mNetworkType); - out.writeInt(mAPNTypes); - out.writeString(mAPN); + out.writeInt(mApnTypes); + out.writeString(mApn); out.writeParcelable(mLinkProperties, flags); out.writeInt(mFailCause); + out.writeParcelable(mApnSetting, flags); } - public static final @android.annotation.NonNull Parcelable.Creator<PreciseDataConnectionState> CREATOR + public static final @NonNull Parcelable.Creator<PreciseDataConnectionState> CREATOR = new Parcelable.Creator<PreciseDataConnectionState>() { public PreciseDataConnectionState createFromParcel(Parcel in) { @@ -176,8 +291,8 @@ public final class PreciseDataConnectionState implements Parcelable { @Override public int hashCode() { - return Objects.hash(mState, mNetworkType, mAPNTypes, mAPN, mLinkProperties, - mFailCause); + return Objects.hash(mState, mNetworkType, mApnTypes, mApn, mLinkProperties, + mFailCause, mApnSetting); } @Override @@ -188,11 +303,12 @@ public final class PreciseDataConnectionState implements Parcelable { } PreciseDataConnectionState other = (PreciseDataConnectionState) obj; - return Objects.equals(mAPN, other.mAPN) && mAPNTypes == other.mAPNTypes + return Objects.equals(mApn, other.mApn) && mApnTypes == other.mApnTypes && mFailCause == other.mFailCause && Objects.equals(mLinkProperties, other.mLinkProperties) && mNetworkType == other.mNetworkType - && mState == other.mState; + && mState == other.mState + && Objects.equals(mApnSetting, other.mApnSetting); } @NonNull @@ -202,10 +318,11 @@ public final class PreciseDataConnectionState implements Parcelable { sb.append("Data Connection state: " + mState); sb.append(", Network type: " + mNetworkType); - sb.append(", APN types: " + ApnSetting.getApnTypesStringFromBitmask(mAPNTypes)); - sb.append(", APN: " + mAPN); + sb.append(", APN types: " + ApnSetting.getApnTypesStringFromBitmask(mApnTypes)); + sb.append(", APN: " + mApn); sb.append(", Link properties: " + mLinkProperties); sb.append(", Fail cause: " + DataFailCause.toString(mFailCause)); + sb.append(", Apn Setting: " + mApnSetting); return sb.toString(); } diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java index 37f33885de20..cab5286f53b7 100644 --- a/telephony/java/android/telephony/SmsManager.java +++ b/telephony/java/android/telephony/SmsManager.java @@ -1998,6 +1998,27 @@ public final class SmsManager { } } + /** + * Gets the total capacity of SMS storage on RUIM and SIM cards + * + * @return the total capacity count of SMS on RUIM and SIM cards + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + public int getSmsCapacityOnIcc() { + int ret = 0; + try { + ISms iccISms = getISmsService(); + if (iccISms != null) { + ret = iccISms.getSmsCapacityOnIccForSubscriber(getSubscriptionId()); + } + } catch (RemoteException ex) { + //ignore it + } + return ret; + } + // see SmsMessage.getStatusOnIcc /** Free space (TS 51.011 10.5.3 / 3GPP2 C.S0023 3.4.27). */ diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 6c321e5a5101..78c5e43033bb 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -38,6 +38,9 @@ import android.annotation.UnsupportedAppUsage; import android.annotation.WorkerThread; import android.app.ActivityThread; import android.app.PendingIntent; +import android.compat.Compatibility; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledAfter; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -142,6 +145,14 @@ public class TelephonyManager { private static final String TAG = "TelephonyManager"; /** + * To expand the error codes for {@link TelephonyManager#updateAvailableNetworks} and + * {@link TelephonyManager#setPreferredOpportunisticDataSubscription}. + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q) + private static final long CALLBACK_ON_MORE_ERROR_CODE_CHANGE = 130595455L; + + /** * The key to use when placing the result of {@link #requestModemActivityInfo(ResultReceiver)} * into the ResultReceiver Bundle. * @hide @@ -790,18 +801,6 @@ public class TelephonyManager { public static final String EXTRA_DATA_APN = PhoneConstants.DATA_APN_KEY; /** - * The lookup key used with the {@link #ACTION_PRECISE_DATA_CONNECTION_STATE_CHANGED} broadcast - * for an String representation of the data interface. - * - * <p class="note"> - * Retrieve with - * {@link android.content.Intent#getParcelableExtra(String name)}. - * - * @hide - */ - public static final String EXTRA_DATA_LINK_PROPERTIES_KEY = PhoneConstants.DATA_LINK_PROPERTIES_KEY; - - /** * Broadcast intent action for letting the default dialer to know to show voicemail * notification. * @@ -5118,6 +5117,7 @@ public class TelephonyManager { DATA_CONNECTING, DATA_CONNECTED, DATA_SUSPENDED, + DATA_DISCONNECTING, }) @Retention(RetentionPolicy.SOURCE) public @interface DataState{} @@ -5134,6 +5134,12 @@ public class TelephonyManager { * traffic is temporarily unavailable. For example, in a 2G network, * data activity may be suspended when a voice call arrives. */ public static final int DATA_SUSPENDED = 3; + /** + * Data connection state: Disconnecting. + * + * IP traffic may be available but will cease working imminently. + */ + public static final int DATA_DISCONNECTING = 4; /** * Returns a constant indicating the current data connection state @@ -5143,14 +5149,21 @@ public class TelephonyManager { * @see #DATA_CONNECTING * @see #DATA_CONNECTED * @see #DATA_SUSPENDED + * @see #DATA_DISCONNECTING */ public int getDataState() { try { ITelephony telephony = getITelephony(); if (telephony == null) return DATA_DISCONNECTED; - return telephony.getDataStateForSubId( + int state = telephony.getDataStateForSubId( getSubId(SubscriptionManager.getActiveDataSubscriptionId())); + if (state == TelephonyManager.DATA_DISCONNECTING + && VMRuntime.getRuntime().getTargetSdkVersion() < Build.VERSION_CODES.R) { + return TelephonyManager.DATA_CONNECTED; + } + + return state; } catch (RemoteException ex) { // the phone process is restarting. return DATA_DISCONNECTED; @@ -5171,6 +5184,7 @@ public class TelephonyManager { case DATA_CONNECTING: return "CONNECTING"; case DATA_CONNECTED: return "CONNECTED"; case DATA_SUSPENDED: return "SUSPENDED"; + case DATA_DISCONNECTING: return "DISCONNECTING"; } return "UNKNOWN(" + state + ")"; } @@ -10285,19 +10299,25 @@ public class TelephonyManager { } /** - * Action set from carrier signalling broadcast receivers to enable/disable radio - * Permissions {@link android.Manifest.permission.MODIFY_PHONE_STATE} is required. - * @param subId the subscription ID that this action applies to. + * Carrier action to enable or disable the radio. + * + * <p>If this object has been created with {@link #createForSubscriptionId}, applies to the + * given subId. Otherwise, applies to {@link SubscriptionManager#getDefaultDataSubscriptionId()} + * + * <p>Requires Permission: + * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}. + * * @param enabled control enable or disable radio. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) - public void carrierActionSetRadioEnabled(int subId, boolean enabled) { + public void setRadioEnabled(boolean enabled) { try { ITelephony service = getITelephony(); if (service != null) { - service.carrierActionSetRadioEnabled(subId, enabled); + service.carrierActionSetRadioEnabled( + getSubId(SubscriptionManager.getDefaultDataSubscriptionId()), enabled); } } catch (RemoteException e) { Log.e(TAG, "Error calling ITelephony#carrierActionSetRadioEnabled", e); @@ -10305,20 +10325,25 @@ public class TelephonyManager { } /** - * Action set from carrier signalling broadcast receivers to start/stop reporting default - * network available events - * Permissions {@link android.Manifest.permission.MODIFY_PHONE_STATE} is required. - * @param subId the subscription ID that this action applies to. + * Carrier action to start or stop reporting default network available events. + * + * <p>If this object has been created with {@link #createForSubscriptionId}, applies to the + * given subId. Otherwise, applies to {@link SubscriptionManager#getDefaultDataSubscriptionId()} + * + * <p>Requires Permission: + * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}. + * * @param report control start/stop reporting network status. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) - public void carrierActionReportDefaultNetworkStatus(int subId, boolean report) { + public void reportDefaultNetworkStatus(boolean report) { try { ITelephony service = getITelephony(); if (service != null) { - service.carrierActionReportDefaultNetworkStatus(subId, report); + service.carrierActionReportDefaultNetworkStatus( + getSubId(SubscriptionManager.getDefaultDataSubscriptionId()), report); } } catch (RemoteException e) { Log.e(TAG, "Error calling ITelephony#carrierActionReportDefaultNetworkStatus", e); @@ -10326,18 +10351,24 @@ public class TelephonyManager { } /** - * Action set from carrier signalling broadcast receivers to reset all carrier actions - * Permissions {@link android.Manifest.permission.MODIFY_PHONE_STATE} is required. - * @param subId the subscription ID that this action applies to. + * Reset all carrier actions previously set by {@link #setRadioEnabled}, + * {@link #reportDefaultNetworkStatus} and {@link #setCarrierDataEnabled}. + * + * <p>If this object has been created with {@link #createForSubscriptionId}, applies to the + * given subId. Otherwise, applies to {@link SubscriptionManager#getDefaultDataSubscriptionId()} + * + * <p>Requires Permission: + * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) - public void carrierActionResetAll(int subId) { + public void resetAllCarrierActions() { try { ITelephony service = getITelephony(); if (service != null) { - service.carrierActionResetAll(subId); + service.carrierActionResetAll( + getSubId(SubscriptionManager.getDefaultDataSubscriptionId())); } } catch (RemoteException e) { Log.e(TAG, "Error calling ITelephony#carrierActionResetAll", e); @@ -11350,7 +11381,11 @@ public class TelephonyManager { final long identity = Binder.clearCallingIdentity(); try { executor.execute(() -> { - callback.accept(SET_OPPORTUNISTIC_SUB_REMOTE_SERVICE_EXCEPTION); + if (Compatibility.isChangeEnabled(CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) { + callback.accept(SET_OPPORTUNISTIC_SUB_REMOTE_SERVICE_EXCEPTION); + } else { + callback.accept(SET_OPPORTUNISTIC_SUB_INACTIVE_SUBSCRIPTION); + } }); } finally { Binder.restoreCallingIdentity(identity); @@ -11447,7 +11482,11 @@ public class TelephonyManager { final long identity = Binder.clearCallingIdentity(); try { executor.execute(() -> { - callback.accept(UPDATE_AVAILABLE_NETWORKS_REMOTE_SERVICE_EXCEPTION); + if (Compatibility.isChangeEnabled(CALLBACK_ON_MORE_ERROR_CODE_CHANGE)) { + callback.accept(UPDATE_AVAILABLE_NETWORKS_REMOTE_SERVICE_EXCEPTION); + } else { + callback.accept(UPDATE_AVAILABLE_NETWORKS_UNKNOWN_FAILURE); + } }); } finally { Binder.restoreCallingIdentity(identity); diff --git a/telephony/java/com/android/internal/telephony/ISms.aidl b/telephony/java/com/android/internal/telephony/ISms.aidl index ac4c8ecea842..9f4d066ff8b4 100644 --- a/telephony/java/com/android/internal/telephony/ISms.aidl +++ b/telephony/java/com/android/internal/telephony/ISms.aidl @@ -594,4 +594,12 @@ interface ISms { * @return true for success, false otherwise. */ boolean setSmscAddressOnIccEfForSubscriber(String smsc, int subId, String callingPackage); + + /** + * Get the capacity count of sms on Icc card. + * + * @param subId for subId which getSmsCapacityOnIcc is queried. + * @return capacity of ICC + */ + int getSmsCapacityOnIccForSubscriber(int subId); } diff --git a/telephony/java/com/android/internal/telephony/ISmsImplBase.java b/telephony/java/com/android/internal/telephony/ISmsImplBase.java index 9865f76d2580..2430d8237d9c 100644 --- a/telephony/java/com/android/internal/telephony/ISmsImplBase.java +++ b/telephony/java/com/android/internal/telephony/ISmsImplBase.java @@ -211,4 +211,9 @@ public class ISmsImplBase extends ISms.Stub { String smsc, int subId, String callingPackage) { throw new UnsupportedOperationException(); } + + @Override + public int getSmsCapacityOnIccForSubscriber(int subId) { + throw new UnsupportedOperationException(); + } } diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 0ec54ecf56df..97b24aef4af5 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -888,12 +888,13 @@ interface ITelephony { /** * @return true if the ImsService to bind to for the slot id specified was set, false otherwise. */ - boolean setImsService(int slotId, boolean isCarrierImsService, String packageName); + boolean setBoundImsServiceOverride(int slotIndex, boolean isCarrierService, + in int[] featureTypes, in String packageName); /** * @return the package name of the carrier/device ImsService associated with this slot. */ - String getImsService(int slotId, boolean isCarrierImsService); + String getBoundImsServicePackage(int slotIndex, boolean isCarrierImsService, int featureType); /** * Get the MmTelFeature state attached to this subscription id. diff --git a/telephony/java/com/android/internal/telephony/PhoneConstantConversions.java b/telephony/java/com/android/internal/telephony/PhoneConstantConversions.java index f7f0f29fcb29..8640acca7c8c 100644 --- a/telephony/java/com/android/internal/telephony/PhoneConstantConversions.java +++ b/telephony/java/com/android/internal/telephony/PhoneConstantConversions.java @@ -16,14 +16,10 @@ package com.android.internal.telephony; -import android.telephony.ServiceState; import android.telephony.TelephonyManager; -import android.telephony.PreciseCallState; import com.android.internal.telephony.PhoneConstants; -import java.util.List; - public class PhoneConstantConversions { /** * Convert the {@link PhoneConstants.State} enum into the TelephonyManager.CALL_STATE_* @@ -67,6 +63,8 @@ public class PhoneConstantConversions { return TelephonyManager.DATA_CONNECTED; case SUSPENDED: return TelephonyManager.DATA_SUSPENDED; + case DISCONNECTING: + return TelephonyManager.DATA_DISCONNECTING; default: return TelephonyManager.DATA_DISCONNECTED; } @@ -84,6 +82,8 @@ public class PhoneConstantConversions { return PhoneConstants.DataState.CONNECTED; case TelephonyManager.DATA_SUSPENDED: return PhoneConstants.DataState.SUSPENDED; + case TelephonyManager.DATA_DISCONNECTING: + return PhoneConstants.DataState.DISCONNECTING; default: return PhoneConstants.DataState.DISCONNECTED; } diff --git a/telephony/java/com/android/internal/telephony/PhoneConstants.java b/telephony/java/com/android/internal/telephony/PhoneConstants.java index fde2c5a9a738..fadb5739e31f 100644 --- a/telephony/java/com/android/internal/telephony/PhoneConstants.java +++ b/telephony/java/com/android/internal/telephony/PhoneConstants.java @@ -46,6 +46,7 @@ public class PhoneConstants { * <ul> * <li>CONNECTED = IP traffic should be available</li> * <li>CONNECTING = Currently setting up data connection</li> + * <li>DISCONNECTING = IP temporarily available</li> * <li>DISCONNECTED = IP not available</li> * <li>SUSPENDED = connection is created but IP traffic is * temperately not available. i.e. voice call is in place @@ -55,10 +56,15 @@ public class PhoneConstants { @UnsupportedAppUsage(implicitMember = "values()[Lcom/android/internal/telephony/PhoneConstants$DataState;") public enum DataState { - @UnsupportedAppUsage CONNECTED, - @UnsupportedAppUsage CONNECTING, - @UnsupportedAppUsage DISCONNECTED, - @UnsupportedAppUsage SUSPENDED; + @UnsupportedAppUsage + CONNECTED, + @UnsupportedAppUsage + CONNECTING, + @UnsupportedAppUsage + DISCONNECTED, + @UnsupportedAppUsage + SUSPENDED, + DISCONNECTING; }; public static final String STATE_KEY = "state"; @@ -91,20 +97,12 @@ public class PhoneConstants { public static final String PHONE_NAME_KEY = "phoneName"; public static final String DATA_NETWORK_TYPE_KEY = "networkType"; - public static final String DATA_FAILURE_CAUSE_KEY = "failCause"; public static final String DATA_APN_TYPE_KEY = "apnType"; public static final String DATA_APN_KEY = "apn"; - public static final String DATA_LINK_PROPERTIES_KEY = "linkProperties"; - public static final String DATA_NETWORK_CAPABILITIES_KEY = "networkCapabilities"; - public static final String DATA_IFACE_NAME_KEY = "iface"; - public static final String NETWORK_UNAVAILABLE_KEY = "networkUnvailable"; - public static final String DATA_NETWORK_ROAMING_KEY = "networkRoaming"; public static final String PHONE_IN_ECM_STATE = "phoneinECMState"; public static final String PHONE_IN_EMERGENCY_CALL = "phoneInEmergencyCall"; - public static final String REASON_LINK_PROPERTIES_CHANGED = "linkPropertiesChanged"; - /** * Return codes for supplyPinReturnResult and * supplyPukReturnResult APIs diff --git a/test-mock/src/android/test/mock/MockContext.java b/test-mock/src/android/test/mock/MockContext.java index 0208c3a0a0de..9d913b9861e5 100644 --- a/test-mock/src/android/test/mock/MockContext.java +++ b/test-mock/src/android/test/mock/MockContext.java @@ -16,6 +16,7 @@ package android.test.mock; +import android.annotation.NonNull; import android.annotation.SystemApi; import android.app.IApplicationThread; import android.app.IServiceConnection; @@ -211,6 +212,15 @@ public class MockContext extends Context { throw new UnsupportedOperationException(); } + /** + * {@inheritDoc Context#getCrateDir()} + * @hide + */ + @Override + public File getCrateDir(@NonNull String crateId) { + throw new UnsupportedOperationException(); + } + @Override public File getNoBackupFilesDir() { throw new UnsupportedOperationException(); diff --git a/tests/ManagedProfileLifecycleStressTest/src/com/android/test/stress/ManagedProfileLifecycleStressTest.java b/tests/ManagedProfileLifecycleStressTest/src/com/android/test/stress/ManagedProfileLifecycleStressTest.java index e323592d40ca..026677e09bed 100644 --- a/tests/ManagedProfileLifecycleStressTest/src/com/android/test/stress/ManagedProfileLifecycleStressTest.java +++ b/tests/ManagedProfileLifecycleStressTest/src/com/android/test/stress/ManagedProfileLifecycleStressTest.java @@ -53,6 +53,8 @@ public class ManagedProfileLifecycleStressTest extends BaseHostJUnit4Test { */ @Test public void testCreateStartDelete() throws Exception { + // Disable package verifier for ADB installs. + getDevice().executeShellCommand("settings put global verifier_verify_adb_installs 0"); int iteration = 0; final long deadline = System.nanoTime() + TimeUnit.MINUTES.toNanos(TIME_LIMIT_MINUTES); while (System.nanoTime() < deadline) { diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java index abe6c6152da5..daa85bd06669 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java @@ -1098,4 +1098,28 @@ public class RollbackTest { InstallUtils.dropShellPermissionIdentity(); } } + + /** + * Test we can't enable rollback for non-whitelisted app without + * TEST_MANAGE_ROLLBACKS permission + */ + @Test + public void testNonRollbackWhitelistedApp() throws Exception { + try { + InstallUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.MANAGE_ROLLBACKS); + + Uninstall.packages(TestApp.A); + Install.single(TestApp.A1).commit(); + assertThat(RollbackUtils.getAvailableRollback(TestApp.A)).isNull(); + + Install.single(TestApp.A2).setEnableRollback().commit(); + Thread.sleep(TimeUnit.SECONDS.toMillis(2)); + assertThat(RollbackUtils.getAvailableRollback(TestApp.A)).isNull(); + } finally { + InstallUtils.dropShellPermissionIdentity(); + } + } } diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java index 7b289d8a7858..879ac64c6a55 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java @@ -40,6 +40,7 @@ import com.android.cts.install.lib.TestApp; import com.android.cts.install.lib.Uninstall; import com.android.cts.rollback.lib.Rollback; import com.android.cts.rollback.lib.RollbackUtils; +import com.android.internal.R; import libcore.io.IoUtils; @@ -341,6 +342,37 @@ public class StagedRollbackTest { getNetworkStackPackageName())).isNull(); } + private static String getModuleMetadataPackageName() { + return InstrumentationRegistry.getInstrumentation().getContext() + .getResources().getString(R.string.config_defaultModuleMetadataProvider); + } + + @Test + public void testRollbackWhitelistedApp_Phase1() throws Exception { + // Remove available rollbacks + String pkgName = getModuleMetadataPackageName(); + RollbackUtils.getRollbackManager().expireRollbackForPackage(pkgName); + assertThat(RollbackUtils.getAvailableRollback(pkgName)).isNull(); + + // Overwrite existing permissions. We don't want TEST_MANAGE_ROLLBACKS which allows us + // to enable rollback for any app + InstallUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.MANAGE_ROLLBACKS); + + // Re-install a whitelisted app with rollbacks enabled + String filePath = InstrumentationRegistry.getInstrumentation().getContext() + .getPackageManager().getPackageInfo(pkgName, 0).applicationInfo.sourceDir; + TestApp app = new TestApp("ModuleMetadata", pkgName, -1, false, new File(filePath)); + Install.single(app).setStaged().setEnableRollback() + .addInstallFlags(PackageManager.INSTALL_REPLACE_EXISTING).commit(); + } + + @Test + public void testRollbackWhitelistedApp_Phase2() throws Exception { + assertThat(RollbackUtils.getAvailableRollback(getModuleMetadataPackageName())).isNotNull(); + } + private static void runShellCommand(String cmd) { ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation().getUiAutomation() .executeShellCommand(cmd); diff --git a/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java b/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java index e4a8febb8bce..07d829d2d0bb 100644 --- a/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java +++ b/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java @@ -175,6 +175,16 @@ public class StagedRollbackTest extends BaseHostJUnit4Test { runPhase("testPreviouslyAbandonedRollbacks_Phase3"); } + /** + * Tests we can enable rollback for a whitelisted app. + */ + @Test + public void testRollbackWhitelistedApp() throws Exception { + runPhase("testRollbackWhitelistedApp_Phase1"); + getDevice().reboot(); + runPhase("testRollbackWhitelistedApp_Phase2"); + } + private void crashProcess(String processName, int numberOfCrashes) throws Exception { String pid = ""; String lastPid = "invalid"; diff --git a/tests/net/java/android/net/TcpKeepalivePacketDataTest.java b/tests/net/java/android/net/TcpKeepalivePacketDataTest.java index 5cb0d7e7a1a9..e632aafde70e 100644 --- a/tests/net/java/android/net/TcpKeepalivePacketDataTest.java +++ b/tests/net/java/android/net/TcpKeepalivePacketDataTest.java @@ -22,8 +22,6 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; -import android.net.SocketKeepalive.InvalidPacketException; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/tools/localedata/extract_icu_data.py b/tools/localedata/extract_icu_data.py index 6b4c34677d96..ca1847af7d06 100755 --- a/tools/localedata/extract_icu_data.py +++ b/tools/localedata/extract_icu_data.py @@ -176,6 +176,9 @@ def read_and_dump_likely_data(icu_data_dir): dump_representative_locales(representative_locales) return likely_script_dict +def escape_script_variable_name(script): + """Escape characters, e.g. '~', in a C++ variable name""" + return script.replace("~", "_") def read_parent_data(icu_data_dir): """Read locale parent data from ICU data files.""" @@ -221,7 +224,7 @@ def dump_parent_data(script_organized_dict): for script in sorted_scripts: parent_dict = script_organized_dict[script] print ('const std::unordered_map<uint32_t, uint32_t> %s_PARENTS({' - % script.upper()) + % escape_script_variable_name(script.upper())) for locale in sorted(parent_dict.keys()): parent = parent_dict[locale] print ' {0x%08Xu, 0x%08Xu}, // %s -> %s' % ( @@ -239,7 +242,7 @@ def dump_parent_data(script_organized_dict): for script in sorted_scripts: print " {{'%c', '%c', '%c', '%c'}, &%s_PARENTS}," % ( script[0], script[1], script[2], script[3], - script.upper()) + escape_script_variable_name(script.upper())) print '};' diff --git a/tools/stats_log_api_gen/Android.bp b/tools/stats_log_api_gen/Android.bp index 7733761eebcc..15c327852001 100644 --- a/tools/stats_log_api_gen/Android.bp +++ b/tools/stats_log_api_gen/Android.bp @@ -21,6 +21,7 @@ cc_binary_host { name: "stats-log-api-gen", srcs: [ "Collation.cpp", + "atoms_info_writer.cpp", "java_writer.cpp", "java_writer_q.cpp", "main.cpp", @@ -102,13 +103,19 @@ genrule { cc_library { name: "libstatslog", host_supported: true, - generated_sources: ["statslog.cpp"], - generated_headers: ["statslog.h"], + generated_sources: [ + "statslog.cpp", + ], + generated_headers: [ + "statslog.h" + ], cflags: [ "-Wall", "-Werror", ], - export_generated_headers: ["statslog.h"], + export_generated_headers: [ + "statslog.h" + ], shared_libs: [ "liblog", "libcutils", @@ -127,3 +134,4 @@ cc_library { }, }, } + diff --git a/tools/stats_log_api_gen/Collation.cpp b/tools/stats_log_api_gen/Collation.cpp index 373adca994a1..fa556010646c 100644 --- a/tools/stats_log_api_gen/Collation.cpp +++ b/tools/stats_log_api_gen/Collation.cpp @@ -379,6 +379,7 @@ int collate_atoms(const Descriptor *descriptor, Atoms *atoms) { int errorCount = 0; const bool dbg = false; + int maxPushedAtomId = 2; for (int i = 0; i < descriptor->field_count(); i++) { const FieldDescriptor *atomField = descriptor->field(i); @@ -447,8 +448,14 @@ int collate_atoms(const Descriptor *descriptor, Atoms *atoms) { } atoms->non_chained_decls.insert(nonChainedAtomDecl); } + + if (atomDecl.code < PULL_ATOM_START_ID && atomDecl.code > maxPushedAtomId) { + maxPushedAtomId = atomDecl.code; + } } + atoms->maxPushedAtomId = maxPushedAtomId; + if (dbg) { printf("signatures = [\n"); for (map<vector<java_type_t>, set<string>>::const_iterator it = diff --git a/tools/stats_log_api_gen/Collation.h b/tools/stats_log_api_gen/Collation.h index 44746c96df1b..3efdd520d7f5 100644 --- a/tools/stats_log_api_gen/Collation.h +++ b/tools/stats_log_api_gen/Collation.h @@ -111,6 +111,7 @@ struct Atoms { set<AtomDecl> decls; set<AtomDecl> non_chained_decls; map<vector<java_type_t>, set<string>> non_chained_signatures_to_modules; + int maxPushedAtomId; }; /** @@ -123,4 +124,4 @@ int collate_atom(const Descriptor *atom, AtomDecl *atomDecl, vector<java_type_t> } // namespace android -#endif // ANDROID_STATS_LOG_API_GEN_COLLATION_H
\ No newline at end of file +#endif // ANDROID_STATS_LOG_API_GEN_COLLATION_H diff --git a/tools/stats_log_api_gen/atoms_info_writer.cpp b/tools/stats_log_api_gen/atoms_info_writer.cpp new file mode 100644 index 000000000000..54a9982bb5c2 --- /dev/null +++ b/tools/stats_log_api_gen/atoms_info_writer.cpp @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2019, 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. + */ + +#include "atoms_info_writer.h" +#include "utils.h" + +#include <map> +#include <set> +#include <vector> + +namespace android { +namespace stats_log_api_gen { + +static void write_atoms_info_header_body(FILE* out, const Atoms& atoms) { + fprintf(out, "struct StateAtomFieldOptions {\n"); + fprintf(out, " std::vector<int> primaryFields;\n"); + fprintf(out, " int exclusiveField;\n"); + fprintf(out, "};\n"); + fprintf(out, "\n"); + + fprintf(out, "struct AtomsInfo {\n"); + fprintf(out, + " const static std::set<int> " + "kTruncatingTimestampAtomBlackList;\n"); + fprintf(out, " const static std::map<int, int> kAtomsWithUidField;\n"); + fprintf(out, + " const static std::set<int> kAtomsWithAttributionChain;\n"); + fprintf(out, + " const static std::map<int, StateAtomFieldOptions> " + "kStateAtomsFieldOptions;\n"); + fprintf(out, + " const static std::map<int, std::vector<int>> " + "kBytesFieldAtoms;\n"); + fprintf(out, + " const static std::set<int> kWhitelistedAtoms;\n"); + fprintf(out, "};\n"); + fprintf(out, "const static int kMaxPushedAtomId = %d;\n\n", atoms.maxPushedAtomId); + +} + +static void write_atoms_info_cpp_body(FILE* out, const Atoms& atoms) { + std::set<string> kTruncatingAtomNames = {"mobile_radio_power_state_changed", + "audio_state_changed", + "call_state_changed", + "phone_signal_strength_changed", + "mobile_bytes_transfer_by_fg_bg", + "mobile_bytes_transfer"}; + fprintf(out, + "const std::set<int> " + "AtomsInfo::kTruncatingTimestampAtomBlackList = {\n"); + for (set<string>::const_iterator blacklistedAtom = kTruncatingAtomNames.begin(); + blacklistedAtom != kTruncatingAtomNames.end(); blacklistedAtom++) { + fprintf(out, " %s,\n", make_constant_name(*blacklistedAtom).c_str()); + } + fprintf(out, "};\n"); + fprintf(out, "\n"); + + fprintf(out, + "const std::set<int> AtomsInfo::kAtomsWithAttributionChain = {\n"); + for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); + atom != atoms.decls.end(); atom++) { + for (vector<AtomField>::const_iterator field = atom->fields.begin(); + field != atom->fields.end(); field++) { + if (field->javaType == JAVA_TYPE_ATTRIBUTION_CHAIN) { + string constant = make_constant_name(atom->name); + fprintf(out, " %s,\n", constant.c_str()); + break; + } + } + } + + fprintf(out, "};\n"); + fprintf(out, "\n"); + + fprintf(out, + "const std::set<int> AtomsInfo::kWhitelistedAtoms = {\n"); + for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); + atom != atoms.decls.end(); atom++) { + if (atom->whitelisted) { + string constant = make_constant_name(atom->name); + fprintf(out, " %s,\n", constant.c_str()); + } + } + + fprintf(out, "};\n"); + fprintf(out, "\n"); + + fprintf(out, "static std::map<int, int> getAtomUidField() {\n"); + fprintf(out, " std::map<int, int> uidField;\n"); + for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); + atom != atoms.decls.end(); atom++) { + if (atom->uidField == 0) { + continue; + } + fprintf(out, + "\n // Adding uid field for atom " + "(%d)%s\n", + atom->code, atom->name.c_str()); + fprintf(out, " uidField[static_cast<int>(%s)] = %d;\n", + make_constant_name(atom->name).c_str(), atom->uidField); + } + + fprintf(out, " return uidField;\n"); + fprintf(out, "};\n"); + + fprintf(out, + "const std::map<int, int> AtomsInfo::kAtomsWithUidField = " + "getAtomUidField();\n"); + + fprintf(out, + "static std::map<int, StateAtomFieldOptions> " + "getStateAtomFieldOptions() {\n"); + fprintf(out, " std::map<int, StateAtomFieldOptions> options;\n"); + fprintf(out, " StateAtomFieldOptions opt;\n"); + for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); + atom != atoms.decls.end(); atom++) { + if (atom->primaryFields.size() == 0 && atom->exclusiveField == 0) { + continue; + } + fprintf(out, + "\n // Adding primary and exclusive fields for atom " + "(%d)%s\n", + atom->code, atom->name.c_str()); + fprintf(out, " opt.primaryFields.clear();\n"); + for (const auto& field : atom->primaryFields) { + fprintf(out, " opt.primaryFields.push_back(%d);\n", field); + } + + fprintf(out, " opt.exclusiveField = %d;\n", atom->exclusiveField); + fprintf(out, " options[static_cast<int>(%s)] = opt;\n", + make_constant_name(atom->name).c_str()); + } + + fprintf(out, " return options;\n"); + fprintf(out, "}\n"); + + fprintf(out, + "const std::map<int, StateAtomFieldOptions> " + "AtomsInfo::kStateAtomsFieldOptions = " + "getStateAtomFieldOptions();\n"); + + fprintf(out, + "static std::map<int, std::vector<int>> " + "getBinaryFieldAtoms() {\n"); + fprintf(out, " std::map<int, std::vector<int>> options;\n"); + for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); + atom != atoms.decls.end(); atom++) { + if (atom->binaryFields.size() == 0) { + continue; + } + fprintf(out, + "\n // Adding binary fields for atom " + "(%d)%s\n", + atom->code, atom->name.c_str()); + + for (const auto& field : atom->binaryFields) { + fprintf(out, " options[static_cast<int>(%s)].push_back(%d);\n", + make_constant_name(atom->name).c_str(), field); + } + } + + fprintf(out, " return options;\n"); + fprintf(out, "}\n"); + + fprintf(out, + "const std::map<int, std::vector<int>> " + "AtomsInfo::kBytesFieldAtoms = " + "getBinaryFieldAtoms();\n"); + +} + +int write_atoms_info_header(FILE* out, const Atoms &atoms, const string& namespaceStr) { + // Print prelude + fprintf(out, "// This file is autogenerated\n"); + fprintf(out, "\n"); + fprintf(out, "#pragma once\n"); + fprintf(out, "\n"); + fprintf(out, "#include <vector>\n"); + fprintf(out, "#include <map>\n"); + fprintf(out, "#include <set>\n"); + fprintf(out, "\n"); + + write_namespace(out, namespaceStr); + + write_atoms_info_header_body(out, atoms); + + fprintf(out, "\n"); + write_closing_namespace(out, namespaceStr); + + return 0; +} + +int write_atoms_info_cpp(FILE *out, const Atoms &atoms, const string& namespaceStr, + const string& importHeader, const string& statslogHeader) { + // Print prelude + fprintf(out, "// This file is autogenerated\n"); + fprintf(out, "\n"); + fprintf(out, "#include <%s>\n", importHeader.c_str()); + fprintf(out, "#include <%s>\n", statslogHeader.c_str()); + fprintf(out, "\n"); + + write_namespace(out, namespaceStr); + + write_atoms_info_cpp_body(out, atoms); + + // Print footer + fprintf(out, "\n"); + write_closing_namespace(out, namespaceStr); + + return 0; +} + +} // namespace stats_log_api_gen +} // namespace android diff --git a/tools/stats_log_api_gen/atoms_info_writer.h b/tools/stats_log_api_gen/atoms_info_writer.h new file mode 100644 index 000000000000..bc677825181f --- /dev/null +++ b/tools/stats_log_api_gen/atoms_info_writer.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2019, 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. + */ + +#pragma once + +#include "Collation.h" + +#include <stdio.h> +#include <string.h> + +namespace android { +namespace stats_log_api_gen { + +using namespace std; + +int write_atoms_info_cpp(FILE* out, const Atoms& atoms, const string& namespaceStr, + const string& importHeader, const string& statslogHeader +); + +int write_atoms_info_header(FILE* out, const Atoms& atoms, const string& namespaceStr); + +} // namespace stats_log_api_gen +} // namespace android diff --git a/tools/stats_log_api_gen/main.cpp b/tools/stats_log_api_gen/main.cpp index bc6d82ad267c..ad171da0511c 100644 --- a/tools/stats_log_api_gen/main.cpp +++ b/tools/stats_log_api_gen/main.cpp @@ -1,6 +1,7 @@ #include "Collation.h" +#include "atoms_info_writer.h" #if !defined(STATS_SCHEMA_LEGACY) #include "java_writer.h" #endif @@ -18,8 +19,6 @@ #include <stdlib.h> #include <string.h> -#include "android-base/strings.h" - using namespace google::protobuf; using namespace std; @@ -28,152 +27,6 @@ namespace stats_log_api_gen { using android::os::statsd::Atom; -static void write_atoms_info_cpp(FILE *out, const Atoms &atoms) { - std::set<string> kTruncatingAtomNames = {"mobile_radio_power_state_changed", - "audio_state_changed", - "call_state_changed", - "phone_signal_strength_changed", - "mobile_bytes_transfer_by_fg_bg", - "mobile_bytes_transfer"}; - fprintf(out, - "const std::set<int> " - "AtomsInfo::kTruncatingTimestampAtomBlackList = {\n"); - for (set<string>::const_iterator blacklistedAtom = kTruncatingAtomNames.begin(); - blacklistedAtom != kTruncatingAtomNames.end(); blacklistedAtom++) { - fprintf(out, " %s,\n", make_constant_name(*blacklistedAtom).c_str()); - } - fprintf(out, "};\n"); - fprintf(out, "\n"); - - fprintf(out, - "const std::set<int> AtomsInfo::kAtomsWithAttributionChain = {\n"); - for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - atom != atoms.decls.end(); atom++) { - for (vector<AtomField>::const_iterator field = atom->fields.begin(); - field != atom->fields.end(); field++) { - if (field->javaType == JAVA_TYPE_ATTRIBUTION_CHAIN) { - string constant = make_constant_name(atom->name); - fprintf(out, " %s,\n", constant.c_str()); - break; - } - } - } - - fprintf(out, "};\n"); - fprintf(out, "\n"); - - fprintf(out, - "const std::set<int> AtomsInfo::kWhitelistedAtoms = {\n"); - for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - atom != atoms.decls.end(); atom++) { - if (atom->whitelisted) { - string constant = make_constant_name(atom->name); - fprintf(out, " %s,\n", constant.c_str()); - } - } - - fprintf(out, "};\n"); - fprintf(out, "\n"); - - fprintf(out, "static std::map<int, int> getAtomUidField() {\n"); - fprintf(out, " std::map<int, int> uidField;\n"); - for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - atom != atoms.decls.end(); atom++) { - if (atom->uidField == 0) { - continue; - } - fprintf(out, - "\n // Adding uid field for atom " - "(%d)%s\n", - atom->code, atom->name.c_str()); - fprintf(out, " uidField[static_cast<int>(%s)] = %d;\n", - make_constant_name(atom->name).c_str(), atom->uidField); - } - - fprintf(out, " return uidField;\n"); - fprintf(out, "};\n"); - - fprintf(out, - "const std::map<int, int> AtomsInfo::kAtomsWithUidField = " - "getAtomUidField();\n"); - - fprintf(out, - "static std::map<int, StateAtomFieldOptions> " - "getStateAtomFieldOptions() {\n"); - fprintf(out, " std::map<int, StateAtomFieldOptions> options;\n"); - fprintf(out, " StateAtomFieldOptions opt;\n"); - for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - atom != atoms.decls.end(); atom++) { - if (atom->primaryFields.size() == 0 && atom->exclusiveField == 0) { - continue; - } - fprintf(out, - "\n // Adding primary and exclusive fields for atom " - "(%d)%s\n", - atom->code, atom->name.c_str()); - fprintf(out, " opt.primaryFields.clear();\n"); - for (const auto& field : atom->primaryFields) { - fprintf(out, " opt.primaryFields.push_back(%d);\n", field); - } - - fprintf(out, " opt.exclusiveField = %d;\n", atom->exclusiveField); - fprintf(out, " options[static_cast<int>(%s)] = opt;\n", - make_constant_name(atom->name).c_str()); - } - - fprintf(out, " return options;\n"); - fprintf(out, "}\n"); - - fprintf(out, - "const std::map<int, StateAtomFieldOptions> " - "AtomsInfo::kStateAtomsFieldOptions = " - "getStateAtomFieldOptions();\n"); - - fprintf(out, - "static std::map<int, std::vector<int>> " - "getBinaryFieldAtoms() {\n"); - fprintf(out, " std::map<int, std::vector<int>> options;\n"); - for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - atom != atoms.decls.end(); atom++) { - if (atom->binaryFields.size() == 0) { - continue; - } - fprintf(out, - "\n // Adding binary fields for atom " - "(%d)%s\n", - atom->code, atom->name.c_str()); - - for (const auto& field : atom->binaryFields) { - fprintf(out, " options[static_cast<int>(%s)].push_back(%d);\n", - make_constant_name(atom->name).c_str(), field); - } - } - - fprintf(out, " return options;\n"); - fprintf(out, "}\n"); - - fprintf(out, - "const std::map<int, std::vector<int>> " - "AtomsInfo::kBytesFieldAtoms = " - "getBinaryFieldAtoms();\n"); -} - -// Writes namespaces for the cpp and header files, returning the number of namespaces written. -void write_namespace(FILE* out, const string& cppNamespaces) { - vector<string> cppNamespaceVec = android::base::Split(cppNamespaces, ","); - for (string cppNamespace : cppNamespaceVec) { - fprintf(out, "namespace %s {\n", cppNamespace.c_str()); - } -} - -// Writes namespace closing brackets for cpp and header files. -void write_closing_namespace(FILE* out, const string& cppNamespaces) { - vector<string> cppNamespaceVec = android::base::Split(cppNamespaces, ","); - for (auto it = cppNamespaceVec.rbegin(); it != cppNamespaceVec.rend(); ++it) { - fprintf(out, "} // namespace %s\n", it->c_str()); - } -} - static int write_stats_log_cpp(FILE *out, const Atoms &atoms, const AtomDecl &attributionDecl, const string& moduleName, const string& cppNamespace, const string& importHeader) { @@ -202,11 +55,6 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, const AtomDecl &at fprintf(out, "const static bool kStatsdEnabled = false;\n"); fprintf(out, "#endif\n"); - // AtomsInfo is only used by statsd internally and is not needed for other modules. - if (moduleName == DEFAULT_MODULE_NAME) { - write_atoms_info_cpp(out, atoms); - } - fprintf(out, "int64_t lastRetryTimestampNs = -1;\n"); fprintf(out, "const int64_t kMinRetryIntervalNs = NS_PER_SEC * 60 * 20; // 20 minutes\n"); fprintf(out, "static std::mutex mLogdRetryMutex;\n"); @@ -543,42 +391,6 @@ static int write_stats_log_cpp(FILE *out, const Atoms &atoms, const AtomDecl &at return 0; } -static void write_cpp_usage( - FILE* out, const string& method_name, const string& atom_code_name, - const AtomDecl& atom, const AtomDecl &attributionDecl) { - fprintf(out, " * Usage: %s(StatsLog.%s", method_name.c_str(), - atom_code_name.c_str()); - - for (vector<AtomField>::const_iterator field = atom.fields.begin(); - field != atom.fields.end(); field++) { - if (field->javaType == JAVA_TYPE_ATTRIBUTION_CHAIN) { - for (auto chainField : attributionDecl.fields) { - if (chainField.javaType == JAVA_TYPE_STRING) { - fprintf(out, ", const std::vector<%s>& %s", - cpp_type_name(chainField.javaType), - chainField.name.c_str()); - } else { - fprintf(out, ", const %s* %s, size_t %s_length", - cpp_type_name(chainField.javaType), - chainField.name.c_str(), chainField.name.c_str()); - } - } - } else if (field->javaType == JAVA_TYPE_KEY_VALUE_PAIR) { - fprintf(out, ", const std::map<int, int32_t>& %s_int" - ", const std::map<int, int64_t>& %s_long" - ", const std::map<int, char const*>& %s_str" - ", const std::map<int, float>& %s_float", - field->name.c_str(), - field->name.c_str(), - field->name.c_str(), - field->name.c_str()); - } else { - fprintf(out, ", %s %s", cpp_type_name(field->javaType), field->name.c_str()); - } - } - fprintf(out, ");\n"); -} - static void write_cpp_method_header( FILE* out, const string& method_name, @@ -645,45 +457,8 @@ write_stats_log_header(FILE* out, const Atoms& atoms, const AtomDecl &attributio fprintf(out, " * API For logging statistics events.\n"); fprintf(out, " */\n"); fprintf(out, "\n"); - fprintf(out, "/**\n"); - fprintf(out, " * Constants for atom codes.\n"); - fprintf(out, " */\n"); - fprintf(out, "enum {\n"); - std::map<int, set<AtomDecl>::const_iterator> atom_code_to_non_chained_decl_map; - build_non_chained_decl_map(atoms, &atom_code_to_non_chained_decl_map); - - size_t i = 0; - int maxPushedAtomId = 2; - // Print atom constants - for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - atom != atoms.decls.end(); atom++) { - // Skip if the atom is not needed for the module. - if (!atom_needed_for_module(*atom, moduleName)) { - continue; - } - string constant = make_constant_name(atom->name); - fprintf(out, "\n"); - fprintf(out, " /**\n"); - fprintf(out, " * %s %s\n", atom->message.c_str(), atom->name.c_str()); - write_cpp_usage(out, "stats_write", constant, *atom, attributionDecl); - - auto non_chained_decl = atom_code_to_non_chained_decl_map.find(atom->code); - if (non_chained_decl != atom_code_to_non_chained_decl_map.end()) { - write_cpp_usage(out, "stats_write_non_chained", constant, *non_chained_decl->second, - attributionDecl); - } - fprintf(out, " */\n"); - char const* const comma = (i == atoms.decls.size() - 1) ? "" : ","; - fprintf(out, " %s = %d%s\n", constant.c_str(), atom->code, comma); - if (atom->code < PULL_ATOM_START_ID && atom->code > maxPushedAtomId) { - maxPushedAtomId = atom->code; - } - i++; - } - fprintf(out, "\n"); - fprintf(out, "};\n"); - fprintf(out, "\n"); + write_native_atom_constants(out, atoms, attributionDecl, moduleName); // Print constants for the enum values. fprintf(out, "//\n"); @@ -723,36 +498,6 @@ write_stats_log_header(FILE* out, const Atoms& atoms, const AtomDecl &attributio fprintf(out, "};\n"); fprintf(out, "\n"); - // This metadata is only used by statsd, which uses the default libstatslog. - if (moduleName == DEFAULT_MODULE_NAME) { - - fprintf(out, "struct StateAtomFieldOptions {\n"); - fprintf(out, " std::vector<int> primaryFields;\n"); - fprintf(out, " int exclusiveField;\n"); - fprintf(out, "};\n"); - fprintf(out, "\n"); - - fprintf(out, "struct AtomsInfo {\n"); - fprintf(out, - " const static std::set<int> " - "kTruncatingTimestampAtomBlackList;\n"); - fprintf(out, " const static std::map<int, int> kAtomsWithUidField;\n"); - fprintf(out, - " const static std::set<int> kAtomsWithAttributionChain;\n"); - fprintf(out, - " const static std::map<int, StateAtomFieldOptions> " - "kStateAtomsFieldOptions;\n"); - fprintf(out, - " const static std::map<int, std::vector<int>> " - "kBytesFieldAtoms;"); - fprintf(out, - " const static std::set<int> kWhitelistedAtoms;\n"); - fprintf(out, "};\n"); - - fprintf(out, "const static int kMaxPushedAtomId = %d;\n\n", - maxPushedAtomId); - } - // Print write methods fprintf(out, "//\n"); fprintf(out, "// Write methods\n"); @@ -1235,15 +980,21 @@ print_usage() fprintf(stderr, "usage: stats-log-api-gen OPTIONS\n"); fprintf(stderr, "\n"); fprintf(stderr, "OPTIONS\n"); - fprintf(stderr, " --cpp FILENAME the header file to output\n"); - fprintf(stderr, " --header FILENAME the cpp file to output\n"); + fprintf(stderr, " --cpp FILENAME the header file to output for write helpers\n"); + fprintf(stderr, " --header FILENAME the cpp file to output for write helpers\n"); + fprintf(stderr, + " --atomsInfoCpp FILENAME the header file to output for statsd metadata\n"); + fprintf(stderr, " --atomsInfoHeader FILENAME the cpp file to output for statsd metadata\n"); fprintf(stderr, " --help this message\n"); fprintf(stderr, " --java FILENAME the java file to output\n"); fprintf(stderr, " --jni FILENAME the jni file to output\n"); fprintf(stderr, " --module NAME optional, module name to generate outputs for\n"); fprintf(stderr, " --namespace COMMA,SEP,NAMESPACE required for cpp/header with module\n"); fprintf(stderr, " comma separated namespace of the files\n"); - fprintf(stderr, " --importHeader NAME required for cpp/jni to say which header to import\n"); + fprintf(stderr," --importHeader NAME required for cpp/jni to say which header to import " + "for write helpers\n"); + fprintf(stderr," --atomsInfoImportHeader NAME required for cpp to say which header to import " + "for statsd metadata\n"); fprintf(stderr, " --javaPackage PACKAGE the package for the java file.\n"); fprintf(stderr, " required for java with module\n"); fprintf(stderr, " --javaClass CLASS the class name of the java class.\n"); @@ -1260,10 +1011,13 @@ run(int argc, char const*const* argv) string headerFilename; string javaFilename; string jniFilename; + string atomsInfoCppFilename; + string atomsInfoHeaderFilename; string moduleName = DEFAULT_MODULE_NAME; string cppNamespace = DEFAULT_CPP_NAMESPACE; string cppHeaderImport = DEFAULT_CPP_HEADER_IMPORT; + string atomsInfoCppHeaderImport = DEFAULT_ATOMS_INFO_CPP_HEADER_IMPORT; string javaPackage = DEFAULT_JAVA_PACKAGE; string javaClass = DEFAULT_JAVA_CLASS; @@ -1335,14 +1089,38 @@ run(int argc, char const*const* argv) return 1; } javaClass = argv[index]; + } else if (0 == strcmp("--atomsInfoHeader", argv[index])) { + index++; + if (index >= argc) { + print_usage(); + return 1; + } + atomsInfoHeaderFilename = argv[index]; + } else if (0 == strcmp("--atomsInfoCpp", argv[index])) { + index++; + if (index >= argc) { + print_usage(); + return 1; + } + atomsInfoCppFilename = argv[index]; + } else if (0 == strcmp("--atomsInfoImportHeader", argv[index])) { + index++; + if (index >= argc) { + print_usage(); + return 1; + } + atomsInfoCppHeaderImport = argv[index]; } + index++; } if (cppFilename.size() == 0 && headerFilename.size() == 0 && javaFilename.size() == 0 - && jniFilename.size() == 0) { + && jniFilename.size() == 0 + && atomsInfoHeaderFilename.size() == 0 + && atomsInfoCppFilename.size() == 0) { print_usage(); return 1; } @@ -1359,6 +1137,30 @@ run(int argc, char const*const* argv) collate_atom(android::os::statsd::AttributionNode::descriptor(), &attributionDecl, &attributionSignature); + // Write the atoms info .cpp file + if (atomsInfoCppFilename.size() != 0) { + FILE* out = fopen(atomsInfoCppFilename.c_str(), "w"); + if (out == NULL) { + fprintf(stderr, "Unable to open file for write: %s\n", atomsInfoCppFilename.c_str()); + return 1; + } + errorCount = android::stats_log_api_gen::write_atoms_info_cpp( + out, atoms, cppNamespace, atomsInfoCppHeaderImport, cppHeaderImport); + fclose(out); + } + + // Write the atoms info .h file + if (atomsInfoHeaderFilename.size() != 0) { + FILE* out = fopen(atomsInfoHeaderFilename.c_str(), "w"); + if (out == NULL) { + fprintf(stderr, "Unable to open file for write: %s\n", atomsInfoHeaderFilename.c_str()); + return 1; + } + errorCount = android::stats_log_api_gen::write_atoms_info_header(out, atoms, cppNamespace); + fclose(out); + } + + // Write the .cpp file if (cppFilename.size() != 0) { FILE* out = fopen(cppFilename.c_str(), "w"); diff --git a/tools/stats_log_api_gen/utils.cpp b/tools/stats_log_api_gen/utils.cpp index 141861d443b0..d6cfe95a34f5 100644 --- a/tools/stats_log_api_gen/utils.cpp +++ b/tools/stats_log_api_gen/utils.cpp @@ -16,9 +16,19 @@ #include "utils.h" +#include "android-base/strings.h" + namespace android { namespace stats_log_api_gen { +static void build_non_chained_decl_map(const Atoms& atoms, + std::map<int, set<AtomDecl>::const_iterator>* decl_map) { + for (set<AtomDecl>::const_iterator atom = atoms.non_chained_decls.begin(); + atom != atoms.non_chained_decls.end(); atom++) { + decl_map->insert(std::make_pair(atom->code, atom)); + } +} + /** * Turn lower and camel case into upper case with underscores. */ @@ -102,14 +112,98 @@ bool signature_needed_for_module(const set<string>& modules, const string& modul return modules.find(moduleName) != modules.end(); } -void build_non_chained_decl_map(const Atoms& atoms, - std::map<int, set<AtomDecl>::const_iterator>* decl_map) { - for (set<AtomDecl>::const_iterator atom = atoms.non_chained_decls.begin(); - atom != atoms.non_chained_decls.end(); atom++) { - decl_map->insert(std::make_pair(atom->code, atom)); +// Native +// Writes namespaces for the cpp and header files, returning the number of namespaces written. +void write_namespace(FILE* out, const string& cppNamespaces) { + vector<string> cppNamespaceVec = android::base::Split(cppNamespaces, ","); + for (string cppNamespace : cppNamespaceVec) { + fprintf(out, "namespace %s {\n", cppNamespace.c_str()); } } +// Writes namespace closing brackets for cpp and header files. +void write_closing_namespace(FILE* out, const string& cppNamespaces) { + vector<string> cppNamespaceVec = android::base::Split(cppNamespaces, ","); + for (auto it = cppNamespaceVec.rbegin(); it != cppNamespaceVec.rend(); ++it) { + fprintf(out, "} // namespace %s\n", it->c_str()); + } +} + +static void write_cpp_usage( + FILE* out, const string& method_name, const string& atom_code_name, + const AtomDecl& atom, const AtomDecl &attributionDecl) { + fprintf(out, " * Usage: %s(StatsLog.%s", method_name.c_str(), + atom_code_name.c_str()); + + for (vector<AtomField>::const_iterator field = atom.fields.begin(); + field != atom.fields.end(); field++) { + if (field->javaType == JAVA_TYPE_ATTRIBUTION_CHAIN) { + for (auto chainField : attributionDecl.fields) { + if (chainField.javaType == JAVA_TYPE_STRING) { + fprintf(out, ", const std::vector<%s>& %s", + cpp_type_name(chainField.javaType), + chainField.name.c_str()); + } else { + fprintf(out, ", const %s* %s, size_t %s_length", + cpp_type_name(chainField.javaType), + chainField.name.c_str(), chainField.name.c_str()); + } + } + } else if (field->javaType == JAVA_TYPE_KEY_VALUE_PAIR) { + fprintf(out, ", const std::map<int, int32_t>& %s_int" + ", const std::map<int, int64_t>& %s_long" + ", const std::map<int, char const*>& %s_str" + ", const std::map<int, float>& %s_float", + field->name.c_str(), + field->name.c_str(), + field->name.c_str(), + field->name.c_str()); + } else { + fprintf(out, ", %s %s", cpp_type_name(field->javaType), field->name.c_str()); + } + } + fprintf(out, ");\n"); +} + +void write_native_atom_constants(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl, + const string& moduleName) { + fprintf(out, "/**\n"); + fprintf(out, " * Constants for atom codes.\n"); + fprintf(out, " */\n"); + fprintf(out, "enum {\n"); + + std::map<int, set<AtomDecl>::const_iterator> atom_code_to_non_chained_decl_map; + build_non_chained_decl_map(atoms, &atom_code_to_non_chained_decl_map); + + size_t i = 0; + // Print atom constants + for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); + atom != atoms.decls.end(); atom++) { + // Skip if the atom is not needed for the module. + if (!atom_needed_for_module(*atom, moduleName)) { + continue; + } + string constant = make_constant_name(atom->name); + fprintf(out, "\n"); + fprintf(out, " /**\n"); + fprintf(out, " * %s %s\n", atom->message.c_str(), atom->name.c_str()); + write_cpp_usage(out, "stats_write", constant, *atom, attributionDecl); + + auto non_chained_decl = atom_code_to_non_chained_decl_map.find(atom->code); + if (non_chained_decl != atom_code_to_non_chained_decl_map.end()) { + write_cpp_usage(out, "stats_write_non_chained", constant, *non_chained_decl->second, + attributionDecl); + } + fprintf(out, " */\n"); + char const* const comma = (i == atoms.decls.size() - 1) ? "" : ","; + fprintf(out, " %s = %d%s\n", constant.c_str(), atom->code, comma); + i++; + } + fprintf(out, "\n"); + fprintf(out, "};\n"); + fprintf(out, "\n"); +} + // Java void write_java_atom_codes(FILE* out, const Atoms& atoms, const string& moduleName) { fprintf(out, " // Constants for atom codes.\n"); diff --git a/tools/stats_log_api_gen/utils.h b/tools/stats_log_api_gen/utils.h index e860fa9045cb..a89387f00bce 100644 --- a/tools/stats_log_api_gen/utils.h +++ b/tools/stats_log_api_gen/utils.h @@ -33,6 +33,7 @@ using namespace std; const string DEFAULT_MODULE_NAME = "DEFAULT"; const string DEFAULT_CPP_NAMESPACE = "android,util"; const string DEFAULT_CPP_HEADER_IMPORT = "statslog.h"; +const string DEFAULT_ATOMS_INFO_CPP_HEADER_IMPORT = "atoms_info.h"; const string DEFAULT_JAVA_PACKAGE = "android.util"; const string DEFAULT_JAVA_CLASS = "StatsLogInternal"; @@ -49,8 +50,14 @@ bool atom_needed_for_module(const AtomDecl& atomDecl, const string& moduleName); bool signature_needed_for_module(const set<string>& modules, const string& moduleName); -void build_non_chained_decl_map(const Atoms& atoms, - std::map<int, set<AtomDecl>::const_iterator>* decl_map); +// Common Native helpers +void write_namespace(FILE* out, const string& cppNamespaces); + +void write_closing_namespace(FILE* out, const string& cppNamespaces); + +void write_native_atom_constants(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl, + const string& moduleName +); // Common Java helpers. void write_java_atom_codes(FILE* out, const Atoms& atoms, const string& moduleName); diff --git a/wifi/Android.bp b/wifi/Android.bp index 26064cbc2fb1..08115ecb6b2b 100644 --- a/wifi/Android.bp +++ b/wifi/Android.bp @@ -58,21 +58,6 @@ java_library { } } -metalava_wifi_docs_args = - "--hide-package com.android.server " + - "--error UnhiddenSystemApi " + - "--hide RequiresPermission " + - "--hide MissingPermission " + - "--hide BroadcastBehavior " + - "--hide HiddenSuperclass " + - "--hide DeprecationMismatch " + - "--hide UnavailableSymbol " + - "--hide SdkConstant " + - "--hide HiddenTypeParameter " + - "--hide Todo --hide Typo " + - "--hide HiddenTypedefConstant " + - "--show-annotation android.annotation.SystemApi " - droidstubs { name: "framework-wifi-stubs-srcs", srcs: [ @@ -82,7 +67,7 @@ droidstubs { aidl: { include_dirs: ["frameworks/base/core/java"], }, - args: metalava_wifi_docs_args, + defaults: [ "framework-module-stubs-defaults-systemapi" ], sdk_version: "core_current", libs: ["android_system_stubs_current"], } diff --git a/wifi/tests/Android.mk b/wifi/tests/Android.mk index 3453d6ec827f..d2c385b46eb1 100644 --- a/wifi/tests/Android.mk +++ b/wifi/tests/Android.mk @@ -59,6 +59,8 @@ LOCAL_JAVA_LIBRARIES := \ LOCAL_PACKAGE_NAME := FrameworksWifiApiTests LOCAL_PRIVATE_PLATFORM_APIS := true -LOCAL_COMPATIBILITY_SUITE := device-tests +LOCAL_COMPATIBILITY_SUITE := \ + device-tests \ + mts \ include $(BUILD_PACKAGE) diff --git a/wifi/tests/AndroidTest.xml b/wifi/tests/AndroidTest.xml index cae19e46c6af..987fee79fefd 100644 --- a/wifi/tests/AndroidTest.xml +++ b/wifi/tests/AndroidTest.xml @@ -14,7 +14,7 @@ limitations under the License. --> <configuration description="Runs Frameworks Wifi API Tests."> - <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="test-file-name" value="FrameworksWifiApiTests.apk" /> </target_preparer> diff --git a/wifi/tests/runtests.sh b/wifi/tests/runtests.sh index 7a0dfb08fcfe..4024371dd97d 100755 --- a/wifi/tests/runtests.sh +++ b/wifi/tests/runtests.sh @@ -16,7 +16,6 @@ $ANDROID_BUILD_TOP/build/soong/soong_ui.bash --make-mode MODULES-IN-frameworks-b set -x # print commands -adb root adb wait-for-device TARGET_ARCH=$($ANDROID_BUILD_TOP/build/soong/soong_ui.bash --dumpvar-mode TARGET_ARCH) |