summaryrefslogtreecommitdiff
path: root/android
diff options
context:
space:
mode:
authorJustin Klaassen <justinklaassen@google.com>2018-01-03 13:39:41 -0500
committerJustin Klaassen <justinklaassen@google.com>2018-01-03 13:39:41 -0500
commit98fe7819c6d14f4f464a5cac047f9e82dee5da58 (patch)
treea6b8b93eb21e205b27590ab5e2a1fb9efe27f892 /android
parent4217cf85c20565a3446a662a7f07f26137b26b7f (diff)
downloadandroid-28-98fe7819c6d14f4f464a5cac047f9e82dee5da58.tar.gz
Import Android SDK Platform P [4524038]
/google/data/ro/projects/android/fetch_artifact \ --bid 4524038 \ --target sdk_phone_armv7-win_sdk \ sdk-repo-linux-sources-4524038.zip AndroidVersion.ApiLevel has been modified to appear as 28 Change-Id: Ic193bf1cf0cae78d4f2bfb4fbddfe42025c5c3c2
Diffstat (limited to 'android')
-rw-r--r--android/accessibilityservice/AccessibilityService.java8
-rw-r--r--android/accessibilityservice/AccessibilityServiceInfo.java6
-rw-r--r--android/accounts/AccountManager.java9
-rw-r--r--android/annotation/CallbackExecutor.java41
-rw-r--r--android/annotation/Condemned.java44
-rw-r--r--android/annotation/IntDef.java6
-rw-r--r--android/annotation/LongDef.java62
-rw-r--r--android/annotation/StringDef.java5
-rw-r--r--android/app/ActionBar.java23
-rw-r--r--android/app/Activity.java14
-rw-r--r--android/app/ActivityManager.java117
-rw-r--r--android/app/ActivityManagerInternal.java22
-rw-r--r--android/app/ActivityOptions.java16
-rw-r--r--android/app/ActivityThread.java1006
-rw-r--r--android/app/AlarmManager.java22
-rw-r--r--android/app/AppComponentFactory.java112
-rw-r--r--android/app/AppOpsManager.java25
-rw-r--r--android/app/Application.java2
-rw-r--r--android/app/ApplicationPackageManager.java19
-rw-r--r--android/app/ClientTransactionHandler.java46
-rw-r--r--android/app/ContextImpl.java216
-rw-r--r--android/app/DialogFragment.java4
-rw-r--r--android/app/Fragment.java4
-rw-r--r--android/app/FragmentContainer.java3
-rw-r--r--android/app/FragmentController.java3
-rw-r--r--android/app/FragmentHostCallback.java3
-rw-r--r--android/app/FragmentManager.java15
-rw-r--r--android/app/FragmentManagerNonConfig.java3
-rw-r--r--android/app/FragmentTransaction.java10
-rw-r--r--android/app/Instrumentation.java28
-rw-r--r--android/app/KeyguardManager.java8
-rw-r--r--android/app/LauncherActivity.java2
-rw-r--r--android/app/ListFragment.java4
-rw-r--r--android/app/LoadedApk.java104
-rw-r--r--android/app/LoaderManager.java6
-rw-r--r--android/app/LocalActivityManager.java13
-rw-r--r--android/app/Notification.java77
-rw-r--r--android/app/SharedPreferencesImpl.java133
-rw-r--r--android/app/StatusBarManager.java11
-rw-r--r--android/app/SystemServiceRegistry.java28
-rw-r--r--android/app/UiAutomation.java49
-rw-r--r--android/app/UiAutomationConnection.java7
-rw-r--r--android/app/UiModeManager.java6
-rw-r--r--android/app/VrManager.java19
-rw-r--r--android/app/WallpaperInfo.java60
-rw-r--r--android/app/WallpaperManager.java15
-rw-r--r--android/app/WindowConfiguration.java17
-rw-r--r--android/app/admin/DeviceAdminReceiver.java6
-rw-r--r--android/app/admin/DevicePolicyManager.java628
-rw-r--r--android/app/admin/DevicePolicyManagerInternal.java22
-rw-r--r--android/app/admin/PasswordMetrics.java7
-rw-r--r--android/app/admin/SecurityLog.java67
-rw-r--r--android/app/admin/SystemUpdateInfo.java6
-rw-r--r--android/app/admin/SystemUpdatePolicy.java9
-rw-r--r--android/app/assist/AssistStructure.java10
-rw-r--r--android/app/backup/BackupAgent.java13
-rw-r--r--android/app/backup/BackupManager.java53
-rw-r--r--android/app/backup/BackupManagerMonitor.java7
-rw-r--r--android/app/servertransaction/ActivityConfigurationChangeItem.java42
-rw-r--r--android/app/servertransaction/ActivityLifecycleItem.java28
-rw-r--r--android/app/servertransaction/ActivityResultItem.java44
-rw-r--r--android/app/servertransaction/BaseClientRequest.java18
-rw-r--r--android/app/servertransaction/ClientTransaction.java94
-rw-r--r--android/app/servertransaction/ConfigurationChangeItem.java44
-rw-r--r--android/app/servertransaction/DestroyActivityItem.java44
-rw-r--r--android/app/servertransaction/LaunchActivityItem.java178
-rw-r--r--android/app/servertransaction/MoveToDisplayItem.java47
-rw-r--r--android/app/servertransaction/MultiWindowModeChangeItem.java45
-rw-r--r--android/app/servertransaction/NewIntentItem.java57
-rw-r--r--android/app/servertransaction/ObjectPool.java77
-rw-r--r--android/app/servertransaction/ObjectPoolItem.java29
-rw-r--r--android/app/servertransaction/PauseActivityItem.java97
-rw-r--r--android/app/servertransaction/PendingTransactionActions.java145
-rw-r--r--android/app/servertransaction/PipModeChangeItem.java46
-rw-r--r--android/app/servertransaction/ResumeActivityItem.java94
-rw-r--r--android/app/servertransaction/StopActivityItem.java60
-rw-r--r--android/app/servertransaction/TransactionExecutor.java248
-rw-r--r--android/app/servertransaction/WindowVisibilityItem.java37
-rw-r--r--android/app/slice/Slice.java108
-rw-r--r--android/app/slice/SliceItem.java72
-rw-r--r--android/app/slice/SliceManager.java239
-rw-r--r--android/app/slice/SliceProvider.java93
-rw-r--r--android/app/slice/SliceSpec.java5
-rw-r--r--android/app/timezone/Callback.java11
-rw-r--r--android/app/timezone/RulesManager.java16
-rw-r--r--android/app/timezone/RulesState.java10
-rw-r--r--android/app/usage/AppStandby.java83
-rw-r--r--android/app/usage/NetworkStats.java18
-rw-r--r--android/app/usage/StorageStatsManager.java10
-rw-r--r--android/app/usage/UsageEvents.java7
-rw-r--r--android/app/usage/UsageStatsManager.java149
-rw-r--r--android/app/usage/UsageStatsManagerInternal.java2
-rw-r--r--android/appwidget/AppWidgetManagerInternal.java39
-rw-r--r--android/appwidget/AppWidgetProviderInfo.java61
-rw-r--r--android/arch/core/executor/ArchTaskExecutor.java1
-rw-r--r--android/arch/core/executor/TaskExecutor.java7
-rw-r--r--android/arch/lifecycle/ComputableLiveData.java139
-rw-r--r--android/arch/lifecycle/Lifecycle.java1
-rw-r--r--android/arch/lifecycle/LifecycleRegistry.java1
-rw-r--r--android/arch/lifecycle/LiveData.java410
-rw-r--r--android/arch/lifecycle/LiveDataReactiveStreams.java14
-rw-r--r--android/arch/lifecycle/LiveDataReactiveStreamsTest.java5
-rw-r--r--android/arch/lifecycle/ViewModelProviderTest.java16
-rw-r--r--android/arch/lifecycle/ViewModelProvidersTest.java40
-rw-r--r--android/arch/lifecycle/ViewModelStores.java6
-rw-r--r--android/arch/paging/ContiguousDataSource.java30
-rw-r--r--android/arch/paging/ContiguousPagedList.java6
-rw-r--r--android/arch/paging/DataSource.java57
-rw-r--r--android/arch/paging/ItemKeyedDataSource.java337
-rw-r--r--android/arch/paging/KeyedDataSource.java260
-rw-r--r--android/arch/paging/ListDataSource.java19
-rw-r--r--android/arch/paging/LivePagedListBuilder.java18
-rw-r--r--android/arch/paging/LivePagedListProvider.java90
-rw-r--r--android/arch/paging/PageKeyedDataSource.java391
-rw-r--r--android/arch/paging/PagedList.java42
-rw-r--r--android/arch/paging/PositionalDataSource.java349
-rw-r--r--android/arch/paging/TiledDataSource.java14
-rw-r--r--android/arch/paging/TiledPagedList.java5
-rw-r--r--android/arch/paging/integration/testapp/ItemDataSource.java34
-rw-r--r--android/arch/persistence/db/SupportSQLiteProgram.java6
-rw-r--r--android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java6
-rw-r--r--android/arch/persistence/db/framework/FrameworkSQLiteProgram.java2
-rw-r--r--android/arch/persistence/db/framework/FrameworkSQLiteStatement.java38
-rw-r--r--android/arch/persistence/room/Database.java2
-rw-r--r--android/arch/persistence/room/RoomDatabase.java25
-rw-r--r--android/arch/persistence/room/RoomSQLiteQuery.java2
-rw-r--r--android/arch/persistence/room/integration/testapp/TestDatabase.java23
-rw-r--r--android/arch/persistence/room/integration/testapp/dao/PetDao.java6
-rw-r--r--android/arch/persistence/room/integration/testapp/dao/UserDao.java9
-rw-r--r--android/arch/persistence/room/integration/testapp/dao/UserPetDao.java4
-rw-r--r--android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java26
-rw-r--r--android/arch/persistence/room/integration/testapp/test/ConstructorTest.java62
-rw-r--r--android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java90
-rw-r--r--android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java48
-rw-r--r--android/arch/persistence/room/integration/testapp/test/TestUtil.java10
-rw-r--r--android/arch/persistence/room/integration/testapp/vo/Day.java27
-rw-r--r--android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java36
-rw-r--r--android/arch/persistence/room/integration/testapp/vo/Pet.java27
-rw-r--r--android/arch/persistence/room/integration/testapp/vo/PetWithToyIds.java68
-rw-r--r--android/arch/persistence/room/integration/testapp/vo/User.java25
-rw-r--r--android/arch/persistence/room/integration/testapp/vo/UserAndPetAdoptionDates.java71
-rw-r--r--android/bluetooth/BluetoothAdapter.java466
-rw-r--r--android/bluetooth/BluetoothHeadset.java138
-rw-r--r--android/bluetooth/BluetoothHidDevice.java200
-rw-r--r--android/bluetooth/BluetoothHidDeviceAppConfiguration.java79
-rw-r--r--android/bluetooth/BluetoothHidDeviceAppQosSettings.java37
-rw-r--r--android/bluetooth/BluetoothHidDeviceAppSdpSettings.java25
-rw-r--r--android/bluetooth/BluetoothHidDeviceCallback.java83
-rw-r--r--android/bluetooth/BluetoothPbap.java153
-rw-r--r--android/bluetooth/BluetoothProfile.java26
-rw-r--r--android/bluetooth/le/PeriodicAdvertisingReport.java2
-rw-r--r--android/companion/DeviceFilter.java6
-rw-r--r--android/content/AsyncTaskLoader.java3
-rw-r--r--android/content/ContentResolver.java11
-rw-r--r--android/content/Context.java99
-rw-r--r--android/content/ContextWrapper.java8
-rw-r--r--android/content/CursorLoader.java3
-rw-r--r--android/content/Intent.java57
-rw-r--r--android/content/Loader.java5
-rw-r--r--android/content/QuickViewConstants.java11
-rw-r--r--android/content/ServiceConnection.java17
-rw-r--r--android/content/pm/ActivityInfo.java47
-rw-r--r--android/content/pm/ApplicationInfo.java35
-rw-r--r--android/content/pm/AuxiliaryResolveInfo.java4
-rw-r--r--android/content/pm/InstantAppResolveInfo.java36
-rw-r--r--android/content/pm/LauncherApps.java37
-rw-r--r--android/content/pm/PackageInfo.java95
-rw-r--r--android/content/pm/PackageInfoLite.java28
-rw-r--r--android/content/pm/PackageList.java74
-rw-r--r--android/content/pm/PackageManager.java29
-rw-r--r--android/content/pm/PackageManagerInternal.java37
-rw-r--r--android/content/pm/PackageParser.java320
-rw-r--r--android/content/pm/PermissionInfo.java20
-rw-r--r--android/content/pm/RegisteredServicesCache.java2
-rw-r--r--android/content/pm/SharedLibraryInfo.java31
-rw-r--r--android/content/pm/ShortcutInfo.java24
-rw-r--r--android/content/pm/ShortcutManager.java207
-rw-r--r--android/content/pm/ShortcutServiceInternal.java3
-rw-r--r--android/content/pm/VersionedPackage.java28
-rw-r--r--android/content/pm/crossprofile/CrossProfileApps.java73
-rw-r--r--android/content/pm/dex/ArtManager.java156
-rw-r--r--android/content/res/AssetFileDescriptor.java4
-rw-r--r--android/content/res/Configuration.java37
-rw-r--r--android/content/res/GradientColor.java7
-rw-r--r--android/content/res/XmlResourceParser.java13
-rw-r--r--android/database/sqlite/SQLiteCompatibilityWalFlags.java134
-rw-r--r--android/database/sqlite/SQLiteConnection.java6
-rw-r--r--android/database/sqlite/SQLiteConnectionPool.java6
-rw-r--r--android/database/sqlite/SQLiteDatabase.java4
-rw-r--r--android/graphics/BitmapFactory_Delegate.java12
-rw-r--r--android/graphics/Bitmap_Delegate.java3
-rw-r--r--android/graphics/ImageDecoder.java665
-rw-r--r--android/graphics/Point.java16
-rw-r--r--android/graphics/PostProcess.java91
-rw-r--r--android/graphics/drawable/RippleComponent.java8
-rw-r--r--android/graphics/drawable/RippleDrawable.java9
-rw-r--r--android/graphics/drawable/RippleForeground.java96
-rw-r--r--android/graphics/drawable/VectorDrawable.java9
-rw-r--r--android/graphics/drawable/VectorDrawable_Delegate.java15
-rw-r--r--android/graphics/perftests/PaintMeasureTextTest.java4
-rw-r--r--android/hardware/HardwareBuffer.java13
-rw-r--r--android/hardware/SensorAdditionalInfo.java11
-rw-r--r--android/hardware/SensorDirectChannel.java17
-rw-r--r--android/hardware/camera2/CameraAccessException.java20
-rw-r--r--android/hardware/camera2/CameraCharacteristics.java105
-rw-r--r--android/hardware/camera2/CameraDevice.java21
-rw-r--r--android/hardware/camera2/CameraMetadata.java16
-rw-r--r--android/hardware/camera2/CaptureRequest.java135
-rw-r--r--android/hardware/camera2/CaptureResult.java24
-rw-r--r--android/hardware/camera2/impl/CameraCaptureSessionImpl.java3
-rw-r--r--android/hardware/camera2/impl/CameraDeviceImpl.java70
-rw-r--r--android/hardware/camera2/impl/ICameraDeviceUserWrapper.java6
-rw-r--r--android/hardware/camera2/legacy/CameraDeviceUserShim.java2
-rw-r--r--android/hardware/camera2/params/OutputConfiguration.java3
-rw-r--r--android/hardware/camera2/params/SessionConfiguration.java200
-rw-r--r--android/hardware/display/BrightnessChangeEvent.java20
-rw-r--r--android/hardware/display/BrightnessConfiguration.java175
-rw-r--r--android/hardware/display/DisplayManager.java27
-rw-r--r--android/hardware/display/DisplayManagerGlobal.java18
-rw-r--r--android/hardware/input/InputManager.java6
-rw-r--r--android/hardware/location/ContextHubInfo.java6
-rw-r--r--android/hardware/location/ContextHubManager.java132
-rw-r--r--android/hardware/location/ContextHubTransaction.java170
-rw-r--r--android/hardware/location/NanoAppFilter.java9
-rw-r--r--android/hardware/location/NanoAppInstanceInfo.java167
-rw-r--r--android/hardware/radio/RadioManager.java3
-rw-r--r--android/inputmethodservice/InputMethodService.java86
-rw-r--r--android/inputmethodservice/KeyboardView.java9
-rw-r--r--android/location/GnssMeasurementsEvent.java8
-rw-r--r--android/location/LocalListenerHelper.java9
-rw-r--r--android/location/LocationManager.java41
-rw-r--r--android/location/LocationRequest.java128
-rw-r--r--android/media/AudioAttributes.java7
-rw-r--r--android/media/AudioDeviceInfo.java62
-rw-r--r--android/media/AudioManager.java105
-rw-r--r--android/media/AudioRecord.java55
-rw-r--r--android/media/AudioTrack.java55
-rw-r--r--android/media/MediaDrm.java4
-rw-r--r--android/media/MediaMetadata.java47
-rw-r--r--android/media/MediaMetadataRetriever.java25
-rw-r--r--android/media/MediaPlayer.java33
-rw-r--r--android/media/NativeRoutingEventHandlerDelegate.java51
-rw-r--r--android/media/session/PlaybackState.java3
-rw-r--r--android/media/tv/TvContract.java8
-rw-r--r--android/mtp/MtpDatabase.java1333
-rw-r--r--android/mtp/MtpPropertyGroup.java404
-rw-r--r--android/mtp/MtpPropertyList.java95
-rw-r--r--android/mtp/MtpStorage.java18
-rw-r--r--android/mtp/MtpStorageManager.java1210
-rw-r--r--android/net/ConnectivityManager.java2
-rw-r--r--android/net/IpSecAlgorithm.java72
-rw-r--r--android/net/IpSecManager.java59
-rw-r--r--android/net/IpSecTransform.java22
-rw-r--r--android/net/MacAddress.java276
-rw-r--r--android/net/TrafficStats.java172
-rw-r--r--android/net/ip/ConnectivityPacketTracker.java6
-rw-r--r--android/net/ip/IpClient.java10
-rw-r--r--android/net/ip/IpNeighborMonitor.java236
-rw-r--r--android/net/ip/IpReachabilityMonitor.java386
-rw-r--r--android/net/metrics/DefaultNetworkEvent.java2
-rw-r--r--android/net/metrics/WakeupStats.java7
-rw-r--r--android/net/netlink/NetlinkSocket.java134
-rw-r--r--android/net/netlink/StructNdMsg.java7
-rw-r--r--android/net/util/PacketReader.java (renamed from android/net/util/BlockingSocketReader.java)8
-rw-r--r--android/net/wifi/WifiInfo.java151
-rw-r--r--android/net/wifi/WifiLinkLayerStats.java211
-rw-r--r--android/net/wifi/WifiManager.java55
-rw-r--r--android/net/wifi/WifiScanner.java30
-rw-r--r--android/net/wifi/aware/PeerHandle.java2
-rw-r--r--android/net/wifi/aware/PublishConfig.java9
-rw-r--r--android/net/wifi/aware/SubscribeConfig.java12
-rw-r--r--android/net/wifi/aware/WifiAwareManager.java4
-rw-r--r--android/net/wifi/hotspot2/ProvisioningCallback.java56
-rw-r--r--android/net/wifi/rtt/RangingRequest.java236
-rw-r--r--android/net/wifi/rtt/RangingResult.java36
-rw-r--r--android/net/wifi/rtt/ResponderConfig.java474
-rw-r--r--android/net/wifi/rtt/WifiRttManager.java2
-rw-r--r--android/os/BatteryStats.java152
-rw-r--r--android/os/Binder.java61
-rw-r--r--android/os/Build.java44
-rw-r--r--android/os/Debug.java8
-rw-r--r--android/os/Environment.java79
-rw-r--r--android/os/HandlerExecutor.java45
-rw-r--r--android/os/HardwarePropertiesManager.java16
-rw-r--r--android/os/Message.java20
-rw-r--r--android/os/MessageQueue.java6
-rw-r--r--android/os/PowerManager.java33
-rw-r--r--android/os/PowerManagerInternal.java2
-rw-r--r--android/os/RemoteCallbackList.java17
-rw-r--r--android/os/ServiceManager.java105
-rw-r--r--android/os/StatsLogEventWrapper.java11
-rw-r--r--android/os/UserManager.java122
-rw-r--r--android/os/UserManagerInternal.java3
-rw-r--r--android/os/VintfObject.java14
-rw-r--r--android/os/WorkSource.java434
-rw-r--r--android/os/connectivity/CellularBatteryStats.java242
-rw-r--r--android/os/storage/StorageManager.java8
-rw-r--r--android/os/storage/StorageVolume.java43
-rw-r--r--android/os/storage/VolumeInfo.java20
-rw-r--r--android/print/PrintAttributes.java11
-rw-r--r--android/print/PrintDocumentInfo.java7
-rw-r--r--android/print/PrintJobInfo.java11
-rw-r--r--android/print/PrinterInfo.java7
-rw-r--r--android/privacy/DifferentialPrivacyConfig.java34
-rw-r--r--android/privacy/DifferentialPrivacyEncoder.java78
-rw-r--r--android/privacy/internal/longitudinalreporting/LongitudinalReportingConfig.java107
-rw-r--r--android/privacy/internal/longitudinalreporting/LongitudinalReportingEncoder.java170
-rw-r--r--android/privacy/internal/rappor/RapporConfig.java87
-rw-r--r--android/privacy/internal/rappor/RapporEncoder.java125
-rw-r--r--android/provider/AlarmClock.java1
-rw-r--r--android/provider/CallLog.java17
-rw-r--r--android/provider/ContactsContract.java42
-rw-r--r--android/provider/FontsContract.java16
-rw-r--r--android/provider/MediaStore.java9
-rw-r--r--android/provider/Settings.java172
-rw-r--r--android/provider/Telephony.java6
-rw-r--r--android/provider/VoicemailContract.java18
-rw-r--r--android/security/AttestedKeyPair.java75
-rw-r--r--android/security/Credentials.java34
-rw-r--r--android/security/KeyStore.java24
-rw-r--r--android/security/keymaster/KeyAttestationPackageInfo.java10
-rw-r--r--android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java2
-rw-r--r--android/security/keystore/AndroidKeyStoreProvider.java90
-rw-r--r--android/security/keystore/AndroidKeyStoreSecretKeyFactorySpi.java5
-rw-r--r--android/security/keystore/AndroidKeyStoreSpi.java55
-rw-r--r--android/security/keystore/AttestationUtils.java53
-rw-r--r--android/security/keystore/KeyAttestationException.java46
-rw-r--r--android/security/keystore/KeyGenParameterSpec.java38
-rw-r--r--android/security/keystore/KeyProperties.java34
-rw-r--r--android/security/keystore/ParcelableKeyGenParameterSpec.java185
-rw-r--r--android/security/recoverablekeystore/KeyDerivationParameters.java112
-rw-r--r--android/security/recoverablekeystore/KeyEntryRecoveryData.java90
-rw-r--r--android/security/recoverablekeystore/KeyStoreRecoveryData.java115
-rw-r--r--android/security/recoverablekeystore/KeyStoreRecoveryMetadata.java180
-rw-r--r--android/security/recoverablekeystore/RecoverableKeyStoreLoader.java467
-rw-r--r--android/service/autofill/AutofillService.java45
-rw-r--r--android/service/autofill/Dataset.java21
-rw-r--r--android/service/autofill/EditDistanceScorer.java97
-rw-r--r--android/service/autofill/FieldClassification.java168
-rw-r--r--android/service/autofill/FieldsDetection.java127
-rw-r--r--android/service/autofill/FillEventHistory.java148
-rw-r--r--android/service/autofill/FillRequest.java19
-rw-r--r--android/service/autofill/FillResponse.java197
-rw-r--r--android/service/autofill/InternalScorer.java40
-rw-r--r--android/service/autofill/SaveInfo.java32
-rw-r--r--android/service/autofill/SaveRequest.java5
-rw-r--r--android/service/autofill/Scorer.java28
-rw-r--r--android/service/autofill/UserData.java307
-rw-r--r--android/service/autofill/Validators.java8
-rw-r--r--android/service/carrier/CarrierService.java4
-rw-r--r--android/service/euicc/EuiccService.java26
-rw-r--r--android/service/notification/Condition.java7
-rw-r--r--android/service/notification/NotificationListenerService.java7
-rw-r--r--android/service/notification/ScheduleCalendar.java177
-rw-r--r--android/service/notification/ZenModeConfig.java58
-rw-r--r--android/service/persistentdata/PersistentDataBlockManager.java8
-rw-r--r--android/service/settings/suggestions/Suggestion.java2
-rw-r--r--android/service/trust/TrustAgentService.java18
-rw-r--r--android/service/voice/AlwaysOnHotwordDetector.java30
-rw-r--r--android/service/wallpaper/WallpaperService.java82
-rw-r--r--android/speech/tts/TextToSpeech.java11
-rw-r--r--android/support/LibraryVersions.java16
-rw-r--r--android/support/Version.java41
-rw-r--r--android/support/animation/AnimationHandler.java4
-rw-r--r--android/support/animation/FloatPropertyCompat.java4
-rw-r--r--android/support/annotation/IntDef.java4
-rw-r--r--android/support/annotation/LongDef.java60
-rw-r--r--android/support/car/drawer/CarDrawerActivity.java152
-rw-r--r--android/support/car/drawer/CarDrawerAdapter.java182
-rw-r--r--android/support/car/drawer/CarDrawerController.java335
-rw-r--r--android/support/car/drawer/DrawerItemViewHolder.java87
-rw-r--r--android/support/car/utils/ColumnCalculator.java141
-rw-r--r--android/support/car/widget/CarItemAnimator.java70
-rw-r--r--android/support/car/widget/CarRecyclerView.java142
-rw-r--r--android/support/car/widget/ColumnCardView.java115
-rw-r--r--android/support/car/widget/DayNightStyle.java66
-rw-r--r--android/support/car/widget/PagedLayoutManager.java1687
-rw-r--r--android/support/car/widget/PagedListView.java996
-rw-r--r--android/support/car/widget/PagedScrollBarView.java264
-rw-r--r--android/support/checkapi/UpdateApiTask.java6
-rw-r--r--android/support/design/widget/CoordinatorLayout.java1
-rw-r--r--android/support/graphics/drawable/VectorDrawableCompat.java13
-rw-r--r--android/support/media/ExifInterface.java6
-rw-r--r--android/support/media/ExifInterfaceTest.java898
-rw-r--r--android/support/media/tv/BasePreviewProgram.java2
-rw-r--r--android/support/media/tv/Channel.java2
-rw-r--r--android/support/media/tv/ChannelLogoUtilsTest.java99
-rw-r--r--android/support/media/tv/ChannelTest.java250
-rw-r--r--android/support/media/tv/PreviewProgram.java2
-rw-r--r--android/support/media/tv/PreviewProgramTest.java387
-rw-r--r--android/support/media/tv/Program.java2
-rw-r--r--android/support/media/tv/ProgramTest.java274
-rw-r--r--android/support/media/tv/TvContractUtilsTest.java159
-rw-r--r--android/support/media/tv/Utils.java (renamed from android/support/car/drawer/DrawerItemClickListener.java)21
-rw-r--r--android/support/media/tv/WatchNextProgram.java2
-rw-r--r--android/support/media/tv/WatchNextProgramTest.java365
-rw-r--r--android/support/mediacompat/testlib/util/PollingCheck.java6
-rw-r--r--android/support/mediacompat/testlib/util/TestUtil.java4
-rw-r--r--android/support/text/emoji/EmojiCompat.java68
-rw-r--r--android/support/text/emoji/EmojiMetadata.java5
-rw-r--r--android/support/text/emoji/EmojiProcessor.java72
-rw-r--r--android/support/text/emoji/MetadataListReader.java3
-rw-r--r--android/support/text/emoji/MetadataRepo.java3
-rw-r--r--android/support/text/emoji/widget/EmojiButton.java4
-rw-r--r--android/support/text/emoji/widget/EmojiEditText.java4
-rw-r--r--android/support/text/emoji/widget/EmojiExtractEditText.java4
-rw-r--r--android/support/transition/ArcMotionTest.java203
-rw-r--r--android/support/transition/AutoTransitionTest.java116
-rw-r--r--android/support/transition/BaseTest.java35
-rw-r--r--android/support/transition/BaseTransitionTest.java134
-rw-r--r--android/support/transition/ChangeBoundsTest.java102
-rw-r--r--android/support/transition/ChangeClipBoundsTest.java121
-rw-r--r--android/support/transition/ChangeImageTransformTest.java302
-rw-r--r--android/support/transition/ChangeScrollTest.java76
-rw-r--r--android/support/transition/ChangeTransformTest.java124
-rw-r--r--android/support/transition/CheckCalledRunnable.java35
-rw-r--r--android/support/transition/ExplodeTest.java166
-rw-r--r--android/support/transition/FadeTest.java275
-rw-r--r--android/support/transition/FragmentTransitionTest.java226
-rw-r--r--android/support/transition/PathMotionTest.java62
-rw-r--r--android/support/transition/PatternPathMotionTest.java77
-rw-r--r--android/support/transition/PropagationTest.java101
-rw-r--r--android/support/transition/SceneTest.java127
-rw-r--r--android/support/transition/SlideBadEdgeTest.java78
-rw-r--r--android/support/transition/SlideDefaultEdgeTest.java39
-rw-r--r--android/support/transition/SlideEdgeTest.java273
-rw-r--r--android/support/transition/SyncRunnable.java40
-rw-r--r--android/support/transition/SyncTransitionListener.java87
-rw-r--r--android/support/transition/TransitionActivity.java40
-rw-r--r--android/support/transition/TransitionInflaterTest.java286
-rw-r--r--android/support/transition/TransitionManagerTest.java183
-rw-r--r--android/support/transition/TransitionSetTest.java123
-rw-r--r--android/support/transition/TransitionTest.java442
-rw-r--r--android/support/transition/VisibilityTest.java200
-rw-r--r--android/support/v13/app/ActivityCompat.java26
-rw-r--r--android/support/v13/app/FragmentCompat.java50
-rw-r--r--android/support/v13/app/FragmentPagerAdapter.java45
-rw-r--r--android/support/v13/app/FragmentStatePagerAdapter.java42
-rw-r--r--android/support/v13/app/FragmentTabHost.java51
-rw-r--r--android/support/v17/leanback/app/BrowseFragment.java3
-rw-r--r--android/support/v17/leanback/app/BrowseSupportFragment.java3
-rw-r--r--android/support/v17/leanback/widget/GridLayoutManager.java63
-rw-r--r--android/support/v17/leanback/widget/WindowAlignment.java6
-rw-r--r--android/support/v4/app/ActivityCompat.java15
-rw-r--r--android/support/v4/app/Fragment.java25
-rw-r--r--android/support/v4/app/NotificationCompat.java62
-rw-r--r--android/support/v4/app/NotificationCompatBuilder.java15
-rw-r--r--android/support/v4/app/NotificationManagerCompat.java4
-rw-r--r--android/support/v4/content/res/FontResourcesParserCompat.java26
-rw-r--r--android/support/v4/graphics/TypefaceCompat.java3
-rw-r--r--android/support/v4/graphics/TypefaceCompatApi24Impl.java3
-rw-r--r--android/support/v4/graphics/TypefaceCompatApi26Impl.java14
-rw-r--r--android/support/v4/graphics/drawable/IconCompat.java1
-rw-r--r--android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java4
-rw-r--r--android/support/v4/media/session/MediaControllerCompat.java2
-rw-r--r--android/support/v4/media/session/PlaybackStateCompat.java5
-rw-r--r--android/support/v4/provider/FontsContractCompat.java5
-rw-r--r--android/support/v4/view/ViewConfigurationCompat.java3
-rw-r--r--android/support/v4/widget/DrawerLayout.java3
-rw-r--r--android/support/v4/widget/NestedScrollView.java37
-rw-r--r--android/support/v7/app/AlertController.java53
-rw-r--r--android/support/v7/app/AlertDialog.java52
-rw-r--r--android/support/v7/graphics/BucketTests.java181
-rw-r--r--android/support/v7/graphics/ConsistencyTest.java58
-rw-r--r--android/support/v7/graphics/MaxColorsTest.java56
-rw-r--r--android/support/v7/graphics/SwatchTests.java110
-rw-r--r--android/support/v7/graphics/TestUtils.java41
-rw-r--r--android/support/v7/media/MediaRouter.java10
-rw-r--r--android/support/v7/util/SortedListTest.java24
-rw-r--r--android/support/v7/view/menu/CascadingMenuPopup.java3
-rw-r--r--android/support/v7/widget/ActionMenuView.java4
-rw-r--r--android/support/v7/widget/AdapterHelperTest.java124
-rw-r--r--android/support/v7/widget/AppCompatTextViewAutoSizeHelper.java10
-rw-r--r--android/support/v7/widget/ButtonBarLayout.java8
-rw-r--r--android/support/v7/widget/CardView.java93
-rw-r--r--android/support/v7/widget/LinearLayoutManager.java45
-rw-r--r--android/support/v7/widget/ListPopupWindow.java2
-rw-r--r--android/support/v7/widget/OrientationHelper.java10
-rw-r--r--android/support/v7/widget/RecyclerView.java176
-rw-r--r--android/support/v7/widget/StaggeredGridLayoutManager.java4
-rw-r--r--android/support/v7/widget/TooltipCompat.java35
-rw-r--r--android/support/v7/widget/ViewInfoStoreTest.java11
-rw-r--r--android/support/v7/widget/helper/ItemTouchHelper.java2
-rw-r--r--android/support/wear/ambient/AmbientDelegateTest.java94
-rw-r--r--android/support/wear/ambient/AmbientMode.java2
-rw-r--r--android/support/wear/ambient/AmbientModeResumeTest.java48
-rw-r--r--android/support/wear/ambient/AmbientModeResumeTestActivity.java29
-rw-r--r--android/support/wear/ambient/AmbientModeSupport.java281
-rw-r--r--android/support/wear/ambient/AmbientModeTest.java88
-rw-r--r--android/support/wear/ambient/AmbientModeTestActivity.java62
-rw-r--r--android/support/wear/utils/MetadataTestActivity.java37
-rw-r--r--android/support/wear/widget/BoxInsetLayoutTest.java364
-rw-r--r--android/support/wear/widget/CircularProgressLayoutControllerTest.java119
-rw-r--r--android/support/wear/widget/CircularProgressLayoutTest.java109
-rw-r--r--android/support/wear/widget/LayoutTestActivity.java37
-rw-r--r--android/support/wear/widget/RoundedDrawableTest.java147
-rw-r--r--android/support/wear/widget/ScrollManagerTest.java202
-rw-r--r--android/support/wear/widget/SwipeDismissFrameLayoutTest.java460
-rw-r--r--android/support/wear/widget/SwipeDismissFrameLayoutTestActivity.java82
-rw-r--r--android/support/wear/widget/SwipeDismissPreferenceFragment.java105
-rw-r--r--android/support/wear/widget/WearableLinearLayoutManagerTest.java160
-rw-r--r--android/support/wear/widget/WearableRecyclerViewTest.java226
-rw-r--r--android/support/wear/widget/WearableRecyclerViewTestActivity.java64
-rw-r--r--android/support/wear/widget/drawer/DrawerTestActivity.java198
-rw-r--r--android/support/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java668
-rw-r--r--android/support/wear/widget/util/ArcSwipe.java176
-rw-r--r--android/support/wear/widget/util/ArcSwipeTest.java70
-rw-r--r--android/support/wear/widget/util/AsyncViewActions.java71
-rw-r--r--android/support/wear/widget/util/MoreViewAssertions.java207
-rw-r--r--android/support/wear/widget/util/WakeLockRule.java57
-rw-r--r--android/system/OsConstants.java1
-rw-r--r--android/telecom/Call.java30
-rw-r--r--android/telecom/Connection.java32
-rw-r--r--android/telecom/ConnectionRequest.java5
-rw-r--r--android/telecom/ConnectionService.java130
-rw-r--r--android/telecom/ConnectionServiceAdapter.java13
-rw-r--r--android/telecom/ConnectionServiceAdapterServant.java9
-rw-r--r--android/telecom/InCallService.java12
-rw-r--r--android/telecom/Phone.java7
-rw-r--r--android/telecom/PhoneAccount.java3
-rw-r--r--android/telecom/RemoteConnectionService.java3
-rw-r--r--android/telecom/TelecomManager.java18
-rw-r--r--android/telephony/CarrierConfigManager.java80
-rw-r--r--android/telephony/CellIdentityGsm.java2
-rw-r--r--android/telephony/CellIdentityLte.java2
-rw-r--r--android/telephony/CellIdentityWcdma.java2
-rw-r--r--android/telephony/DisconnectCause.java40
-rw-r--r--android/telephony/MbmsDownloadSession.java59
-rw-r--r--android/telephony/NetworkScan.java76
-rw-r--r--android/telephony/NetworkScanRequest.java168
-rw-r--r--android/telephony/PhoneStateListener.java4
-rw-r--r--android/telephony/RadioAccessSpecifier.java75
-rw-r--r--android/telephony/RadioNetworkConstants.java1
-rw-r--r--android/telephony/SmsManager.java252
-rw-r--r--android/telephony/SmsMessage.java34
-rw-r--r--android/telephony/TelephonyManager.java460
-rw-r--r--android/telephony/TelephonyScanManager.java8
-rw-r--r--android/telephony/data/DataCallResponse.java267
-rw-r--r--android/telephony/data/InterfaceAddress.java127
-rw-r--r--android/telephony/euicc/EuiccManager.java87
-rw-r--r--android/telephony/ims/feature/ImsFeature.java5
-rw-r--r--android/telephony/ims/feature/MMTelFeature.java5
-rw-r--r--android/telephony/ims/feature/RcsFeature.java5
-rw-r--r--android/telephony/ims/internal/ImsCallSessionListener.java364
-rw-r--r--android/telephony/ims/internal/ImsService.java339
-rw-r--r--android/telephony/ims/internal/SmsImplBase.java260
-rw-r--r--android/telephony/ims/internal/feature/CapabilityChangeRequest.java197
-rw-r--r--android/telephony/ims/internal/feature/ImsFeature.java462
-rw-r--r--android/telephony/ims/internal/feature/MmTelFeature.java495
-rw-r--r--android/telephony/ims/internal/feature/RcsFeature.java59
-rw-r--r--android/telephony/ims/internal/stub/ImsConfigImplBase.java173
-rw-r--r--android/telephony/ims/internal/stub/ImsFeatureConfiguration.java147
-rw-r--r--android/telephony/ims/internal/stub/ImsRegistrationImplBase.java276
-rw-r--r--android/telephony/ims/stub/ImsConfigImplBase.java265
-rw-r--r--android/telephony/ims/stub/ImsUtImplBase.java18
-rw-r--r--android/telephony/ims/stub/ImsUtListenerImplBase.java7
-rw-r--r--android/telephony/mbms/ServiceInfo.java4
-rw-r--r--android/test/mock/MockContext.java11
-rw-r--r--android/test/mock/MockPackageManager.java9
-rw-r--r--android/text/AutoGrowArray.java374
-rw-r--r--android/text/DynamicLayout.java43
-rw-r--r--android/text/FontConfig.java6
-rw-r--r--android/text/Layout.java55
-rw-r--r--android/text/MeasuredText.java724
-rw-r--r--android/text/MeasuredText_Delegate.java178
-rw-r--r--android/text/PremeasuredText.java272
-rw-r--r--android/text/StaticLayout.java235
-rw-r--r--android/text/StaticLayoutPerfTest.java223
-rw-r--r--android/text/StaticLayout_Delegate.java76
-rw-r--r--android/text/TextUtils.java67
-rw-r--r--android/transition/Visibility.java5
-rw-r--r--android/util/FeatureFlagUtils.java22
-rw-r--r--android/util/KeyValueListParser.java28
-rw-r--r--android/util/Log.java295
-rw-r--r--android/util/LruCache.java25
-rw-r--r--android/util/Pools.java13
-rw-r--r--android/util/SparseBooleanArray.java6
-rw-r--r--android/util/StatsLog.java70
-rw-r--r--android/util/StatsManager.java29
-rw-r--r--android/util/apk/ApkSignatureSchemeV2Verifier.java890
-rw-r--r--android/util/apk/ApkSignatureSchemeV3Verifier.java558
-rw-r--r--android/util/apk/ApkSignatureVerifier.java381
-rw-r--r--android/util/apk/ApkSigningBlockUtils.java663
-rw-r--r--android/util/apk/SignatureNotFoundException.java34
-rw-r--r--android/util/apk/VerbatimX509Certificate.java38
-rw-r--r--android/util/apk/WrappedX509Certificate.java175
-rw-r--r--android/util/jar/StrictJarVerifier.java29
-rw-r--r--android/view/Choreographer.java14
-rw-r--r--android/view/Display.java8
-rw-r--r--android/view/DisplayCutout.java145
-rw-r--r--android/view/FrameInfo.java4
-rw-r--r--android/view/GestureDetector.java289
-rw-r--r--android/view/IWindowManagerImpl.java6
-rw-r--r--android/view/Surface.java15
-rw-r--r--android/view/SurfaceControl.java180
-rw-r--r--android/view/SurfaceView.java1142
-rw-r--r--android/view/ThreadedRenderer.java29
-rw-r--r--android/view/View.java211
-rw-r--r--android/view/ViewConfiguration.java12
-rw-r--r--android/view/ViewRootImpl.java324
-rw-r--r--android/view/View_Delegate.java49
-rw-r--r--android/view/WindowInsets.java31
-rw-r--r--android/view/WindowManager.java49
-rw-r--r--android/view/accessibility/AccessibilityInteractionClient.java79
-rw-r--r--android/view/accessibility/AccessibilityManager.java925
-rw-r--r--android/view/accessibility/AccessibilityNodeInfo.java11
-rw-r--r--android/view/accessibility/AccessibilityRequestPreparer.java7
-rw-r--r--android/view/accessibility/AccessibilityWindowInfo.java38
-rw-r--r--android/view/autofill/AutofillManager.java151
-rw-r--r--android/view/autofill/AutofillPopupWindow.java11
-rw-r--r--android/view/autofill/AutofillValue.java2
-rw-r--r--android/view/autofill/Helper.java51
-rw-r--r--android/view/inputmethod/InputMethodInfo.java28
-rw-r--r--android/view/inputmethod/InputMethodManager.java206
-rw-r--r--android/view/inputmethod/InputMethodManagerInternal.java7
-rw-r--r--android/view/textclassifier/EntityConfidence.java57
-rw-r--r--android/view/textclassifier/TextClassification.java394
-rw-r--r--android/view/textclassifier/TextClassifier.java99
-rw-r--r--android/view/textclassifier/TextClassifierImpl.java163
-rw-r--r--android/view/textclassifier/TextLinks.java46
-rw-r--r--android/view/textclassifier/TextSelection.java72
-rw-r--r--android/view/textclassifier/logging/SmartSelectionEventTracker.java37
-rw-r--r--android/view/textservice/TextServicesManager.java200
-rw-r--r--android/webkit/FilterMethods.java28
-rw-r--r--android/webkit/SafeBrowsingResponse.java35
-rw-r--r--android/webkit/SingleClassAndMethod.java23
-rw-r--r--android/webkit/TracingConfig.java203
-rw-r--r--android/webkit/TracingController.java126
-rw-r--r--android/webkit/TracingFileOutputStream.java63
-rw-r--r--android/webkit/UserPackage.java2
-rw-r--r--android/webkit/WebKitTypeAsMethodParameter.java27
-rw-r--r--android/webkit/WebKitTypeAsMethodReturn.java26
-rw-r--r--android/webkit/WebSettings.java21
-rw-r--r--android/webkit/WebView.java2897
-rw-r--r--android/webkit/WebViewClient.java540
-rw-r--r--android/webkit/WebViewDelegate.java7
-rw-r--r--android/webkit/WebViewFactory.java57
-rw-r--r--android/webkit/WebViewFactoryProvider.java8
-rw-r--r--android/webkit/WebViewLibraryLoader.java9
-rw-r--r--android/widget/DatePicker.java5
-rw-r--r--android/widget/EditText.java5
-rw-r--r--android/widget/Editor.java123
-rw-r--r--android/widget/GridLayout.java10
-rw-r--r--android/widget/GridView.java7
-rw-r--r--android/widget/LinearLayout.java13
-rw-r--r--android/widget/Magnifier.java2
-rw-r--r--android/widget/NumberPicker.java13
-rw-r--r--android/widget/SelectionActionModeHelper.java70
-rw-r--r--android/widget/TextClock.java24
-rw-r--r--android/widget/TextView.java39
-rw-r--r--android/widget/TextViewMetrics.java25
-rw-r--r--android/widget/TimePicker.java5
-rw-r--r--android/widget/Toast.java8
653 files changed, 42913 insertions, 20093 deletions
diff --git a/android/accessibilityservice/AccessibilityService.java b/android/accessibilityservice/AccessibilityService.java
index 8824643d..97dcb90b 100644
--- a/android/accessibilityservice/AccessibilityService.java
+++ b/android/accessibilityservice/AccessibilityService.java
@@ -391,8 +391,12 @@ public abstract class AccessibilityService extends Service {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({SHOW_MODE_AUTO, SHOW_MODE_HIDDEN})
- public @interface SoftKeyboardShowMode {};
+ @IntDef(prefix = { "SHOW_MODE_" }, value = {
+ SHOW_MODE_AUTO,
+ SHOW_MODE_HIDDEN
+ })
+ public @interface SoftKeyboardShowMode {}
+
public static final int SHOW_MODE_AUTO = 0;
public static final int SHOW_MODE_HIDDEN = 1;
diff --git a/android/accessibilityservice/AccessibilityServiceInfo.java b/android/accessibilityservice/AccessibilityServiceInfo.java
index e0d60cd0..06a9b067 100644
--- a/android/accessibilityservice/AccessibilityServiceInfo.java
+++ b/android/accessibilityservice/AccessibilityServiceInfo.java
@@ -16,8 +16,6 @@
package android.accessibilityservice;
-import static android.content.pm.PackageManager.FEATURE_FINGERPRINT;
-
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -49,6 +47,8 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import static android.content.pm.PackageManager.FEATURE_FINGERPRINT;
+
/**
* This class describes an {@link AccessibilityService}. The system notifies an
* {@link AccessibilityService} for {@link android.view.accessibility.AccessibilityEvent}s
@@ -554,7 +554,7 @@ public class AccessibilityServiceInfo implements Parcelable {
}
/**
- * Updates the properties that an AccessibilityService can change dynamically.
+ * Updates the properties that an AccessibilitySerivice can change dynamically.
*
* @param other The info from which to update the properties.
*
diff --git a/android/accounts/AccountManager.java b/android/accounts/AccountManager.java
index bd9c9fa3..782733f5 100644
--- a/android/accounts/AccountManager.java
+++ b/android/accounts/AccountManager.java
@@ -290,8 +290,13 @@ public class AccountManager {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({VISIBILITY_UNDEFINED, VISIBILITY_VISIBLE, VISIBILITY_USER_MANAGED_VISIBLE,
- VISIBILITY_NOT_VISIBLE, VISIBILITY_USER_MANAGED_NOT_VISIBLE})
+ @IntDef(prefix = { "VISIBILITY_" }, value = {
+ VISIBILITY_UNDEFINED,
+ VISIBILITY_VISIBLE,
+ VISIBILITY_USER_MANAGED_VISIBLE,
+ VISIBILITY_NOT_VISIBLE,
+ VISIBILITY_USER_MANAGED_NOT_VISIBLE
+ })
public @interface AccountVisibility {
}
diff --git a/android/annotation/CallbackExecutor.java b/android/annotation/CallbackExecutor.java
new file mode 100644
index 00000000..5671a3d2
--- /dev/null
+++ b/android/annotation/CallbackExecutor.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.annotation;
+
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.content.Context;
+import android.os.AsyncTask;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.concurrent.Executor;
+
+/**
+ * @paramDoc Callback and listener events are dispatched through this
+ * {@link Executor}, providing an easy way to control which thread is
+ * used. To dispatch events through the main thread of your
+ * application, you can use {@link Context#getMainExecutor()}. To
+ * dispatch events through a shared thread pool, you can use
+ * {@link AsyncTask#THREAD_POOL_EXECUTOR}.
+ * @hide
+ */
+@Retention(SOURCE)
+@Target(PARAMETER)
+public @interface CallbackExecutor {
+}
diff --git a/android/annotation/Condemned.java b/android/annotation/Condemned.java
new file mode 100644
index 00000000..186409b3
--- /dev/null
+++ b/android/annotation/Condemned.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.annotation;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PACKAGE;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * A program element annotated &#64;Condemned is one that programmers are
+ * blocked from using, typically because it's about to be completely destroyed.
+ * <p>
+ * This is a stronger version of &#64;Deprecated, and it's typically used to
+ * mark APIs that only existed temporarily in a preview SDK, and which only
+ * continue to exist temporarily to support binary compatibility.
+ *
+ * @hide
+ */
+@Retention(SOURCE)
+@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
+public @interface Condemned {
+}
diff --git a/android/annotation/IntDef.java b/android/annotation/IntDef.java
index 3f9064e4..f84a6765 100644
--- a/android/annotation/IntDef.java
+++ b/android/annotation/IntDef.java
@@ -52,10 +52,12 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
@Target({ANNOTATION_TYPE})
public @interface IntDef {
/** Defines the constant prefix for this element */
- String[] prefix() default "";
+ String[] prefix() default {};
+ /** Defines the constant suffix for this element */
+ String[] suffix() default {};
/** Defines the allowed constants for this element */
- long[] value() default {};
+ int[] value() default {};
/** Defines whether the constants can be used as a flag, or just as an enum (the default) */
boolean flag() default false;
diff --git a/android/annotation/LongDef.java b/android/annotation/LongDef.java
new file mode 100644
index 00000000..8723eef8
--- /dev/null
+++ b/android/annotation/LongDef.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.annotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+/**
+ * Denotes that the annotated long element represents
+ * a logical type and that its value should be one of the explicitly
+ * named constants. If the {@link #flag()} attribute is set to true,
+ * multiple constants can be combined.
+ * <p>
+ * <pre><code>
+ * &#64;Retention(SOURCE)
+ * &#64;LongDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
+ * public @interface NavigationMode {}
+ * public static final long NAVIGATION_MODE_STANDARD = 0;
+ * public static final long NAVIGATION_MODE_LIST = 1;
+ * public static final long NAVIGATION_MODE_TABS = 2;
+ * ...
+ * public abstract void setNavigationMode(@NavigationMode long mode);
+ * &#64;NavigationMode
+ * public abstract long getNavigationMode();
+ * </code></pre>
+ * For a flag, set the flag attribute:
+ * <pre><code>
+ * &#64;LongDef(
+ * flag = true,
+ * value = {NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
+ * </code></pre>
+ *
+ * @hide
+ */
+@Retention(SOURCE)
+@Target({ANNOTATION_TYPE})
+public @interface LongDef {
+ /** Defines the constant prefix for this element */
+ String[] prefix() default "";
+
+ /** Defines the allowed constants for this element */
+ long[] value() default {};
+
+ /** Defines whether the constants can be used as a flag, or just as an enum (the default) */
+ boolean flag() default false;
+}
diff --git a/android/annotation/StringDef.java b/android/annotation/StringDef.java
index d5157c3a..a37535b9 100644
--- a/android/annotation/StringDef.java
+++ b/android/annotation/StringDef.java
@@ -46,6 +46,11 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface StringDef {
+ /** Defines the constant prefix for this element */
+ String[] prefix() default {};
+ /** Defines the constant suffix for this element */
+ String[] suffix() default {};
+
/** Defines the allowed constants for this element */
String[] value() default {};
}
diff --git a/android/app/ActionBar.java b/android/app/ActionBar.java
index 0e8326de..04ff48ce 100644
--- a/android/app/ActionBar.java
+++ b/android/app/ActionBar.java
@@ -95,7 +95,11 @@ import java.lang.annotation.RetentionPolicy;
public abstract class ActionBar {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
+ @IntDef(prefix = { "NAVIGATION_MODE_" }, value = {
+ NAVIGATION_MODE_STANDARD,
+ NAVIGATION_MODE_LIST,
+ NAVIGATION_MODE_TABS
+ })
public @interface NavigationMode {}
/**
@@ -139,15 +143,14 @@ public abstract class ActionBar {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true,
- value = {
- DISPLAY_USE_LOGO,
- DISPLAY_SHOW_HOME,
- DISPLAY_HOME_AS_UP,
- DISPLAY_SHOW_TITLE,
- DISPLAY_SHOW_CUSTOM,
- DISPLAY_TITLE_MULTIPLE_LINES
- })
+ @IntDef(flag = true, prefix = { "DISPLAY_" }, value = {
+ DISPLAY_USE_LOGO,
+ DISPLAY_SHOW_HOME,
+ DISPLAY_HOME_AS_UP,
+ DISPLAY_SHOW_TITLE,
+ DISPLAY_SHOW_CUSTOM,
+ DISPLAY_TITLE_MULTIPLE_LINES
+ })
public @interface DisplayOptions {}
/**
diff --git a/android/app/Activity.java b/android/app/Activity.java
index 03a3631b..aa099eb1 100644
--- a/android/app/Activity.java
+++ b/android/app/Activity.java
@@ -7070,7 +7070,13 @@ public class Activity extends ContextThemeWrapper
mActivityTransitionState.enterReady(this);
}
- final void performRestart() {
+ /**
+ * Restart the activity.
+ * @param start Indicates whether the activity should also be started after restart.
+ * The option to not start immediately is needed in case a transaction with
+ * multiple lifecycle transitions is in progress.
+ */
+ final void performRestart(boolean start) {
mCanEnterPictureInPicture = true;
mFragments.noteStateNotSaved();
@@ -7108,12 +7114,14 @@ public class Activity extends ContextThemeWrapper
"Activity " + mComponent.toShortString() +
" did not call through to super.onRestart()");
}
- performStart();
+ if (start) {
+ performStart();
+ }
}
}
final void performResume() {
- performRestart();
+ performRestart(true /* start */);
mFragments.execPendingActions();
diff --git a/android/app/ActivityManager.java b/android/app/ActivityManager.java
index 02b7f8c5..1adae7a8 100644
--- a/android/app/ActivityManager.java
+++ b/android/app/ActivityManager.java
@@ -175,7 +175,7 @@ public class ActivityManager {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({
+ @IntDef(prefix = { "BUGREPORT_OPTION_" }, value = {
BUGREPORT_OPTION_FULL,
BUGREPORT_OPTION_INTERACTIVE,
BUGREPORT_OPTION_REMOTE,
@@ -457,6 +457,20 @@ public class ActivityManager {
/** @hide User operation call: one of related users cannot be stopped. */
public static final int USER_OP_ERROR_RELATED_USERS_CANNOT_STOP = -4;
+ /**
+ * @hide
+ * Process states, describing the kind of state a particular process is in.
+ * When updating these, make sure to also check all related references to the
+ * constant in code, and update these arrays:
+ *
+ * @see com.android.internal.app.procstats.ProcessState#PROCESS_STATE_TO_STATE
+ * @see com.android.server.am.ProcessList#sProcStateToProcMem
+ * @see com.android.server.am.ProcessList#sFirstAwakePssTimes
+ * @see com.android.server.am.ProcessList#sSameAwakePssTimes
+ * @see com.android.server.am.ProcessList#sTestFirstPssTimes
+ * @see com.android.server.am.ProcessList#sTestSamePssTimes
+ */
+
/** @hide Not a real process state. */
public static final int PROCESS_STATE_UNKNOWN = -1;
@@ -476,35 +490,35 @@ public class ActivityManager {
/** @hide Process is hosting a foreground service. */
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4;
- /** @hide Same as {@link #PROCESS_STATE_TOP} but while device is sleeping. */
- public static final int PROCESS_STATE_TOP_SLEEPING = 5;
-
/** @hide Process is important to the user, and something they are aware of. */
- public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 6;
+ public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 5;
/** @hide Process is important to the user, but not something they are aware of. */
- public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 7;
+ public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 6;
/** @hide Process is in the background transient so we will try to keep running. */
- public static final int PROCESS_STATE_TRANSIENT_BACKGROUND = 8;
+ public static final int PROCESS_STATE_TRANSIENT_BACKGROUND = 7;
/** @hide Process is in the background running a backup/restore operation. */
- public static final int PROCESS_STATE_BACKUP = 9;
-
- /** @hide Process is in the background, but it can't restore its state so we want
- * to try to avoid killing it. */
- public static final int PROCESS_STATE_HEAVY_WEIGHT = 10;
+ public static final int PROCESS_STATE_BACKUP = 8;
/** @hide Process is in the background running a service. Unlike oom_adj, this level
* is used for both the normal running in background state and the executing
* operations state. */
- public static final int PROCESS_STATE_SERVICE = 11;
+ public static final int PROCESS_STATE_SERVICE = 9;
/** @hide Process is in the background running a receiver. Note that from the
* perspective of oom_adj, receivers run at a higher foreground level, but for our
* prioritization here that is not necessary and putting them below services means
* many fewer changes in some process states as they receive broadcasts. */
- public static final int PROCESS_STATE_RECEIVER = 12;
+ public static final int PROCESS_STATE_RECEIVER = 10;
+
+ /** @hide Same as {@link #PROCESS_STATE_TOP} but while device is sleeping. */
+ public static final int PROCESS_STATE_TOP_SLEEPING = 11;
+
+ /** @hide Process is in the background, but it can't restore its state so we want
+ * to try to avoid killing it. */
+ public static final int PROCESS_STATE_HEAVY_WEIGHT = 12;
/** @hide Process is in the background but hosts the home activity. */
public static final int PROCESS_STATE_HOME = 13;
@@ -533,10 +547,10 @@ public class ActivityManager {
// to frameworks/base/core/proto/android/app/activitymanager.proto and the following method must
// be updated to correctly map between them.
/**
- * Maps ActivityManager.PROCESS_STATE_ values to ActivityManagerProto.ProcessState enum.
+ * Maps ActivityManager.PROCESS_STATE_ values to ProcessState enum.
*
* @param amInt a process state of the form ActivityManager.PROCESS_STATE_
- * @return the value of the corresponding android.app.ActivityManagerProto's ProcessState enum.
+ * @return the value of the corresponding ActivityManager's ProcessState enum.
* @hide
*/
public static final int processStateAmToProto(int amInt) {
@@ -810,7 +824,7 @@ public class ActivityManager {
* impose on your application to let the overall system work best. The
* returned value is in megabytes; the baseline Android memory class is
* 16 (which happens to be the Java heap limit of those devices); some
- * device with more memory may return 24 or even higher numbers.
+ * devices with more memory may return 24 or even higher numbers.
*/
public int getMemoryClass() {
return staticGetMemoryClass();
@@ -837,7 +851,7 @@ public class ActivityManager {
* constrained devices, or it may be significantly larger on devices with
* a large amount of available RAM.
*
- * <p>The is the size of the application's Dalvik heap if it has
+ * <p>This is the size of the application's Dalvik heap if it has
* specified <code>android:largeHeap="true"</code> in its manifest.
*/
public int getLargeMemoryClass() {
@@ -1944,15 +1958,17 @@ public class ActivityManager {
* @param animate Whether we should play an animation for the moving the task
* @param initialBounds If the primary stack gets created, it will use these bounds for the
* docked stack. Pass {@code null} to use default bounds.
+ * @param showRecents If the recents activity should be shown on the other side of the task
+ * going into split-screen mode.
* @hide
*/
@TestApi
@RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS)
public void setTaskWindowingModeSplitScreenPrimary(int taskId, int createMode, boolean toTop,
- boolean animate, Rect initialBounds) throws SecurityException {
+ boolean animate, Rect initialBounds, boolean showRecents) throws SecurityException {
try {
getService().setTaskWindowingModeSplitScreenPrimary(taskId, createMode, toTop, animate,
- initialBounds);
+ initialBounds, showRecents);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -2611,7 +2627,7 @@ public class ActivityManager {
Manifest.permission.ACCESS_INSTANT_APPS})
public boolean clearApplicationUserData(String packageName, IPackageDataObserver observer) {
try {
- return getService().clearApplicationUserData(packageName,
+ return getService().clearApplicationUserData(packageName, false,
observer, UserHandle.myUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -2882,13 +2898,13 @@ public class ActivityManager {
public static final int IMPORTANCE_FOREGROUND_SERVICE = 125;
/**
- * Constant for {@link #importance}: This process is running the foreground
- * UI, but the device is asleep so it is not visible to the user. This means
- * the user is not really aware of the process, because they can not see or
- * interact with it, but it is quite important because it what they expect to
- * return to once unlocking the device.
+ * @deprecated Pre-{@link android.os.Build.VERSION_CODES#P} version of
+ * {@link #IMPORTANCE_TOP_SLEEPING}. As of Android
+ * {@link android.os.Build.VERSION_CODES#P}, this is considered much less
+ * important since we want to reduce what apps can do when the screen is off.
*/
- public static final int IMPORTANCE_TOP_SLEEPING = 150;
+ @Deprecated
+ public static final int IMPORTANCE_TOP_SLEEPING_PRE_28 = 150;
/**
* Constant for {@link #importance}: This process is running something
@@ -2940,14 +2956,6 @@ public class ActivityManager {
public static final int IMPORTANCE_CANT_SAVE_STATE_PRE_26 = 170;
/**
- * Constant for {@link #importance}: This process is running an
- * application that can not save its state, and thus can't be killed
- * while in the background.
- * @hide
- */
- public static final int IMPORTANCE_CANT_SAVE_STATE= 270;
-
- /**
* Constant for {@link #importance}: This process is contains services
* that should remain running. These are background services apps have
* started, not something the user is aware of, so they may be killed by
@@ -2957,6 +2965,23 @@ public class ActivityManager {
public static final int IMPORTANCE_SERVICE = 300;
/**
+ * Constant for {@link #importance}: This process is running the foreground
+ * UI, but the device is asleep so it is not visible to the user. Though the
+ * system will try hard to keep its process from being killed, in all other
+ * ways we consider it a kind of cached process, with the limitations that go
+ * along with that state: network access, running background services, etc.
+ */
+ public static final int IMPORTANCE_TOP_SLEEPING = 325;
+
+ /**
+ * Constant for {@link #importance}: This process is running an
+ * application that can not save its state, and thus can't be killed
+ * while in the background. This will be used with apps that have
+ * {@link android.R.attr#cantSaveState} set on their application tag.
+ */
+ public static final int IMPORTANCE_CANT_SAVE_STATE = 350;
+
+ /**
* Constant for {@link #importance}: This process process contains
* cached code that is expendable, not actively running any app components
* we care about.
@@ -2991,16 +3016,16 @@ public class ActivityManager {
return IMPORTANCE_GONE;
} else if (procState >= PROCESS_STATE_HOME) {
return IMPORTANCE_CACHED;
+ } else if (procState == PROCESS_STATE_HEAVY_WEIGHT) {
+ return IMPORTANCE_CANT_SAVE_STATE;
+ } else if (procState >= PROCESS_STATE_TOP_SLEEPING) {
+ return IMPORTANCE_TOP_SLEEPING;
} else if (procState >= PROCESS_STATE_SERVICE) {
return IMPORTANCE_SERVICE;
- } else if (procState > PROCESS_STATE_HEAVY_WEIGHT) {
- return IMPORTANCE_CANT_SAVE_STATE;
} else if (procState >= PROCESS_STATE_TRANSIENT_BACKGROUND) {
return IMPORTANCE_PERCEPTIBLE;
} else if (procState >= PROCESS_STATE_IMPORTANT_FOREGROUND) {
return IMPORTANCE_VISIBLE;
- } else if (procState >= PROCESS_STATE_TOP_SLEEPING) {
- return IMPORTANCE_TOP_SLEEPING;
} else if (procState >= PROCESS_STATE_FOREGROUND_SERVICE) {
return IMPORTANCE_FOREGROUND_SERVICE;
} else {
@@ -3034,6 +3059,8 @@ public class ActivityManager {
switch (importance) {
case IMPORTANCE_PERCEPTIBLE:
return IMPORTANCE_PERCEPTIBLE_PRE_26;
+ case IMPORTANCE_TOP_SLEEPING:
+ return IMPORTANCE_TOP_SLEEPING_PRE_28;
case IMPORTANCE_CANT_SAVE_STATE:
return IMPORTANCE_CANT_SAVE_STATE_PRE_26;
}
@@ -3047,16 +3074,18 @@ public class ActivityManager {
return PROCESS_STATE_NONEXISTENT;
} else if (importance >= IMPORTANCE_CACHED) {
return PROCESS_STATE_HOME;
+ } else if (importance >= IMPORTANCE_CANT_SAVE_STATE) {
+ return PROCESS_STATE_HEAVY_WEIGHT;
+ } else if (importance >= IMPORTANCE_TOP_SLEEPING) {
+ return PROCESS_STATE_TOP_SLEEPING;
} else if (importance >= IMPORTANCE_SERVICE) {
return PROCESS_STATE_SERVICE;
- } else if (importance > IMPORTANCE_CANT_SAVE_STATE) {
- return PROCESS_STATE_HEAVY_WEIGHT;
} else if (importance >= IMPORTANCE_PERCEPTIBLE) {
return PROCESS_STATE_TRANSIENT_BACKGROUND;
} else if (importance >= IMPORTANCE_VISIBLE) {
return PROCESS_STATE_IMPORTANT_FOREGROUND;
- } else if (importance >= IMPORTANCE_TOP_SLEEPING) {
- return PROCESS_STATE_TOP_SLEEPING;
+ } else if (importance >= IMPORTANCE_TOP_SLEEPING_PRE_28) {
+ return PROCESS_STATE_FOREGROUND_SERVICE;
} else if (importance >= IMPORTANCE_FOREGROUND_SERVICE) {
return PROCESS_STATE_FOREGROUND_SERVICE;
} else {
@@ -3835,7 +3864,7 @@ public class ActivityManager {
pw.println();
dumpService(pw, fd, ProcessStats.SERVICE_NAME, new String[] { packageName });
pw.println();
- dumpService(pw, fd, "usagestats", new String[] { "--packages", packageName });
+ dumpService(pw, fd, "usagestats", new String[] { packageName });
pw.println();
dumpService(pw, fd, BatteryStats.SERVICE_NAME, new String[] { packageName });
pw.flush();
diff --git a/android/app/ActivityManagerInternal.java b/android/app/ActivityManagerInternal.java
index d7efa91f..60a5a110 100644
--- a/android/app/ActivityManagerInternal.java
+++ b/android/app/ActivityManagerInternal.java
@@ -99,7 +99,10 @@ public abstract class ActivityManagerInternal {
// Called by the power manager.
public abstract void onWakefulnessChanged(int wakefulness);
- public abstract int startIsolatedProcess(String entryPoint, String[] mainArgs,
+ /**
+ * @return {@code true} if process start is successful, {@code false} otherwise.
+ */
+ public abstract boolean startIsolatedProcess(String entryPoint, String[] mainArgs,
String processName, String abiOverride, int uid, Runnable crashHandler);
/**
@@ -261,6 +264,11 @@ public abstract class ActivityManagerInternal {
public abstract void notifyNetworkPolicyRulesUpdated(int uid, long procStateSeq);
/**
+ * Called after the voice interaction service has changed.
+ */
+ public abstract void notifyActiveVoiceInteractionServiceChanged(ComponentName component);
+
+ /**
* Called after virtual display Id is updated by
* {@link com.android.server.vr.Vr2dDisplay} with a specific
* {@param vr2dDisplayId}.
@@ -299,4 +307,16 @@ public abstract class ActivityManagerInternal {
* @return true if runtime was restarted, false if it's normal boot
*/
public abstract boolean isRuntimeRestarted();
+
+ /**
+ * Returns {@code true} if {@code uid} is running an activity from {@code packageName}.
+ */
+ public abstract boolean hasRunningActivity(int uid, @Nullable String packageName);
+
+ public interface ScreenObserver {
+ public void onAwakeStateChanged(boolean isAwake);
+ public void onKeyguardStateChanged(boolean isShowing);
+ }
+
+ public abstract void registerScreenObserver(ScreenObserver observer);
}
diff --git a/android/app/ActivityOptions.java b/android/app/ActivityOptions.java
index 4a21f5c4..e61c5b7c 100644
--- a/android/app/ActivityOptions.java
+++ b/android/app/ActivityOptions.java
@@ -36,6 +36,7 @@ import android.os.IRemoteCallback;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.ResultReceiver;
+import android.os.UserHandle;
import android.transition.Transition;
import android.transition.TransitionListenerAdapter;
import android.transition.TransitionManager;
@@ -265,6 +266,8 @@ public class ActivityOptions {
public static final int ANIM_CUSTOM_IN_PLACE = 10;
/** @hide */
public static final int ANIM_CLIP_REVEAL = 11;
+ /** @hide */
+ public static final int ANIM_OPEN_CROSS_PROFILE_APPS = 12;
private String mPackageName;
private Rect mLaunchBounds;
@@ -486,6 +489,19 @@ public class ActivityOptions {
}
/**
+ * Creates an {@link ActivityOptions} object specifying an animation where the new activity
+ * is started in another user profile by calling {@link
+ * android.content.pm.crossprofile.CrossProfileApps#startMainActivity(ComponentName, UserHandle)
+ * }.
+ * @hide
+ */
+ public static ActivityOptions makeOpenCrossProfileAppsAnimation() {
+ ActivityOptions options = new ActivityOptions();
+ options.mAnimationType = ANIM_OPEN_CROSS_PROFILE_APPS;
+ return options;
+ }
+
+ /**
* Create an ActivityOptions specifying an animation where a thumbnail
* is scaled from a given position to the new activity window that is
* being started.
diff --git a/android/app/ActivityThread.java b/android/app/ActivityThread.java
index ffd012d9..aaa6bf03 100644
--- a/android/app/ActivityThread.java
+++ b/android/app/ActivityThread.java
@@ -16,6 +16,13 @@
package android.app;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_CREATE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_DESTROY;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_RESUME;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_START;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
+import static android.app.servertransaction.ActivityLifecycleItem.PRE_ON_CREATE;
import static android.view.Display.INVALID_DISPLAY;
import android.annotation.NonNull;
@@ -23,8 +30,12 @@ import android.annotation.Nullable;
import android.app.assist.AssistContent;
import android.app.assist.AssistStructure;
import android.app.backup.BackupAgent;
+import android.app.servertransaction.ActivityLifecycleItem.LifecycleState;
import android.app.servertransaction.ActivityResultItem;
import android.app.servertransaction.ClientTransaction;
+import android.app.servertransaction.PendingTransactionActions;
+import android.app.servertransaction.PendingTransactionActions.StopInfo;
+import android.app.servertransaction.TransactionExecutor;
import android.content.BroadcastReceiver;
import android.content.ComponentCallbacks2;
import android.content.ComponentName;
@@ -69,6 +80,7 @@ import android.os.DropBoxManager;
import android.os.Environment;
import android.os.GraphicsEnvironment;
import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.LocaleList;
import android.os.Looper;
@@ -84,7 +96,6 @@ import android.os.StrictMode;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
-import android.os.TransactionTooLargeException;
import android.os.UserHandle;
import android.provider.BlockedNumberContract;
import android.provider.CalendarContract;
@@ -102,12 +113,12 @@ import android.util.DisplayMetrics;
import android.util.EventLog;
import android.util.Log;
import android.util.LogPrinter;
-import android.util.LogWriter;
import android.util.Pair;
import android.util.PrintWriterPrinter;
import android.util.Slog;
import android.util.SparseIntArray;
import android.util.SuperNotCalledException;
+import android.util.proto.ProtoOutputStream;
import android.view.ContextThemeWrapper;
import android.view.Display;
import android.view.ThreadedRenderer;
@@ -121,6 +132,7 @@ import android.view.WindowManagerGlobal;
import android.webkit.WebView;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IVoiceInteractor;
import com.android.internal.content.ReferrerIntent;
import com.android.internal.os.BinderInternal;
@@ -128,9 +140,9 @@ import com.android.internal.os.RuntimeInit;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FastPrintWriter;
-import com.android.internal.util.IndentingPrintWriter;
import com.android.org.conscrypt.OpenSSLSocketImpl;
import com.android.org.conscrypt.TrustedCertificateStore;
+import com.android.server.am.proto.MemInfoProto;
import dalvik.system.BaseDexClassLoader;
import dalvik.system.CloseGuard;
@@ -161,6 +173,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.TimeZone;
+import java.util.concurrent.Executor;
final class RemoteServiceException extends AndroidRuntimeException {
public RemoteServiceException(String msg) {
@@ -188,7 +201,7 @@ public final class ActivityThread extends ClientTransactionHandler {
private static final boolean DEBUG_BACKUP = false;
public static final boolean DEBUG_CONFIGURATION = false;
private static final boolean DEBUG_SERVICE = false;
- private static final boolean DEBUG_MEMORY_TRIM = false;
+ public static final boolean DEBUG_MEMORY_TRIM = false;
private static final boolean DEBUG_PROVIDER = false;
private static final boolean DEBUG_ORDER = false;
private static final long MIN_TIME_BETWEEN_GCS = 5*1000;
@@ -204,10 +217,6 @@ public final class ActivityThread extends ClientTransactionHandler {
/** Type for IActivityManager.serviceDoneExecuting: done stopping (destroying) service */
public static final int SERVICE_DONE_EXECUTING_STOP = 2;
- // Details for pausing activity.
- private static final int USER_LEAVING = 1;
- private static final int DONT_REPORT = 2;
-
// Whether to invoke an activity callback after delivering new configuration.
private static final boolean REPORT_TO_ACTIVITY = true;
@@ -216,6 +225,12 @@ public final class ActivityThread extends ClientTransactionHandler {
*/
public static final long INVALID_PROC_STATE_SEQ = -1;
+ /**
+ * Identifier for the sequence no. associated with this process start. It will be provided
+ * as one of the arguments when the process starts.
+ */
+ public static final String PROC_START_SEQ_IDENT = "seq=";
+
private final Object mNetworkPolicyLock = new Object();
/**
@@ -235,6 +250,7 @@ public final class ActivityThread extends ClientTransactionHandler {
final ApplicationThread mAppThread = new ApplicationThread();
final Looper mLooper = Looper.myLooper();
final H mH = new H();
+ final Executor mExecutor = new HandlerExecutor(mH);
final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();
// List of new activities (via ActivityRecord.nextIdle) that should
// be reported when next we idle.
@@ -287,12 +303,8 @@ public final class ActivityThread extends ClientTransactionHandler {
final ArrayList<ActivityClientRecord> mRelaunchingActivities = new ArrayList<>();
@GuardedBy("mResourcesManager")
Configuration mPendingConfiguration = null;
- // Because we merge activity relaunch operations we can't depend on the ordering provided by
- // the handler messages. We need to introduce secondary ordering mechanism, which will allow
- // us to drop certain events, if we know that they happened before relaunch we already executed.
- // This represents the order of receiving the request from AM.
- @GuardedBy("mResourcesManager")
- int mLifecycleSeq = 0;
+ // An executor that performs multi-step transactions.
+ private final TransactionExecutor mTransactionExecutor = new TransactionExecutor(this);
private final ResourcesManager mResourcesManager;
@@ -340,8 +352,9 @@ public final class ActivityThread extends ClientTransactionHandler {
Bundle mCoreSettings = null;
- static final class ActivityClientRecord {
- IBinder token;
+ /** Activity client record, used for bookkeeping for the real {@link Activity} instance. */
+ public static final class ActivityClientRecord {
+ public IBinder token;
int ident;
Intent intent;
String referrer;
@@ -353,6 +366,7 @@ public final class ActivityThread extends ClientTransactionHandler {
Activity parent;
String embeddedID;
Activity.NonConfigurationInstances lastNonConfigurationInstances;
+ // TODO(lifecycler): Use mLifecycleState instead.
boolean paused;
boolean stopped;
boolean hideForNow;
@@ -369,13 +383,13 @@ public final class ActivityThread extends ClientTransactionHandler {
ActivityInfo activityInfo;
CompatibilityInfo compatInfo;
- LoadedApk packageInfo;
+ public LoadedApk loadedApk;
List<ResultInfo> pendingResults;
List<ReferrerIntent> pendingIntents;
boolean startsNotResumed;
- boolean isForward;
+ public final boolean isForward;
int pendingConfigChanges;
boolean onlyLocalRequest;
@@ -383,15 +397,42 @@ public final class ActivityThread extends ClientTransactionHandler {
WindowManager mPendingRemoveWindowManager;
boolean mPreserveWindow;
- // Set for relaunch requests, indicates the order number of the relaunch operation, so it
- // can be compared with other lifecycle operations.
- int relaunchSeq = 0;
+ @LifecycleState
+ private int mLifecycleState = PRE_ON_CREATE;
- // Can only be accessed from the UI thread. This represents the latest processed message
- // that is related to lifecycle events/
- int lastProcessedSeq = 0;
+ @VisibleForTesting
+ public ActivityClientRecord() {
+ this.isForward = false;
+ init();
+ }
- ActivityClientRecord() {
+ public ActivityClientRecord(IBinder token, Intent intent, int ident,
+ ActivityInfo info, Configuration overrideConfig, CompatibilityInfo compatInfo,
+ String referrer, IVoiceInteractor voiceInteractor, Bundle state,
+ PersistableBundle persistentState, List<ResultInfo> pendingResults,
+ List<ReferrerIntent> pendingNewIntents, boolean isForward,
+ ProfilerInfo profilerInfo, ClientTransactionHandler client) {
+ this.token = token;
+ this.ident = ident;
+ this.intent = intent;
+ this.referrer = referrer;
+ this.voiceInteractor = voiceInteractor;
+ this.activityInfo = info;
+ this.compatInfo = compatInfo;
+ this.state = state;
+ this.persistentState = persistentState;
+ this.pendingResults = pendingResults;
+ this.pendingIntents = pendingNewIntents;
+ this.isForward = isForward;
+ this.profilerInfo = profilerInfo;
+ this.overrideConfig = overrideConfig;
+ this.loadedApk = client.getLoadedApkNoCheck(activityInfo.applicationInfo,
+ compatInfo);
+ init();
+ }
+
+ /** Common initializer for all constructors. */
+ private void init() {
parent = null;
embeddedID = null;
paused = false;
@@ -408,6 +449,38 @@ public final class ActivityThread extends ClientTransactionHandler {
};
}
+ /** Get the current lifecycle state. */
+ public int getLifecycleState() {
+ return mLifecycleState;
+ }
+
+ /** Update the current lifecycle state for internal bookkeeping. */
+ public void setState(@LifecycleState int newLifecycleState) {
+ mLifecycleState = newLifecycleState;
+ switch (mLifecycleState) {
+ case ON_CREATE:
+ paused = true;
+ stopped = true;
+ break;
+ case ON_START:
+ paused = true;
+ stopped = false;
+ break;
+ case ON_RESUME:
+ paused = false;
+ stopped = false;
+ break;
+ case ON_PAUSE:
+ paused = true;
+ stopped = false;
+ break;
+ case ON_STOP:
+ paused = true;
+ stopped = true;
+ break;
+ }
+ }
+
public boolean isPreHoneycomb() {
if (activity != null) {
return activity.getApplicationInfo().targetSdkVersion
@@ -535,7 +608,7 @@ public final class ActivityThread extends ClientTransactionHandler {
}
static final class AppBindData {
- LoadedApk info;
+ LoadedApk loadedApk;
String processName;
ApplicationInfo appInfo;
List<ProviderInfo> providers;
@@ -1079,7 +1152,7 @@ public final class ActivityThread extends ClientTransactionHandler {
int N = stats.dbStats.size();
if (N > 0) {
pw.println(" DATABASES");
- printRow(pw, " %8s %8s %14s %14s %s", "pgsz", "dbsz", "Lookaside(b)", "cache",
+ printRow(pw, DB_INFO_FORMAT, "pgsz", "dbsz", "Lookaside(b)", "cache",
"Dbname");
for (int i = 0; i < N; i++) {
DbStats dbStats = stats.dbStats.get(i);
@@ -1111,6 +1184,124 @@ public final class ActivityThread extends ClientTransactionHandler {
}
@Override
+ public void dumpMemInfoProto(ParcelFileDescriptor pfd, Debug.MemoryInfo mem,
+ boolean dumpFullInfo, boolean dumpDalvik, boolean dumpSummaryOnly,
+ boolean dumpUnreachable, String[] args) {
+ ProtoOutputStream proto = new ProtoOutputStream(pfd.getFileDescriptor());
+ try {
+ dumpMemInfo(proto, mem, dumpFullInfo, dumpDalvik, dumpSummaryOnly, dumpUnreachable);
+ } finally {
+ proto.flush();
+ IoUtils.closeQuietly(pfd);
+ }
+ }
+
+ private void dumpMemInfo(ProtoOutputStream proto, Debug.MemoryInfo memInfo,
+ boolean dumpFullInfo, boolean dumpDalvik,
+ boolean dumpSummaryOnly, boolean dumpUnreachable) {
+ long nativeMax = Debug.getNativeHeapSize() / 1024;
+ long nativeAllocated = Debug.getNativeHeapAllocatedSize() / 1024;
+ long nativeFree = Debug.getNativeHeapFreeSize() / 1024;
+
+ Runtime runtime = Runtime.getRuntime();
+ runtime.gc(); // Do GC since countInstancesOfClass counts unreachable objects.
+ long dalvikMax = runtime.totalMemory() / 1024;
+ long dalvikFree = runtime.freeMemory() / 1024;
+ long dalvikAllocated = dalvikMax - dalvikFree;
+
+ Class[] classesToCount = new Class[] {
+ ContextImpl.class,
+ Activity.class,
+ WebView.class,
+ OpenSSLSocketImpl.class
+ };
+ long[] instanceCounts = VMDebug.countInstancesOfClasses(classesToCount, true);
+ long appContextInstanceCount = instanceCounts[0];
+ long activityInstanceCount = instanceCounts[1];
+ long webviewInstanceCount = instanceCounts[2];
+ long openSslSocketCount = instanceCounts[3];
+
+ long viewInstanceCount = ViewDebug.getViewInstanceCount();
+ long viewRootInstanceCount = ViewDebug.getViewRootImplCount();
+ int globalAssetCount = AssetManager.getGlobalAssetCount();
+ int globalAssetManagerCount = AssetManager.getGlobalAssetManagerCount();
+ int binderLocalObjectCount = Debug.getBinderLocalObjectCount();
+ int binderProxyObjectCount = Debug.getBinderProxyObjectCount();
+ int binderDeathObjectCount = Debug.getBinderDeathObjectCount();
+ long parcelSize = Parcel.getGlobalAllocSize();
+ long parcelCount = Parcel.getGlobalAllocCount();
+ SQLiteDebug.PagerStats stats = SQLiteDebug.getDatabaseInfo();
+
+ final long mToken = proto.start(MemInfoProto.AppData.PROCESS_MEMORY);
+ proto.write(MemInfoProto.ProcessMemory.PID, Process.myPid());
+ proto.write(MemInfoProto.ProcessMemory.PROCESS_NAME,
+ (mBoundApplication != null) ? mBoundApplication.processName : "unknown");
+ dumpMemInfoTable(proto, memInfo, dumpDalvik, dumpSummaryOnly,
+ nativeMax, nativeAllocated, nativeFree,
+ dalvikMax, dalvikAllocated, dalvikFree);
+ proto.end(mToken);
+
+ final long oToken = proto.start(MemInfoProto.AppData.OBJECTS);
+ proto.write(MemInfoProto.AppData.ObjectStats.VIEW_INSTANCE_COUNT, viewInstanceCount);
+ proto.write(MemInfoProto.AppData.ObjectStats.VIEW_ROOT_INSTANCE_COUNT,
+ viewRootInstanceCount);
+ proto.write(MemInfoProto.AppData.ObjectStats.APP_CONTEXT_INSTANCE_COUNT,
+ appContextInstanceCount);
+ proto.write(MemInfoProto.AppData.ObjectStats.ACTIVITY_INSTANCE_COUNT,
+ activityInstanceCount);
+ proto.write(MemInfoProto.AppData.ObjectStats.GLOBAL_ASSET_COUNT, globalAssetCount);
+ proto.write(MemInfoProto.AppData.ObjectStats.GLOBAL_ASSET_MANAGER_COUNT,
+ globalAssetManagerCount);
+ proto.write(MemInfoProto.AppData.ObjectStats.LOCAL_BINDER_OBJECT_COUNT,
+ binderLocalObjectCount);
+ proto.write(MemInfoProto.AppData.ObjectStats.PROXY_BINDER_OBJECT_COUNT,
+ binderProxyObjectCount);
+ proto.write(MemInfoProto.AppData.ObjectStats.PARCEL_MEMORY_KB, parcelSize / 1024);
+ proto.write(MemInfoProto.AppData.ObjectStats.PARCEL_COUNT, parcelCount);
+ proto.write(MemInfoProto.AppData.ObjectStats.BINDER_OBJECT_DEATH_COUNT,
+ binderDeathObjectCount);
+ proto.write(MemInfoProto.AppData.ObjectStats.OPEN_SSL_SOCKET_COUNT, openSslSocketCount);
+ proto.write(MemInfoProto.AppData.ObjectStats.WEBVIEW_INSTANCE_COUNT,
+ webviewInstanceCount);
+ proto.end(oToken);
+
+ // SQLite mem info
+ final long sToken = proto.start(MemInfoProto.AppData.SQL);
+ proto.write(MemInfoProto.AppData.SqlStats.MEMORY_USED_KB, stats.memoryUsed / 1024);
+ proto.write(MemInfoProto.AppData.SqlStats.PAGECACHE_OVERFLOW_KB,
+ stats.pageCacheOverflow / 1024);
+ proto.write(MemInfoProto.AppData.SqlStats.MALLOC_SIZE_KB, stats.largestMemAlloc / 1024);
+ int n = stats.dbStats.size();
+ for (int i = 0; i < n; i++) {
+ DbStats dbStats = stats.dbStats.get(i);
+
+ final long dToken = proto.start(MemInfoProto.AppData.SqlStats.DATABASES);
+ proto.write(MemInfoProto.AppData.SqlStats.Database.NAME, dbStats.dbName);
+ proto.write(MemInfoProto.AppData.SqlStats.Database.PAGE_SIZE, dbStats.pageSize);
+ proto.write(MemInfoProto.AppData.SqlStats.Database.DB_SIZE, dbStats.dbSize);
+ proto.write(MemInfoProto.AppData.SqlStats.Database.LOOKASIDE_B, dbStats.lookaside);
+ proto.write(MemInfoProto.AppData.SqlStats.Database.CACHE, dbStats.cache);
+ proto.end(dToken);
+ }
+ proto.end(sToken);
+
+ // Asset details.
+ String assetAlloc = AssetManager.getAssetAllocations();
+ if (assetAlloc != null) {
+ proto.write(MemInfoProto.AppData.ASSET_ALLOCATIONS, assetAlloc);
+ }
+
+ // Unreachable native memory
+ if (dumpUnreachable) {
+ int flags = mBoundApplication == null ? 0 : mBoundApplication.appInfo.flags;
+ boolean showContents = (flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0
+ || android.os.Build.IS_DEBUGGABLE;
+ proto.write(MemInfoProto.AppData.UNREACHABLE_MEMORY,
+ Debug.getUnreachableMemory(100, showContents));
+ }
+ }
+
+ @Override
public void dumpGfxInfo(ParcelFileDescriptor pfd, String[] args) {
nDumpGraphicsInfo(pfd.getFileDescriptor());
WindowManagerGlobal.getInstance().dumpGfxInfo(pfd.getFileDescriptor(), args);
@@ -1313,13 +1504,6 @@ public final class ActivityThread extends ClientTransactionHandler {
mAppThread.updateProcessState(processState, fromIpc);
}
- @Override
- public int getLifecycleSeq() {
- synchronized (mResourcesManager) {
- return mLifecycleSeq++;
- }
- }
-
class H extends Handler {
public static final int BIND_APPLICATION = 110;
public static final int EXIT_APPLICATION = 111;
@@ -1584,7 +1768,9 @@ public final class ActivityThread extends ClientTransactionHandler {
(String[]) ((SomeArgs) msg.obj).arg2);
break;
case EXECUTE_TRANSACTION:
- ((ClientTransaction) msg.obj).execute(ActivityThread.this);
+ final ClientTransaction transaction = (ClientTransaction) msg.obj;
+ mTransactionExecutor.execute(transaction);
+ transaction.recycle();
break;
}
Object obj = msg.obj;
@@ -1714,13 +1900,13 @@ public final class ActivityThread extends ClientTransactionHandler {
return mH;
}
- public final LoadedApk getPackageInfo(String packageName, CompatibilityInfo compatInfo,
- int flags) {
- return getPackageInfo(packageName, compatInfo, flags, UserHandle.myUserId());
+ public final LoadedApk getLoadedApkForPackageName(String packageName,
+ CompatibilityInfo compatInfo, int flags) {
+ return getLoadedApkForPackageName(packageName, compatInfo, flags, UserHandle.myUserId());
}
- public final LoadedApk getPackageInfo(String packageName, CompatibilityInfo compatInfo,
- int flags, int userId) {
+ public final LoadedApk getLoadedApkForPackageName(String packageName,
+ CompatibilityInfo compatInfo, int flags, int userId) {
final boolean differentUser = (UserHandle.myUserId() != userId);
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
@@ -1733,13 +1919,13 @@ public final class ActivityThread extends ClientTransactionHandler {
ref = mResourcePackages.get(packageName);
}
- LoadedApk packageInfo = ref != null ? ref.get() : null;
- //Slog.i(TAG, "getPackageInfo " + packageName + ": " + packageInfo);
- //if (packageInfo != null) Slog.i(TAG, "isUptoDate " + packageInfo.mResDir
- // + ": " + packageInfo.mResources.getAssets().isUpToDate());
- if (packageInfo != null && (packageInfo.mResources == null
- || packageInfo.mResources.getAssets().isUpToDate())) {
- if (packageInfo.isSecurityViolation()
+ LoadedApk loadedApk = ref != null ? ref.get() : null;
+ //Slog.i(TAG, "getLoadedApkForPackageName " + packageName + ": " + loadedApk);
+ //if (loadedApk != null) Slog.i(TAG, "isUptoDate " + loadedApk.mResDir
+ // + ": " + loadedApk.mResources.getAssets().isUpToDate());
+ if (loadedApk != null && (loadedApk.mResources == null
+ || loadedApk.mResources.getAssets().isUpToDate())) {
+ if (loadedApk.isSecurityViolation()
&& (flags&Context.CONTEXT_IGNORE_SECURITY) == 0) {
throw new SecurityException(
"Requesting code from " + packageName
@@ -1747,7 +1933,7 @@ public final class ActivityThread extends ClientTransactionHandler {
+ mBoundApplication.processName
+ "/" + mBoundApplication.appInfo.uid);
}
- return packageInfo;
+ return loadedApk;
}
}
@@ -1762,13 +1948,13 @@ public final class ActivityThread extends ClientTransactionHandler {
}
if (ai != null) {
- return getPackageInfo(ai, compatInfo, flags);
+ return getLoadedApk(ai, compatInfo, flags);
}
return null;
}
- public final LoadedApk getPackageInfo(ApplicationInfo ai, CompatibilityInfo compatInfo,
+ public final LoadedApk getLoadedApk(ApplicationInfo ai, CompatibilityInfo compatInfo,
int flags) {
boolean includeCode = (flags&Context.CONTEXT_INCLUDE_CODE) != 0;
boolean securityViolation = includeCode && ai.uid != 0
@@ -1790,16 +1976,17 @@ public final class ActivityThread extends ClientTransactionHandler {
throw new SecurityException(msg);
}
}
- return getPackageInfo(ai, compatInfo, null, securityViolation, includeCode,
+ return getLoadedApk(ai, compatInfo, null, securityViolation, includeCode,
registerPackage);
}
- public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
+ @Override
+ public final LoadedApk getLoadedApkNoCheck(ApplicationInfo ai,
CompatibilityInfo compatInfo) {
- return getPackageInfo(ai, compatInfo, null, false, true, false);
+ return getLoadedApk(ai, compatInfo, null, false, true, false);
}
- public final LoadedApk peekPackageInfo(String packageName, boolean includeCode) {
+ public final LoadedApk peekLoadedApk(String packageName, boolean includeCode) {
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (includeCode) {
@@ -1811,7 +1998,7 @@ public final class ActivityThread extends ClientTransactionHandler {
}
}
- private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
+ private LoadedApk getLoadedApk(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
@@ -1826,35 +2013,35 @@ public final class ActivityThread extends ClientTransactionHandler {
ref = mResourcePackages.get(aInfo.packageName);
}
- LoadedApk packageInfo = ref != null ? ref.get() : null;
- if (packageInfo == null || (packageInfo.mResources != null
- && !packageInfo.mResources.getAssets().isUpToDate())) {
+ LoadedApk loadedApk = ref != null ? ref.get() : null;
+ if (loadedApk == null || (loadedApk.mResources != null
+ && !loadedApk.mResources.getAssets().isUpToDate())) {
if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package "
: "Loading resource-only package ") + aInfo.packageName
+ " (in " + (mBoundApplication != null
? mBoundApplication.processName : null)
+ ")");
- packageInfo =
+ loadedApk =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
if (mSystemThread && "android".equals(aInfo.packageName)) {
- packageInfo.installSystemApplicationInfo(aInfo,
- getSystemContext().mPackageInfo.getClassLoader());
+ loadedApk.installSystemApplicationInfo(aInfo,
+ getSystemContext().mLoadedApk.getClassLoader());
}
if (differentUser) {
// Caching not supported across users
} else if (includeCode) {
mPackages.put(aInfo.packageName,
- new WeakReference<LoadedApk>(packageInfo));
+ new WeakReference<LoadedApk>(loadedApk));
} else {
mResourcePackages.put(aInfo.packageName,
- new WeakReference<LoadedApk>(packageInfo));
+ new WeakReference<LoadedApk>(loadedApk));
}
}
- return packageInfo;
+ return loadedApk;
}
}
@@ -1885,6 +2072,10 @@ public final class ActivityThread extends ClientTransactionHandler {
return mLooper;
}
+ public Executor getExecutor() {
+ return mExecutor;
+ }
+
public Application getApplication() {
return mInitialApplication;
}
@@ -2259,6 +2450,167 @@ public final class ActivityThread extends ClientTransactionHandler {
}
}
+ /**
+ * Dump heap info to proto.
+ *
+ * @param hasSwappedOutPss determines whether to use dirtySwap or dirtySwapPss
+ */
+ private static void dumpMemoryInfo(ProtoOutputStream proto, long fieldId, String name,
+ int pss, int cleanPss, int sharedDirty, int privateDirty,
+ int sharedClean, int privateClean,
+ boolean hasSwappedOutPss, int dirtySwap, int dirtySwapPss) {
+ final long token = proto.start(fieldId);
+
+ proto.write(MemInfoProto.ProcessMemory.MemoryInfo.NAME, name);
+ proto.write(MemInfoProto.ProcessMemory.MemoryInfo.TOTAL_PSS_KB, pss);
+ proto.write(MemInfoProto.ProcessMemory.MemoryInfo.CLEAN_PSS_KB, cleanPss);
+ proto.write(MemInfoProto.ProcessMemory.MemoryInfo.SHARED_DIRTY_KB, sharedDirty);
+ proto.write(MemInfoProto.ProcessMemory.MemoryInfo.PRIVATE_DIRTY_KB, privateDirty);
+ proto.write(MemInfoProto.ProcessMemory.MemoryInfo.SHARED_CLEAN_KB, sharedClean);
+ proto.write(MemInfoProto.ProcessMemory.MemoryInfo.PRIVATE_CLEAN_KB, privateClean);
+ if (hasSwappedOutPss) {
+ proto.write(MemInfoProto.ProcessMemory.MemoryInfo.DIRTY_SWAP_PSS_KB, dirtySwapPss);
+ } else {
+ proto.write(MemInfoProto.ProcessMemory.MemoryInfo.DIRTY_SWAP_KB, dirtySwap);
+ }
+
+ proto.end(token);
+ }
+
+ /**
+ * Dump mem info data to proto.
+ */
+ public static void dumpMemInfoTable(ProtoOutputStream proto, Debug.MemoryInfo memInfo,
+ boolean dumpDalvik, boolean dumpSummaryOnly,
+ long nativeMax, long nativeAllocated, long nativeFree,
+ long dalvikMax, long dalvikAllocated, long dalvikFree) {
+
+ if (!dumpSummaryOnly) {
+ final long nhToken = proto.start(MemInfoProto.ProcessMemory.NATIVE_HEAP);
+ dumpMemoryInfo(proto, MemInfoProto.ProcessMemory.HeapInfo.MEM_INFO, "Native Heap",
+ memInfo.nativePss, memInfo.nativeSwappablePss, memInfo.nativeSharedDirty,
+ memInfo.nativePrivateDirty, memInfo.nativeSharedClean,
+ memInfo.nativePrivateClean, memInfo.hasSwappedOutPss,
+ memInfo.nativeSwappedOut, memInfo.nativeSwappedOutPss);
+ proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_SIZE_KB, nativeMax);
+ proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_ALLOC_KB, nativeAllocated);
+ proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_FREE_KB, nativeFree);
+ proto.end(nhToken);
+
+ final long dvToken = proto.start(MemInfoProto.ProcessMemory.DALVIK_HEAP);
+ dumpMemoryInfo(proto, MemInfoProto.ProcessMemory.HeapInfo.MEM_INFO, "Dalvik Heap",
+ memInfo.dalvikPss, memInfo.dalvikSwappablePss, memInfo.dalvikSharedDirty,
+ memInfo.dalvikPrivateDirty, memInfo.dalvikSharedClean,
+ memInfo.dalvikPrivateClean, memInfo.hasSwappedOutPss,
+ memInfo.dalvikSwappedOut, memInfo.dalvikSwappedOutPss);
+ proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_SIZE_KB, dalvikMax);
+ proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_ALLOC_KB, dalvikAllocated);
+ proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_FREE_KB, dalvikFree);
+ proto.end(dvToken);
+
+ int otherPss = memInfo.otherPss;
+ int otherSwappablePss = memInfo.otherSwappablePss;
+ int otherSharedDirty = memInfo.otherSharedDirty;
+ int otherPrivateDirty = memInfo.otherPrivateDirty;
+ int otherSharedClean = memInfo.otherSharedClean;
+ int otherPrivateClean = memInfo.otherPrivateClean;
+ int otherSwappedOut = memInfo.otherSwappedOut;
+ int otherSwappedOutPss = memInfo.otherSwappedOutPss;
+
+ for (int i = 0; i < Debug.MemoryInfo.NUM_OTHER_STATS; i++) {
+ final int myPss = memInfo.getOtherPss(i);
+ final int mySwappablePss = memInfo.getOtherSwappablePss(i);
+ final int mySharedDirty = memInfo.getOtherSharedDirty(i);
+ final int myPrivateDirty = memInfo.getOtherPrivateDirty(i);
+ final int mySharedClean = memInfo.getOtherSharedClean(i);
+ final int myPrivateClean = memInfo.getOtherPrivateClean(i);
+ final int mySwappedOut = memInfo.getOtherSwappedOut(i);
+ final int mySwappedOutPss = memInfo.getOtherSwappedOutPss(i);
+ if (myPss != 0 || mySharedDirty != 0 || myPrivateDirty != 0
+ || mySharedClean != 0 || myPrivateClean != 0
+ || (memInfo.hasSwappedOutPss ? mySwappedOutPss : mySwappedOut) != 0) {
+ dumpMemoryInfo(proto, MemInfoProto.ProcessMemory.OTHER_HEAPS,
+ Debug.MemoryInfo.getOtherLabel(i),
+ myPss, mySwappablePss, mySharedDirty, myPrivateDirty,
+ mySharedClean, myPrivateClean,
+ memInfo.hasSwappedOutPss, mySwappedOut, mySwappedOutPss);
+
+ otherPss -= myPss;
+ otherSwappablePss -= mySwappablePss;
+ otherSharedDirty -= mySharedDirty;
+ otherPrivateDirty -= myPrivateDirty;
+ otherSharedClean -= mySharedClean;
+ otherPrivateClean -= myPrivateClean;
+ otherSwappedOut -= mySwappedOut;
+ otherSwappedOutPss -= mySwappedOutPss;
+ }
+ }
+
+ dumpMemoryInfo(proto, MemInfoProto.ProcessMemory.UNKNOWN_HEAP, "Unknown",
+ otherPss, otherSwappablePss,
+ otherSharedDirty, otherPrivateDirty, otherSharedClean, otherPrivateClean,
+ memInfo.hasSwappedOutPss, otherSwappedOut, otherSwappedOutPss);
+ final long tToken = proto.start(MemInfoProto.ProcessMemory.TOTAL_HEAP);
+ dumpMemoryInfo(proto, MemInfoProto.ProcessMemory.HeapInfo.MEM_INFO, "TOTAL",
+ memInfo.getTotalPss(), memInfo.getTotalSwappablePss(),
+ memInfo.getTotalSharedDirty(), memInfo.getTotalPrivateDirty(),
+ memInfo.getTotalSharedClean(), memInfo.getTotalPrivateClean(),
+ memInfo.hasSwappedOutPss, memInfo.getTotalSwappedOut(),
+ memInfo.getTotalSwappedOutPss());
+ proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_SIZE_KB, nativeMax + dalvikMax);
+ proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_ALLOC_KB,
+ nativeAllocated + dalvikAllocated);
+ proto.write(MemInfoProto.ProcessMemory.HeapInfo.HEAP_FREE_KB, nativeFree + dalvikFree);
+ proto.end(tToken);
+
+ if (dumpDalvik) {
+ for (int i = Debug.MemoryInfo.NUM_OTHER_STATS;
+ i < Debug.MemoryInfo.NUM_OTHER_STATS + Debug.MemoryInfo.NUM_DVK_STATS;
+ i++) {
+ final int myPss = memInfo.getOtherPss(i);
+ final int mySwappablePss = memInfo.getOtherSwappablePss(i);
+ final int mySharedDirty = memInfo.getOtherSharedDirty(i);
+ final int myPrivateDirty = memInfo.getOtherPrivateDirty(i);
+ final int mySharedClean = memInfo.getOtherSharedClean(i);
+ final int myPrivateClean = memInfo.getOtherPrivateClean(i);
+ final int mySwappedOut = memInfo.getOtherSwappedOut(i);
+ final int mySwappedOutPss = memInfo.getOtherSwappedOutPss(i);
+ if (myPss != 0 || mySharedDirty != 0 || myPrivateDirty != 0
+ || mySharedClean != 0 || myPrivateClean != 0
+ || (memInfo.hasSwappedOutPss ? mySwappedOutPss : mySwappedOut) != 0) {
+ dumpMemoryInfo(proto, MemInfoProto.ProcessMemory.DALVIK_DETAILS,
+ Debug.MemoryInfo.getOtherLabel(i),
+ myPss, mySwappablePss, mySharedDirty, myPrivateDirty,
+ mySharedClean, myPrivateClean,
+ memInfo.hasSwappedOutPss, mySwappedOut, mySwappedOutPss);
+ }
+ }
+ }
+ }
+
+ final long asToken = proto.start(MemInfoProto.ProcessMemory.APP_SUMMARY);
+ proto.write(MemInfoProto.ProcessMemory.AppSummary.JAVA_HEAP_PSS_KB,
+ memInfo.getSummaryJavaHeap());
+ proto.write(MemInfoProto.ProcessMemory.AppSummary.NATIVE_HEAP_PSS_KB,
+ memInfo.getSummaryNativeHeap());
+ proto.write(MemInfoProto.ProcessMemory.AppSummary.CODE_PSS_KB, memInfo.getSummaryCode());
+ proto.write(MemInfoProto.ProcessMemory.AppSummary.STACK_PSS_KB, memInfo.getSummaryStack());
+ proto.write(MemInfoProto.ProcessMemory.AppSummary.GRAPHICS_PSS_KB,
+ memInfo.getSummaryGraphics());
+ proto.write(MemInfoProto.ProcessMemory.AppSummary.PRIVATE_OTHER_PSS_KB,
+ memInfo.getSummaryPrivateOther());
+ proto.write(MemInfoProto.ProcessMemory.AppSummary.SYSTEM_PSS_KB,
+ memInfo.getSummarySystem());
+ if (memInfo.hasSwappedOutPss) {
+ proto.write(MemInfoProto.ProcessMemory.AppSummary.TOTAL_SWAP_PSS,
+ memInfo.getSummaryTotalSwapPss());
+ } else {
+ proto.write(MemInfoProto.ProcessMemory.AppSummary.TOTAL_SWAP_PSS,
+ memInfo.getSummaryTotalSwap());
+ }
+ proto.end(asToken);
+ }
+
public void registerOnActivityPausedListener(Activity activity,
OnActivityPausedListener listener) {
synchronized (mOnPauseListeners) {
@@ -2316,13 +2668,21 @@ public final class ActivityThread extends ClientTransactionHandler {
+ ", comp=" + name
+ ", token=" + token);
}
- return performLaunchActivity(r, null);
+ // TODO(lifecycler): Can't switch to use #handleLaunchActivity() because it will try to
+ // call #reportSizeConfigurations(), but the server might not know anything about the
+ // activity if it was launched from LocalAcvitivyManager.
+ return performLaunchActivity(r);
}
public final Activity getActivity(IBinder token) {
return mActivities.get(token).activity;
}
+ @Override
+ public ActivityClientRecord getActivityClient(IBinder token) {
+ return mActivities.get(token);
+ }
+
public final void sendActivityResult(
IBinder token, String id, int requestCode,
int resultCode, Intent data) {
@@ -2330,8 +2690,8 @@ public final class ActivityThread extends ClientTransactionHandler {
+ " req=" + requestCode + " res=" + resultCode + " data=" + data);
ArrayList<ResultInfo> list = new ArrayList<ResultInfo>();
list.add(new ResultInfo(id, requestCode, resultCode, data));
- final ClientTransaction clientTransaction = new ClientTransaction(mAppThread, token);
- clientTransaction.addCallback(new ActivityResultItem(list));
+ final ClientTransaction clientTransaction = ClientTransaction.obtain(mAppThread, token);
+ clientTransaction.addCallback(ActivityResultItem.obtain(list));
try {
mAppThread.scheduleTransaction(clientTransaction);
} catch (RemoteException e) {
@@ -2390,12 +2750,11 @@ public final class ActivityThread extends ClientTransactionHandler {
sendMessage(H.CLEAN_UP_CONTEXT, cci);
}
- private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
- // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")");
-
+ /** Core implementation of activity launch. */
+ private Activity performLaunchActivity(ActivityClientRecord r) {
ActivityInfo aInfo = r.activityInfo;
- if (r.packageInfo == null) {
- r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
+ if (r.loadedApk == null) {
+ r.loadedApk = getLoadedApk(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
@@ -2432,15 +2791,15 @@ public final class ActivityThread extends ClientTransactionHandler {
}
try {
- Application app = r.packageInfo.makeApplication(false, mInstrumentation);
+ Application app = r.loadedApk.makeApplication(false, mInstrumentation);
if (localLOGV) Slog.v(TAG, "Performing launch of " + r);
if (localLOGV) Slog.v(
TAG, r + ": app=" + app
+ ", appName=" + app.getPackageName()
- + ", pkg=" + r.packageInfo.getPackageName()
+ + ", pkg=" + r.loadedApk.getPackageName()
+ ", comp=" + r.intent.getComponent().toShortString()
- + ", dir=" + r.packageInfo.getAppDir());
+ + ", dir=" + r.loadedApk.getAppDir());
if (activity != null) {
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
@@ -2462,9 +2821,6 @@ public final class ActivityThread extends ClientTransactionHandler {
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
- if (customIntent != null) {
- activity.mIntent = customIntent;
- }
r.lastNonConfigurationInstances = null;
checkAndBlockForNetworkAccess();
activity.mStartedActivity = false;
@@ -2485,37 +2841,8 @@ public final class ActivityThread extends ClientTransactionHandler {
" did not call through to super.onCreate()");
}
r.activity = activity;
- r.stopped = true;
- if (!r.activity.mFinished) {
- activity.performStart();
- r.stopped = false;
- }
- if (!r.activity.mFinished) {
- if (r.isPersistable()) {
- if (r.state != null || r.persistentState != null) {
- mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
- r.persistentState);
- }
- } else if (r.state != null) {
- mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
- }
- }
- if (!r.activity.mFinished) {
- activity.mCalled = false;
- if (r.isPersistable()) {
- mInstrumentation.callActivityOnPostCreate(activity, r.state,
- r.persistentState);
- } else {
- mInstrumentation.callActivityOnPostCreate(activity, r.state);
- }
- if (!activity.mCalled) {
- throw new SuperNotCalledException(
- "Activity " + r.intent.getComponent().toShortString() +
- " did not call through to super.onPostCreate()");
- }
- }
}
- r.paused = true;
+ r.setState(ON_CREATE);
mActivities.put(r.token, r);
@@ -2533,6 +2860,60 @@ public final class ActivityThread extends ClientTransactionHandler {
return activity;
}
+ @Override
+ public void handleStartActivity(ActivityClientRecord r,
+ PendingTransactionActions pendingActions) {
+ final Activity activity = r.activity;
+ if (r.activity == null) {
+ // TODO(lifecycler): What do we do in this case?
+ return;
+ }
+ if (!r.stopped) {
+ throw new IllegalStateException("Can't start activity that is not stopped.");
+ }
+ if (r.activity.mFinished) {
+ // TODO(lifecycler): How can this happen?
+ return;
+ }
+
+ // Start
+ activity.performStart();
+ r.setState(ON_START);
+
+ if (pendingActions == null) {
+ // No more work to do.
+ return;
+ }
+
+ // Restore instance state
+ if (pendingActions.shouldRestoreInstanceState()) {
+ if (r.isPersistable()) {
+ if (r.state != null || r.persistentState != null) {
+ mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
+ r.persistentState);
+ }
+ } else if (r.state != null) {
+ mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
+ }
+ }
+
+ // Call postOnCreate()
+ if (pendingActions.shouldCallOnPostCreate()) {
+ activity.mCalled = false;
+ if (r.isPersistable()) {
+ mInstrumentation.callActivityOnPostCreate(activity, r.state,
+ r.persistentState);
+ } else {
+ mInstrumentation.callActivityOnPostCreate(activity, r.state);
+ }
+ if (!activity.mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + r.intent.getComponent().toShortString()
+ + " did not call through to super.onPostCreate()");
+ }
+ }
+ }
+
/**
* Checks if {@link #mNetworkBlockSeq} is {@link #INVALID_PROC_STATE_SEQ} and if so, returns
* immediately. Otherwise, makes a blocking call to ActivityManagerService to wait for the
@@ -2558,7 +2939,7 @@ public final class ActivityThread extends ClientTransactionHandler {
}
ContextImpl appContext = ContextImpl.createActivityContext(
- this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);
+ this, r.loadedApk, r.activityInfo, r.token, displayId, r.overrideConfig);
final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
// For debugging purposes, if the activity's package name contains the value of
@@ -2566,7 +2947,7 @@ public final class ActivityThread extends ClientTransactionHandler {
// its content on a secondary display if there is one.
String pkgName = SystemProperties.get("debug.second-display.pkg");
if (pkgName != null && !pkgName.isEmpty()
- && r.packageInfo.mPackageName.contains(pkgName)) {
+ && r.loadedApk.mPackageName.contains(pkgName)) {
for (int id : dm.getDisplayIds()) {
if (id != Display.DEFAULT_DISPLAY) {
Display display =
@@ -2579,38 +2960,12 @@ public final class ActivityThread extends ClientTransactionHandler {
return appContext;
}
+ /**
+ * Extended implementation of activity launch. Used when server requests a launch or relaunch.
+ */
@Override
- public void handleLaunchActivity(IBinder token, Intent intent, int ident, ActivityInfo info,
- Configuration overrideConfig, CompatibilityInfo compatInfo, String referrer,
- IVoiceInteractor voiceInteractor, Bundle state, PersistableBundle persistentState,
- List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
- boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
- ActivityClientRecord r = new ActivityClientRecord();
-
- r.token = token;
- r.ident = ident;
- r.intent = intent;
- r.referrer = referrer;
- r.voiceInteractor = voiceInteractor;
- r.activityInfo = info;
- r.compatInfo = compatInfo;
- r.state = state;
- r.persistentState = persistentState;
-
- r.pendingResults = pendingResults;
- r.pendingIntents = pendingNewIntents;
-
- r.startsNotResumed = notResumed;
- r.isForward = isForward;
-
- r.profilerInfo = profilerInfo;
-
- r.overrideConfig = overrideConfig;
- r.packageInfo = getPackageInfoNoCheck(r.activityInfo.applicationInfo, r.compatInfo);
- handleLaunchActivity(r, null /* customIntent */, "LAUNCH_ACTIVITY");
- }
-
- private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
+ public Activity handleLaunchActivity(ActivityClientRecord r,
+ PendingTransactionActions pendingActions) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
@@ -2633,44 +2988,28 @@ public final class ActivityThread extends ClientTransactionHandler {
}
WindowManagerGlobal.initialize();
- Activity a = performLaunchActivity(r, customIntent);
+ final Activity a = performLaunchActivity(r);
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
reportSizeConfigurations(r);
- Bundle oldState = r.state;
- handleResumeActivity(r.token, false, r.isForward,
- !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
-
- if (!r.activity.mFinished && r.startsNotResumed) {
- // The activity manager actually wants this one to start out paused, because it
- // needs to be visible but isn't in the foreground. We accomplish this by going
- // through the normal startup (because activities expect to go through onResume()
- // the first time they run, before their window is displayed), and then pausing it.
- // However, in this case we do -not- need to do the full pause cycle (of freezing
- // and such) because the activity manager assumes it can just retain the current
- // state it has.
- performPauseActivityIfNeeded(r, reason);
-
- // We need to keep around the original state, in case we need to be created again.
- // But we only do this for pre-Honeycomb apps, which always save their state when
- // pausing, so we can not have them save their state when restarting from a paused
- // state. For HC and later, we want to (and can) let the state be saved as the
- // normal part of stopping the activity.
- if (r.isPreHoneycomb()) {
- r.state = oldState;
- }
+ if (!r.activity.mFinished && pendingActions != null) {
+ pendingActions.setOldState(r.state);
+ pendingActions.setRestoreInstanceState(true);
+ pendingActions.setCallOnPostCreate(true);
}
} else {
// If there was an error, for any reason, tell the activity manager to stop us.
try {
ActivityManager.getService()
- .finishActivity(r.token, Activity.RESULT_CANCELED, null,
- Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
+ .finishActivity(r.token, Activity.RESULT_CANCELED, null,
+ Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
+
+ return a;
}
private void reportSizeConfigurations(ActivityClientRecord r) {
@@ -2928,7 +3267,7 @@ public final class ActivityThread extends ClientTransactionHandler {
String component = data.intent.getComponent().getClassName();
- LoadedApk packageInfo = getPackageInfoNoCheck(
+ LoadedApk loadedApk = getLoadedApkNoCheck(
data.info.applicationInfo, data.compatInfo);
IActivityManager mgr = ActivityManager.getService();
@@ -2937,7 +3276,7 @@ public final class ActivityThread extends ClientTransactionHandler {
BroadcastReceiver receiver;
ContextImpl context;
try {
- app = packageInfo.makeApplication(false, mInstrumentation);
+ app = loadedApk.makeApplication(false, mInstrumentation);
context = (ContextImpl) app.getBaseContext();
if (data.info.splitName != null) {
context = (ContextImpl) context.createContextForSplit(data.info.splitName);
@@ -2946,7 +3285,8 @@ public final class ActivityThread extends ClientTransactionHandler {
data.intent.setExtrasClassLoader(cl);
data.intent.prepareToEnterProcess();
data.setExtrasClassLoader(cl);
- receiver = (BroadcastReceiver)cl.loadClass(component).newInstance();
+ receiver = loadedApk.getAppFactory()
+ .instantiateReceiver(cl, data.info.name, data.intent);
} catch (Exception e) {
if (DEBUG_BROADCAST) Slog.i(TAG,
"Finishing failed broadcast to " + data.intent.getComponent());
@@ -2961,9 +3301,9 @@ public final class ActivityThread extends ClientTransactionHandler {
TAG, "Performing receive of " + data.intent
+ ": app=" + app
+ ", appName=" + app.getPackageName()
- + ", pkg=" + packageInfo.getPackageName()
+ + ", pkg=" + loadedApk.getPackageName()
+ ", comp=" + data.intent.getComponent().toShortString()
- + ", dir=" + packageInfo.getAppDir());
+ + ", dir=" + loadedApk.getAppDir());
sCurrentBroadcastIntent.set(data.intent);
receiver.setPendingResult(data);
@@ -3008,8 +3348,8 @@ public final class ActivityThread extends ClientTransactionHandler {
unscheduleGcIdler();
// instantiate the BackupAgent class named in the manifest
- LoadedApk packageInfo = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
- String packageName = packageInfo.mPackageName;
+ LoadedApk loadedApk = getLoadedApkNoCheck(data.appInfo, data.compatInfo);
+ String packageName = loadedApk.mPackageName;
if (packageName == null) {
Slog.d(TAG, "Asked to create backup agent for nonexistent package");
return;
@@ -3035,11 +3375,11 @@ public final class ActivityThread extends ClientTransactionHandler {
try {
if (DEBUG_BACKUP) Slog.v(TAG, "Initializing agent class " + classname);
- java.lang.ClassLoader cl = packageInfo.getClassLoader();
+ java.lang.ClassLoader cl = loadedApk.getClassLoader();
agent = (BackupAgent) cl.loadClass(classname).newInstance();
// set up the agent's context
- ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
+ ContextImpl context = ContextImpl.createAppContext(this, loadedApk);
context.setOuterContext(agent);
agent.attach(context);
@@ -3075,8 +3415,8 @@ public final class ActivityThread extends ClientTransactionHandler {
private void handleDestroyBackupAgent(CreateBackupAgentData data) {
if (DEBUG_BACKUP) Slog.v(TAG, "handleDestroyBackupAgent: " + data);
- LoadedApk packageInfo = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
- String packageName = packageInfo.mPackageName;
+ LoadedApk loadedApk = getLoadedApkNoCheck(data.appInfo, data.compatInfo);
+ String packageName = loadedApk.mPackageName;
BackupAgent agent = mBackupAgents.get(packageName);
if (agent != null) {
try {
@@ -3096,12 +3436,13 @@ public final class ActivityThread extends ClientTransactionHandler {
// we are back active so skip it.
unscheduleGcIdler();
- LoadedApk packageInfo = getPackageInfoNoCheck(
+ LoadedApk loadedApk = getLoadedApkNoCheck(
data.info.applicationInfo, data.compatInfo);
Service service = null;
try {
- java.lang.ClassLoader cl = packageInfo.getClassLoader();
- service = (Service) cl.loadClass(data.info.name).newInstance();
+ java.lang.ClassLoader cl = loadedApk.getClassLoader();
+ service = loadedApk.getAppFactory()
+ .instantiateService(cl, data.info.name, data.intent);
} catch (Exception e) {
if (!mInstrumentation.onException(service, e)) {
throw new RuntimeException(
@@ -3113,10 +3454,10 @@ public final class ActivityThread extends ClientTransactionHandler {
try {
if (localLOGV) Slog.v(TAG, "Creating service " + data.info.name);
- ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
+ ContextImpl context = ContextImpl.createAppContext(this, loadedApk);
context.setOuterContext(service);
- Application app = packageInfo.makeApplication(false, mInstrumentation);
+ Application app = loadedApk.makeApplication(false, mInstrumentation);
service.attach(context, this, data.info.name, data.token, app,
ActivityManager.getService());
service.onCreate();
@@ -3314,8 +3655,7 @@ public final class ActivityThread extends ClientTransactionHandler {
//Slog.i(TAG, "Running services: " + mServices);
}
- public final ActivityClientRecord performResumeActivity(IBinder token,
- boolean clearHide, String reason) {
+ ActivityClientRecord performResumeActivity(IBinder token, boolean clearHide, String reason) {
ActivityClientRecord r = mActivities.get(token);
if (localLOGV) Slog.v(TAG, "Performing resume of " + r
+ " finished=" + r.activity.mFinished);
@@ -3355,10 +3695,9 @@ public final class ActivityThread extends ClientTransactionHandler {
EventLog.writeEvent(LOG_AM_ON_RESUME_CALLED, UserHandle.myUserId(),
r.activity.getComponentName().getClassName(), reason);
- r.paused = false;
- r.stopped = false;
r.state = null;
r.persistentState = null;
+ r.setState(ON_RESUME);
} catch (Exception e) {
if (!mInstrumentation.onException(r.activity, e)) {
throw new RuntimeException(
@@ -3390,19 +3729,14 @@ public final class ActivityThread extends ClientTransactionHandler {
@Override
public void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward,
- boolean reallyResume, int seq, String reason) {
- ActivityClientRecord r = mActivities.get(token);
- if (!checkAndUpdateLifecycleSeq(seq, r, "resumeActivity")) {
- return;
- }
-
+ String reason) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
mSomeActivitiesChanged = true;
// TODO Push resumeArgs into the activity for consideration
- r = performResumeActivity(token, clearHide, reason);
+ final ActivityClientRecord r = performResumeActivity(token, clearHide, reason);
if (r != null) {
final Activity a = r.activity;
@@ -3514,16 +3848,6 @@ public final class ActivityThread extends ClientTransactionHandler {
Looper.myQueue().addIdleHandler(new Idler());
}
r.onlyLocalRequest = false;
-
- // Tell the activity manager we have resumed.
- if (reallyResume) {
- try {
- ActivityManager.getService().activityResumed(token);
- } catch (RemoteException ex) {
- throw ex.rethrowFromSystemServer();
- }
- }
-
} else {
// If an exception was thrown when trying to resume, then
// just end this activity.
@@ -3594,21 +3918,17 @@ public final class ActivityThread extends ClientTransactionHandler {
}
@Override
- public void handlePauseActivity(IBinder token, boolean finished,
- boolean userLeaving, int configChanges, boolean dontReport, int seq) {
+ public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
+ int configChanges, boolean dontReport, PendingTransactionActions pendingActions) {
ActivityClientRecord r = mActivities.get(token);
- if (DEBUG_ORDER) Slog.d(TAG, "handlePauseActivity " + r + ", seq: " + seq);
- if (!checkAndUpdateLifecycleSeq(seq, r, "pauseActivity")) {
- return;
- }
if (r != null) {
- //Slog.v(TAG, "userLeaving=" + userLeaving + " handling pause of " + r);
if (userLeaving) {
performUserLeavingActivity(r);
}
r.activity.mConfigChangeFlags |= configChanges;
- performPauseActivity(token, finished, r.isPreHoneycomb(), "handlePauseActivity");
+ performPauseActivity(token, finished, r.isPreHoneycomb(), "handlePauseActivity",
+ pendingActions);
// Make sure any pending writes are now committed.
if (r.isPreHoneycomb()) {
@@ -3632,13 +3952,15 @@ public final class ActivityThread extends ClientTransactionHandler {
}
final Bundle performPauseActivity(IBinder token, boolean finished,
- boolean saveState, String reason) {
+ boolean saveState, String reason, PendingTransactionActions pendingActions) {
ActivityClientRecord r = mActivities.get(token);
- return r != null ? performPauseActivity(r, finished, saveState, reason) : null;
+ return r != null
+ ? performPauseActivity(r, finished, saveState, reason, pendingActions)
+ : null;
}
- final Bundle performPauseActivity(ActivityClientRecord r, boolean finished,
- boolean saveState, String reason) {
+ private Bundle performPauseActivity(ActivityClientRecord r, boolean finished, boolean saveState,
+ String reason, PendingTransactionActions pendingActions) {
if (r.paused) {
if (r.activity.mFinished) {
// If we are finishing, we won't call onResume() in certain cases.
@@ -3672,6 +3994,18 @@ public final class ActivityThread extends ClientTransactionHandler {
listeners.get(i).onPaused(r.activity);
}
+ final Bundle oldState = pendingActions != null ? pendingActions.getOldState() : null;
+ if (oldState != null) {
+ // We need to keep around the original state, in case we need to be created again.
+ // But we only do this for pre-Honeycomb apps, which always save their state when
+ // pausing, so we can not have them save their state when restarting from a paused
+ // state. For HC and later, we want to (and can) let the state be saved as the
+ // normal part of stopping the activity.
+ if (r.isPreHoneycomb()) {
+ r.state = oldState;
+ }
+ }
+
return !r.activity.mFinished && saveState ? r.state : null;
}
@@ -3698,7 +4032,7 @@ public final class ActivityThread extends ClientTransactionHandler {
+ safeToComponentShortString(r.intent) + ": " + e.toString(), e);
}
}
- r.paused = true;
+ r.setState(ON_PAUSE);
}
final void performStopActivity(IBinder token, boolean saveState, String reason) {
@@ -3706,37 +4040,6 @@ public final class ActivityThread extends ClientTransactionHandler {
performStopActivityInner(r, null, false, saveState, reason);
}
- private static class StopInfo implements Runnable {
- ActivityClientRecord activity;
- Bundle state;
- PersistableBundle persistentState;
- CharSequence description;
-
- @Override public void run() {
- // Tell activity manager we have been stopped.
- try {
- if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Reporting activity stopped: " + activity);
- ActivityManager.getService().activityStopped(
- activity.token, state, persistentState, description);
- } catch (RemoteException ex) {
- // Dump statistics about bundle to help developers debug
- final LogWriter writer = new LogWriter(Log.WARN, TAG);
- final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
- pw.println("Bundle stats:");
- Bundle.dumpStats(pw, state);
- pw.println("PersistableBundle stats:");
- Bundle.dumpStats(pw, persistentState);
-
- if (ex instanceof TransactionTooLargeException
- && activity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) {
- Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex);
- return;
- }
- throw ex.rethrowFromSystemServer();
- }
- }
- }
-
private static final class ProviderRefCount {
public final ContentProviderHolder holder;
public final ProviderClientRecord client;
@@ -3767,8 +4070,8 @@ public final class ActivityThread extends ClientTransactionHandler {
* For the client, we want to call onStop()/onStart() to indicate when
* the activity's UI visibility changes.
*/
- private void performStopActivityInner(ActivityClientRecord r,
- StopInfo info, boolean keepShown, boolean saveState, String reason) {
+ private void performStopActivityInner(ActivityClientRecord r, StopInfo info, boolean keepShown,
+ boolean saveState, String reason) {
if (localLOGV) Slog.v(TAG, "Performing stop of " + r);
if (r != null) {
if (!keepShown && r.stopped) {
@@ -3793,7 +4096,7 @@ public final class ActivityThread extends ClientTransactionHandler {
// First create a thumbnail for the activity...
// For now, don't create the thumbnail here; we are
// doing that by doing a screen snapshot.
- info.description = r.activity.onCreateDescription();
+ info.setDescription(r.activity.onCreateDescription());
} catch (Exception e) {
if (!mInstrumentation.onException(r.activity, e)) {
throw new RuntimeException(
@@ -3823,7 +4126,7 @@ public final class ActivityThread extends ClientTransactionHandler {
+ ": " + e.toString(), e);
}
}
- r.stopped = true;
+ r.setState(ON_STOP);
EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(),
r.activity.getComponentName().getClassName(), reason);
}
@@ -3859,15 +4162,13 @@ public final class ActivityThread extends ClientTransactionHandler {
}
@Override
- public void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {
- ActivityClientRecord r = mActivities.get(token);
- if (!checkAndUpdateLifecycleSeq(seq, r, "stopActivity")) {
- return;
- }
+ public void handleStopActivity(IBinder token, boolean show, int configChanges,
+ PendingTransactionActions pendingActions) {
+ final ActivityClientRecord r = mActivities.get(token);
r.activity.mConfigChangeFlags |= configChanges;
- StopInfo info = new StopInfo();
- performStopActivityInner(r, info, show, true, "handleStopActivity");
+ final StopInfo stopInfo = new StopInfo();
+ performStopActivityInner(r, stopInfo, show, true, "handleStopActivity");
if (localLOGV) Slog.v(
TAG, "Finishing stop of " + r + ": show=" + show
@@ -3880,37 +4181,32 @@ public final class ActivityThread extends ClientTransactionHandler {
QueuedWork.waitToFinish();
}
- // Schedule the call to tell the activity manager we have
- // stopped. We don't do this immediately, because we want to
- // have a chance for any other pending work (in particular memory
- // trim requests) to complete before you tell the activity
- // manager to proceed and allow us to go fully into the background.
- info.activity = r;
- info.state = r.state;
- info.persistentState = r.persistentState;
- mH.post(info);
+ stopInfo.setActivity(r);
+ stopInfo.setState(r.state);
+ stopInfo.setPersistentState(r.persistentState);
+ pendingActions.setStopInfo(stopInfo);
mSomeActivitiesChanged = true;
}
- private static boolean checkAndUpdateLifecycleSeq(int seq, ActivityClientRecord r,
- String action) {
- if (r == null) {
- return true;
- }
- if (seq < r.lastProcessedSeq) {
- if (DEBUG_ORDER) Slog.d(TAG, action + " for " + r + " ignored, because seq=" + seq
- + " < mCurrentLifecycleSeq=" + r.lastProcessedSeq);
- return false;
- }
- r.lastProcessedSeq = seq;
- return true;
+ /**
+ * Schedule the call to tell the activity manager we have stopped. We don't do this
+ * immediately, because we want to have a chance for any other pending work (in particular
+ * memory trim requests) to complete before you tell the activity manager to proceed and allow
+ * us to go fully into the background.
+ */
+ @Override
+ public void reportStop(PendingTransactionActions pendingActions) {
+ mH.post(pendingActions.getStopInfo());
}
- final void performRestartActivity(IBinder token) {
+ @Override
+ public void performRestartActivity(IBinder token, boolean start) {
ActivityClientRecord r = mActivities.get(token);
if (r.stopped) {
- r.activity.performRestart();
- r.stopped = false;
+ r.activity.performRestart(start);
+ if (start) {
+ r.setState(ON_START);
+ }
}
}
@@ -3930,8 +4226,8 @@ public final class ActivityThread extends ClientTransactionHandler {
// we are back active so skip it.
unscheduleGcIdler();
- r.activity.performRestart();
- r.stopped = false;
+ r.activity.performRestart(true /* start */);
+ r.setState(ON_START);
}
if (r.activity.mDecor != null) {
if (false) Slog.v(
@@ -3969,7 +4265,7 @@ public final class ActivityThread extends ClientTransactionHandler {
+ ": " + e.toString(), e);
}
}
- r.stopped = true;
+ r.setState(ON_STOP);
EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(),
r.activity.getComponentName().getClassName(), "sleeping");
}
@@ -3987,8 +4283,8 @@ public final class ActivityThread extends ClientTransactionHandler {
}
} else {
if (r.stopped && r.activity.mVisibleFromServer) {
- r.activity.performRestart();
- r.stopped = false;
+ r.activity.performRestart(true /* start */);
+ r.setState(ON_START);
}
}
}
@@ -4025,11 +4321,11 @@ public final class ActivityThread extends ClientTransactionHandler {
}
private void handleUpdatePackageCompatibilityInfo(UpdateCompatibilityData data) {
- LoadedApk apk = peekPackageInfo(data.pkg, false);
+ LoadedApk apk = peekLoadedApk(data.pkg, false);
if (apk != null) {
apk.setCompatibilityInfo(data.info);
}
- apk = peekPackageInfo(data.pkg, true);
+ apk = peekLoadedApk(data.pkg, true);
if (apk != null) {
apk.setCompatibilityInfo(data.info);
}
@@ -4105,11 +4401,8 @@ public final class ActivityThread extends ClientTransactionHandler {
}
}
- public final ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing) {
- return performDestroyActivity(token, finishing, 0, false);
- }
-
- private ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
+ /** Core implementation of activity destroy call. */
+ ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
int configChanges, boolean getNonConfigInstance) {
ActivityClientRecord r = mActivities.get(token);
Class<? extends Activity> activityClass = null;
@@ -4136,7 +4429,7 @@ public final class ActivityThread extends ClientTransactionHandler {
+ ": " + e.toString(), e);
}
}
- r.stopped = true;
+ r.setState(ON_STOP);
EventLog.writeEvent(LOG_AM_ON_STOP_CALLED, UserHandle.myUserId(),
r.activity.getComponentName().getClassName(), "destroy");
}
@@ -4173,6 +4466,7 @@ public final class ActivityThread extends ClientTransactionHandler {
+ ": " + e.toString(), e);
}
}
+ r.setState(ON_DESTROY);
}
mActivities.remove(token);
StrictMode.decrementExpectedActivityCount(activityClass);
@@ -4333,10 +4627,7 @@ public final class ActivityThread extends ClientTransactionHandler {
target.overrideConfig = overrideConfig;
}
target.pendingConfigChanges |= configChanges;
- target.relaunchSeq = getLifecycleSeq();
}
- if (DEBUG_ORDER) Slog.d(TAG, "relaunchActivity " + ActivityThread.this + ", target "
- + target + " operation received seq: " + target.relaunchSeq);
}
private void handleRelaunchActivity(ActivityClientRecord tmp) {
@@ -4381,12 +4672,6 @@ public final class ActivityThread extends ClientTransactionHandler {
}
}
- if (tmp.lastProcessedSeq > tmp.relaunchSeq) {
- Slog.wtf(TAG, "For some reason target: " + tmp + " has lower sequence: "
- + tmp.relaunchSeq + " than current sequence: " + tmp.lastProcessedSeq);
- } else {
- tmp.lastProcessedSeq = tmp.relaunchSeq;
- }
if (tmp.createdConfig != null) {
// If the activity manager is passing us its current config,
// assume that is really what we want regardless of what we
@@ -4427,9 +4712,6 @@ public final class ActivityThread extends ClientTransactionHandler {
r.activity.mConfigChangeFlags |= configChanges;
r.onlyLocalRequest = tmp.onlyLocalRequest;
r.mPreserveWindow = tmp.mPreserveWindow;
- r.lastProcessedSeq = tmp.lastProcessedSeq;
- r.relaunchSeq = tmp.relaunchSeq;
- Intent currentIntent = r.activity.mIntent;
r.activity.mChangingConfigurations = true;
@@ -4455,7 +4737,8 @@ public final class ActivityThread extends ClientTransactionHandler {
// Need to ensure state is saved.
if (!r.paused) {
- performPauseActivity(r.token, false, r.isPreHoneycomb(), "handleRelaunchActivity");
+ performPauseActivity(r.token, false, r.isPreHoneycomb(), "handleRelaunchActivity",
+ null /* pendingActions */);
}
if (r.state == null && !r.stopped && !r.isPreHoneycomb()) {
callCallActivityOnSaveInstanceState(r);
@@ -4485,7 +4768,15 @@ public final class ActivityThread extends ClientTransactionHandler {
r.startsNotResumed = tmp.startsNotResumed;
r.overrideConfig = tmp.overrideConfig;
- handleLaunchActivity(r, currentIntent, "handleRelaunchActivity");
+ // TODO(lifecycler): Move relaunch to lifecycler.
+ PendingTransactionActions pendingActions = new PendingTransactionActions();
+ handleLaunchActivity(r, pendingActions);
+ handleStartActivity(r, pendingActions);
+ handleResumeActivity(r.token, false /* clearHide */, r.isForward, "relaunch");
+ if (r.startsNotResumed) {
+ performPauseActivity(r, false /* finished */, r.isPreHoneycomb(), "relaunch",
+ pendingActions);
+ }
if (!tmp.onlyLocalRequest) {
try {
@@ -4528,7 +4819,7 @@ public final class ActivityThread extends ClientTransactionHandler {
if (a != null) {
Configuration thisConfig = applyConfigCompatMainThread(
mCurDefaultDisplayDpi, newConfig,
- ar.packageInfo.getCompatibilityInfo());
+ ar.loadedApk.getCompatibilityInfo());
if (!ar.activity.mFinished && (allActivities || !ar.paused)) {
// If the activity is currently resumed, its configuration
// needs to change right now.
@@ -5014,7 +5305,7 @@ public final class ActivityThread extends ClientTransactionHandler {
}
final void handleDispatchPackageBroadcast(int cmd, String[] packages) {
- boolean hasPkgInfo = false;
+ boolean hasLoadedApk = false;
switch (cmd) {
case ApplicationThreadConstants.PACKAGE_REMOVED:
case ApplicationThreadConstants.PACKAGE_REMOVED_DONT_KILL:
@@ -5025,14 +5316,14 @@ public final class ActivityThread extends ClientTransactionHandler {
}
synchronized (mResourcesManager) {
for (int i = packages.length - 1; i >= 0; i--) {
- if (!hasPkgInfo) {
+ if (!hasLoadedApk) {
WeakReference<LoadedApk> ref = mPackages.get(packages[i]);
if (ref != null && ref.get() != null) {
- hasPkgInfo = true;
+ hasLoadedApk = true;
} else {
ref = mResourcePackages.get(packages[i]);
if (ref != null && ref.get() != null) {
- hasPkgInfo = true;
+ hasLoadedApk = true;
}
}
}
@@ -5052,21 +5343,21 @@ public final class ActivityThread extends ClientTransactionHandler {
synchronized (mResourcesManager) {
for (int i = packages.length - 1; i >= 0; i--) {
WeakReference<LoadedApk> ref = mPackages.get(packages[i]);
- LoadedApk pkgInfo = ref != null ? ref.get() : null;
- if (pkgInfo != null) {
- hasPkgInfo = true;
+ LoadedApk loadedApk = ref != null ? ref.get() : null;
+ if (loadedApk != null) {
+ hasLoadedApk = true;
} else {
ref = mResourcePackages.get(packages[i]);
- pkgInfo = ref != null ? ref.get() : null;
- if (pkgInfo != null) {
- hasPkgInfo = true;
+ loadedApk = ref != null ? ref.get() : null;
+ if (loadedApk != null) {
+ hasLoadedApk = true;
}
}
// If the package is being replaced, yet it still has a valid
// LoadedApk object, the package was updated with _DONT_KILL.
// Adjust it's internal references to the application info and
// resources.
- if (pkgInfo != null) {
+ if (loadedApk != null) {
try {
final String packageName = packages[i];
final ApplicationInfo aInfo =
@@ -5080,13 +5371,13 @@ public final class ActivityThread extends ClientTransactionHandler {
if (ar.activityInfo.applicationInfo.packageName
.equals(packageName)) {
ar.activityInfo.applicationInfo = aInfo;
- ar.packageInfo = pkgInfo;
+ ar.loadedApk = loadedApk;
}
}
}
final List<String> oldPaths =
sPackageManager.getPreviousCodePaths(packageName);
- pkgInfo.updateApplicationInfo(aInfo, oldPaths);
+ loadedApk.updateApplicationInfo(aInfo, oldPaths);
} catch (RemoteException e) {
}
}
@@ -5095,7 +5386,7 @@ public final class ActivityThread extends ClientTransactionHandler {
break;
}
}
- ApplicationPackageManager.handlePackageBroadcast(cmd, packages, hasPkgInfo);
+ ApplicationPackageManager.handlePackageBroadcast(cmd, packages, hasLoadedApk);
}
final void handleLowMemory() {
@@ -5299,7 +5590,7 @@ public final class ActivityThread extends ClientTransactionHandler {
applyCompatConfiguration(mCurDefaultDisplayDpi);
}
- data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
+ data.loadedApk = getLoadedApkNoCheck(data.appInfo, data.compatInfo);
/**
* Switch this process to density compatibility mode if needed.
@@ -5343,7 +5634,7 @@ public final class ActivityThread extends ClientTransactionHandler {
// XXX should have option to change the port.
Debug.changeDebugPort(8100);
if (data.debugMode == ApplicationThreadConstants.DEBUG_WAIT) {
- Slog.w(TAG, "Application " + data.info.getPackageName()
+ Slog.w(TAG, "Application " + data.loadedApk.getPackageName()
+ " is waiting for the debugger on port 8100...");
IActivityManager mgr = ActivityManager.getService();
@@ -5362,7 +5653,7 @@ public final class ActivityThread extends ClientTransactionHandler {
}
} else {
- Slog.w(TAG, "Application " + data.info.getPackageName()
+ Slog.w(TAG, "Application " + data.loadedApk.getPackageName()
+ " can be debugged on port 8100...");
}
}
@@ -5410,14 +5701,14 @@ public final class ActivityThread extends ClientTransactionHandler {
mInstrumentationAppDir = ii.sourceDir;
mInstrumentationSplitAppDirs = ii.splitSourceDirs;
mInstrumentationLibDir = getInstrumentationLibrary(data.appInfo, ii);
- mInstrumentedAppDir = data.info.getAppDir();
- mInstrumentedSplitAppDirs = data.info.getSplitAppDirs();
- mInstrumentedLibDir = data.info.getLibDir();
+ mInstrumentedAppDir = data.loadedApk.getAppDir();
+ mInstrumentedSplitAppDirs = data.loadedApk.getSplitAppDirs();
+ mInstrumentedLibDir = data.loadedApk.getLibDir();
} else {
ii = null;
}
- final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
+ final ContextImpl appContext = ContextImpl.createAppContext(this, data.loadedApk);
updateLocaleListFromAppContext(appContext,
mResourcesManager.getConfiguration().getLocales());
@@ -5449,12 +5740,21 @@ public final class ActivityThread extends ClientTransactionHandler {
// Continue loading instrumentation.
if (ii != null) {
- final ApplicationInfo instrApp = new ApplicationInfo();
+ ApplicationInfo instrApp;
+ try {
+ instrApp = getPackageManager().getApplicationInfo(ii.packageName, 0,
+ UserHandle.myUserId());
+ } catch (RemoteException e) {
+ instrApp = null;
+ }
+ if (instrApp == null) {
+ instrApp = new ApplicationInfo();
+ }
ii.copyTo(instrApp);
instrApp.initForUser(UserHandle.myUserId());
- final LoadedApk pi = getPackageInfo(instrApp, data.compatInfo,
+ final LoadedApk loadedApk = getLoadedApk(instrApp, data.compatInfo,
appContext.getClassLoader(), false, true, false);
- final ContextImpl instrContext = ContextImpl.createAppContext(this, pi);
+ final ContextImpl instrContext = ContextImpl.createAppContext(this, loadedApk);
try {
final ClassLoader cl = instrContext.getClassLoader();
@@ -5479,6 +5779,7 @@ public final class ActivityThread extends ClientTransactionHandler {
}
} else {
mInstrumentation = new Instrumentation();
+ mInstrumentation.basicInit(this);
}
if ((data.appInfo.flags&ApplicationInfo.FLAG_LARGE_HEAP) != 0) {
@@ -5498,7 +5799,7 @@ public final class ActivityThread extends ClientTransactionHandler {
try {
// If the app is being launched for full backup or restore, bring it up in
// a restricted environment with the base application class.
- app = data.info.makeApplication(data.restrictedBackupMode, null);
+ app = data.loadedApk.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;
// don't bring up providers in restricted mode; they may depend on the
@@ -5552,7 +5853,7 @@ public final class ActivityThread extends ClientTransactionHandler {
final int preloadedFontsResource = info.metaData.getInt(
ApplicationInfo.METADATA_PRELOADED_FONTS, 0);
if (preloadedFontsResource != 0) {
- data.info.getResources().preloadFonts(preloadedFontsResource);
+ data.loadedApk.getResources().preloadFonts(preloadedFontsResource);
}
}
} catch (RemoteException e) {
@@ -6010,8 +6311,13 @@ public final class ActivityThread extends ClientTransactionHandler {
try {
final java.lang.ClassLoader cl = c.getClassLoader();
- localProvider = (ContentProvider)cl.
- loadClass(info.name).newInstance();
+ LoadedApk loadedApk = peekLoadedApk(ai.packageName, true);
+ if (loadedApk == null) {
+ // System startup case.
+ loadedApk = getSystemContext().mLoadedApk;
+ }
+ localProvider = loadedApk.getAppFactory()
+ .instantiateProvider(cl, info.name);
provider = localProvider.getIContentProvider();
if (provider == null) {
Slog.e(TAG, "Failed to instantiate class " +
@@ -6109,7 +6415,7 @@ public final class ActivityThread extends ClientTransactionHandler {
System.exit(0);
}
- private void attach(boolean system) {
+ private void attach(boolean system, long startSeq) {
sCurrentActivityThread = this;
mSystemThread = system;
if (!system) {
@@ -6124,7 +6430,7 @@ public final class ActivityThread extends ClientTransactionHandler {
RuntimeInit.setApplicationObject(mAppThread.asBinder());
final IActivityManager mgr = ActivityManager.getService();
try {
- mgr.attachApplication(mAppThread);
+ mgr.attachApplication(mAppThread, startSeq);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
@@ -6157,9 +6463,10 @@ public final class ActivityThread extends ClientTransactionHandler {
UserHandle.myUserId());
try {
mInstrumentation = new Instrumentation();
+ mInstrumentation.basicInit(this);
ContextImpl context = ContextImpl.createAppContext(
- this, getSystemContext().mPackageInfo);
- mInitialApplication = context.mPackageInfo.makeApplication(true, null);
+ this, getSystemContext().mLoadedApk);
+ mInitialApplication = context.mLoadedApk.makeApplication(true, null);
mInitialApplication.onCreate();
} catch (Exception e) {
throw new RuntimeException(
@@ -6202,7 +6509,7 @@ public final class ActivityThread extends ClientTransactionHandler {
ThreadedRenderer.enableForegroundTrimming();
}
ActivityThread thread = new ActivityThread();
- thread.attach(true);
+ thread.attach(true, 0);
return thread;
}
@@ -6274,8 +6581,19 @@ public final class ActivityThread extends ClientTransactionHandler {
Looper.prepareMainLooper();
+ // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.
+ // It will be in the format "seq=114"
+ long startSeq = 0;
+ if (args != null) {
+ for (int i = args.length - 1; i >= 0; --i) {
+ if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
+ startSeq = Long.parseLong(
+ args[i].substring(PROC_START_SEQ_IDENT.length()));
+ }
+ }
+ }
ActivityThread thread = new ActivityThread();
- thread.attach(false);
+ thread.attach(false, startSeq);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
diff --git a/android/app/AlarmManager.java b/android/app/AlarmManager.java
index 55f9e289..382719b4 100644
--- a/android/app/AlarmManager.java
+++ b/android/app/AlarmManager.java
@@ -568,9 +568,21 @@ public class AlarmManager {
}
/**
- * Schedule an alarm that represents an alarm clock.
+ * Schedule an alarm that represents an alarm clock, which will be used to notify the user
+ * when it goes off. The expectation is that when this alarm triggers, the application will
+ * further wake up the device to tell the user about the alarm -- turning on the screen,
+ * playing a sound, vibrating, etc. As such, the system will typically also use the
+ * information supplied here to tell the user about this upcoming alarm if appropriate.
*
- * The system may choose to display information about this alarm to the user.
+ * <p>Due to the nature of this kind of alarm, similar to {@link #setExactAndAllowWhileIdle},
+ * these alarms will be allowed to trigger even if the system is in a low-power idle
+ * (a.k.a. doze) mode. The system may also do some prep-work when it sees that such an
+ * alarm coming up, to reduce the amount of background work that could happen if this
+ * causes the device to fully wake up -- this is to avoid situations such as a large number
+ * of devices having an alarm set at the same time in the morning, all waking up at that
+ * time and suddenly swamping the network with pending background work. As such, these
+ * types of alarms can be extremely expensive on battery use and should only be used for
+ * their intended purpose.</p>
*
* <p>
* This method is like {@link #setExact(int, long, PendingIntent)}, but implies
@@ -783,9 +795,9 @@ public class AlarmManager {
/**
* Like {@link #set(int, long, PendingIntent)}, but this alarm will be allowed to execute
- * even when the system is in low-power idle modes. This type of alarm must <b>only</b>
- * be used for situations where it is actually required that the alarm go off while in
- * idle -- a reasonable example would be for a calendar notification that should make a
+ * even when the system is in low-power idle (a.k.a. doze) modes. This type of alarm must
+ * <b>only</b> be used for situations where it is actually required that the alarm go off while
+ * in idle -- a reasonable example would be for a calendar notification that should make a
* sound so the user is aware of it. When the alarm is dispatched, the app will also be
* added to the system's temporary whitelist for approximately 10 seconds to allow that
* application to acquire further wake locks in which to complete its work.</p>
diff --git a/android/app/AppComponentFactory.java b/android/app/AppComponentFactory.java
new file mode 100644
index 00000000..4df73799
--- /dev/null
+++ b/android/app/AppComponentFactory.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.ContentProvider;
+import android.content.Intent;
+
+/**
+ * Interface used to control the instantiation of manifest elements.
+ *
+ * @see #instantiateApplication
+ * @see #instantiateActivity
+ * @see #instantiateService
+ * @see #instantiateReceiver
+ * @see #instantiateProvider
+ */
+public class AppComponentFactory {
+
+ /**
+ * Allows application to override the creation of the application object. This can be used to
+ * perform things such as dependency injection or class loader changes to these
+ * classes.
+ *
+ * @param cl The default classloader to use for instantiation.
+ * @param className The class to be instantiated.
+ */
+ public @NonNull Application instantiateApplication(@NonNull ClassLoader cl,
+ @NonNull String className)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ return (Application) cl.loadClass(className).newInstance();
+ }
+
+ /**
+ * Allows application to override the creation of activities. This can be used to
+ * perform things such as dependency injection or class loader changes to these
+ * classes.
+ *
+ * @param cl The default classloader to use for instantiation.
+ * @param className The class to be instantiated.
+ * @param intent Intent creating the class.
+ */
+ public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className,
+ @Nullable Intent intent)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ return (Activity) cl.loadClass(className).newInstance();
+ }
+
+ /**
+ * Allows application to override the creation of receivers. This can be used to
+ * perform things such as dependency injection or class loader changes to these
+ * classes.
+ *
+ * @param cl The default classloader to use for instantiation.
+ * @param className The class to be instantiated.
+ * @param intent Intent creating the class.
+ */
+ public @NonNull BroadcastReceiver instantiateReceiver(@NonNull ClassLoader cl,
+ @NonNull String className, @Nullable Intent intent)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ return (BroadcastReceiver) cl.loadClass(className).newInstance();
+ }
+
+ /**
+ * Allows application to override the creation of services. This can be used to
+ * perform things such as dependency injection or class loader changes to these
+ * classes.
+ *
+ * @param cl The default classloader to use for instantiation.
+ * @param className The class to be instantiated.
+ * @param intent Intent creating the class.
+ */
+ public @NonNull Service instantiateService(@NonNull ClassLoader cl,
+ @NonNull String className, @Nullable Intent intent)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ return (Service) cl.loadClass(className).newInstance();
+ }
+
+ /**
+ * Allows application to override the creation of providers. This can be used to
+ * perform things such as dependency injection or class loader changes to these
+ * classes.
+ *
+ * @param cl The default classloader to use for instantiation.
+ * @param className The class to be instantiated.
+ */
+ public @NonNull ContentProvider instantiateProvider(@NonNull ClassLoader cl,
+ @NonNull String className)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ return (ContentProvider) cl.loadClass(className).newInstance();
+ }
+
+ /**
+ * @hide
+ */
+ public static AppComponentFactory DEFAULT = new AppComponentFactory();
+}
diff --git a/android/app/AppOpsManager.java b/android/app/AppOpsManager.java
index b6fb1201..ea22d332 100644
--- a/android/app/AppOpsManager.java
+++ b/android/app/AppOpsManager.java
@@ -160,7 +160,7 @@ public class AppOpsManager {
public static final int OP_WRITE_ICC_SMS = 22;
/** @hide */
public static final int OP_WRITE_SETTINGS = 23;
- /** @hide */
+ /** @hide Required to draw on top of other apps. */
public static final int OP_SYSTEM_ALERT_WINDOW = 24;
/** @hide */
public static final int OP_ACCESS_NOTIFICATIONS = 25;
@@ -256,8 +256,12 @@ public class AppOpsManager {
public static final int OP_RUN_ANY_IN_BACKGROUND = 70;
/** @hide Change Wi-Fi connectivity state */
public static final int OP_CHANGE_WIFI_STATE = 71;
+ /** @hide Request package deletion through package installer */
+ public static final int OP_REQUEST_DELETE_PACKAGES = 72;
+ /** @hide Bind an accessibility service. */
+ public static final int OP_BIND_ACCESSIBILITY_SERVICE = 73;
/** @hide */
- public static final int _NUM_OP = 72;
+ public static final int _NUM_OP = 74;
/** Access to coarse location information. */
public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
@@ -410,6 +414,7 @@ public class AppOpsManager {
OP_CAMERA,
// Body sensors
OP_BODY_SENSORS,
+ OP_REQUEST_DELETE_PACKAGES,
// APPOP PERMISSIONS
OP_ACCESS_NOTIFICATIONS,
@@ -499,6 +504,8 @@ public class AppOpsManager {
OP_ANSWER_PHONE_CALLS,
OP_RUN_ANY_IN_BACKGROUND,
OP_CHANGE_WIFI_STATE,
+ OP_REQUEST_DELETE_PACKAGES,
+ OP_BIND_ACCESSIBILITY_SERVICE,
};
/**
@@ -578,6 +585,8 @@ public class AppOpsManager {
OPSTR_ANSWER_PHONE_CALLS,
null, // OP_RUN_ANY_IN_BACKGROUND
null, // OP_CHANGE_WIFI_STATE
+ null, // OP_REQUEST_DELETE_PACKAGES
+ null, // OP_BIND_ACCESSIBILITY_SERVICE
};
/**
@@ -657,6 +666,8 @@ public class AppOpsManager {
"ANSWER_PHONE_CALLS",
"RUN_ANY_IN_BACKGROUND",
"CHANGE_WIFI_STATE",
+ "REQUEST_DELETE_PACKAGES",
+ "BIND_ACCESSIBILITY_SERVICE",
};
/**
@@ -736,6 +747,8 @@ public class AppOpsManager {
Manifest.permission.ANSWER_PHONE_CALLS,
null, // no permission for OP_RUN_ANY_IN_BACKGROUND
Manifest.permission.CHANGE_WIFI_STATE,
+ Manifest.permission.REQUEST_DELETE_PACKAGES,
+ Manifest.permission.BIND_ACCESSIBILITY_SERVICE,
};
/**
@@ -816,6 +829,8 @@ public class AppOpsManager {
null, // ANSWER_PHONE_CALLS
null, // OP_RUN_ANY_IN_BACKGROUND
null, // OP_CHANGE_WIFI_STATE
+ null, // REQUEST_DELETE_PACKAGES
+ null, // OP_BIND_ACCESSIBILITY_SERVICE
};
/**
@@ -895,6 +910,8 @@ public class AppOpsManager {
false, // ANSWER_PHONE_CALLS
false, // OP_RUN_ANY_IN_BACKGROUND
false, // OP_CHANGE_WIFI_STATE
+ false, // OP_REQUEST_DELETE_PACKAGES
+ false, // OP_BIND_ACCESSIBILITY_SERVICE
};
/**
@@ -973,6 +990,8 @@ public class AppOpsManager {
AppOpsManager.MODE_ALLOWED, // ANSWER_PHONE_CALLS
AppOpsManager.MODE_ALLOWED, // OP_RUN_ANY_IN_BACKGROUND
AppOpsManager.MODE_ALLOWED, // OP_CHANGE_WIFI_STATE
+ AppOpsManager.MODE_ALLOWED, // REQUEST_DELETE_PACKAGES
+ AppOpsManager.MODE_ALLOWED, // OP_BIND_ACCESSIBILITY_SERVICE
};
/**
@@ -1055,6 +1074,8 @@ public class AppOpsManager {
false, // ANSWER_PHONE_CALLS
false, // OP_RUN_ANY_IN_BACKGROUND
false, // OP_CHANGE_WIFI_STATE
+ false, // OP_REQUEST_DELETE_PACKAGES
+ false, // OP_BIND_ACCESSIBILITY_SERVICE
};
/**
diff --git a/android/app/Application.java b/android/app/Application.java
index 156df36a..5822f5c8 100644
--- a/android/app/Application.java
+++ b/android/app/Application.java
@@ -187,7 +187,7 @@ public class Application extends ContextWrapper implements ComponentCallbacks2 {
*/
/* package */ final void attach(Context context) {
attachBaseContext(context);
- mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
+ mLoadedApk = ContextImpl.getImpl(context).mLoadedApk;
}
/* package */ void dispatchActivityCreated(Activity activity, Bundle savedInstanceState) {
diff --git a/android/app/ApplicationPackageManager.java b/android/app/ApplicationPackageManager.java
index 005b7c38..8641a21a 100644
--- a/android/app/ApplicationPackageManager.java
+++ b/android/app/ApplicationPackageManager.java
@@ -55,6 +55,7 @@ import android.content.pm.ServiceInfo;
import android.content.pm.SharedLibraryInfo;
import android.content.pm.VerifierDeviceIdentity;
import android.content.pm.VersionedPackage;
+import android.content.pm.dex.ArtManager;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.Bitmap;
@@ -121,6 +122,8 @@ public class ApplicationPackageManager extends PackageManager {
private UserManager mUserManager;
@GuardedBy("mLock")
private PackageInstaller mInstaller;
+ @GuardedBy("mLock")
+ private ArtManager mArtManager;
@GuardedBy("mDelegates")
private final ArrayList<MoveCallbackDelegate> mDelegates = new ArrayList<>();
@@ -1378,7 +1381,7 @@ public class ApplicationPackageManager extends PackageManager {
sameUid ? app.sourceDir : app.publicSourceDir,
sameUid ? app.splitSourceDirs : app.splitPublicSourceDirs,
app.resourceDirs, app.sharedLibraryFiles, Display.DEFAULT_DISPLAY,
- mContext.mPackageInfo);
+ mContext.mLoadedApk);
if (r != null) {
return r;
}
@@ -2750,4 +2753,18 @@ public class ApplicationPackageManager extends PackageManager {
throw e.rethrowAsRuntimeException();
}
}
+
+ @Override
+ public ArtManager getArtManager() {
+ synchronized (mLock) {
+ if (mArtManager == null) {
+ try {
+ mArtManager = new ArtManager(mPM.getArtManager());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return mArtManager;
+ }
+ }
}
diff --git a/android/app/ClientTransactionHandler.java b/android/app/ClientTransactionHandler.java
index f7f4c716..45c0e0cd 100644
--- a/android/app/ClientTransactionHandler.java
+++ b/android/app/ClientTransactionHandler.java
@@ -16,15 +16,12 @@
package android.app;
import android.app.servertransaction.ClientTransaction;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
+import android.app.servertransaction.PendingTransactionActions;
+import android.content.pm.ApplicationInfo;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
-import android.os.Bundle;
import android.os.IBinder;
-import android.os.PersistableBundle;
-import com.android.internal.app.IVoiceInteractor;
import com.android.internal.content.ReferrerIntent;
import java.util.List;
@@ -40,7 +37,7 @@ public abstract class ClientTransactionHandler {
/** Prepare and schedule transaction for execution. */
void scheduleTransaction(ClientTransaction transaction) {
- transaction.prepare(this);
+ transaction.preExecute(this);
sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction);
}
@@ -50,9 +47,6 @@ public abstract class ClientTransactionHandler {
// Prepare phase related logic and handlers. Methods that inform about about pending changes or
// do other internal bookkeeping.
- /** Get current lifecycle request number to maintain correct ordering. */
- public abstract int getLifecycleSeq();
-
/** Set pending config in case it will be updated by other transaction item. */
public abstract void updatePendingConfiguration(Configuration config);
@@ -69,15 +63,21 @@ public abstract class ClientTransactionHandler {
/** Pause the activity. */
public abstract void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
- int configChanges, boolean dontReport, int seq);
+ int configChanges, boolean dontReport, PendingTransactionActions pendingActions);
/** Resume the activity. */
public abstract void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward,
- boolean reallyResume, int seq, String reason);
+ String reason);
/** Stop the activity. */
public abstract void handleStopActivity(IBinder token, boolean show, int configChanges,
- int seq);
+ PendingTransactionActions pendingActions);
+
+ /** Report that activity was stopped to server. */
+ public abstract void reportStop(PendingTransactionActions pendingActions);
+
+ /** Restart the activity after it was stopped. */
+ public abstract void performRestartActivity(IBinder token, boolean start);
/** Deliver activity (override) configuration change. */
public abstract void handleActivityConfigurationChanged(IBinder activityToken,
@@ -102,13 +102,23 @@ public abstract class ClientTransactionHandler {
public abstract void handleWindowVisibility(IBinder token, boolean show);
/** Perform activity launch. */
- public abstract void handleLaunchActivity(IBinder token, Intent intent, int ident,
- ActivityInfo info, Configuration overrideConfig, CompatibilityInfo compatInfo,
- String referrer, IVoiceInteractor voiceInteractor, Bundle state,
- PersistableBundle persistentState, List<ResultInfo> pendingResults,
- List<ReferrerIntent> pendingNewIntents, boolean notResumed, boolean isForward,
- ProfilerInfo profilerInfo);
+ public abstract Activity handleLaunchActivity(ActivityThread.ActivityClientRecord r,
+ PendingTransactionActions pendingActions);
+
+ /** Perform activity start. */
+ public abstract void handleStartActivity(ActivityThread.ActivityClientRecord r,
+ PendingTransactionActions pendingActions);
+
+ /** Get package info. */
+ public abstract LoadedApk getLoadedApkNoCheck(ApplicationInfo ai,
+ CompatibilityInfo compatInfo);
/** Deliver app configuration change notification. */
public abstract void handleConfigurationChanged(Configuration config);
+
+ /**
+ * Get {@link android.app.ActivityThread.ActivityClientRecord} instance that corresponds to the
+ * provided token.
+ */
+ public abstract ActivityThread.ActivityClientRecord getActivityClient(IBinder token);
}
diff --git a/android/app/ContextImpl.java b/android/app/ContextImpl.java
index b0d020a7..16534305 100644
--- a/android/app/ContextImpl.java
+++ b/android/app/ContextImpl.java
@@ -90,6 +90,7 @@ import java.io.InputStream;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Objects;
+import java.util.concurrent.Executor;
class ReceiverRestrictedContext extends ContextWrapper {
ReceiverRestrictedContext(Context base) {
@@ -158,7 +159,7 @@ class ContextImpl extends Context {
private ArrayMap<String, File> mSharedPrefsPaths;
final @NonNull ActivityThread mMainThread;
- final @NonNull LoadedApk mPackageInfo;
+ final @NonNull LoadedApk mLoadedApk;
private @Nullable ClassLoader mClassLoader;
private final @Nullable IBinder mActivityToken;
@@ -250,9 +251,14 @@ class ContextImpl extends Context {
}
@Override
+ public Executor getMainExecutor() {
+ return mMainThread.getExecutor();
+ }
+
+ @Override
public Context getApplicationContext() {
- return (mPackageInfo != null) ?
- mPackageInfo.getApplication() : mMainThread.getApplication();
+ return (mLoadedApk != null) ?
+ mLoadedApk.getApplication() : mMainThread.getApplication();
}
@Override
@@ -296,15 +302,15 @@ class ContextImpl extends Context {
@Override
public ClassLoader getClassLoader() {
- return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());
+ return mClassLoader != null ? mClassLoader : (mLoadedApk != null ? mLoadedApk.getClassLoader() : ClassLoader.getSystemClassLoader());
}
@Override
public String getPackageName() {
- if (mPackageInfo != null) {
- return mPackageInfo.getPackageName();
+ if (mLoadedApk != null) {
+ return mLoadedApk.getPackageName();
}
- // No mPackageInfo means this is a Context for the system itself,
+ // No mLoadedApk means this is a Context for the system itself,
// and this here is its name.
return "android";
}
@@ -323,24 +329,24 @@ class ContextImpl extends Context {
@Override
public ApplicationInfo getApplicationInfo() {
- if (mPackageInfo != null) {
- return mPackageInfo.getApplicationInfo();
+ if (mLoadedApk != null) {
+ return mLoadedApk.getApplicationInfo();
}
throw new RuntimeException("Not supported in system context");
}
@Override
public String getPackageResourcePath() {
- if (mPackageInfo != null) {
- return mPackageInfo.getResDir();
+ if (mLoadedApk != null) {
+ return mLoadedApk.getResDir();
}
throw new RuntimeException("Not supported in system context");
}
@Override
public String getPackageCodePath() {
- if (mPackageInfo != null) {
- return mPackageInfo.getAppDir();
+ if (mLoadedApk != null) {
+ return mLoadedApk.getAppDir();
}
throw new RuntimeException("Not supported in system context");
}
@@ -350,7 +356,7 @@ class ContextImpl extends Context {
// At least one application in the world actually passes in a null
// name. This happened to work because when we generated the file name
// we would stringify it to "null.xml". Nice.
- if (mPackageInfo.getApplicationInfo().targetSdkVersion <
+ if (mLoadedApk.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
@@ -1092,11 +1098,11 @@ class ContextImpl extends Context {
warnIfCallingFromSystemProcess();
IIntentReceiver rd = null;
if (resultReceiver != null) {
- if (mPackageInfo != null) {
+ if (mLoadedApk != null) {
if (scheduler == null) {
scheduler = mMainThread.getHandler();
}
- rd = mPackageInfo.getReceiverDispatcher(
+ rd = mLoadedApk.getReceiverDispatcher(
resultReceiver, getOuterContext(), scheduler,
mMainThread.getInstrumentation(), false);
} else {
@@ -1196,11 +1202,11 @@ class ContextImpl extends Context {
Handler scheduler, int initialCode, String initialData, Bundle initialExtras) {
IIntentReceiver rd = null;
if (resultReceiver != null) {
- if (mPackageInfo != null) {
+ if (mLoadedApk != null) {
if (scheduler == null) {
scheduler = mMainThread.getHandler();
}
- rd = mPackageInfo.getReceiverDispatcher(
+ rd = mLoadedApk.getReceiverDispatcher(
resultReceiver, getOuterContext(), scheduler,
mMainThread.getInstrumentation(), false);
} else {
@@ -1250,11 +1256,11 @@ class ContextImpl extends Context {
warnIfCallingFromSystemProcess();
IIntentReceiver rd = null;
if (resultReceiver != null) {
- if (mPackageInfo != null) {
+ if (mLoadedApk != null) {
if (scheduler == null) {
scheduler = mMainThread.getHandler();
}
- rd = mPackageInfo.getReceiverDispatcher(
+ rd = mLoadedApk.getReceiverDispatcher(
resultReceiver, getOuterContext(), scheduler,
mMainThread.getInstrumentation(), false);
} else {
@@ -1332,11 +1338,11 @@ class ContextImpl extends Context {
Bundle initialExtras) {
IIntentReceiver rd = null;
if (resultReceiver != null) {
- if (mPackageInfo != null) {
+ if (mLoadedApk != null) {
if (scheduler == null) {
scheduler = mMainThread.getHandler();
}
- rd = mPackageInfo.getReceiverDispatcher(
+ rd = mLoadedApk.getReceiverDispatcher(
resultReceiver, getOuterContext(), scheduler,
mMainThread.getInstrumentation(), false);
} else {
@@ -1413,11 +1419,11 @@ class ContextImpl extends Context {
Handler scheduler, Context context, int flags) {
IIntentReceiver rd = null;
if (receiver != null) {
- if (mPackageInfo != null && context != null) {
+ if (mLoadedApk != null && context != null) {
if (scheduler == null) {
scheduler = mMainThread.getHandler();
}
- rd = mPackageInfo.getReceiverDispatcher(
+ rd = mLoadedApk.getReceiverDispatcher(
receiver, context, scheduler,
mMainThread.getInstrumentation(), true);
} else {
@@ -1444,8 +1450,8 @@ class ContextImpl extends Context {
@Override
public void unregisterReceiver(BroadcastReceiver receiver) {
- if (mPackageInfo != null) {
- IIntentReceiver rd = mPackageInfo.forgetReceiverDispatcher(
+ if (mLoadedApk != null) {
+ IIntentReceiver rd = mLoadedApk.forgetReceiverDispatcher(
getOuterContext(), receiver);
try {
ActivityManager.getService().unregisterReceiver(rd);
@@ -1578,7 +1584,7 @@ class ContextImpl extends Context {
@Override
public IServiceConnection getServiceDispatcher(ServiceConnection conn, Handler handler,
int flags) {
- return mPackageInfo.getServiceDispatcher(conn, getOuterContext(), handler, flags);
+ return mLoadedApk.getServiceDispatcher(conn, getOuterContext(), handler, flags);
}
/** @hide */
@@ -1600,16 +1606,16 @@ class ContextImpl extends Context {
if (conn == null) {
throw new IllegalArgumentException("connection is null");
}
- if (mPackageInfo != null) {
- sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), handler, flags);
+ if (mLoadedApk != null) {
+ sd = mLoadedApk.getServiceDispatcher(conn, getOuterContext(), handler, flags);
} else {
throw new RuntimeException("Not supported in system context");
}
validateServiceIntent(service);
try {
IBinder token = getActivityToken();
- if (token == null && (flags&BIND_AUTO_CREATE) == 0 && mPackageInfo != null
- && mPackageInfo.getApplicationInfo().targetSdkVersion
+ if (token == null && (flags&BIND_AUTO_CREATE) == 0 && mLoadedApk != null
+ && mLoadedApk.getApplicationInfo().targetSdkVersion
< android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
flags |= BIND_WAIVE_PRIORITY;
}
@@ -1633,8 +1639,8 @@ class ContextImpl extends Context {
if (conn == null) {
throw new IllegalArgumentException("connection is null");
}
- if (mPackageInfo != null) {
- IServiceConnection sd = mPackageInfo.forgetServiceDispatcher(
+ if (mLoadedApk != null) {
+ IServiceConnection sd = mLoadedApk.forgetServiceDispatcher(
getOuterContext(), conn);
try {
ActivityManager.getService().unbindService(sd);
@@ -1979,40 +1985,20 @@ class ContextImpl extends Context {
}
}
- private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,
- int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
- final String[] splitResDirs;
- final ClassLoader classLoader;
- try {
- splitResDirs = pi.getSplitPaths(splitName);
- classLoader = pi.getSplitClassLoader(splitName);
- } catch (NameNotFoundException e) {
- throw new RuntimeException(e);
- }
- return ResourcesManager.getInstance().getResources(activityToken,
- pi.getResDir(),
- splitResDirs,
- pi.getOverlayDirs(),
- pi.getApplicationInfo().sharedLibraryFiles,
- displayId,
- overrideConfig,
- compatInfo,
- classLoader);
- }
-
@Override
public Context createApplicationContext(ApplicationInfo application, int flags)
throws NameNotFoundException {
- LoadedApk pi = mMainThread.getPackageInfo(application, mResources.getCompatibilityInfo(),
+ LoadedApk loadedApk = mMainThread.getLoadedApk(application,
+ mResources.getCompatibilityInfo(),
flags | CONTEXT_REGISTER_PACKAGE);
- if (pi != null) {
- ContextImpl c = new ContextImpl(this, mMainThread, pi, null, mActivityToken,
+ if (loadedApk != null) {
+ ContextImpl c = new ContextImpl(this, mMainThread, loadedApk, null, mActivityToken,
new UserHandle(UserHandle.getUserId(application.uid)), flags, null);
final int displayId = mDisplay != null
? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY;
- c.setResources(createResources(mActivityToken, pi, null, displayId, null,
+ c.setResources(loadedApk.createResources(mActivityToken, null, displayId, null,
getDisplayAdjustments(displayId).getCompatibilityInfo()));
if (c.mResources != null) {
return c;
@@ -2036,20 +2022,21 @@ class ContextImpl extends Context {
if (packageName.equals("system") || packageName.equals("android")) {
// The system resources are loaded in every application, so we can safely copy
// the context without reloading Resources.
- return new ContextImpl(this, mMainThread, mPackageInfo, null, mActivityToken, user,
+ return new ContextImpl(this, mMainThread, mLoadedApk, null, mActivityToken, user,
flags, null);
}
- LoadedApk pi = mMainThread.getPackageInfo(packageName, mResources.getCompatibilityInfo(),
+ LoadedApk loadedApk = mMainThread.getLoadedApkForPackageName(packageName,
+ mResources.getCompatibilityInfo(),
flags | CONTEXT_REGISTER_PACKAGE, user.getIdentifier());
- if (pi != null) {
- ContextImpl c = new ContextImpl(this, mMainThread, pi, null, mActivityToken, user,
+ if (loadedApk != null) {
+ ContextImpl c = new ContextImpl(this, mMainThread, loadedApk, null, mActivityToken, user,
flags, null);
final int displayId = mDisplay != null
? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY;
- c.setResources(createResources(mActivityToken, pi, null, displayId, null,
+ c.setResources(loadedApk.createResources(mActivityToken, null, displayId, null,
getDisplayAdjustments(displayId).getCompatibilityInfo()));
if (c.mResources != null) {
return c;
@@ -2063,30 +2050,21 @@ class ContextImpl extends Context {
@Override
public Context createContextForSplit(String splitName) throws NameNotFoundException {
- if (!mPackageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) {
+ if (!mLoadedApk.getApplicationInfo().requestsIsolatedSplitLoading()) {
// All Splits are always loaded.
return this;
}
- final ClassLoader classLoader = mPackageInfo.getSplitClassLoader(splitName);
- final String[] paths = mPackageInfo.getSplitPaths(splitName);
+ final ClassLoader classLoader = mLoadedApk.getSplitClassLoader(splitName);
- final ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, splitName,
+ final ContextImpl context = new ContextImpl(this, mMainThread, mLoadedApk, splitName,
mActivityToken, mUser, mFlags, classLoader);
final int displayId = mDisplay != null
? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY;
- context.setResources(ResourcesManager.getInstance().getResources(
- mActivityToken,
- mPackageInfo.getResDir(),
- paths,
- mPackageInfo.getOverlayDirs(),
- mPackageInfo.getApplicationInfo().sharedLibraryFiles,
- displayId,
- null,
- mPackageInfo.getCompatibilityInfo(),
- classLoader));
+ context.setResources(mLoadedApk.getOrCreateResourcesForSplit(splitName,
+ mActivityToken, displayId));
return context;
}
@@ -2096,11 +2074,11 @@ class ContextImpl extends Context {
throw new IllegalArgumentException("overrideConfiguration must not be null");
}
- ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mSplitName,
+ ContextImpl context = new ContextImpl(this, mMainThread, mLoadedApk, mSplitName,
mActivityToken, mUser, mFlags, mClassLoader);
final int displayId = mDisplay != null ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY;
- context.setResources(createResources(mActivityToken, mPackageInfo, mSplitName, displayId,
+ context.setResources(mLoadedApk.createResources(mActivityToken, mSplitName, displayId,
overrideConfiguration, getDisplayAdjustments(displayId).getCompatibilityInfo()));
return context;
}
@@ -2111,11 +2089,11 @@ class ContextImpl extends Context {
throw new IllegalArgumentException("display must not be null");
}
- ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mSplitName,
+ ContextImpl context = new ContextImpl(this, mMainThread, mLoadedApk, mSplitName,
mActivityToken, mUser, mFlags, mClassLoader);
final int displayId = display.getDisplayId();
- context.setResources(createResources(mActivityToken, mPackageInfo, mSplitName, displayId,
+ context.setResources(mLoadedApk.createResources(mActivityToken, mSplitName, displayId,
null, getDisplayAdjustments(displayId).getCompatibilityInfo()));
context.mDisplay = display;
return context;
@@ -2125,7 +2103,7 @@ class ContextImpl extends Context {
public Context createDeviceProtectedStorageContext() {
final int flags = (mFlags & ~Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE)
| Context.CONTEXT_DEVICE_PROTECTED_STORAGE;
- return new ContextImpl(this, mMainThread, mPackageInfo, mSplitName, mActivityToken, mUser,
+ return new ContextImpl(this, mMainThread, mLoadedApk, mSplitName, mActivityToken, mUser,
flags, mClassLoader);
}
@@ -2133,7 +2111,7 @@ class ContextImpl extends Context {
public Context createCredentialProtectedStorageContext() {
final int flags = (mFlags & ~Context.CONTEXT_DEVICE_PROTECTED_STORAGE)
| Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE;
- return new ContextImpl(this, mMainThread, mPackageInfo, mSplitName, mActivityToken, mUser,
+ return new ContextImpl(this, mMainThread, mLoadedApk, mSplitName, mActivityToken, mUser,
flags, mClassLoader);
}
@@ -2182,14 +2160,14 @@ class ContextImpl extends Context {
@Override
public File getDataDir() {
- if (mPackageInfo != null) {
+ if (mLoadedApk != null) {
File res = null;
if (isCredentialProtectedStorage()) {
- res = mPackageInfo.getCredentialProtectedDataDirFile();
+ res = mLoadedApk.getCredentialProtectedDataDirFile();
} else if (isDeviceProtectedStorage()) {
- res = mPackageInfo.getDeviceProtectedDataDirFile();
+ res = mLoadedApk.getDeviceProtectedDataDirFile();
} else {
- res = mPackageInfo.getDataDirFile();
+ res = mLoadedApk.getDataDirFile();
}
if (res != null) {
@@ -2240,10 +2218,10 @@ class ContextImpl extends Context {
}
static ContextImpl createSystemContext(ActivityThread mainThread) {
- LoadedApk packageInfo = new LoadedApk(mainThread);
- ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
+ LoadedApk loadedApk = new LoadedApk(mainThread);
+ ContextImpl context = new ContextImpl(null, mainThread, loadedApk, null, null, null, 0,
null);
- context.setResources(packageInfo.getResources());
+ context.setResources(loadedApk.getResources());
context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(),
context.mResourcesManager.getDisplayMetrics());
return context;
@@ -2254,35 +2232,35 @@ class ContextImpl extends Context {
* Make sure that the created system UI context shares the same LoadedApk as the system context.
*/
static ContextImpl createSystemUiContext(ContextImpl systemContext) {
- final LoadedApk packageInfo = systemContext.mPackageInfo;
- ContextImpl context = new ContextImpl(null, systemContext.mMainThread, packageInfo, null,
+ final LoadedApk loadedApk = systemContext.mLoadedApk;
+ ContextImpl context = new ContextImpl(null, systemContext.mMainThread, loadedApk, null,
null, null, 0, null);
- context.setResources(createResources(null, packageInfo, null, Display.DEFAULT_DISPLAY, null,
- packageInfo.getCompatibilityInfo()));
+ context.setResources(loadedApk.createResources(null, null, Display.DEFAULT_DISPLAY, null,
+ loadedApk.getCompatibilityInfo()));
return context;
}
- static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
- if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
- ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
+ static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk loadedApk) {
+ if (loadedApk == null) throw new IllegalArgumentException("loadedApk");
+ ContextImpl context = new ContextImpl(null, mainThread, loadedApk, null, null, null, 0,
null);
- context.setResources(packageInfo.getResources());
+ context.setResources(loadedApk.getResources());
return context;
}
static ContextImpl createActivityContext(ActivityThread mainThread,
- LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
+ LoadedApk loadedApk, ActivityInfo activityInfo, IBinder activityToken, int displayId,
Configuration overrideConfiguration) {
- if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
+ if (loadedApk == null) throw new IllegalArgumentException("loadedApk");
- String[] splitDirs = packageInfo.getSplitResDirs();
- ClassLoader classLoader = packageInfo.getClassLoader();
+ String[] splitDirs = loadedApk.getSplitResDirs();
+ ClassLoader classLoader = loadedApk.getClassLoader();
- if (packageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) {
+ if (loadedApk.getApplicationInfo().requestsIsolatedSplitLoading()) {
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "SplitDependencies");
try {
- classLoader = packageInfo.getSplitClassLoader(activityInfo.splitName);
- splitDirs = packageInfo.getSplitPaths(activityInfo.splitName);
+ classLoader = loadedApk.getSplitClassLoader(activityInfo.splitName);
+ splitDirs = loadedApk.getSplitPaths(activityInfo.splitName);
} catch (NameNotFoundException e) {
// Nothing above us can handle a NameNotFoundException, better crash.
throw new RuntimeException(e);
@@ -2291,14 +2269,14 @@ class ContextImpl extends Context {
}
}
- ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName,
+ ContextImpl context = new ContextImpl(null, mainThread, loadedApk, activityInfo.splitName,
activityToken, null, 0, classLoader);
// Clamp display ID to DEFAULT_DISPLAY if it is INVALID_DISPLAY.
displayId = (displayId != Display.INVALID_DISPLAY) ? displayId : Display.DEFAULT_DISPLAY;
final CompatibilityInfo compatInfo = (displayId == Display.DEFAULT_DISPLAY)
- ? packageInfo.getCompatibilityInfo()
+ ? loadedApk.getCompatibilityInfo()
: CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
final ResourcesManager resourcesManager = ResourcesManager.getInstance();
@@ -2306,10 +2284,10 @@ class ContextImpl extends Context {
// Create the base resources for which all configuration contexts for this Activity
// will be rebased upon.
context.setResources(resourcesManager.createBaseActivityResources(activityToken,
- packageInfo.getResDir(),
+ loadedApk.getResDir(),
splitDirs,
- packageInfo.getOverlayDirs(),
- packageInfo.getApplicationInfo().sharedLibraryFiles,
+ loadedApk.getOverlayDirs(),
+ loadedApk.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfiguration,
compatInfo,
@@ -2320,7 +2298,7 @@ class ContextImpl extends Context {
}
private ContextImpl(@Nullable ContextImpl container, @NonNull ActivityThread mainThread,
- @NonNull LoadedApk packageInfo, @Nullable String splitName,
+ @NonNull LoadedApk loadedApk, @Nullable String splitName,
@Nullable IBinder activityToken, @Nullable UserHandle user, int flags,
@Nullable ClassLoader classLoader) {
mOuterContext = this;
@@ -2329,10 +2307,10 @@ class ContextImpl extends Context {
// location for application.
if ((flags & (Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE
| Context.CONTEXT_DEVICE_PROTECTED_STORAGE)) == 0) {
- final File dataDir = packageInfo.getDataDirFile();
- if (Objects.equals(dataDir, packageInfo.getCredentialProtectedDataDirFile())) {
+ final File dataDir = loadedApk.getDataDirFile();
+ if (Objects.equals(dataDir, loadedApk.getCredentialProtectedDataDirFile())) {
flags |= Context.CONTEXT_CREDENTIAL_PROTECTED_STORAGE;
- } else if (Objects.equals(dataDir, packageInfo.getDeviceProtectedDataDirFile())) {
+ } else if (Objects.equals(dataDir, loadedApk.getDeviceProtectedDataDirFile())) {
flags |= Context.CONTEXT_DEVICE_PROTECTED_STORAGE;
}
}
@@ -2346,7 +2324,7 @@ class ContextImpl extends Context {
}
mUser = user;
- mPackageInfo = packageInfo;
+ mLoadedApk = loadedApk;
mSplitName = splitName;
mClassLoader = classLoader;
mResourcesManager = ResourcesManager.getInstance();
@@ -2357,8 +2335,8 @@ class ContextImpl extends Context {
setResources(container.mResources);
mDisplay = container.mDisplay;
} else {
- mBasePackageName = packageInfo.mPackageName;
- ApplicationInfo ainfo = packageInfo.getApplicationInfo();
+ mBasePackageName = loadedApk.mPackageName;
+ ApplicationInfo ainfo = loadedApk.getApplicationInfo();
if (ainfo.uid == Process.SYSTEM_UID && ainfo.uid != Process.myUid()) {
// Special case: system components allow themselves to be loaded in to other
// processes. For purposes of app ops, we must then consider the context as
@@ -2381,7 +2359,7 @@ class ContextImpl extends Context {
}
void installSystemApplicationInfo(ApplicationInfo info, ClassLoader classLoader) {
- mPackageInfo.installSystemApplicationInfo(info, classLoader);
+ mLoadedApk.installSystemApplicationInfo(info, classLoader);
}
final void scheduleFinalCleanup(String who, String what) {
@@ -2390,7 +2368,7 @@ class ContextImpl extends Context {
final void performFinalCleanup(String who, String what) {
//Log.i(TAG, "Cleanup up context: " + this);
- mPackageInfo.removeContextRegistrations(getOuterContext(), who, what);
+ mLoadedApk.removeContextRegistrations(getOuterContext(), who, what);
}
final Context getReceiverRestrictedContext() {
diff --git a/android/app/DialogFragment.java b/android/app/DialogFragment.java
index a0fb6eeb..3a355d97 100644
--- a/android/app/DialogFragment.java
+++ b/android/app/DialogFragment.java
@@ -137,7 +137,9 @@ import java.io.PrintWriter;
* {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentDialogOrActivity.java
* embed}
*
- * @deprecated Use {@link android.support.v4.app.DialogFragment}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link android.support.v4.app.DialogFragment} for consistent behavior across all devices
+ * and access to <a href="{@docRoot}topic/libraries/architecture/lifecycle.html">Lifecycle</a>.
*/
@Deprecated
public class DialogFragment extends Fragment
diff --git a/android/app/Fragment.java b/android/app/Fragment.java
index a92684b5..4ff07f2e 100644
--- a/android/app/Fragment.java
+++ b/android/app/Fragment.java
@@ -257,7 +257,9 @@ import java.lang.reflect.InvocationTargetException;
* pressing back will pop it to return the user to whatever previous state
* the activity UI was in.
*
- * @deprecated Use {@link android.support.v4.app.Fragment}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link android.support.v4.app.Fragment} for consistent behavior across all devices
+ * and access to <a href="{@docRoot}topic/libraries/architecture/lifecycle.html">Lifecycle</a>.
*/
@Deprecated
public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListener {
diff --git a/android/app/FragmentContainer.java b/android/app/FragmentContainer.java
index a1dd32ff..536c8660 100644
--- a/android/app/FragmentContainer.java
+++ b/android/app/FragmentContainer.java
@@ -25,7 +25,8 @@ import android.view.View;
/**
* Callbacks to a {@link Fragment}'s container.
*
- * @deprecated Use {@link android.support.v4.app.FragmentContainer}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link android.support.v4.app.FragmentContainer}.
*/
@Deprecated
public abstract class FragmentContainer {
diff --git a/android/app/FragmentController.java b/android/app/FragmentController.java
index cbb58d40..40bc2483 100644
--- a/android/app/FragmentController.java
+++ b/android/app/FragmentController.java
@@ -38,7 +38,8 @@ import java.util.List;
* It is the responsibility of the host to take care of the Fragment's lifecycle.
* The methods provided by {@link FragmentController} are for that purpose.
*
- * @deprecated Use {@link android.support.v4.app.FragmentController}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link android.support.v4.app.FragmentController}
*/
@Deprecated
public class FragmentController {
diff --git a/android/app/FragmentHostCallback.java b/android/app/FragmentHostCallback.java
index 1edc68ed..b48817b1 100644
--- a/android/app/FragmentHostCallback.java
+++ b/android/app/FragmentHostCallback.java
@@ -38,7 +38,8 @@ import java.io.PrintWriter;
* host fragments, implement {@link FragmentHostCallback}, overriding the methods
* applicable to the host.
*
- * @deprecated Use {@link android.support.v4.app.FragmentHostCallback}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link android.support.v4.app.FragmentHostCallback}
*/
@Deprecated
public abstract class FragmentHostCallback<E> extends FragmentContainer {
diff --git a/android/app/FragmentManager.java b/android/app/FragmentManager.java
index 12e60b87..708450f6 100644
--- a/android/app/FragmentManager.java
+++ b/android/app/FragmentManager.java
@@ -75,7 +75,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
* <a href="http://android-developers.blogspot.com/2011/03/fragments-for-all.html">
* Fragments For All</a> for more details.
*
- * @deprecated Use {@link android.support.v4.app.FragmentManager}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link android.support.v4.app.FragmentManager} for consistent behavior across all devices
+ * and access to <a href="{@docRoot}topic/libraries/architecture/lifecycle.html">Lifecycle</a>.
*/
@Deprecated
public abstract class FragmentManager {
@@ -90,7 +92,8 @@ public abstract class FragmentManager {
* the identifier as returned by {@link #getId} is the only thing that
* will be persisted across activity instances.
*
- * @deprecated Use {@link android.support.v4.app.FragmentManager.BackStackEntry}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">
+ * Support Library</a> {@link android.support.v4.app.FragmentManager.BackStackEntry}
*/
@Deprecated
public interface BackStackEntry {
@@ -136,7 +139,9 @@ public abstract class FragmentManager {
/**
* Interface to watch for changes to the back stack.
*
- * @deprecated Use {@link android.support.v4.app.FragmentManager.OnBackStackChangedListener}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">
+ * Support Library</a>
+ * {@link android.support.v4.app.FragmentManager.OnBackStackChangedListener}
*/
@Deprecated
public interface OnBackStackChangedListener {
@@ -438,7 +443,9 @@ public abstract class FragmentManager {
* Callback interface for listening to fragment state changes that happen
* within a given FragmentManager.
*
- * @deprecated Use {@link android.support.v4.app.FragmentManager.FragmentLifecycleCallbacks}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">
+ * Support Library</a>
+ * {@link android.support.v4.app.FragmentManager.FragmentLifecycleCallbacks}
*/
@Deprecated
public abstract static class FragmentLifecycleCallbacks {
diff --git a/android/app/FragmentManagerNonConfig.java b/android/app/FragmentManagerNonConfig.java
index beb1a15a..326438af 100644
--- a/android/app/FragmentManagerNonConfig.java
+++ b/android/app/FragmentManagerNonConfig.java
@@ -28,7 +28,8 @@ import java.util.List;
* {@link FragmentController#retainNonConfig()} and
* {@link FragmentController#restoreAllState(Parcelable, FragmentManagerNonConfig)}.</p>
*
- * @deprecated Use {@link android.support.v4.app.FragmentManagerNonConfig}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link android.support.v4.app.FragmentManagerNonConfig}
*/
@Deprecated
public class FragmentManagerNonConfig {
diff --git a/android/app/FragmentTransaction.java b/android/app/FragmentTransaction.java
index 0f4a7fb5..713a559a 100644
--- a/android/app/FragmentTransaction.java
+++ b/android/app/FragmentTransaction.java
@@ -22,7 +22,8 @@ import java.lang.annotation.RetentionPolicy;
* guide.</p>
* </div>
*
- * @deprecated Use {@link android.support.v4.app.FragmentTransaction}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link android.support.v4.app.FragmentTransaction}
*/
@Deprecated
public abstract class FragmentTransaction {
@@ -182,7 +183,12 @@ public abstract class FragmentTransaction {
public static final int TRANSIT_FRAGMENT_FADE = 3 | TRANSIT_ENTER_MASK;
/** @hide */
- @IntDef({TRANSIT_NONE, TRANSIT_FRAGMENT_OPEN, TRANSIT_FRAGMENT_CLOSE, TRANSIT_FRAGMENT_FADE})
+ @IntDef(prefix = { "TRANSIT_" }, value = {
+ TRANSIT_NONE,
+ TRANSIT_FRAGMENT_OPEN,
+ TRANSIT_FRAGMENT_CLOSE,
+ TRANSIT_FRAGMENT_FADE
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface Transit {}
diff --git a/android/app/Instrumentation.java b/android/app/Instrumentation.java
index d49e11f4..b469de56 100644
--- a/android/app/Instrumentation.java
+++ b/android/app/Instrumentation.java
@@ -1114,7 +1114,10 @@ public class Instrumentation {
public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
- return newApplication(cl.loadClass(className), context);
+ Application app = getFactory(context.getPackageName())
+ .instantiateApplication(cl, className);
+ app.attach(context);
+ return app;
}
/**
@@ -1201,7 +1204,20 @@ public class Instrumentation {
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
- return (Activity)cl.loadClass(className).newInstance();
+ String pkg = intent.getComponent().getPackageName();
+ return getFactory(pkg).instantiateActivity(cl, className, intent);
+ }
+
+ private AppComponentFactory getFactory(String pkg) {
+ if (mThread == null) {
+ Log.e(TAG, "Uninitialized ActivityThread, likely app-created Instrumentation,"
+ + " disabling AppComponentFactory", new Throwable());
+ return AppComponentFactory.DEFAULT;
+ }
+ LoadedApk loadedApk = mThread.peekLoadedApk(pkg, true);
+ // This is in the case of starting up "android".
+ if (loadedApk == null) loadedApk = mThread.getSystemContext().mLoadedApk;
+ return loadedApk.getAppFactory();
}
private void prePerformCreate(Activity activity) {
@@ -1950,6 +1966,14 @@ public class Instrumentation {
mUiAutomationConnection = uiAutomationConnection;
}
+ /**
+ * Only sets the ActivityThread up, keeps everything else null because app is not being
+ * instrumented.
+ */
+ final void basicInit(ActivityThread thread) {
+ mThread = thread;
+ }
+
/** @hide */
public static void checkStartActivityResult(int res, Object intent) {
if (!ActivityManager.isStartResultFatalError(res)) {
diff --git a/android/app/KeyguardManager.java b/android/app/KeyguardManager.java
index 1fe29004..d0f84c8e 100644
--- a/android/app/KeyguardManager.java
+++ b/android/app/KeyguardManager.java
@@ -382,6 +382,8 @@ public class KeyguardManager {
}
/**
+ * @deprecated Use {@link #isKeyguardLocked()} instead.
+ *
* If keyguard screen is showing or in restricted key input mode (i.e. in
* keyguard password emergency screen). When in such mode, certain keys,
* such as the Home key and the right soft keys, don't work.
@@ -389,11 +391,7 @@ public class KeyguardManager {
* @return true if in keyguard restricted input mode.
*/
public boolean inKeyguardRestrictedInputMode() {
- try {
- return mWM.inKeyguardRestrictedInputMode();
- } catch (RemoteException ex) {
- return false;
- }
+ return isKeyguardLocked();
}
/**
diff --git a/android/app/LauncherActivity.java b/android/app/LauncherActivity.java
index 9ec7f413..88e23566 100644
--- a/android/app/LauncherActivity.java
+++ b/android/app/LauncherActivity.java
@@ -166,7 +166,7 @@ public abstract class LauncherActivity extends ListActivity {
if (item.icon == null) {
item.icon = mIconResizer.createIconThumbnail(item.resolveInfo.loadIcon(getPackageManager()));
}
- text.setCompoundDrawablesWithIntrinsicBounds(item.icon, null, null, null);
+ text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, null, null, null);
}
}
diff --git a/android/app/ListFragment.java b/android/app/ListFragment.java
index 90b77b39..7790f70b 100644
--- a/android/app/ListFragment.java
+++ b/android/app/ListFragment.java
@@ -145,7 +145,9 @@ import android.widget.TextView;
* @see #setListAdapter
* @see android.widget.ListView
*
- * @deprecated Use {@link android.support.v4.app.ListFragment}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link android.support.v4.app.ListFragment} for consistent behavior across all devices
+ * and access to <a href="{@docRoot}topic/libraries/architecture/lifecycle.html">Lifecycle</a>.
*/
@Deprecated
public class ListFragment extends Fragment {
diff --git a/android/app/LoadedApk.java b/android/app/LoadedApk.java
index f6d9710d..26f49808 100644
--- a/android/app/LoadedApk.java
+++ b/android/app/LoadedApk.java
@@ -31,6 +31,7 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.split.SplitDependencyLoader;
import android.content.res.AssetManager;
import android.content.res.CompatibilityInfo;
+import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
@@ -48,15 +49,13 @@ import android.text.TextUtils;
import android.util.AndroidRuntimeException;
import android.util.ArrayMap;
import android.util.Log;
+import android.util.LogPrinter;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayAdjustments;
-
import com.android.internal.util.ArrayUtils;
-
import dalvik.system.VMRuntime;
-
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -125,6 +124,7 @@ public final class LoadedApk {
= new ArrayMap<>();
private final ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>> mUnboundServices
= new ArrayMap<>();
+ private AppComponentFactory mAppComponentFactory;
Application getApplication() {
return mApplication;
@@ -148,6 +148,7 @@ public final class LoadedApk {
mIncludeCode = includeCode;
mRegisterPackage = registerPackage;
mDisplayAdjustments.setCompatibilityInfo(compatInfo);
+ mAppComponentFactory = createAppFactory(mApplicationInfo, mBaseClassLoader);
}
private static ApplicationInfo adjustNativeLibraryPaths(ApplicationInfo info) {
@@ -203,6 +204,7 @@ public final class LoadedApk {
mRegisterPackage = false;
mClassLoader = ClassLoader.getSystemClassLoader();
mResources = Resources.getSystem();
+ mAppComponentFactory = createAppFactory(mApplicationInfo, mClassLoader);
}
/**
@@ -212,6 +214,23 @@ public final class LoadedApk {
assert info.packageName.equals("android");
mApplicationInfo = info;
mClassLoader = classLoader;
+ mAppComponentFactory = createAppFactory(info, classLoader);
+ }
+
+ private AppComponentFactory createAppFactory(ApplicationInfo appInfo, ClassLoader cl) {
+ if (appInfo.appComponentFactory != null) {
+ try {
+ return (AppComponentFactory) cl.loadClass(appInfo.appComponentFactory)
+ .newInstance();
+ } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
+ Slog.e(TAG, "Unable to instantiate appComponentFactory", e);
+ }
+ }
+ return AppComponentFactory.DEFAULT;
+ }
+
+ public AppComponentFactory getAppFactory() {
+ return mAppComponentFactory;
}
public String getPackageName() {
@@ -313,6 +332,7 @@ public final class LoadedApk {
getClassLoader());
}
}
+ mAppComponentFactory = createAppFactory(aInfo, mClassLoader);
}
private void setApplicationInfo(ApplicationInfo aInfo) {
@@ -638,8 +658,7 @@ public final class LoadedApk {
final String defaultSearchPaths = System.getProperty("java.library.path");
final boolean treatVendorApkAsUnbundled = !defaultSearchPaths.contains("/vendor/lib");
if (mApplicationInfo.getCodePath() != null
- && mApplicationInfo.getCodePath().startsWith("/vendor/")
- && treatVendorApkAsUnbundled) {
+ && mApplicationInfo.isVendor() && treatVendorApkAsUnbundled) {
isBundledApp = false;
}
@@ -948,14 +967,78 @@ public final class LoadedApk {
throw new AssertionError("null split not found");
}
- mResources = ResourcesManager.getInstance().getResources(null, mResDir,
- splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
- Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
+ mResources = ResourcesManager.getInstance().getResources(
+ null,
+ mResDir,
+ splitPaths,
+ mOverlayDirs,
+ mApplicationInfo.sharedLibraryFiles,
+ Display.DEFAULT_DISPLAY,
+ null,
+ getCompatibilityInfo(),
getClassLoader());
}
return mResources;
}
+ public Resources getOrCreateResourcesForSplit(@NonNull String splitName,
+ @Nullable IBinder activityToken, int displayId) throws NameNotFoundException {
+ return ResourcesManager.getInstance().getResources(
+ activityToken,
+ mResDir,
+ getSplitPaths(splitName),
+ mOverlayDirs,
+ mApplicationInfo.sharedLibraryFiles,
+ displayId,
+ null,
+ getCompatibilityInfo(),
+ getSplitClassLoader(splitName));
+ }
+
+ /**
+ * Creates the top level resources for the given package. Will return an existing
+ * Resources if one has already been created.
+ */
+ public Resources getOrCreateTopLevelResources(@NonNull ApplicationInfo appInfo) {
+ // Request for this app, short circuit
+ if (appInfo.uid == Process.myUid()) {
+ return getResources();
+ }
+
+ // Get resources for a different package
+ return ResourcesManager.getInstance().getResources(
+ null,
+ appInfo.publicSourceDir,
+ appInfo.splitPublicSourceDirs,
+ appInfo.resourceDirs,
+ appInfo.sharedLibraryFiles,
+ Display.DEFAULT_DISPLAY,
+ null,
+ getCompatibilityInfo(),
+ getClassLoader());
+ }
+
+ public Resources createResources(IBinder activityToken, String splitName,
+ int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
+ final String[] splitResDirs;
+ final ClassLoader classLoader;
+ try {
+ splitResDirs = getSplitPaths(splitName);
+ classLoader = getSplitClassLoader(splitName);
+ } catch (NameNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ return ResourcesManager.getInstance().getResources(activityToken,
+ mResDir,
+ splitResDirs,
+ mOverlayDirs,
+ mApplicationInfo.sharedLibraryFiles,
+ displayId,
+ overrideConfig,
+ compatInfo,
+ classLoader);
+ }
+
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
if (mApplication != null) {
@@ -1647,9 +1730,12 @@ public final class LoadedApk {
if (dead) {
mConnection.onBindingDied(name);
}
- // If there is a new service, it is now connected.
+ // If there is a new viable service, it is now connected.
if (service != null) {
mConnection.onServiceConnected(name, service);
+ } else {
+ // The binding machinery worked, but the remote returned null from onBind().
+ mConnection.onNullBinding(name);
}
}
diff --git a/android/app/LoaderManager.java b/android/app/LoaderManager.java
index 7969684a..86d0fd62 100644
--- a/android/app/LoaderManager.java
+++ b/android/app/LoaderManager.java
@@ -55,14 +55,16 @@ import java.lang.reflect.Modifier;
* <a href="{@docRoot}guide/topics/fundamentals/loaders.html">Loaders</a> developer guide.</p>
* </div>
*
- * @deprecated Use {@link android.support.v4.app.LoaderManager}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link android.support.v4.app.LoaderManager}
*/
@Deprecated
public abstract class LoaderManager {
/**
* Callback interface for a client to interact with the manager.
*
- * @deprecated Use {@link android.support.v4.app.LoaderManager.LoaderCallbacks}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">
+ * Support Library</a> {@link android.support.v4.app.LoaderManager.LoaderCallbacks}
*/
@Deprecated
public interface LoaderCallbacks<D> {
diff --git a/android/app/LocalActivityManager.java b/android/app/LocalActivityManager.java
index 3b273bc1..998ac5f2 100644
--- a/android/app/LocalActivityManager.java
+++ b/android/app/LocalActivityManager.java
@@ -22,6 +22,7 @@ import android.os.Binder;
import android.os.Bundle;
import android.util.Log;
import android.view.Window;
+
import com.android.internal.content.ReferrerIntent;
import java.util.ArrayList;
@@ -161,12 +162,12 @@ public class LocalActivityManager {
case CREATED:
if (desiredState == STARTED) {
if (localLOGV) Log.v(TAG, r.id + ": restarting");
- mActivityThread.performRestartActivity(r);
+ mActivityThread.performRestartActivity(r, true /* start */);
r.curState = STARTED;
}
if (desiredState == RESUMED) {
if (localLOGV) Log.v(TAG, r.id + ": restarting and resuming");
- mActivityThread.performRestartActivity(r);
+ mActivityThread.performRestartActivity(r, true /* start */);
mActivityThread.performResumeActivity(r, true, "moveToState-CREATED");
r.curState = RESUMED;
}
@@ -207,7 +208,7 @@ public class LocalActivityManager {
private void performPause(LocalActivityRecord r, boolean finishing) {
final boolean needState = r.instanceState == null;
final Bundle instanceState = mActivityThread.performPauseActivity(
- r, finishing, needState, "performPause");
+ r, finishing, needState, "performPause", null /* pendingActions */);
if (needState) {
r.instanceState = instanceState;
}
@@ -361,7 +362,8 @@ public class LocalActivityManager {
performPause(r, finish);
}
if (localLOGV) Log.v(TAG, r.id + ": destroying");
- mActivityThread.performDestroyActivity(r, finish);
+ mActivityThread.performDestroyActivity(r, finish, 0 /* configChanges */,
+ false /* getNonConfigInstance */);
r.activity = null;
r.window = null;
if (finish) {
@@ -625,7 +627,8 @@ public class LocalActivityManager {
for (int i=0; i<N; i++) {
LocalActivityRecord r = mActivityArray.get(i);
if (localLOGV) Log.v(TAG, r.id + ": destroying");
- mActivityThread.performDestroyActivity(r, finishing);
+ mActivityThread.performDestroyActivity(r, finishing, 0 /* configChanges */,
+ false /* getNonConfigInstance */);
}
mActivities.clear();
mActivityArray.clear();
diff --git a/android/app/Notification.java b/android/app/Notification.java
index 42c1347e..85c3be82 100644
--- a/android/app/Notification.java
+++ b/android/app/Notification.java
@@ -577,7 +577,13 @@ public class Notification implements Parcelable
public int flags;
/** @hide */
- @IntDef({PRIORITY_DEFAULT,PRIORITY_LOW,PRIORITY_MIN,PRIORITY_HIGH,PRIORITY_MAX})
+ @IntDef(prefix = { "PRIORITY_" }, value = {
+ PRIORITY_DEFAULT,
+ PRIORITY_LOW,
+ PRIORITY_MIN,
+ PRIORITY_HIGH,
+ PRIORITY_MAX
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface Priority {}
@@ -1084,6 +1090,12 @@ public class Notification implements Parcelable
public static final String EXTRA_HISTORIC_MESSAGES = "android.messages.historic";
/**
+ * {@link #extras} key: whether the {@link android.app.Notification.MessagingStyle} notification
+ * represents a group conversation.
+ */
+ public static final String EXTRA_IS_GROUP_CONVERSATION = "android.isGroupConversation";
+
+ /**
* {@link #extras} key: whether the notification should be colorized as
* supplied to {@link Builder#setColorized(boolean)}}.
*/
@@ -1941,6 +1953,7 @@ public class Notification implements Parcelable
mSortKey = parcel.readString();
extras = Bundle.setDefusable(parcel.readBundle(), true); // may be null
+ fixDuplicateExtras();
actions = parcel.createTypedArray(Action.CREATOR); // may be null
@@ -2389,6 +2402,33 @@ public class Notification implements Parcelable
};
/**
+ * Parcelling creates multiple copies of objects in {@code extras}. Fix them.
+ * <p>
+ * For backwards compatibility {@code extras} holds some references to "real" member data such
+ * as {@link getLargeIcon()} which is mirrored by {@link #EXTRA_LARGE_ICON}. This is mostly
+ * fine as long as the object stays in one process.
+ * <p>
+ * However, once the notification goes into a parcel each reference gets marshalled separately,
+ * wasting memory. Especially with large images on Auto and TV, this is worth fixing.
+ */
+ private void fixDuplicateExtras() {
+ if (extras != null) {
+ fixDuplicateExtra(mSmallIcon, EXTRA_SMALL_ICON);
+ fixDuplicateExtra(mLargeIcon, EXTRA_LARGE_ICON);
+ }
+ }
+
+ /**
+ * If we find an extra that's exactly the same as one of the "real" fields but refers to a
+ * separate object, replace it with the field's version to avoid holding duplicate copies.
+ */
+ private void fixDuplicateExtra(@Nullable Parcelable original, @NonNull String extraName) {
+ if (original != null && extras.getParcelable(extraName) != null) {
+ extras.putParcelable(extraName, original);
+ }
+ }
+
+ /**
* Sets the {@link #contentView} field to be a view with the standard "Latest Event"
* layout.
*
@@ -5926,9 +5966,10 @@ public class Notification implements Parcelable
public static final int MAXIMUM_RETAINED_MESSAGES = 25;
CharSequence mUserDisplayName;
- CharSequence mConversationTitle;
+ @Nullable CharSequence mConversationTitle;
List<Message> mMessages = new ArrayList<>();
List<Message> mHistoricMessages = new ArrayList<>();
+ boolean mIsGroupConversation;
MessagingStyle() {
}
@@ -5951,20 +5992,20 @@ public class Notification implements Parcelable
}
/**
- * Sets the title to be displayed on this conversation. This should only be used for
- * group messaging and left unset for one-on-one conversations.
- * @param conversationTitle
+ * Sets the title to be displayed on this conversation. May be set to {@code null}.
+ *
+ * @param conversationTitle A name for the conversation, or {@code null}
* @return this object for method chaining.
*/
- public MessagingStyle setConversationTitle(CharSequence conversationTitle) {
+ public MessagingStyle setConversationTitle(@Nullable CharSequence conversationTitle) {
mConversationTitle = conversationTitle;
return this;
}
/**
- * Return the title to be displayed on this conversation. Can be <code>null</code> and
- * should be for one-on-one conversations
+ * Return the title to be displayed on this conversation. May return {@code null}.
*/
+ @Nullable
public CharSequence getConversationTitle() {
return mConversationTitle;
}
@@ -6041,6 +6082,24 @@ public class Notification implements Parcelable
}
/**
+ * Sets whether this conversation notification represents a group.
+ * @param isGroupConversation {@code true} if the conversation represents a group,
+ * {@code false} otherwise.
+ * @return this object for method chaining
+ */
+ public MessagingStyle setGroupConversation(boolean isGroupConversation) {
+ mIsGroupConversation = isGroupConversation;
+ return this;
+ }
+
+ /**
+ * Returns {@code true} if this notification represents a group conversation.
+ */
+ public boolean isGroupConversation() {
+ return mIsGroupConversation;
+ }
+
+ /**
* @hide
*/
@Override
@@ -6060,6 +6119,7 @@ public class Notification implements Parcelable
}
fixTitleAndTextExtras(extras);
+ extras.putBoolean(EXTRA_IS_GROUP_CONVERSATION, mIsGroupConversation);
}
private void fixTitleAndTextExtras(Bundle extras) {
@@ -6102,6 +6162,7 @@ public class Notification implements Parcelable
mMessages = Message.getMessagesFromBundleArray(messages);
Parcelable[] histMessages = extras.getParcelableArray(EXTRA_HISTORIC_MESSAGES);
mHistoricMessages = Message.getMessagesFromBundleArray(histMessages);
+ mIsGroupConversation = extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION);
}
/**
diff --git a/android/app/SharedPreferencesImpl.java b/android/app/SharedPreferencesImpl.java
index 8c47598f..6dca4004 100644
--- a/android/app/SharedPreferencesImpl.java
+++ b/android/app/SharedPreferencesImpl.java
@@ -50,6 +50,11 @@ import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
final class SharedPreferencesImpl implements SharedPreferences {
private static final String TAG = "SharedPreferencesImpl";
@@ -69,16 +74,12 @@ final class SharedPreferencesImpl implements SharedPreferences {
private final Object mLock = new Object();
private final Object mWritingToDiskLock = new Object();
- @GuardedBy("mLock")
- private Map<String, Object> mMap;
+ private Future<Map<String, Object>> mMap;
@GuardedBy("mLock")
private int mDiskWritesInFlight = 0;
@GuardedBy("mLock")
- private boolean mLoaded = false;
-
- @GuardedBy("mLock")
private StructTimespec mStatTimestamp;
@GuardedBy("mLock")
@@ -105,27 +106,18 @@ final class SharedPreferencesImpl implements SharedPreferences {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
- mLoaded = false;
mMap = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
- synchronized (mLock) {
- mLoaded = false;
- }
- new Thread("SharedPreferencesImpl-load") {
- public void run() {
- loadFromDisk();
- }
- }.start();
+ FutureTask<Map<String, Object>> futureTask = new FutureTask<>(() -> loadFromDisk());
+ mMap = futureTask;
+ new Thread(futureTask, "SharedPreferencesImpl-load").start();
}
- private void loadFromDisk() {
+ private Map<String, Object> loadFromDisk() {
synchronized (mLock) {
- if (mLoaded) {
- return;
- }
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
@@ -158,16 +150,14 @@ final class SharedPreferencesImpl implements SharedPreferences {
}
synchronized (mLock) {
- mLoaded = true;
if (map != null) {
- mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
- mMap = new HashMap<>();
+ map = new HashMap<>();
}
- mLock.notifyAll();
}
+ return map;
}
static File makeBackupFile(File prefsFile) {
@@ -226,36 +216,42 @@ final class SharedPreferencesImpl implements SharedPreferences {
}
}
- private void awaitLoadedLocked() {
- if (!mLoaded) {
+ private @GuardedBy("mLock") Map<String, Object> getLoaded() {
+ // For backwards compatibility, we need to ignore any interrupts. b/70122540.
+ for (;;) {
+ try {
+ return mMap.get();
+ } catch (ExecutionException e) {
+ throw new IllegalStateException(e);
+ } catch (InterruptedException e) {
+ // Ignore and try again.
+ }
+ }
+ }
+ private @GuardedBy("mLock") Map<String, Object> getLoadedWithBlockGuard() {
+ if (!mMap.isDone()) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
- while (!mLoaded) {
- try {
- mLock.wait();
- } catch (InterruptedException unused) {
- }
- }
+ return getLoaded();
}
@Override
public Map<String, ?> getAll() {
+ Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- awaitLoadedLocked();
- //noinspection unchecked
- return new HashMap<String, Object>(mMap);
+ return new HashMap<String, Object>(map);
}
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
+ Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- awaitLoadedLocked();
- String v = (String)mMap.get(key);
+ String v = (String) map.get(key);
return v != null ? v : defValue;
}
}
@@ -263,66 +259,65 @@ final class SharedPreferencesImpl implements SharedPreferences {
@Override
@Nullable
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
+ Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- awaitLoadedLocked();
- Set<String> v = (Set<String>) mMap.get(key);
+ @SuppressWarnings("unchecked")
+ Set<String> v = (Set<String>) map.get(key);
return v != null ? v : defValues;
}
}
@Override
public int getInt(String key, int defValue) {
+ Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- awaitLoadedLocked();
- Integer v = (Integer)mMap.get(key);
+ Integer v = (Integer) map.get(key);
return v != null ? v : defValue;
}
}
@Override
public long getLong(String key, long defValue) {
+ Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- awaitLoadedLocked();
- Long v = (Long)mMap.get(key);
+ Long v = (Long) map.get(key);
return v != null ? v : defValue;
}
}
@Override
public float getFloat(String key, float defValue) {
+ Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- awaitLoadedLocked();
- Float v = (Float)mMap.get(key);
+ Float v = (Float) map.get(key);
return v != null ? v : defValue;
}
}
@Override
public boolean getBoolean(String key, boolean defValue) {
+ Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- awaitLoadedLocked();
- Boolean v = (Boolean)mMap.get(key);
+ Boolean v = (Boolean) map.get(key);
return v != null ? v : defValue;
}
}
@Override
public boolean contains(String key) {
+ Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- awaitLoadedLocked();
- return mMap.containsKey(key);
+ return map.containsKey(key);
}
}
@Override
public Editor edit() {
- // TODO: remove the need to call awaitLoadedLocked() when
+ // TODO: remove the need to call getLoaded() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
//
// context.getSharedPreferences(..).edit().putString(..).apply()
//
// ... all without blocking.
- synchronized (mLock) {
- awaitLoadedLocked();
- }
+ getLoadedWithBlockGuard();
return new EditorImpl();
}
@@ -476,13 +471,43 @@ final class SharedPreferencesImpl implements SharedPreferences {
// a memory commit comes in when we're already
// writing to disk.
if (mDiskWritesInFlight > 0) {
- // We can't modify our mMap as a currently
+ // We can't modify our map as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
- mMap = new HashMap<String, Object>(mMap);
+ mMap = new Future<Map<String, Object>>() {
+ private Map<String, Object> mCopiedMap =
+ new HashMap<String, Object>(getLoaded());
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return false;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return true;
+ }
+
+ @Override
+ public Map<String, Object> get()
+ throws InterruptedException, ExecutionException {
+ return mCopiedMap;
+ }
+
+ @Override
+ public Map<String, Object> get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return mCopiedMap;
+ }
+ };
}
- mapToWriteToDisk = mMap;
+ mapToWriteToDisk = getLoaded();
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
diff --git a/android/app/StatusBarManager.java b/android/app/StatusBarManager.java
index 23c4166d..85a9be35 100644
--- a/android/app/StatusBarManager.java
+++ b/android/app/StatusBarManager.java
@@ -80,9 +80,14 @@ public class StatusBarManager {
public static final int DISABLE2_MASK = DISABLE2_QUICK_SETTINGS | DISABLE2_SYSTEM_ICONS
| DISABLE2_NOTIFICATION_SHADE | DISABLE2_GLOBAL_ACTIONS;
- @IntDef(flag = true,
- value = {DISABLE2_NONE, DISABLE2_MASK, DISABLE2_QUICK_SETTINGS, DISABLE2_SYSTEM_ICONS,
- DISABLE2_NOTIFICATION_SHADE, DISABLE2_GLOBAL_ACTIONS})
+ @IntDef(flag = true, prefix = { "DISABLE2_" }, value = {
+ DISABLE2_NONE,
+ DISABLE2_MASK,
+ DISABLE2_QUICK_SETTINGS,
+ DISABLE2_SYSTEM_ICONS,
+ DISABLE2_NOTIFICATION_SHADE,
+ DISABLE2_GLOBAL_ACTIONS
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface Disable2Flags {}
diff --git a/android/app/SystemServiceRegistry.java b/android/app/SystemServiceRegistry.java
index e48946f2..66cf9915 100644
--- a/android/app/SystemServiceRegistry.java
+++ b/android/app/SystemServiceRegistry.java
@@ -22,6 +22,7 @@ import android.app.admin.DevicePolicyManager;
import android.app.admin.IDevicePolicyManager;
import android.app.job.IJobScheduler;
import android.app.job.JobScheduler;
+import android.app.slice.SliceManager;
import android.app.timezone.RulesManager;
import android.app.trust.TrustManager;
import android.app.usage.IStorageStatsManager;
@@ -551,8 +552,16 @@ final class SystemServiceRegistry {
registerService(Context.WALLPAPER_SERVICE, WallpaperManager.class,
new CachedServiceFetcher<WallpaperManager>() {
@Override
- public WallpaperManager createService(ContextImpl ctx) {
- return new WallpaperManager(ctx.getOuterContext(),
+ public WallpaperManager createService(ContextImpl ctx)
+ throws ServiceNotFoundException {
+ final IBinder b;
+ if (ctx.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) {
+ b = ServiceManager.getServiceOrThrow(Context.WALLPAPER_SERVICE);
+ } else {
+ b = ServiceManager.getService(Context.WALLPAPER_SERVICE);
+ }
+ IWallpaperManager service = IWallpaperManager.Stub.asInterface(b);
+ return new WallpaperManager(service, ctx.getOuterContext(),
ctx.mMainThread.getHandler());
}});
@@ -617,12 +626,13 @@ final class SystemServiceRegistry {
ConnectivityThread.getInstanceLooper());
}});
- registerService(Context.WIFI_RTT2_SERVICE, WifiRttManager.class,
+ registerService(Context.WIFI_RTT_RANGING_SERVICE, WifiRttManager.class,
new CachedServiceFetcher<WifiRttManager>() {
@Override
public WifiRttManager createService(ContextImpl ctx)
throws ServiceNotFoundException {
- IBinder b = ServiceManager.getServiceOrThrow(Context.WIFI_RTT2_SERVICE);
+ IBinder b = ServiceManager.getServiceOrThrow(
+ Context.WIFI_RTT_RANGING_SERVICE);
IWifiRttManager service = IWifiRttManager.Stub.asInterface(b);
return new WifiRttManager(ctx.getOuterContext(), service);
}});
@@ -944,6 +954,16 @@ final class SystemServiceRegistry {
ICrossProfileApps.Stub.asInterface(b));
}
});
+
+ registerService(Context.SLICE_SERVICE, SliceManager.class,
+ new CachedServiceFetcher<SliceManager>() {
+ @Override
+ public SliceManager createService(ContextImpl ctx)
+ throws ServiceNotFoundException {
+ return new SliceManager(ctx.getOuterContext(),
+ ctx.mMainThread.getHandler());
+ }
+ });
}
/**
diff --git a/android/app/UiAutomation.java b/android/app/UiAutomation.java
index c99de5dd..8f016853 100644
--- a/android/app/UiAutomation.java
+++ b/android/app/UiAutomation.java
@@ -26,6 +26,7 @@ import android.annotation.TestApi;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Point;
+import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.display.DisplayManagerGlobal;
import android.os.IBinder;
@@ -690,42 +691,15 @@ public final class UiAutomation {
.getRealDisplay(Display.DEFAULT_DISPLAY);
Point displaySize = new Point();
display.getRealSize(displaySize);
- final int displayWidth = displaySize.x;
- final int displayHeight = displaySize.y;
- final float screenshotWidth;
- final float screenshotHeight;
-
- final int rotation = display.getRotation();
- switch (rotation) {
- case ROTATION_FREEZE_0: {
- screenshotWidth = displayWidth;
- screenshotHeight = displayHeight;
- } break;
- case ROTATION_FREEZE_90: {
- screenshotWidth = displayHeight;
- screenshotHeight = displayWidth;
- } break;
- case ROTATION_FREEZE_180: {
- screenshotWidth = displayWidth;
- screenshotHeight = displayHeight;
- } break;
- case ROTATION_FREEZE_270: {
- screenshotWidth = displayHeight;
- screenshotHeight = displayWidth;
- } break;
- default: {
- throw new IllegalArgumentException("Invalid rotation: "
- + rotation);
- }
- }
+ int rotation = display.getRotation();
// Take the screenshot
Bitmap screenShot = null;
try {
// Calling out without a lock held.
- screenShot = mUiAutomationConnection.takeScreenshot((int) screenshotWidth,
- (int) screenshotHeight);
+ screenShot = mUiAutomationConnection.takeScreenshot(
+ new Rect(0, 0, displaySize.x, displaySize.y), rotation);
if (screenShot == null) {
return null;
}
@@ -734,21 +708,6 @@ public final class UiAutomation {
return null;
}
- // Rotate the screenshot to the current orientation
- if (rotation != ROTATION_FREEZE_0) {
- Bitmap unrotatedScreenShot = Bitmap.createBitmap(displayWidth, displayHeight,
- Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(unrotatedScreenShot);
- canvas.translate(unrotatedScreenShot.getWidth() / 2,
- unrotatedScreenShot.getHeight() / 2);
- canvas.rotate(getDegreesForRotation(rotation));
- canvas.translate(- screenshotWidth / 2, - screenshotHeight / 2);
- canvas.drawBitmap(screenShot, 0, 0, null);
- canvas.setBitmap(null);
- screenShot.recycle();
- screenShot = unrotatedScreenShot;
- }
-
// Optimization
screenShot.setHasAlpha(false);
diff --git a/android/app/UiAutomationConnection.java b/android/app/UiAutomationConnection.java
index 5e414b83..d3828ab4 100644
--- a/android/app/UiAutomationConnection.java
+++ b/android/app/UiAutomationConnection.java
@@ -21,6 +21,7 @@ import android.accessibilityservice.IAccessibilityServiceClient;
import android.content.Context;
import android.content.pm.IPackageManager;
import android.graphics.Bitmap;
+import android.graphics.Rect;
import android.hardware.input.InputManager;
import android.os.Binder;
import android.os.IBinder;
@@ -153,7 +154,7 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub {
}
@Override
- public Bitmap takeScreenshot(int width, int height) {
+ public Bitmap takeScreenshot(Rect crop, int rotation) {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
@@ -161,7 +162,9 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub {
}
final long identity = Binder.clearCallingIdentity();
try {
- return SurfaceControl.screenshot(width, height);
+ int width = crop.width();
+ int height = crop.height();
+ return SurfaceControl.screenshot(crop, width, height, rotation);
} finally {
Binder.restoreCallingIdentity(identity);
}
diff --git a/android/app/UiModeManager.java b/android/app/UiModeManager.java
index bc616686..0da5e249 100644
--- a/android/app/UiModeManager.java
+++ b/android/app/UiModeManager.java
@@ -98,7 +98,11 @@ public class UiModeManager {
public static String ACTION_EXIT_DESK_MODE = "android.app.action.EXIT_DESK_MODE";
/** @hide */
- @IntDef({MODE_NIGHT_AUTO, MODE_NIGHT_NO, MODE_NIGHT_YES})
+ @IntDef(prefix = { "MODE_" }, value = {
+ MODE_NIGHT_AUTO,
+ MODE_NIGHT_NO,
+ MODE_NIGHT_YES
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface NightMode {}
diff --git a/android/app/VrManager.java b/android/app/VrManager.java
index 392387a9..61b90e17 100644
--- a/android/app/VrManager.java
+++ b/android/app/VrManager.java
@@ -4,6 +4,7 @@ import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.annotation.TestApi;
import android.content.ComponentName;
import android.content.Context;
import android.os.Handler;
@@ -214,4 +215,22 @@ public class VrManager {
e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Start VR Input method for the packageName in {@link ComponentName}.
+ * This method notifies InputMethodManagerService to use VR IME instead of
+ * regular phone IME.
+ * @param componentName ComponentName of a VR InputMethod that should be set as selected
+ * input by InputMethodManagerService.
+ * @hide
+ */
+ @TestApi
+ @RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS)
+ public void setVrInputMethod(ComponentName componentName) {
+ try {
+ mService.setVrInputMethod(componentName);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/android/app/WallpaperInfo.java b/android/app/WallpaperInfo.java
index 9d40381f..35a17892 100644
--- a/android/app/WallpaperInfo.java
+++ b/android/app/WallpaperInfo.java
@@ -16,18 +16,15 @@
package android.app;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Resources.NotFoundException;
import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.graphics.drawable.Drawable;
@@ -39,6 +36,9 @@ import android.util.AttributeSet;
import android.util.Printer;
import android.util.Xml;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
import java.io.IOException;
/**
@@ -76,6 +76,7 @@ public final class WallpaperInfo implements Parcelable {
final int mContextUriResource;
final int mContextDescriptionResource;
final boolean mShowMetadataInPreview;
+ final boolean mSupportsAmbientMode;
/**
* Constructor.
@@ -89,15 +90,7 @@ public final class WallpaperInfo implements Parcelable {
mService = service;
ServiceInfo si = service.serviceInfo;
- PackageManager pm = context.getPackageManager();
- String settingsActivityComponent = null;
- int thumbnailRes = -1;
- int authorRes = -1;
- int descriptionRes = -1;
- int contextUriRes = -1;
- int contextDescriptionRes = -1;
- boolean showMetadataInPreview = false;
-
+ final PackageManager pm = context.getPackageManager();
XmlResourceParser parser = null;
try {
parser = si.loadXmlMetaData(pm, WallpaperService.SERVICE_META_DATA);
@@ -123,27 +116,30 @@ public final class WallpaperInfo implements Parcelable {
TypedArray sa = res.obtainAttributes(attrs,
com.android.internal.R.styleable.Wallpaper);
- settingsActivityComponent = sa.getString(
+ mSettingsActivityName = sa.getString(
com.android.internal.R.styleable.Wallpaper_settingsActivity);
-
- thumbnailRes = sa.getResourceId(
+
+ mThumbnailResource = sa.getResourceId(
com.android.internal.R.styleable.Wallpaper_thumbnail,
-1);
- authorRes = sa.getResourceId(
+ mAuthorResource = sa.getResourceId(
com.android.internal.R.styleable.Wallpaper_author,
-1);
- descriptionRes = sa.getResourceId(
+ mDescriptionResource = sa.getResourceId(
com.android.internal.R.styleable.Wallpaper_description,
-1);
- contextUriRes = sa.getResourceId(
+ mContextUriResource = sa.getResourceId(
com.android.internal.R.styleable.Wallpaper_contextUri,
-1);
- contextDescriptionRes = sa.getResourceId(
+ mContextDescriptionResource = sa.getResourceId(
com.android.internal.R.styleable.Wallpaper_contextDescription,
-1);
- showMetadataInPreview = sa.getBoolean(
+ mShowMetadataInPreview = sa.getBoolean(
com.android.internal.R.styleable.Wallpaper_showMetadataInPreview,
false);
+ mSupportsAmbientMode = sa.getBoolean(
+ com.android.internal.R.styleable.Wallpaper_supportsAmbientMode,
+ false);
sa.recycle();
} catch (NameNotFoundException e) {
@@ -152,14 +148,6 @@ public final class WallpaperInfo implements Parcelable {
} finally {
if (parser != null) parser.close();
}
-
- mSettingsActivityName = settingsActivityComponent;
- mThumbnailResource = thumbnailRes;
- mAuthorResource = authorRes;
- mDescriptionResource = descriptionRes;
- mContextUriResource = contextUriRes;
- mContextDescriptionResource = contextDescriptionRes;
- mShowMetadataInPreview = showMetadataInPreview;
}
WallpaperInfo(Parcel source) {
@@ -170,6 +158,7 @@ public final class WallpaperInfo implements Parcelable {
mContextUriResource = source.readInt();
mContextDescriptionResource = source.readInt();
mShowMetadataInPreview = source.readInt() != 0;
+ mSupportsAmbientMode = source.readInt() != 0;
mService = ResolveInfo.CREATOR.createFromParcel(source);
}
@@ -326,6 +315,16 @@ public final class WallpaperInfo implements Parcelable {
}
/**
+ * Returns whether a wallpaper was optimized or not for ambient mode.
+ *
+ * @return {@code true} if wallpaper can draw in ambient mode.
+ * @hide
+ */
+ public boolean getSupportsAmbientMode() {
+ return mSupportsAmbientMode;
+ }
+
+ /**
* Return the class name of an activity that provides a settings UI for
* the wallpaper. You can launch this activity be starting it with
* an {@link android.content.Intent} whose action is MAIN and with an
@@ -366,6 +365,7 @@ public final class WallpaperInfo implements Parcelable {
dest.writeInt(mContextUriResource);
dest.writeInt(mContextDescriptionResource);
dest.writeInt(mShowMetadataInPreview ? 1 : 0);
+ dest.writeInt(mSupportsAmbientMode ? 1 : 0);
mService.writeToParcel(dest, flags);
}
diff --git a/android/app/WallpaperManager.java b/android/app/WallpaperManager.java
index 081bd814..f21746cd 100644
--- a/android/app/WallpaperManager.java
+++ b/android/app/WallpaperManager.java
@@ -176,7 +176,7 @@ public class WallpaperManager {
// flags for which kind of wallpaper to act on
/** @hide */
- @IntDef(flag = true, value = {
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
FLAG_SYSTEM,
FLAG_LOCK
})
@@ -286,9 +286,8 @@ public class WallpaperManager {
private Bitmap mDefaultWallpaper;
private Handler mMainLooperHandler;
- Globals(Looper looper) {
- IBinder b = ServiceManager.getService(Context.WALLPAPER_SERVICE);
- mService = IWallpaperManager.Stub.asInterface(b);
+ Globals(IWallpaperManager service, Looper looper) {
+ mService = service;
mMainLooperHandler = new Handler(looper);
forgetLoadedWallpaper();
}
@@ -497,17 +496,17 @@ public class WallpaperManager {
private static final Object sSync = new Object[0];
private static Globals sGlobals;
- static void initGlobals(Looper looper) {
+ static void initGlobals(IWallpaperManager service, Looper looper) {
synchronized (sSync) {
if (sGlobals == null) {
- sGlobals = new Globals(looper);
+ sGlobals = new Globals(service, looper);
}
}
}
- /*package*/ WallpaperManager(Context context, Handler handler) {
+ /*package*/ WallpaperManager(IWallpaperManager service, Context context, Handler handler) {
mContext = context;
- initGlobals(context.getMainLooper());
+ initGlobals(service, context.getMainLooper());
}
/**
diff --git a/android/app/WindowConfiguration.java b/android/app/WindowConfiguration.java
index 80399ae6..085fc79f 100644
--- a/android/app/WindowConfiguration.java
+++ b/android/app/WindowConfiguration.java
@@ -89,7 +89,7 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu
public static final int WINDOWING_MODE_FREEFORM = 5;
/** @hide */
- @IntDef({
+ @IntDef(prefix = { "WINDOWING_MODE_" }, value = {
WINDOWING_MODE_UNDEFINED,
WINDOWING_MODE_FULLSCREEN,
WINDOWING_MODE_PINNED,
@@ -115,7 +115,7 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu
public static final int ACTIVITY_TYPE_ASSISTANT = 4;
/** @hide */
- @IntDef({
+ @IntDef(prefix = { "ACTIVITY_TYPE_" }, value = {
ACTIVITY_TYPE_UNDEFINED,
ACTIVITY_TYPE_STANDARD,
ACTIVITY_TYPE_HOME,
@@ -138,13 +138,12 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu
public static final int WINDOW_CONFIG_ACTIVITY_TYPE = 1 << 3;
/** @hide */
- @IntDef(flag = true,
- value = {
- WINDOW_CONFIG_BOUNDS,
- WINDOW_CONFIG_APP_BOUNDS,
- WINDOW_CONFIG_WINDOWING_MODE,
- WINDOW_CONFIG_ACTIVITY_TYPE
- })
+ @IntDef(flag = true, prefix = { "WINDOW_CONFIG_" }, value = {
+ WINDOW_CONFIG_BOUNDS,
+ WINDOW_CONFIG_APP_BOUNDS,
+ WINDOW_CONFIG_WINDOWING_MODE,
+ WINDOW_CONFIG_ACTIVITY_TYPE
+ })
public @interface WindowConfig {}
public WindowConfiguration() {
diff --git a/android/app/admin/DeviceAdminReceiver.java b/android/app/admin/DeviceAdminReceiver.java
index d0d98c9f..2e697ac0 100644
--- a/android/app/admin/DeviceAdminReceiver.java
+++ b/android/app/admin/DeviceAdminReceiver.java
@@ -368,9 +368,9 @@ public class DeviceAdminReceiver extends BroadcastReceiver {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({
- BUGREPORT_FAILURE_FAILED_COMPLETING,
- BUGREPORT_FAILURE_FILE_NO_LONGER_AVAILABLE
+ @IntDef(prefix = { "BUGREPORT_FAILURE_" }, value = {
+ BUGREPORT_FAILURE_FAILED_COMPLETING,
+ BUGREPORT_FAILURE_FILE_NO_LONGER_AVAILABLE
})
public @interface BugreportFailureCode {}
diff --git a/android/app/admin/DevicePolicyManager.java b/android/app/admin/DevicePolicyManager.java
index 0bca9690..7e80ac7b 100644
--- a/android/app/admin/DevicePolicyManager.java
+++ b/android/app/admin/DevicePolicyManager.java
@@ -16,7 +16,9 @@
package android.app.admin;
+import android.annotation.CallbackExecutor;
import android.annotation.ColorInt;
+import android.annotation.Condemned;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -49,6 +51,7 @@ import android.net.ProxyInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.os.Process;
@@ -57,7 +60,15 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.ContactsContract.Directory;
+import android.security.AttestedKeyPair;
import android.security.Credentials;
+import android.security.KeyChain;
+import android.security.KeyChainException;
+import android.security.keymaster.KeymasterCertificateChain;
+import android.security.keystore.AttestationUtils;
+import android.security.keystore.KeyAttestationException;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.ParcelableKeyGenParameterSpec;
import android.service.restrictions.RestrictionsReceiver;
import android.telephony.TelephonyManager;
import android.util.ArraySet;
@@ -75,6 +86,7 @@ import java.lang.annotation.RetentionPolicy;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.security.KeyFactory;
+import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
@@ -88,6 +100,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.Executor;
/**
* Public interface for managing policies enforced on a device. Most clients of this class must be
@@ -1315,9 +1328,15 @@ public class DevicePolicyManager {
public static final String DELEGATION_ENABLE_SYSTEM_APP = "delegation-enable-system-app";
/**
+ * Delegation for installing existing packages. This scope grants access to the
+ * {@link #installExistingPackage} API.
+ */
+ public static final String DELEGATION_INSTALL_EXISTING_PACKAGE =
+ "delegation-install-existing-package";
+
+ /**
* Delegation of management of uninstalled packages. This scope grants access to the
* {@code #setKeepUninstalledPackages} and {@code #getKeepUninstalledPackages} APIs.
- * @hide
*/
public static final String DELEGATION_KEEP_UNINSTALLED_PACKAGES =
"delegation-keep-uninstalled-packages";
@@ -1360,8 +1379,13 @@ public class DevicePolicyManager {
/**
* @hide
*/
- @IntDef({STATE_USER_UNMANAGED, STATE_USER_SETUP_INCOMPLETE, STATE_USER_SETUP_COMPLETE,
- STATE_USER_SETUP_FINALIZED, STATE_USER_PROFILE_COMPLETE})
+ @IntDef(prefix = { "STATE_USER_" }, value = {
+ STATE_USER_UNMANAGED,
+ STATE_USER_SETUP_INCOMPLETE,
+ STATE_USER_SETUP_COMPLETE,
+ STATE_USER_SETUP_FINALIZED,
+ STATE_USER_PROFILE_COMPLETE
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface UserProvisioningState {}
@@ -1534,11 +1558,13 @@ public class DevicePolicyManager {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({CODE_OK, CODE_HAS_DEVICE_OWNER, CODE_USER_HAS_PROFILE_OWNER, CODE_USER_NOT_RUNNING,
+ @IntDef(prefix = { "CODE_" }, value = {
+ CODE_OK, CODE_HAS_DEVICE_OWNER, CODE_USER_HAS_PROFILE_OWNER, CODE_USER_NOT_RUNNING,
CODE_USER_SETUP_COMPLETED, CODE_NOT_SYSTEM_USER, CODE_HAS_PAIRED,
CODE_MANAGED_USERS_NOT_SUPPORTED, CODE_SYSTEM_USER, CODE_CANNOT_ADD_MANAGED_PROFILE,
CODE_NOT_SYSTEM_USER_SPLIT, CODE_DEVICE_ADMIN_NOT_SUPPORTED,
- CODE_SPLIT_SYSTEM_USER_DEVICE_SYSTEM_USER, CODE_ADD_MANAGED_PROFILE_DISALLOWED})
+ CODE_SPLIT_SYSTEM_USER_DEVICE_SYSTEM_USER, CODE_ADD_MANAGED_PROFILE_DISALLOWED
+ })
public @interface ProvisioningPreCondition {}
/**
@@ -1620,11 +1646,15 @@ public class DevicePolicyManager {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true,
- value = {LOCK_TASK_FEATURE_NONE, LOCK_TASK_FEATURE_SYSTEM_INFO,
- LOCK_TASK_FEATURE_NOTIFICATIONS, LOCK_TASK_FEATURE_HOME,
- LOCK_TASK_FEATURE_RECENTS, LOCK_TASK_FEATURE_GLOBAL_ACTIONS,
- LOCK_TASK_FEATURE_KEYGUARD})
+ @IntDef(flag = true, prefix = { "LOCK_TASK_FEATURE_" }, value = {
+ LOCK_TASK_FEATURE_NONE,
+ LOCK_TASK_FEATURE_SYSTEM_INFO,
+ LOCK_TASK_FEATURE_NOTIFICATIONS,
+ LOCK_TASK_FEATURE_HOME,
+ LOCK_TASK_FEATURE_RECENTS,
+ LOCK_TASK_FEATURE_GLOBAL_ACTIONS,
+ LOCK_TASK_FEATURE_KEYGUARD
+ })
public @interface LockTaskFeature {}
/**
@@ -2605,10 +2635,115 @@ public class DevicePolicyManager {
}
/**
+ * The maximum number of characters allowed in the password blacklist.
+ */
+ private static final int PASSWORD_BLACKLIST_CHARACTER_LIMIT = 128 * 1000;
+
+ /**
+ * Throws an exception if the password blacklist is too large.
+ *
+ * @hide
+ */
+ public static void enforcePasswordBlacklistSize(List<String> blacklist) {
+ if (blacklist == null) {
+ return;
+ }
+ long characterCount = 0;
+ for (final String item : blacklist) {
+ characterCount += item.length();
+ }
+ if (characterCount > PASSWORD_BLACKLIST_CHARACTER_LIMIT) {
+ throw new IllegalArgumentException("128 thousand blacklist character limit exceeded by "
+ + (characterCount - PASSWORD_BLACKLIST_CHARACTER_LIMIT) + " characters");
+ }
+ }
+
+ /**
+ * Called by an application that is administering the device to blacklist passwords.
+ * <p>
+ * Any blacklisted password or PIN is prevented from being enrolled by the user or the admin.
+ * Note that the match against the blacklist is case insensitive. The blacklist applies for all
+ * password qualities requested by {@link #setPasswordQuality} however it is not taken into
+ * consideration by {@link #isActivePasswordSufficient}.
+ * <p>
+ * The blacklist can be cleared by passing {@code null} or an empty list. The blacklist is
+ * given a name that is used to track which blacklist is currently set by calling {@link
+ * #getPasswordBlacklistName}. If the blacklist is being cleared, the name is ignored and {@link
+ * #getPasswordBlacklistName} will return {@code null}. The name can only be {@code null} when
+ * the blacklist is being cleared.
+ * <p>
+ * The blacklist is limited to a total of 128 thousand characters rather than limiting to a
+ * number of entries.
+ * <p>
+ * This method can be called on the {@link DevicePolicyManager} instance returned by
+ * {@link #getParentProfileInstance(ComponentName)} in order to set restrictions on the parent
+ * profile.
+ *
+ * @param admin the {@link DeviceAdminReceiver} this request is associated with
+ * @param name name to associate with the blacklist
+ * @param blacklist list of passwords to blacklist or {@code null} to clear the blacklist
+ * @return whether the new blacklist was successfully installed
+ * @throws SecurityException if {@code admin} is not a device or profile owner
+ * @throws IllegalArgumentException if the blacklist surpasses the character limit
+ * @throws NullPointerException if {@code name} is {@code null} when setting a non-empty list
+ *
+ * @see #getPasswordBlacklistName
+ * @see #isActivePasswordSufficient
+ * @see #resetPasswordWithToken
+ */
+ public boolean setPasswordBlacklist(@NonNull ComponentName admin, @Nullable String name,
+ @Nullable List<String> blacklist) {
+ enforcePasswordBlacklistSize(blacklist);
+
+ try {
+ return mService.setPasswordBlacklist(admin, name, blacklist, mParentInstance);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get the name of the password blacklist set by the given admin.
+ *
+ * @param admin the {@link DeviceAdminReceiver} this request is associated with
+ * @return the name of the blacklist or {@code null} if no blacklist is set
+ *
+ * @see #setPasswordBlacklist
+ */
+ public @Nullable String getPasswordBlacklistName(@NonNull ComponentName admin) {
+ try {
+ return mService.getPasswordBlacklistName(admin, myUserId(), mParentInstance);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Test if a given password is blacklisted.
+ *
+ * @param userId the user to valiate for
+ * @param password the password to check against the blacklist
+ * @return whether the password is blacklisted
+ *
+ * @see #setPasswordBlacklist
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.TEST_BLACKLISTED_PASSWORD)
+ public boolean isPasswordBlacklisted(@UserIdInt int userId, @NonNull String password) {
+ try {
+ return mService.isPasswordBlacklisted(userId, password);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Determine whether the current password the user has set is sufficient to meet the policy
* requirements (e.g. quality, minimum length) that have been requested by the admins of this
* user and its participating profiles. Restrictions on profiles that have a separate challenge
- * are not taken into account. The user must be unlocked in order to perform the check.
+ * are not taken into account. The user must be unlocked in order to perform the check. The
+ * password blacklist is not considered when checking sufficiency.
* <p>
* The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call this method; if it has
@@ -2635,6 +2770,29 @@ public class DevicePolicyManager {
}
/**
+ * When called by a profile owner of a managed profile returns true if the profile uses unified
+ * challenge with its parent user.
+ *
+ * <strong>Note: This method is not concerned with password quality and will return false if
+ * the profile has empty password as a separate challenge.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @throws SecurityException if {@code admin} is not a profile owner of a managed profile.
+ * @see UserManager#DISALLOW_UNIFIED_PASSWORD
+ */
+ public boolean isUsingUnifiedPassword(@NonNull ComponentName admin) {
+ throwIfParentInstance("isUsingUnifiedPassword");
+ if (mService != null) {
+ try {
+ return mService.isUsingUnifiedPassword(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return true;
+ }
+
+ /**
* Determine whether the current profile password the user has set is sufficient
* to meet the policy requirements (e.g. quality, minimum length) that have been
* requested by the admins of the parent user and its profiles.
@@ -3049,23 +3207,6 @@ public class DevicePolicyManager {
}
/**
- * Returns maximum time to lock that applied by all profiles in this user. We do this because we
- * do not have a separate timeout to lock for work challenge only.
- *
- * @hide
- */
- public long getMaximumTimeToLockForUserAndProfiles(int userHandle) {
- if (mService != null) {
- try {
- return mService.getMaximumTimeToLockForUserAndProfiles(userHandle);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
- return 0;
- }
-
- /**
* Called by a device/profile owner to set the timeout after which unlocking with secondary, non
* strong auth (e.g. fingerprint, trust agents) times out, i.e. the user has to use a strong
* authentication method like password, pin or pattern.
@@ -3153,7 +3294,9 @@ public class DevicePolicyManager {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag=true, value={FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY})
+ @IntDef(flag = true, prefix = { "FLAG_EVICT_" }, value = {
+ FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY
+ })
public @interface LockNowFlag {}
/**
@@ -3468,6 +3611,16 @@ public class DevicePolicyManager {
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_START_ENCRYPTION
= "android.app.action.START_ENCRYPTION";
+
+ /**
+ * Broadcast action: notify managed provisioning that new managed user is created.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MANAGED_USER_CREATED =
+ "android.app.action.MANAGED_USER_CREATED";
+
/**
* Widgets are enabled in keyguard
*/
@@ -3943,6 +4096,108 @@ public class DevicePolicyManager {
}
/**
+ * Called by a device or profile owner, or delegated certificate installer, to generate a
+ * new private/public key pair. If the device supports key generation via secure hardware,
+ * this method is useful for creating a key in KeyChain that never left the secure hardware.
+ *
+ * Access to the key is controlled the same way as in {@link #installKeyPair}.
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if calling from a delegated certificate installer.
+ * @param algorithm The key generation algorithm, see {@link java.security.KeyPairGenerator}.
+ * @param keySpec Specification of the key to generate, see
+ * {@link java.security.KeyPairGenerator}.
+ * @return A non-null {@code AttestedKeyPair} if the key generation succeeded, null otherwise.
+ * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile
+ * owner.
+ * @throws IllegalArgumentException if the alias in {@code keySpec} is empty, or if the
+ * algorithm specification in {@code keySpec} is not {@code RSAKeyGenParameterSpec}
+ * or {@code ECGenParameterSpec}.
+ */
+ public AttestedKeyPair generateKeyPair(@Nullable ComponentName admin,
+ @NonNull String algorithm, @NonNull KeyGenParameterSpec keySpec) {
+ throwIfParentInstance("generateKeyPair");
+ try {
+ final ParcelableKeyGenParameterSpec parcelableSpec =
+ new ParcelableKeyGenParameterSpec(keySpec);
+ KeymasterCertificateChain attestationChain = new KeymasterCertificateChain();
+ final boolean success = mService.generateKeyPair(
+ admin, mContext.getPackageName(), algorithm, parcelableSpec, attestationChain);
+ if (!success) {
+ Log.e(TAG, "Error generating key via DevicePolicyManagerService.");
+ return null;
+ }
+
+ final String alias = keySpec.getKeystoreAlias();
+ final KeyPair keyPair = KeyChain.getKeyPair(mContext, alias);
+ Certificate[] outputChain = null;
+ try {
+ if (AttestationUtils.isChainValid(attestationChain)) {
+ outputChain = AttestationUtils.parseCertificateChain(attestationChain);
+ }
+ } catch (KeyAttestationException e) {
+ Log.e(TAG, "Error parsing attestation chain for alias " + alias, e);
+ mService.removeKeyPair(admin, mContext.getPackageName(), alias);
+ return null;
+ }
+ return new AttestedKeyPair(keyPair, outputChain);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (KeyChainException e) {
+ Log.w(TAG, "Failed to generate key", e);
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Interrupted while generating key", e);
+ Thread.currentThread().interrupt();
+ }
+ return null;
+ }
+
+
+ /**
+ * Called by a device or profile owner, or delegated certificate installer, to associate
+ * certificates with a key pair that was generated using {@link #generateKeyPair}, and
+ * set whether the key is available for the user to choose in the certificate selection
+ * prompt.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
+ * {@code null} if calling from a delegated certificate installer.
+ * @param alias The private key alias under which to install the certificate. The {@code alias}
+ * should denote an existing private key. If a certificate with that alias already
+ * exists, it will be overwritten.
+ * @param certs The certificate chain to install. The chain should start with the leaf
+ * certificate and include the chain of trust in order. This will be returned by
+ * {@link android.security.KeyChain#getCertificateChain}.
+ * @param isUserSelectable {@code true} to indicate that a user can select this key via the
+ * certificate selection prompt, {@code false} to indicate that this key can only be
+ * granted access by implementing
+ * {@link android.app.admin.DeviceAdminReceiver#onChoosePrivateKeyAlias}.
+ * @return {@code true} if the provided {@code alias} exists and the certificates has been
+ * successfully associated with it, {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile
+ * owner, or {@code admin} is null but the calling application is not a delegated
+ * certificate installer.
+ */
+ public boolean setKeyPairCertificate(@Nullable ComponentName admin,
+ @NonNull String alias, @NonNull List<Certificate> certs, boolean isUserSelectable) {
+ throwIfParentInstance("setKeyPairCertificate");
+ try {
+ final byte[] pemCert = Credentials.convertToPem(certs.get(0));
+ byte[] pemChain = null;
+ if (certs.size() > 1) {
+ pemChain = Credentials.convertToPem(
+ certs.subList(1, certs.size()).toArray(new Certificate[0]));
+ }
+ return mService.setKeyPairCertificate(admin, mContext.getPackageName(), alias, pemCert,
+ pemChain, isUserSelectable);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (CertificateException | IOException e) {
+ Log.w(TAG, "Could not pem-encode certificate", e);
+ }
+ return false;
+ }
+
+
+ /**
* @return the alias of a given CA certificate in the certificate store, or {@code null} if it
* doesn't exist.
*/
@@ -4212,16 +4467,16 @@ public class DevicePolicyManager {
/**
* Called by a device owner to request a bugreport.
* <p>
- * If the device contains secondary users or profiles, they must be affiliated with the device
- * owner user. Otherwise a {@link SecurityException} will be thrown. See
- * {@link #setAffiliationIds}.
+ * If the device contains secondary users or profiles, they must be affiliated with the device.
+ * Otherwise a {@link SecurityException} will be thrown. See {@link #isAffiliatedUser}.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @return {@code true} if the bugreport collection started successfully, or {@code false} if it
* wasn't triggered because a previous bugreport operation is still active (either the
* bugreport is still running or waiting for the user to share or decline)
* @throws SecurityException if {@code admin} is not a device owner, or there is at least one
- * profile or secondary user that is not affiliated with the device owner user.
+ * profile or secondary user that is not affiliated with the device.
+ * @see #isAffiliatedUser
*/
public boolean requestBugreport(@NonNull ComponentName admin) {
throwIfParentInstance("requestBugreport");
@@ -6035,7 +6290,6 @@ public class DevicePolicyManager {
* @return List of package names to keep cached.
* @see #setDelegatedScopes
* @see #DELEGATION_KEEP_UNINSTALLED_PACKAGES
- * @hide
*/
public @Nullable List<String> getKeepUninstalledPackages(@Nullable ComponentName admin) {
throwIfParentInstance("getKeepUninstalledPackages");
@@ -6063,7 +6317,6 @@ public class DevicePolicyManager {
* @throws SecurityException if {@code admin} is not a device owner.
* @see #setDelegatedScopes
* @see #DELEGATION_KEEP_UNINSTALLED_PACKAGES
- * @hide
*/
public void setKeepUninstalledPackages(@Nullable ComponentName admin,
@NonNull List<String> packageNames) {
@@ -6151,21 +6404,27 @@ public class DevicePolicyManager {
public static final int MAKE_USER_DEMO = 0x0004;
/**
- * Flag used by {@link #createAndManageUser} to specificy that the newly created user should be
+ * Flag used by {@link #createAndManageUser} to specify that the newly created user should be
* started in the background as part of the user creation.
*/
- // TODO: Investigate solutions for the case where reboot happens before setup is completed.
public static final int START_USER_IN_BACKGROUND = 0x0008;
/**
+ * Flag used by {@link #createAndManageUser} to specify that the newly created user should skip
+ * the disabling of system apps during provisioning.
+ */
+ public static final int LEAVE_ALL_SYSTEM_APPS_ENABLED = 0x0010;
+
+ /**
* @hide
*/
- @IntDef(
- flag = true,
- prefix = {"SKIP_", "MAKE_USER_", "START_"},
- value = {SKIP_SETUP_WIZARD, MAKE_USER_EPHEMERAL, MAKE_USER_DEMO,
- START_USER_IN_BACKGROUND}
- )
+ @IntDef(flag = true, prefix = { "SKIP_", "MAKE_USER_", "START_", "LEAVE_" }, value = {
+ SKIP_SETUP_WIZARD,
+ MAKE_USER_EPHEMERAL,
+ MAKE_USER_DEMO,
+ START_USER_IN_BACKGROUND,
+ LEAVE_ALL_SYSTEM_APPS_ENABLED
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface CreateAndManageUserFlags {}
@@ -6219,7 +6478,7 @@ public class DevicePolicyManager {
* @return {@code true} if the user was removed, {@code false} otherwise.
* @throws SecurityException if {@code admin} is not a device owner.
*/
- public boolean removeUser(@NonNull ComponentName admin, UserHandle userHandle) {
+ public boolean removeUser(@NonNull ComponentName admin, @NonNull UserHandle userHandle) {
throwIfParentInstance("removeUser");
try {
return mService.removeUser(admin, userHandle);
@@ -6230,6 +6489,7 @@ public class DevicePolicyManager {
/**
* Called by a device owner to switch the specified user to the foreground.
+ * <p> This cannot be used to switch to a managed profile.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param userHandle the user to switch to; null will switch to primary.
@@ -6247,6 +6507,80 @@ public class DevicePolicyManager {
}
/**
+ * Called by a device owner to stop the specified secondary user.
+ * <p> This cannot be used to stop the primary user or a managed profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param userHandle the user to be stopped.
+ * @return {@code true} if the user can be stopped, {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public boolean stopUser(@NonNull ComponentName admin, @NonNull UserHandle userHandle) {
+ throwIfParentInstance("stopUser");
+ try {
+ return mService.stopUser(admin, userHandle);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by a profile owner that is affiliated with the device to stop the calling user
+ * and switch back to primary.
+ * <p> This has no effect when called on a managed profile.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return {@code true} if the exit was successful, {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not a profile owner affiliated with the device.
+ * @see #isAffiliatedUser
+ */
+ public boolean logoutUser(@NonNull ComponentName admin) {
+ throwIfParentInstance("logoutUser");
+ try {
+ return mService.logoutUser(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by a device owner to list all secondary users on the device, excluding managed
+ * profiles.
+ * <p> Used for various user management APIs, including {@link #switchUser}, {@link #removeUser}
+ * and {@link #stopUser}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return list of other {@link UserHandle}s on the device.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ * @see #switchUser
+ * @see #removeUser
+ * @see #stopUser
+ */
+ public List<UserHandle> getSecondaryUsers(@NonNull ComponentName admin) {
+ throwIfParentInstance("getSecondaryUsers");
+ try {
+ return mService.getSecondaryUsers(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Checks if the profile owner is running in an ephemeral user.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return whether the profile owner is running in an ephemeral user.
+ */
+ public boolean isEphemeralUser(@NonNull ComponentName admin) {
+ throwIfParentInstance("isEphemeralUser");
+ try {
+ return mService.isEphemeralUser(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Retrieves the application restrictions for a given target application running in the calling
* user.
* <p>
@@ -6481,6 +6815,37 @@ public class DevicePolicyManager {
}
/**
+ * Install an existing package that has been installed in another user, or has been kept after
+ * removal via {@link #setKeepUninstalledPackages}.
+ * This function can be called by a device owner, profile owner or a delegate given
+ * the {@link #DELEGATION_INSTALL_EXISTING_PACKAGE} scope via {@link #setDelegatedScopes}.
+ * When called in a secondary user or managed profile, the user/profile must be affiliated with
+ * the device. See {@link #isAffiliatedUser}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param packageName The package to be installed in the calling profile.
+ * @return {@code true} if the app is installed; {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of
+ * an affiliated user or profile.
+ * @see #setKeepUninstalledPackages
+ * @see #setDelegatedScopes
+ * @see #isAffiliatedUser
+ * @see #DELEGATION_PACKAGE_ACCESS
+ */
+ public boolean installExistingPackage(@NonNull ComponentName admin, String packageName) {
+ throwIfParentInstance("installExistingPackage");
+ if (mService != null) {
+ try {
+ return mService.installExistingPackage(admin, mContext.getPackageName(),
+ packageName);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
* Called by a device owner or profile owner to disable account management for a specific type
* of account.
* <p>
@@ -6551,13 +6916,14 @@ public class DevicePolicyManager {
* package list results in locked tasks belonging to those packages to be finished.
* <p>
* This function can only be called by the device owner or by a profile owner of a user/profile
- * that is affiliated with the device owner user. See {@link #setAffiliationIds}. Any packages
+ * that is affiliated with the device. See {@link #isAffiliatedUser}. Any packages
* set via this method will be cleared if the user becomes unaffiliated.
*
* @param packages The list of packages allowed to enter lock task mode
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @throws SecurityException if {@code admin} is not the device owner, or the profile owner of
* an affiliated user or profile.
+ * @see #isAffiliatedUser
* @see Activity#startLockTask()
* @see DeviceAdminReceiver#onLockTaskModeEntering(Context, Intent, String)
* @see DeviceAdminReceiver#onLockTaskModeExiting(Context, Intent)
@@ -6580,6 +6946,7 @@ public class DevicePolicyManager {
*
* @throws SecurityException if {@code admin} is not the device owner, or the profile owner of
* an affiliated user or profile.
+ * @see #isAffiliatedUser
* @see #setLockTaskPackages
*/
public @NonNull String[] getLockTaskPackages(@NonNull ComponentName admin) {
@@ -6619,7 +6986,7 @@ public class DevicePolicyManager {
* enabled.
* <p>
* This function can only be called by the device owner or by a profile owner of a user/profile
- * that is affiliated with the device owner user. See {@link #setAffiliationIds}. Any features
+ * that is affiliated with the device. See {@link #isAffiliatedUser}. Any features
* set via this method will be cleared if the user becomes unaffiliated.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
@@ -6633,6 +7000,7 @@ public class DevicePolicyManager {
* {@link #LOCK_TASK_FEATURE_KEYGUARD}
* @throws SecurityException if {@code admin} is not the device owner, or the profile owner of
* an affiliated user or profile.
+ * @see #isAffiliatedUser
*/
public void setLockTaskFeatures(@NonNull ComponentName admin, @LockTaskFeature int flags) {
throwIfParentInstance("setLockTaskFeatures");
@@ -6652,7 +7020,8 @@ public class DevicePolicyManager {
* @return bitfield of flags. See {@link #setLockTaskFeatures(ComponentName, int)} for a list.
* @throws SecurityException if {@code admin} is not the device owner, or the profile owner of
* an affiliated user or profile.
- * @see #setLockTaskFeatures(ComponentName, int)
+ * @see #isAffiliatedUser
+ * @see #setLockTaskFeatures
*/
public @LockTaskFeature int getLockTaskFeatures(@NonNull ComponentName admin) {
throwIfParentInstance("getLockTaskFeatures");
@@ -6667,7 +7036,7 @@ public class DevicePolicyManager {
}
/**
- * Called by device owners to update {@link android.provider.Settings.Global} settings.
+ * Called by device owner to update {@link android.provider.Settings.Global} settings.
* Validation that the value of the setting is in the correct form for the setting type should
* be performed by the caller.
* <p>
@@ -6716,6 +7085,37 @@ public class DevicePolicyManager {
}
/**
+ * Called by device owner to update {@link android.provider.Settings.System} settings.
+ * Validation that the value of the setting is in the correct form for the setting type should
+ * be performed by the caller.
+ * <p>
+ * The settings that can be updated with this method are:
+ * <ul>
+ * <li>{@link android.provider.Settings.System#SCREEN_BRIGHTNESS}</li>
+ * <li>{@link android.provider.Settings.System#SCREEN_BRIGHTNESS_MODE}</li>
+ * <li>{@link android.provider.Settings.System#SCREEN_OFF_TIMEOUT}</li>
+ * </ul>
+ * <p>
+ *
+ * @see android.provider.Settings.System#SCREEN_OFF_TIMEOUT
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param setting The name of the setting to update.
+ * @param value The value to update the setting to.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public void setSystemSetting(@NonNull ComponentName admin, @NonNull String setting,
+ String value) {
+ throwIfParentInstance("setSystemSetting");
+ if (mService != null) {
+ try {
+ mService.setSystemSetting(admin, setting, value);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
* Called by device owner to set the system wall clock time. This only takes effect if called
* when {@link android.provider.Settings.Global#AUTO_TIME} is 0, otherwise {@code false} will be
* returned.
@@ -7619,6 +8019,7 @@ public class DevicePolicyManager {
* @param admin Which device owner this request is associated with.
* @param enabled whether security logging should be enabled or not.
* @throws SecurityException if {@code admin} is not a device owner.
+ * @see #setAffiliationIds
* @see #retrieveSecurityLogs
*/
public void setSecurityLoggingEnabled(@NonNull ComponentName admin, boolean enabled) {
@@ -7657,14 +8058,14 @@ public class DevicePolicyManager {
* owner has been notified via {@link DeviceAdminReceiver#onSecurityLogsAvailable}.
*
* <p>If there is any other user or profile on the device, it must be affiliated with the
- * device owner. Otherwise a {@link SecurityException} will be thrown. See
- * {@link #setAffiliationIds}
+ * device. Otherwise a {@link SecurityException} will be thrown. See {@link #isAffiliatedUser}.
*
* @param admin Which device owner this request is associated with.
* @return the new batch of security logs which is a list of {@link SecurityEvent},
* or {@code null} if rate limitation is exceeded or if logging is currently disabled.
* @throws SecurityException if {@code admin} is not a device owner, or there is at least one
- * profile or secondary user that is not affiliated with the device owner user.
+ * profile or secondary user that is not affiliated with the device.
+ * @see #isAffiliatedUser
* @see DeviceAdminReceiver#onSecurityLogsAvailable
*/
public @Nullable List<SecurityEvent> retrieveSecurityLogs(@NonNull ComponentName admin) {
@@ -7707,14 +8108,14 @@ public class DevicePolicyManager {
* about data corruption when parsing. </strong>
*
* <p>If there is any other user or profile on the device, it must be affiliated with the
- * device owner. Otherwise a {@link SecurityException} will be thrown. See
- * {@link #setAffiliationIds}
+ * device. Otherwise a {@link SecurityException} will be thrown. See {@link #isAffiliatedUser}.
*
* @param admin Which device owner this request is associated with.
* @return Device logs from before the latest reboot of the system, or {@code null} if this API
* is not supported on the device.
* @throws SecurityException if {@code admin} is not a device owner, or there is at least one
- * profile or secondary user that is not affiliated with the device owner user.
+ * profile or secondary user that is not affiliated with the device.
+ * @see #isAffiliatedUser
* @see #retrieveSecurityLogs
*/
public @Nullable List<SecurityEvent> retrievePreRebootSecurityLogs(
@@ -7922,6 +8323,9 @@ public class DevicePolicyManager {
* Indicates the entity that controls the device or profile owner. Two users/profiles are
* affiliated if the set of ids set by their device or profile owners intersect.
*
+ * <p>A user/profile that is affiliated with the device owner user is considered to be
+ * affiliated with the device.
+ *
* <p><strong>Note:</strong> Features that depend on user affiliation (such as security logging
* or {@link #bindDeviceAdminServiceAsUser}) won't be available when a secondary user or profile
* is created, until it becomes affiliated. Therefore it is recommended that the appropriate
@@ -7932,6 +8336,7 @@ public class DevicePolicyManager {
* @param ids A set of opaque non-empty affiliation ids.
*
* @throws IllegalArgumentException if {@code ids} is null or contains an empty string.
+ * @see #isAffiliatedUser
*/
public void setAffiliationIds(@NonNull ComponentName admin, @NonNull Set<String> ids) {
throwIfParentInstance("setAffiliationIds");
@@ -7959,13 +8364,12 @@ public class DevicePolicyManager {
}
/**
- * @hide
* Returns whether this user/profile is affiliated with the device.
* <p>
* By definition, the user that the device owner runs on is always affiliated with the device.
* Any other user/profile is considered affiliated with the device if the set specified by its
* profile owner via {@link #setAffiliationIds} intersects with the device owner's.
- *
+ * @see #setAffiliationIds
*/
public boolean isAffiliatedUser() {
throwIfParentInstance("isAffiliatedUser");
@@ -8178,6 +8582,7 @@ public class DevicePolicyManager {
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param enabled whether network logging should be enabled or not.
* @throws SecurityException if {@code admin} is not a device owner.
+ * @see #setAffiliationIds
* @see #retrieveNetworkLogs
*/
public void setNetworkLoggingEnabled(@NonNull ComponentName admin, boolean enabled) {
@@ -8233,7 +8638,8 @@ public class DevicePolicyManager {
* {@code null} if the batch represented by batchToken is no longer available or if
* logging is disabled.
* @throws SecurityException if {@code admin} is not a device owner, or there is at least one
- * profile or secondary user that is not affiliated with the device owner user.
+ * profile or secondary user that is not affiliated with the device.
+ * @see #setAffiliationIds
* @see DeviceAdminReceiver#onNetworkLogsAvailable
*/
public @Nullable List<NetworkEvent> retrieveNetworkLogs(@NonNull ComponentName admin,
@@ -8411,6 +8817,15 @@ public class DevicePolicyManager {
}
}
+ /** {@hide} */
+ @Condemned
+ @Deprecated
+ public boolean clearApplicationUserData(@NonNull ComponentName admin,
+ @NonNull String packageName, @NonNull OnClearApplicationUserDataListener listener,
+ @NonNull Handler handler) {
+ return clearApplicationUserData(admin, packageName, listener, new HandlerExecutor(handler));
+ }
+
/**
* Called by the device owner or profile owner to clear application user data of a given
* package. The behaviour of this is equivalent to the target application calling
@@ -8422,19 +8837,20 @@ public class DevicePolicyManager {
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param packageName The name of the package which will have its user data wiped.
* @param listener A callback object that will inform the caller when the clearing is done.
- * @param handler The handler indicating the thread on which the listener should be invoked.
+ * @param executor The executor through which the listener should be invoked.
* @throws SecurityException if the caller is not the device owner/profile owner.
* @return whether the clearing succeeded.
*/
public boolean clearApplicationUserData(@NonNull ComponentName admin,
@NonNull String packageName, @NonNull OnClearApplicationUserDataListener listener,
- @NonNull Handler handler) {
+ @NonNull @CallbackExecutor Executor executor) {
throwIfParentInstance("clearAppData");
+ Preconditions.checkNotNull(executor);
try {
return mService.clearApplicationUserData(admin, packageName,
new IPackageDataObserver.Stub() {
public void onRemoveCompleted(String pkg, boolean succeeded) {
- handler.post(() ->
+ executor.execute(() ->
listener.onApplicationUserDataCleared(pkg, succeeded));
}
});
@@ -8444,6 +8860,37 @@ public class DevicePolicyManager {
}
/**
+ * Called by a device owner to specify whether logout is enabled for all secondary users. The
+ * system may show a logout button that stops the user and switches back to the primary user.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param enabled whether logout should be enabled or not.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public void setLogoutEnabled(@NonNull ComponentName admin, boolean enabled) {
+ throwIfParentInstance("setLogoutEnabled");
+ try {
+ mService.setLogoutEnabled(admin, enabled);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns whether logout is enabled by a device owner.
+ *
+ * @return {@code true} if logout is enabled by device owner, {@code false} otherwise.
+ */
+ public boolean isLogoutEnabled() {
+ throwIfParentInstance("isLogoutEnabled");
+ try {
+ return mService.isLogoutEnabled();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Callback used in {@link #clearApplicationUserData}
* to indicate that the clearing of an application's user data is done.
*/
@@ -8457,4 +8904,65 @@ public class DevicePolicyManager {
*/
void onApplicationUserDataCleared(String packageName, boolean succeeded);
}
+
+ /**
+ * Returns set of system apps that should be removed during provisioning.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param userId ID of the user to be provisioned.
+ * @param provisioningAction action indicating type of provisioning, should be one of
+ * {@link #ACTION_PROVISION_MANAGED_DEVICE}, {@link #ACTION_PROVISION_MANAGED_PROFILE} or
+ * {@link #ACTION_PROVISION_MANAGED_USER}.
+ *
+ * @hide
+ */
+ public Set<String> getDisallowedSystemApps(ComponentName admin, int userId,
+ String provisioningAction) {
+ try {
+ return new ArraySet<>(
+ mService.getDisallowedSystemApps(admin, userId, provisioningAction));
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ //TODO STOPSHIP Add link to onTransferComplete callback when implemented.
+ /**
+ * Transfers the current administrator. All policies from the current administrator are
+ * migrated to the new administrator. The whole operation is atomic - the transfer is either
+ * complete or not done at all.
+ *
+ * Depending on the current administrator (device owner, profile owner, corporate owned
+ * profile owner), you have the following expected behaviour:
+ * <ul>
+ * <li>A device owner can only be transferred to a new device owner</li>
+ * <li>A profile owner can only be transferred to a new profile owner</li>
+ * <li>A corporate owned managed profile can have two cases:
+ * <ul>
+ * <li>If the device owner and profile owner are the same package,
+ * both will be transferred.</li>
+ * <li>If the device owner and profile owner are different packages,
+ * and if this method is called from the profile owner, only the profile owner
+ * is transferred. Similarly, if it is called from the device owner, only
+ * the device owner is transferred.</li>
+ * </ul>
+ * </li>
+ * </ul>
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param target Which {@link DeviceAdminReceiver} we want the new administrator to be.
+ * @param bundle Parameters - This bundle allows the current administrator to pass data to the
+ * new administrator. The parameters will be received in the
+ * onTransferComplete callback.
+ * @hide
+ */
+ public void transferOwner(@NonNull ComponentName admin, @NonNull ComponentName target,
+ PersistableBundle bundle) {
+ throwIfParentInstance("transferOwner");
+ try {
+ mService.transferOwner(admin, target, bundle);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/android/app/admin/DevicePolicyManagerInternal.java b/android/app/admin/DevicePolicyManagerInternal.java
index eef2f983..b692ffd9 100644
--- a/android/app/admin/DevicePolicyManagerInternal.java
+++ b/android/app/admin/DevicePolicyManagerInternal.java
@@ -16,6 +16,7 @@
package android.app.admin;
+import android.annotation.UserIdInt;
import android.content.Intent;
import java.util.List;
@@ -101,4 +102,25 @@ public abstract class DevicePolicyManagerInternal {
* not enforced by the profile/device owner.
*/
public abstract Intent createUserRestrictionSupportIntent(int userId, String userRestriction);
+
+ /**
+ * Returns whether this user/profile is affiliated with the device.
+ *
+ * <p>
+ * By definition, the user that the device owner runs on is always affiliated with the device.
+ * Any other user/profile is considered affiliated with the device if the set specified by its
+ * profile owner via {@link DevicePolicyManager#setAffiliationIds} intersects with the device
+ * owner's.
+ * <p>
+ * Profile owner on the primary user will never be considered as affiliated as there is no
+ * device owner to be affiliated with.
+ */
+ public abstract boolean isUserAffiliatedWithDevice(int userId);
+
+ /**
+ * Reports that a profile has changed to use a unified or separate credential.
+ *
+ * @param userId User ID of the profile.
+ */
+ public abstract void reportSeparateProfileChallengeChanged(@UserIdInt int userId);
}
diff --git a/android/app/admin/PasswordMetrics.java b/android/app/admin/PasswordMetrics.java
index 4658a474..5fee8532 100644
--- a/android/app/admin/PasswordMetrics.java
+++ b/android/app/admin/PasswordMetrics.java
@@ -223,7 +223,12 @@ public class PasswordMetrics implements Parcelable {
}
@Retention(RetentionPolicy.SOURCE)
- @IntDef({CHAR_UPPER_CASE, CHAR_LOWER_CASE, CHAR_DIGIT, CHAR_SYMBOL})
+ @IntDef(prefix = { "CHAR_" }, value = {
+ CHAR_UPPER_CASE,
+ CHAR_LOWER_CASE,
+ CHAR_DIGIT,
+ CHAR_SYMBOL
+ })
private @interface CharacterCatagory {}
private static final int CHAR_LOWER_CASE = 0;
private static final int CHAR_UPPER_CASE = 1;
diff --git a/android/app/admin/SecurityLog.java b/android/app/admin/SecurityLog.java
index 2b590e0d..d3b66d0d 100644
--- a/android/app/admin/SecurityLog.java
+++ b/android/app/admin/SecurityLog.java
@@ -17,6 +17,7 @@
package android.app.admin;
import android.annotation.IntDef;
+import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemProperties;
@@ -26,6 +27,7 @@ import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collection;
+import java.util.Objects;
/**
* Definitions for working with security logs.
@@ -43,10 +45,17 @@ public class SecurityLog {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({TAG_ADB_SHELL_INTERACTIVE, TAG_ADB_SHELL_CMD, TAG_SYNC_RECV_FILE, TAG_SYNC_SEND_FILE,
- TAG_APP_PROCESS_START, TAG_KEYGUARD_DISMISSED, TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT,
- TAG_KEYGUARD_SECURED})
- public @interface SECURITY_LOG_TAG {}
+ @IntDef(prefix = { "TAG_" }, value = {
+ TAG_ADB_SHELL_INTERACTIVE,
+ TAG_ADB_SHELL_CMD,
+ TAG_SYNC_RECV_FILE,
+ TAG_SYNC_SEND_FILE,
+ TAG_APP_PROCESS_START,
+ TAG_KEYGUARD_DISMISSED,
+ TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT,
+ TAG_KEYGUARD_SECURED
+ })
+ public @interface SecurityLogTag {}
/**
* Indicate that an ADB interactive shell was opened via "adb shell".
@@ -128,9 +137,28 @@ public class SecurityLog {
*/
public static final class SecurityEvent implements Parcelable {
private Event mEvent;
+ private long mId;
+
+ /**
+ * Constructor used by native classes to generate SecurityEvent instances.
+ * @hide
+ */
+ /* package */ SecurityEvent(byte[] data) {
+ this(0, data);
+ }
+
+ /**
+ * Constructor used by Parcelable.Creator to generate SecurityEvent instances.
+ * @hide
+ */
+ /* package */ SecurityEvent(Parcel source) {
+ this(source.readLong(), source.createByteArray());
+ }
/** @hide */
- /*package*/ SecurityEvent(byte[] data) {
+ @TestApi
+ public SecurityEvent(long id, byte[] data) {
+ mId = id;
mEvent = Event.fromBytes(data);
}
@@ -143,13 +171,8 @@ public class SecurityLog {
/**
* Returns the tag of this log entry, which specifies entry's semantics.
- * Could be one of {@link SecurityLog#TAG_SYNC_RECV_FILE},
- * {@link SecurityLog#TAG_SYNC_SEND_FILE}, {@link SecurityLog#TAG_ADB_SHELL_CMD},
- * {@link SecurityLog#TAG_ADB_SHELL_INTERACTIVE}, {@link SecurityLog#TAG_APP_PROCESS_START},
- * {@link SecurityLog#TAG_KEYGUARD_DISMISSED}, {@link SecurityLog#TAG_KEYGUARD_SECURED},
- * {@link SecurityLog#TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT}.
*/
- public @SECURITY_LOG_TAG int getTag() {
+ public @SecurityLogTag int getTag() {
return mEvent.getTag();
}
@@ -160,6 +183,21 @@ public class SecurityLog {
return mEvent.getData();
}
+ /**
+ * @hide
+ */
+ public void setId(long id) {
+ this.mId = id;
+ }
+
+ /**
+ * Returns the id of the event, where the id monotonically increases for each event. The id
+ * is reset when the device reboots, and when security logging is enabled.
+ */
+ public long getId() {
+ return mId;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -167,6 +205,7 @@ public class SecurityLog {
@Override
public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(mId);
dest.writeByteArray(mEvent.getBytes());
}
@@ -174,7 +213,7 @@ public class SecurityLog {
new Parcelable.Creator<SecurityEvent>() {
@Override
public SecurityEvent createFromParcel(Parcel source) {
- return new SecurityEvent(source.createByteArray());
+ return new SecurityEvent(source);
}
@Override
@@ -191,7 +230,7 @@ public class SecurityLog {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SecurityEvent other = (SecurityEvent) o;
- return mEvent.equals(other.mEvent);
+ return mEvent.equals(other.mEvent) && mId == other.mId;
}
/**
@@ -199,7 +238,7 @@ public class SecurityLog {
*/
@Override
public int hashCode() {
- return mEvent.hashCode();
+ return Objects.hash(mEvent, mId);
}
}
/**
diff --git a/android/app/admin/SystemUpdateInfo.java b/android/app/admin/SystemUpdateInfo.java
index fa31273e..b0376b50 100644
--- a/android/app/admin/SystemUpdateInfo.java
+++ b/android/app/admin/SystemUpdateInfo.java
@@ -52,7 +52,11 @@ public final class SystemUpdateInfo implements Parcelable {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({SECURITY_PATCH_STATE_FALSE, SECURITY_PATCH_STATE_TRUE, SECURITY_PATCH_STATE_UNKNOWN})
+ @IntDef(prefix = { "SECURITY_PATCH_STATE_" }, value = {
+ SECURITY_PATCH_STATE_FALSE,
+ SECURITY_PATCH_STATE_TRUE,
+ SECURITY_PATCH_STATE_UNKNOWN
+ })
public @interface SecurityPatchState {}
private static final String ATTR_RECEIVED_TIME = "received-time";
diff --git a/android/app/admin/SystemUpdatePolicy.java b/android/app/admin/SystemUpdatePolicy.java
index 995d98a7..232a6887 100644
--- a/android/app/admin/SystemUpdatePolicy.java
+++ b/android/app/admin/SystemUpdatePolicy.java
@@ -36,10 +36,11 @@ import java.lang.annotation.RetentionPolicy;
public class SystemUpdatePolicy implements Parcelable {
/** @hide */
- @IntDef({
- TYPE_INSTALL_AUTOMATIC,
- TYPE_INSTALL_WINDOWED,
- TYPE_POSTPONE})
+ @IntDef(prefix = { "TYPE_" }, value = {
+ TYPE_INSTALL_AUTOMATIC,
+ TYPE_INSTALL_WINDOWED,
+ TYPE_POSTPONE
+ })
@Retention(RetentionPolicy.SOURCE)
@interface SystemUpdatePolicyType {}
diff --git a/android/app/assist/AssistStructure.java b/android/app/assist/AssistStructure.java
index da5569d2..7b549cd5 100644
--- a/android/app/assist/AssistStructure.java
+++ b/android/app/assist/AssistStructure.java
@@ -2139,6 +2139,16 @@ public class AssistStructure implements Parcelable {
return mActivityComponent;
}
+ /**
+ * Called by Autofill server when app forged a different value.
+ *
+ * @hide
+ */
+ public void setActivityComponent(ComponentName componentName) {
+ ensureData();
+ mActivityComponent = componentName;
+ }
+
/** @hide */
public int getFlags() {
return mFlags;
diff --git a/android/app/backup/BackupAgent.java b/android/app/backup/BackupAgent.java
index 7aa80d26..861cb9a8 100644
--- a/android/app/backup/BackupAgent.java
+++ b/android/app/backup/BackupAgent.java
@@ -263,6 +263,17 @@ public abstract class BackupAgent extends ContextWrapper {
ParcelFileDescriptor newState) throws IOException;
/**
+ * New version of {@link #onRestore(BackupDataInput, int, android.os.ParcelFileDescriptor)}
+ * that handles a long app version code. Default implementation casts the version code to
+ * an int and calls {@link #onRestore(BackupDataInput, int, android.os.ParcelFileDescriptor)}.
+ */
+ public void onRestore(BackupDataInput data, long appVersionCode,
+ ParcelFileDescriptor newState)
+ throws IOException {
+ onRestore(data, (int) appVersionCode, newState);
+ }
+
+ /**
* The application is having its entire file system contents backed up. {@code data}
* points to the backup destination, and the app has the opportunity to choose which
* files are to be stored. To commit a file as part of the backup, call the
@@ -947,7 +958,7 @@ public abstract class BackupAgent extends ContextWrapper {
}
@Override
- public void doRestore(ParcelFileDescriptor data, int appVersionCode,
+ public void doRestore(ParcelFileDescriptor data, long appVersionCode,
ParcelFileDescriptor newState,
int token, IBackupManager callbackBinder) throws RemoteException {
// Ensure that we're running with the app's normal permission level
diff --git a/android/app/backup/BackupManager.java b/android/app/backup/BackupManager.java
index 9f9b2170..6512b98c 100644
--- a/android/app/backup/BackupManager.java
+++ b/android/app/backup/BackupManager.java
@@ -16,10 +16,12 @@
package android.app.backup;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
@@ -446,6 +448,57 @@ public class BackupManager {
}
/**
+ * Update the attributes of the transport identified by {@code transportComponent}. If the
+ * specified transport has not been bound at least once (for registration), this call will be
+ * ignored. Only the host process of the transport can change its description, otherwise a
+ * {@link SecurityException} will be thrown.
+ *
+ * @param transportComponent The identity of the transport being described.
+ * @param name A {@link String} with the new name for the transport. This is NOT for
+ * identification. MUST NOT be {@code null}.
+ * @param configurationIntent An {@link Intent} that can be passed to
+ * {@link Context#startActivity} in order to launch the transport's configuration UI. It may
+ * be {@code null} if the transport does not offer any user-facing configuration UI.
+ * @param currentDestinationString A {@link String} describing the destination to which the
+ * transport is currently sending data. MUST NOT be {@code null}.
+ * @param dataManagementIntent An {@link Intent} that can be passed to
+ * {@link Context#startActivity} in order to launch the transport's data-management UI. It
+ * may be {@code null} if the transport does not offer any user-facing data
+ * management UI.
+ * @param dataManagementLabel A {@link String} to be used as the label for the transport's data
+ * management affordance. This MUST be {@code null} when dataManagementIntent is
+ * {@code null} and MUST NOT be {@code null} when dataManagementIntent is not {@code null}.
+ * @throws SecurityException If the UID of the calling process differs from the package UID of
+ * {@code transportComponent} or if the caller does NOT have BACKUP permission.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public void updateTransportAttributes(
+ ComponentName transportComponent,
+ String name,
+ @Nullable Intent configurationIntent,
+ String currentDestinationString,
+ @Nullable Intent dataManagementIntent,
+ @Nullable String dataManagementLabel) {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ sService.updateTransportAttributes(
+ transportComponent,
+ name,
+ configurationIntent,
+ currentDestinationString,
+ dataManagementIntent,
+ dataManagementLabel);
+ } catch (RemoteException e) {
+ Log.e(TAG, "describeTransport() couldn't connect");
+ }
+ }
+ }
+
+ /**
* Specify the current backup transport.
*
* @param transport The name of the transport to select. This should be one
diff --git a/android/app/backup/BackupManagerMonitor.java b/android/app/backup/BackupManagerMonitor.java
index ebad16e0..ae4a98a4 100644
--- a/android/app/backup/BackupManagerMonitor.java
+++ b/android/app/backup/BackupManagerMonitor.java
@@ -40,9 +40,14 @@ public class BackupManagerMonitor {
/** string : the package name */
public static final String EXTRA_LOG_EVENT_PACKAGE_NAME =
"android.app.backup.extra.LOG_EVENT_PACKAGE_NAME";
- /** int : the versionCode of the package named by EXTRA_LOG_EVENT_PACKAGE_NAME */
+ /** int : the versionCode of the package named by EXTRA_LOG_EVENT_PACKAGE_NAME
+ * @deprecated Use {@link #EXTRA_LOG_EVENT_PACKAGE_LONG_VERSION} */
+ @Deprecated
public static final String EXTRA_LOG_EVENT_PACKAGE_VERSION =
"android.app.backup.extra.LOG_EVENT_PACKAGE_VERSION";
+ /** long : the full versionCode of the package named by EXTRA_LOG_EVENT_PACKAGE_NAME */
+ public static final String EXTRA_LOG_EVENT_PACKAGE_LONG_VERSION =
+ "android.app.backup.extra.LOG_EVENT_PACKAGE_FULL_VERSION";
/** int : the id of the log message, will be a unique identifier */
public static final String EXTRA_LOG_EVENT_ID = "android.app.backup.extra.LOG_EVENT_ID";
/**
diff --git a/android/app/servertransaction/ActivityConfigurationChangeItem.java b/android/app/servertransaction/ActivityConfigurationChangeItem.java
index 07001e2b..a2b7d580 100644
--- a/android/app/servertransaction/ActivityConfigurationChangeItem.java
+++ b/android/app/servertransaction/ActivityConfigurationChangeItem.java
@@ -19,25 +19,25 @@ package android.app.servertransaction;
import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
import static android.view.Display.INVALID_DISPLAY;
+import android.app.ClientTransactionHandler;
import android.content.res.Configuration;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Trace;
+import java.util.Objects;
+
/**
* Activity configuration changed callback.
* @hide
*/
public class ActivityConfigurationChangeItem extends ClientTransactionItem {
- private final Configuration mConfiguration;
-
- public ActivityConfigurationChangeItem(Configuration configuration) {
- mConfiguration = configuration;
- }
+ private Configuration mConfiguration;
@Override
- public void execute(android.app.ClientTransactionHandler client, IBinder token) {
+ public void execute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
// TODO(lifecycler): detect if PIP or multi-window mode changed and report it here.
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityConfigChanged");
client.handleActivityConfigurationChanged(token, mConfiguration, INVALID_DISPLAY);
@@ -45,6 +45,29 @@ public class ActivityConfigurationChangeItem extends ClientTransactionItem {
}
+ // ObjectPoolItem implementation
+
+ private ActivityConfigurationChangeItem() {}
+
+ /** Obtain an instance initialized with provided params. */
+ public static ActivityConfigurationChangeItem obtain(Configuration config) {
+ ActivityConfigurationChangeItem instance =
+ ObjectPool.obtain(ActivityConfigurationChangeItem.class);
+ if (instance == null) {
+ instance = new ActivityConfigurationChangeItem();
+ }
+ instance.mConfiguration = config;
+
+ return instance;
+ }
+
+ @Override
+ public void recycle() {
+ mConfiguration = null;
+ ObjectPool.recycle(this);
+ }
+
+
// Parcelable implementation
/** Write to Parcel. */
@@ -78,11 +101,16 @@ public class ActivityConfigurationChangeItem extends ClientTransactionItem {
return false;
}
final ActivityConfigurationChangeItem other = (ActivityConfigurationChangeItem) o;
- return mConfiguration.equals(other.mConfiguration);
+ return Objects.equals(mConfiguration, other.mConfiguration);
}
@Override
public int hashCode() {
return mConfiguration.hashCode();
}
+
+ @Override
+ public String toString() {
+ return "ActivityConfigurationChange{config=" + mConfiguration + "}";
+ }
}
diff --git a/android/app/servertransaction/ActivityLifecycleItem.java b/android/app/servertransaction/ActivityLifecycleItem.java
index a64108db..0fdc7c56 100644
--- a/android/app/servertransaction/ActivityLifecycleItem.java
+++ b/android/app/servertransaction/ActivityLifecycleItem.java
@@ -27,16 +27,28 @@ import java.lang.annotation.RetentionPolicy;
*/
public abstract class ActivityLifecycleItem extends ClientTransactionItem {
- static final boolean DEBUG_ORDER = false;
-
- @IntDef({UNDEFINED, RESUMED, PAUSED, STOPPED, DESTROYED})
+ @IntDef(prefix = { "UNDEFINED", "PRE_", "ON_" }, value = {
+ UNDEFINED,
+ PRE_ON_CREATE,
+ ON_CREATE,
+ ON_START,
+ ON_RESUME,
+ ON_PAUSE,
+ ON_STOP,
+ ON_DESTROY,
+ ON_RESTART
+ })
@Retention(RetentionPolicy.SOURCE)
- @interface LifecycleState{}
+ public @interface LifecycleState{}
public static final int UNDEFINED = -1;
- public static final int RESUMED = 0;
- public static final int PAUSED = 1;
- public static final int STOPPED = 2;
- public static final int DESTROYED = 3;
+ public static final int PRE_ON_CREATE = 0;
+ public static final int ON_CREATE = 1;
+ public static final int ON_START = 2;
+ public static final int ON_RESUME = 3;
+ public static final int ON_PAUSE = 4;
+ public static final int ON_STOP = 5;
+ public static final int ON_DESTROY = 6;
+ public static final int ON_RESTART = 7;
/** A final lifecycle state that an activity should reach. */
@LifecycleState
diff --git a/android/app/servertransaction/ActivityResultItem.java b/android/app/servertransaction/ActivityResultItem.java
index 76664d8e..73b5ec44 100644
--- a/android/app/servertransaction/ActivityResultItem.java
+++ b/android/app/servertransaction/ActivityResultItem.java
@@ -16,9 +16,10 @@
package android.app.servertransaction;
-import static android.app.servertransaction.ActivityLifecycleItem.PAUSED;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
+import android.app.ClientTransactionHandler;
import android.app.ResultInfo;
import android.os.IBinder;
import android.os.Parcel;
@@ -26,6 +27,7 @@ import android.os.Parcelable;
import android.os.Trace;
import java.util.List;
+import java.util.Objects;
/**
* Activity result delivery callback.
@@ -33,25 +35,44 @@ import java.util.List;
*/
public class ActivityResultItem extends ClientTransactionItem {
- private final List<ResultInfo> mResultInfoList;
-
- public ActivityResultItem(List<ResultInfo> resultInfos) {
- mResultInfoList = resultInfos;
- }
+ private List<ResultInfo> mResultInfoList;
@Override
public int getPreExecutionState() {
- return PAUSED;
+ return ON_PAUSE;
}
@Override
- public void execute(android.app.ClientTransactionHandler client, IBinder token) {
+ public void execute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityDeliverResult");
client.handleSendResult(token, mResultInfoList);
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}
+ // ObjectPoolItem implementation
+
+ private ActivityResultItem() {}
+
+ /** Obtain an instance initialized with provided params. */
+ public static ActivityResultItem obtain(List<ResultInfo> resultInfoList) {
+ ActivityResultItem instance = ObjectPool.obtain(ActivityResultItem.class);
+ if (instance == null) {
+ instance = new ActivityResultItem();
+ }
+ instance.mResultInfoList = resultInfoList;
+
+ return instance;
+ }
+
+ @Override
+ public void recycle() {
+ mResultInfoList = null;
+ ObjectPool.recycle(this);
+ }
+
+
// Parcelable implementation
/** Write to Parcel. */
@@ -85,11 +106,16 @@ public class ActivityResultItem extends ClientTransactionItem {
return false;
}
final ActivityResultItem other = (ActivityResultItem) o;
- return mResultInfoList.equals(other.mResultInfoList);
+ return Objects.equals(mResultInfoList, other.mResultInfoList);
}
@Override
public int hashCode() {
return mResultInfoList.hashCode();
}
+
+ @Override
+ public String toString() {
+ return "ActivityResultItem{resultInfoList=" + mResultInfoList + "}";
+ }
}
diff --git a/android/app/servertransaction/BaseClientRequest.java b/android/app/servertransaction/BaseClientRequest.java
index 4bd01afb..c91e0ca5 100644
--- a/android/app/servertransaction/BaseClientRequest.java
+++ b/android/app/servertransaction/BaseClientRequest.java
@@ -24,7 +24,7 @@ import android.os.IBinder;
* Each of them can be prepared before scheduling and, eventually, executed.
* @hide
*/
-public interface BaseClientRequest {
+public interface BaseClientRequest extends ObjectPoolItem {
/**
* Prepare the client request before scheduling.
@@ -33,13 +33,25 @@ public interface BaseClientRequest {
* @param client Target client handler.
* @param token Target activity token.
*/
- default void prepare(ClientTransactionHandler client, IBinder token) {
+ default void preExecute(ClientTransactionHandler client, IBinder token) {
}
/**
* Execute the request.
* @param client Target client handler.
* @param token Target activity token.
+ * @param pendingActions Container that may have data pending to be used.
*/
- void execute(ClientTransactionHandler client, IBinder token);
+ void execute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions);
+
+ /**
+ * Perform all actions that need to happen after execution, e.g. report the result to server.
+ * @param client Target client handler.
+ * @param token Target activity token.
+ * @param pendingActions Container that may have data pending to be used.
+ */
+ default void postExecute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
+ }
}
diff --git a/android/app/servertransaction/ClientTransaction.java b/android/app/servertransaction/ClientTransaction.java
index d2289ba0..3c96f069 100644
--- a/android/app/servertransaction/ClientTransaction.java
+++ b/android/app/servertransaction/ClientTransaction.java
@@ -16,6 +16,7 @@
package android.app.servertransaction;
+import android.annotation.Nullable;
import android.app.ClientTransactionHandler;
import android.app.IApplicationThread;
import android.os.IBinder;
@@ -36,7 +37,7 @@ import java.util.Objects;
* @see ActivityLifecycleItem
* @hide
*/
-public class ClientTransaction implements Parcelable {
+public class ClientTransaction implements Parcelable, ObjectPoolItem {
/** A list of individual callbacks to a client. */
private List<ClientTransactionItem> mActivityCallbacks;
@@ -53,9 +54,9 @@ public class ClientTransaction implements Parcelable {
/** Target client activity. Might be null if the entire transaction is targeting an app. */
private IBinder mActivityToken;
- public ClientTransaction(IApplicationThread client, IBinder activityToken) {
- mClient = client;
- mActivityToken = activityToken;
+ /** Get the target client of the transaction. */
+ public IApplicationThread getClient() {
+ return mClient;
}
/**
@@ -69,6 +70,23 @@ public class ClientTransaction implements Parcelable {
mActivityCallbacks.add(activityCallback);
}
+ /** Get the list of callbacks. */
+ @Nullable
+ List<ClientTransactionItem> getCallbacks() {
+ return mActivityCallbacks;
+ }
+
+ /** Get the target activity. */
+ @Nullable
+ public IBinder getActivityToken() {
+ return mActivityToken;
+ }
+
+ /** Get the target state lifecycle request. */
+ ActivityLifecycleItem getLifecycleStateRequest() {
+ return mLifecycleStateRequest;
+ }
+
/**
* Set the lifecycle state in which the client should be after executing the transaction.
* @param stateRequest A lifecycle request initialized with right parameters.
@@ -82,50 +100,68 @@ public class ClientTransaction implements Parcelable {
* @param clientTransactionHandler Handler on the client side that will executed all operations
* requested by transaction items.
*/
- public void prepare(android.app.ClientTransactionHandler clientTransactionHandler) {
+ public void preExecute(android.app.ClientTransactionHandler clientTransactionHandler) {
if (mActivityCallbacks != null) {
final int size = mActivityCallbacks.size();
for (int i = 0; i < size; ++i) {
- mActivityCallbacks.get(i).prepare(clientTransactionHandler, mActivityToken);
+ mActivityCallbacks.get(i).preExecute(clientTransactionHandler, mActivityToken);
}
}
if (mLifecycleStateRequest != null) {
- mLifecycleStateRequest.prepare(clientTransactionHandler, mActivityToken);
- }
- }
-
- /**
- * Execute the transaction.
- * @param clientTransactionHandler Handler on the client side that will execute all operations
- * requested by transaction items.
- */
- public void execute(android.app.ClientTransactionHandler clientTransactionHandler) {
- if (mActivityCallbacks != null) {
- final int size = mActivityCallbacks.size();
- for (int i = 0; i < size; ++i) {
- mActivityCallbacks.get(i).execute(clientTransactionHandler, mActivityToken);
- }
- }
- if (mLifecycleStateRequest != null) {
- mLifecycleStateRequest.execute(clientTransactionHandler, mActivityToken);
+ mLifecycleStateRequest.preExecute(clientTransactionHandler, mActivityToken);
}
}
/**
* Schedule the transaction after it was initialized. It will be send to client and all its
* individual parts will be applied in the following sequence:
- * 1. The client calls {@link #prepare(ClientTransactionHandler)}, which triggers all work that
- * needs to be done before actually scheduling the transaction for callbacks and lifecycle
- * state request.
+ * 1. The client calls {@link #preExecute(ClientTransactionHandler)}, which triggers all work
+ * that needs to be done before actually scheduling the transaction for callbacks and
+ * lifecycle state request.
* 2. The transaction message is scheduled.
- * 3. The client calls {@link #execute(ClientTransactionHandler)}, which executes all callbacks
- * and necessary lifecycle transitions.
+ * 3. The client calls {@link TransactionExecutor#execute(ClientTransaction)}, which executes
+ * all callbacks and necessary lifecycle transitions.
*/
public void schedule() throws RemoteException {
mClient.scheduleTransaction(this);
}
+ // ObjectPoolItem implementation
+
+ private ClientTransaction() {}
+
+ /** Obtain an instance initialized with provided params. */
+ public static ClientTransaction obtain(IApplicationThread client, IBinder activityToken) {
+ ClientTransaction instance = ObjectPool.obtain(ClientTransaction.class);
+ if (instance == null) {
+ instance = new ClientTransaction();
+ }
+ instance.mClient = client;
+ instance.mActivityToken = activityToken;
+
+ return instance;
+ }
+
+ @Override
+ public void recycle() {
+ if (mActivityCallbacks != null) {
+ int size = mActivityCallbacks.size();
+ for (int i = 0; i < size; i++) {
+ mActivityCallbacks.get(i).recycle();
+ }
+ mActivityCallbacks.clear();
+ }
+ if (mLifecycleStateRequest != null) {
+ mLifecycleStateRequest.recycle();
+ mLifecycleStateRequest = null;
+ }
+ mClient = null;
+ mActivityToken = null;
+ ObjectPool.recycle(this);
+ }
+
+
// Parcelable implementation
/** Write to Parcel. */
diff --git a/android/app/servertransaction/ConfigurationChangeItem.java b/android/app/servertransaction/ConfigurationChangeItem.java
index 055923ec..4ab7251e 100644
--- a/android/app/servertransaction/ConfigurationChangeItem.java
+++ b/android/app/servertransaction/ConfigurationChangeItem.java
@@ -16,32 +16,55 @@
package android.app.servertransaction;
+import android.app.ClientTransactionHandler;
import android.content.res.Configuration;
import android.os.IBinder;
import android.os.Parcel;
+import java.util.Objects;
+
/**
* App configuration change message.
* @hide
*/
public class ConfigurationChangeItem extends ClientTransactionItem {
- private final Configuration mConfiguration;
-
- public ConfigurationChangeItem(Configuration configuration) {
- mConfiguration = new Configuration(configuration);
- }
+ private Configuration mConfiguration;
@Override
- public void prepare(android.app.ClientTransactionHandler client, IBinder token) {
+ public void preExecute(android.app.ClientTransactionHandler client, IBinder token) {
client.updatePendingConfiguration(mConfiguration);
}
@Override
- public void execute(android.app.ClientTransactionHandler client, IBinder token) {
+ public void execute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
client.handleConfigurationChanged(mConfiguration);
}
+
+ // ObjectPoolItem implementation
+
+ private ConfigurationChangeItem() {}
+
+ /** Obtain an instance initialized with provided params. */
+ public static ConfigurationChangeItem obtain(Configuration config) {
+ ConfigurationChangeItem instance = ObjectPool.obtain(ConfigurationChangeItem.class);
+ if (instance == null) {
+ instance = new ConfigurationChangeItem();
+ }
+ instance.mConfiguration = config;
+
+ return instance;
+ }
+
+ @Override
+ public void recycle() {
+ mConfiguration = null;
+ ObjectPool.recycle(this);
+ }
+
+
// Parcelable implementation
/** Write to Parcel. */
@@ -75,11 +98,16 @@ public class ConfigurationChangeItem extends ClientTransactionItem {
return false;
}
final ConfigurationChangeItem other = (ConfigurationChangeItem) o;
- return mConfiguration.equals(other.mConfiguration);
+ return Objects.equals(mConfiguration, other.mConfiguration);
}
@Override
public int hashCode() {
return mConfiguration.hashCode();
}
+
+ @Override
+ public String toString() {
+ return "ConfigurationChangeItem{config=" + mConfiguration + "}";
+ }
}
diff --git a/android/app/servertransaction/DestroyActivityItem.java b/android/app/servertransaction/DestroyActivityItem.java
index 38fd5fb6..83da5f33 100644
--- a/android/app/servertransaction/DestroyActivityItem.java
+++ b/android/app/servertransaction/DestroyActivityItem.java
@@ -29,16 +29,12 @@ import android.os.Trace;
*/
public class DestroyActivityItem extends ActivityLifecycleItem {
- private final boolean mFinished;
- private final int mConfigChanges;
-
- public DestroyActivityItem(boolean finished, int configChanges) {
- mFinished = finished;
- mConfigChanges = configChanges;
- }
+ private boolean mFinished;
+ private int mConfigChanges;
@Override
- public void execute(ClientTransactionHandler client, IBinder token) {
+ public void execute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityDestroy");
client.handleDestroyActivity(token, mFinished, mConfigChanges,
false /* getNonConfigInstance */);
@@ -47,7 +43,31 @@ public class DestroyActivityItem extends ActivityLifecycleItem {
@Override
public int getTargetState() {
- return DESTROYED;
+ return ON_DESTROY;
+ }
+
+
+ // ObjectPoolItem implementation
+
+ private DestroyActivityItem() {}
+
+ /** Obtain an instance initialized with provided params. */
+ public static DestroyActivityItem obtain(boolean finished, int configChanges) {
+ DestroyActivityItem instance = ObjectPool.obtain(DestroyActivityItem.class);
+ if (instance == null) {
+ instance = new DestroyActivityItem();
+ }
+ instance.mFinished = finished;
+ instance.mConfigChanges = configChanges;
+
+ return instance;
+ }
+
+ @Override
+ public void recycle() {
+ mFinished = false;
+ mConfigChanges = 0;
+ ObjectPool.recycle(this);
}
@@ -96,4 +116,10 @@ public class DestroyActivityItem extends ActivityLifecycleItem {
result = 31 * result + mConfigChanges;
return result;
}
+
+ @Override
+ public String toString() {
+ return "DestroyActivityItem{finished=" + mFinished + ",mConfigChanges="
+ + mConfigChanges + "}";
+ }
}
diff --git a/android/app/servertransaction/LaunchActivityItem.java b/android/app/servertransaction/LaunchActivityItem.java
index 417ebac8..7be82bf9 100644
--- a/android/app/servertransaction/LaunchActivityItem.java
+++ b/android/app/servertransaction/LaunchActivityItem.java
@@ -18,6 +18,7 @@ package android.app.servertransaction;
import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
+import android.app.ActivityThread.ActivityClientRecord;
import android.app.ClientTransactionHandler;
import android.app.ProfilerInfo;
import android.app.ResultInfo;
@@ -42,68 +43,69 @@ import java.util.Objects;
* Request to launch an activity.
* @hide
*/
-public class LaunchActivityItem extends ActivityLifecycleItem {
-
- private final Intent mIntent;
- private final int mIdent;
- private final ActivityInfo mInfo;
- private final Configuration mCurConfig;
- private final Configuration mOverrideConfig;
- private final CompatibilityInfo mCompatInfo;
- private final String mReferrer;
- private final IVoiceInteractor mVoiceInteractor;
- private final int mProcState;
- private final Bundle mState;
- private final PersistableBundle mPersistentState;
- private final List<ResultInfo> mPendingResults;
- private final List<ReferrerIntent> mPendingNewIntents;
- // TODO(lifecycler): use lifecycle request instead of this param.
- private final boolean mNotResumed;
- private final boolean mIsForward;
- private final ProfilerInfo mProfilerInfo;
-
- public LaunchActivityItem(Intent intent, int ident, ActivityInfo info,
- Configuration curConfig, Configuration overrideConfig, CompatibilityInfo compatInfo,
- String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state,
- PersistableBundle persistentState, List<ResultInfo> pendingResults,
- List<ReferrerIntent> pendingNewIntents, boolean notResumed, boolean isForward,
- ProfilerInfo profilerInfo) {
- mIntent = intent;
- mIdent = ident;
- mInfo = info;
- mCurConfig = curConfig;
- mOverrideConfig = overrideConfig;
- mCompatInfo = compatInfo;
- mReferrer = referrer;
- mVoiceInteractor = voiceInteractor;
- mProcState = procState;
- mState = state;
- mPersistentState = persistentState;
- mPendingResults = pendingResults;
- mPendingNewIntents = pendingNewIntents;
- mNotResumed = notResumed;
- mIsForward = isForward;
- mProfilerInfo = profilerInfo;
- }
+public class LaunchActivityItem extends ClientTransactionItem {
+
+ private Intent mIntent;
+ private int mIdent;
+ private ActivityInfo mInfo;
+ private Configuration mCurConfig;
+ private Configuration mOverrideConfig;
+ private CompatibilityInfo mCompatInfo;
+ private String mReferrer;
+ private IVoiceInteractor mVoiceInteractor;
+ private int mProcState;
+ private Bundle mState;
+ private PersistableBundle mPersistentState;
+ private List<ResultInfo> mPendingResults;
+ private List<ReferrerIntent> mPendingNewIntents;
+ private boolean mIsForward;
+ private ProfilerInfo mProfilerInfo;
@Override
- public void prepare(ClientTransactionHandler client, IBinder token) {
+ public void preExecute(ClientTransactionHandler client, IBinder token) {
client.updateProcessState(mProcState, false);
client.updatePendingConfiguration(mCurConfig);
}
@Override
- public void execute(ClientTransactionHandler client, IBinder token) {
+ public void execute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
- client.handleLaunchActivity(token, mIntent, mIdent, mInfo, mOverrideConfig, mCompatInfo,
- mReferrer, mVoiceInteractor, mState, mPersistentState, mPendingResults,
- mPendingNewIntents, mNotResumed, mIsForward, mProfilerInfo);
+ ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
+ mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
+ mPendingResults, mPendingNewIntents, mIsForward,
+ mProfilerInfo, client);
+ client.handleLaunchActivity(r, pendingActions);
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}
+
+ // ObjectPoolItem implementation
+
+ private LaunchActivityItem() {}
+
+ /** Obtain an instance initialized with provided params. */
+ public static LaunchActivityItem obtain(Intent intent, int ident, ActivityInfo info,
+ Configuration curConfig, Configuration overrideConfig, CompatibilityInfo compatInfo,
+ String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state,
+ PersistableBundle persistentState, List<ResultInfo> pendingResults,
+ List<ReferrerIntent> pendingNewIntents, boolean isForward, ProfilerInfo profilerInfo) {
+ LaunchActivityItem instance = ObjectPool.obtain(LaunchActivityItem.class);
+ if (instance == null) {
+ instance = new LaunchActivityItem();
+ }
+ setValues(instance, intent, ident, info, curConfig, overrideConfig, compatInfo, referrer,
+ voiceInteractor, procState, state, persistentState, pendingResults,
+ pendingNewIntents, isForward, profilerInfo);
+
+ return instance;
+ }
+
@Override
- public int getTargetState() {
- return mNotResumed ? PAUSED : RESUMED;
+ public void recycle() {
+ setValues(this, null, 0, null, null, null, null, null, null, 0, null, null, null, null,
+ false, null);
+ ObjectPool.recycle(this);
}
@@ -119,35 +121,28 @@ public class LaunchActivityItem extends ActivityLifecycleItem {
dest.writeTypedObject(mOverrideConfig, flags);
dest.writeTypedObject(mCompatInfo, flags);
dest.writeString(mReferrer);
- dest.writeStrongBinder(mVoiceInteractor != null ? mVoiceInteractor.asBinder() : null);
+ dest.writeStrongInterface(mVoiceInteractor);
dest.writeInt(mProcState);
dest.writeBundle(mState);
dest.writePersistableBundle(mPersistentState);
dest.writeTypedList(mPendingResults, flags);
dest.writeTypedList(mPendingNewIntents, flags);
- dest.writeBoolean(mNotResumed);
dest.writeBoolean(mIsForward);
dest.writeTypedObject(mProfilerInfo, flags);
}
/** Read from Parcel. */
private LaunchActivityItem(Parcel in) {
- mIntent = in.readTypedObject(Intent.CREATOR);
- mIdent = in.readInt();
- mInfo = in.readTypedObject(ActivityInfo.CREATOR);
- mCurConfig = in.readTypedObject(Configuration.CREATOR);
- mOverrideConfig = in.readTypedObject(Configuration.CREATOR);
- mCompatInfo = in.readTypedObject(CompatibilityInfo.CREATOR);
- mReferrer = in.readString();
- mVoiceInteractor = (IVoiceInteractor) in.readStrongBinder();
- mProcState = in.readInt();
- mState = in.readBundle(getClass().getClassLoader());
- mPersistentState = in.readPersistableBundle(getClass().getClassLoader());
- mPendingResults = in.createTypedArrayList(ResultInfo.CREATOR);
- mPendingNewIntents = in.createTypedArrayList(ReferrerIntent.CREATOR);
- mNotResumed = in.readBoolean();
- mIsForward = in.readBoolean();
- mProfilerInfo = in.readTypedObject(ProfilerInfo.CREATOR);
+ setValues(this, in.readTypedObject(Intent.CREATOR), in.readInt(),
+ in.readTypedObject(ActivityInfo.CREATOR), in.readTypedObject(Configuration.CREATOR),
+ in.readTypedObject(Configuration.CREATOR),
+ in.readTypedObject(CompatibilityInfo.CREATOR), in.readString(),
+ IVoiceInteractor.Stub.asInterface(in.readStrongBinder()), in.readInt(),
+ in.readBundle(getClass().getClassLoader()),
+ in.readPersistableBundle(getClass().getClassLoader()),
+ in.createTypedArrayList(ResultInfo.CREATOR),
+ in.createTypedArrayList(ReferrerIntent.CREATOR), in.readBoolean(),
+ in.readTypedObject(ProfilerInfo.CREATOR));
}
public static final Creator<LaunchActivityItem> CREATOR =
@@ -170,7 +165,9 @@ public class LaunchActivityItem extends ActivityLifecycleItem {
return false;
}
final LaunchActivityItem other = (LaunchActivityItem) o;
- return mIntent.filterEquals(other.mIntent) && mIdent == other.mIdent
+ final boolean intentsEqual = (mIntent == null && other.mIntent == null)
+ || (mIntent != null && mIntent.filterEquals(other.mIntent));
+ return intentsEqual && mIdent == other.mIdent
&& activityInfoEqual(other.mInfo) && Objects.equals(mCurConfig, other.mCurConfig)
&& Objects.equals(mOverrideConfig, other.mOverrideConfig)
&& Objects.equals(mCompatInfo, other.mCompatInfo)
@@ -179,7 +176,7 @@ public class LaunchActivityItem extends ActivityLifecycleItem {
&& areBundlesEqual(mPersistentState, other.mPersistentState)
&& Objects.equals(mPendingResults, other.mPendingResults)
&& Objects.equals(mPendingNewIntents, other.mPendingNewIntents)
- && mNotResumed == other.mNotResumed && mIsForward == other.mIsForward
+ && mIsForward == other.mIsForward
&& Objects.equals(mProfilerInfo, other.mProfilerInfo);
}
@@ -197,14 +194,17 @@ public class LaunchActivityItem extends ActivityLifecycleItem {
result = 31 * result + (mPersistentState != null ? mPersistentState.size() : 0);
result = 31 * result + Objects.hashCode(mPendingResults);
result = 31 * result + Objects.hashCode(mPendingNewIntents);
- result = 31 * result + (mNotResumed ? 1 : 0);
result = 31 * result + (mIsForward ? 1 : 0);
result = 31 * result + Objects.hashCode(mProfilerInfo);
return result;
}
private boolean activityInfoEqual(ActivityInfo other) {
- return mInfo.flags == other.flags && mInfo.maxAspectRatio == other.maxAspectRatio
+ if (mInfo == null) {
+ return other == null;
+ }
+ return other != null && mInfo.flags == other.flags
+ && mInfo.maxAspectRatio == other.maxAspectRatio
&& Objects.equals(mInfo.launchToken, other.launchToken)
&& Objects.equals(mInfo.getComponentName(), other.getComponentName());
}
@@ -229,4 +229,38 @@ public class LaunchActivityItem extends ActivityLifecycleItem {
}
return true;
}
+
+ @Override
+ public String toString() {
+ return "LaunchActivityItem{intent=" + mIntent + ",ident=" + mIdent + ",info=" + mInfo
+ + ",curConfig=" + mCurConfig + ",overrideConfig=" + mOverrideConfig
+ + ",referrer=" + mReferrer + ",procState=" + mProcState + ",state=" + mState
+ + ",persistentState=" + mPersistentState + ",pendingResults=" + mPendingResults
+ + ",pendingNewIntents=" + mPendingNewIntents + ",profilerInfo=" + mProfilerInfo
+ + "}";
+ }
+
+ // Using the same method to set and clear values to make sure we don't forget anything
+ private static void setValues(LaunchActivityItem instance, Intent intent, int ident,
+ ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
+ CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
+ int procState, Bundle state, PersistableBundle persistentState,
+ List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
+ boolean isForward, ProfilerInfo profilerInfo) {
+ instance.mIntent = intent;
+ instance.mIdent = ident;
+ instance.mInfo = info;
+ instance.mCurConfig = curConfig;
+ instance.mOverrideConfig = overrideConfig;
+ instance.mCompatInfo = compatInfo;
+ instance.mReferrer = referrer;
+ instance.mVoiceInteractor = voiceInteractor;
+ instance.mProcState = procState;
+ instance.mState = state;
+ instance.mPersistentState = persistentState;
+ instance.mPendingResults = pendingResults;
+ instance.mPendingNewIntents = pendingNewIntents;
+ instance.mIsForward = isForward;
+ instance.mProfilerInfo = profilerInfo;
+ }
}
diff --git a/android/app/servertransaction/MoveToDisplayItem.java b/android/app/servertransaction/MoveToDisplayItem.java
index ccd80d88..b3dddfb3 100644
--- a/android/app/servertransaction/MoveToDisplayItem.java
+++ b/android/app/servertransaction/MoveToDisplayItem.java
@@ -18,33 +18,56 @@ package android.app.servertransaction;
import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
+import android.app.ClientTransactionHandler;
import android.content.res.Configuration;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Trace;
+import java.util.Objects;
+
/**
* Activity move to a different display message.
* @hide
*/
public class MoveToDisplayItem extends ClientTransactionItem {
- private final int mTargetDisplayId;
- private final Configuration mConfiguration;
-
- public MoveToDisplayItem(int targetDisplayId, Configuration configuration) {
- mTargetDisplayId = targetDisplayId;
- mConfiguration = configuration;
- }
+ private int mTargetDisplayId;
+ private Configuration mConfiguration;
@Override
- public void execute(android.app.ClientTransactionHandler client, IBinder token) {
+ public void execute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityMovedToDisplay");
client.handleActivityConfigurationChanged(token, mConfiguration, mTargetDisplayId);
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}
+ // ObjectPoolItem implementation
+
+ private MoveToDisplayItem() {}
+
+ /** Obtain an instance initialized with provided params. */
+ public static MoveToDisplayItem obtain(int targetDisplayId, Configuration configuration) {
+ MoveToDisplayItem instance = ObjectPool.obtain(MoveToDisplayItem.class);
+ if (instance == null) {
+ instance = new MoveToDisplayItem();
+ }
+ instance.mTargetDisplayId = targetDisplayId;
+ instance.mConfiguration = configuration;
+
+ return instance;
+ }
+
+ @Override
+ public void recycle() {
+ mTargetDisplayId = 0;
+ mConfiguration = null;
+ ObjectPool.recycle(this);
+ }
+
+
// Parcelable implementation
/** Write to Parcel. */
@@ -80,7 +103,7 @@ public class MoveToDisplayItem extends ClientTransactionItem {
}
final MoveToDisplayItem other = (MoveToDisplayItem) o;
return mTargetDisplayId == other.mTargetDisplayId
- && mConfiguration.equals(other.mConfiguration);
+ && Objects.equals(mConfiguration, other.mConfiguration);
}
@Override
@@ -90,4 +113,10 @@ public class MoveToDisplayItem extends ClientTransactionItem {
result = 31 * result + mConfiguration.hashCode();
return result;
}
+
+ @Override
+ public String toString() {
+ return "MoveToDisplayItem{targetDisplayId=" + mTargetDisplayId
+ + ",configuration=" + mConfiguration + "}";
+ }
}
diff --git a/android/app/servertransaction/MultiWindowModeChangeItem.java b/android/app/servertransaction/MultiWindowModeChangeItem.java
index a0c617fa..c3022d6f 100644
--- a/android/app/servertransaction/MultiWindowModeChangeItem.java
+++ b/android/app/servertransaction/MultiWindowModeChangeItem.java
@@ -16,10 +16,13 @@
package android.app.servertransaction;
+import android.app.ClientTransactionHandler;
import android.content.res.Configuration;
import android.os.IBinder;
import android.os.Parcel;
+import java.util.Objects;
+
/**
* Multi-window mode change message.
* @hide
@@ -28,18 +31,38 @@ import android.os.Parcel;
// communicate multi-window mode change with WindowConfiguration.
public class MultiWindowModeChangeItem extends ClientTransactionItem {
- private final boolean mIsInMultiWindowMode;
- private final Configuration mOverrideConfig;
+ private boolean mIsInMultiWindowMode;
+ private Configuration mOverrideConfig;
+
+ @Override
+ public void execute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
+ client.handleMultiWindowModeChanged(token, mIsInMultiWindowMode, mOverrideConfig);
+ }
+
+
+ // ObjectPoolItem implementation
- public MultiWindowModeChangeItem(boolean isInMultiWindowMode,
+ private MultiWindowModeChangeItem() {}
+
+ /** Obtain an instance initialized with provided params. */
+ public static MultiWindowModeChangeItem obtain(boolean isInMultiWindowMode,
Configuration overrideConfig) {
- mIsInMultiWindowMode = isInMultiWindowMode;
- mOverrideConfig = overrideConfig;
+ MultiWindowModeChangeItem instance = ObjectPool.obtain(MultiWindowModeChangeItem.class);
+ if (instance == null) {
+ instance = new MultiWindowModeChangeItem();
+ }
+ instance.mIsInMultiWindowMode = isInMultiWindowMode;
+ instance.mOverrideConfig = overrideConfig;
+
+ return instance;
}
@Override
- public void execute(android.app.ClientTransactionHandler client, IBinder token) {
- client.handleMultiWindowModeChanged(token, mIsInMultiWindowMode, mOverrideConfig);
+ public void recycle() {
+ mIsInMultiWindowMode = false;
+ mOverrideConfig = null;
+ ObjectPool.recycle(this);
}
@@ -79,7 +102,7 @@ public class MultiWindowModeChangeItem extends ClientTransactionItem {
}
final MultiWindowModeChangeItem other = (MultiWindowModeChangeItem) o;
return mIsInMultiWindowMode == other.mIsInMultiWindowMode
- && mOverrideConfig.equals(other.mOverrideConfig);
+ && Objects.equals(mOverrideConfig, other.mOverrideConfig);
}
@Override
@@ -89,4 +112,10 @@ public class MultiWindowModeChangeItem extends ClientTransactionItem {
result = 31 * result + mOverrideConfig.hashCode();
return result;
}
+
+ @Override
+ public String toString() {
+ return "MultiWindowModeChangeItem{isInMultiWindowMode=" + mIsInMultiWindowMode
+ + ",overrideConfig=" + mOverrideConfig + "}";
+ }
}
diff --git a/android/app/servertransaction/NewIntentItem.java b/android/app/servertransaction/NewIntentItem.java
index 61a8965a..7dfde73c 100644
--- a/android/app/servertransaction/NewIntentItem.java
+++ b/android/app/servertransaction/NewIntentItem.java
@@ -16,9 +16,7 @@
package android.app.servertransaction;
-import static android.app.servertransaction.ActivityLifecycleItem.PAUSED;
-import static android.app.servertransaction.ActivityLifecycleItem.RESUMED;
-
+import android.app.ClientTransactionHandler;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
@@ -27,6 +25,7 @@ import android.os.Trace;
import com.android.internal.content.ReferrerIntent;
import java.util.List;
+import java.util.Objects;
/**
* New intent message.
@@ -34,32 +33,53 @@ import java.util.List;
*/
public class NewIntentItem extends ClientTransactionItem {
- private final List<ReferrerIntent> mIntents;
- private final boolean mPause;
-
- public NewIntentItem(List<ReferrerIntent> intents, boolean pause) {
- mIntents = intents;
- mPause = pause;
- }
+ private List<ReferrerIntent> mIntents;
+ private boolean mPause;
- @Override
+ // TODO(lifecycler): Switch new intent handling to this scheme.
+ /*@Override
public int getPreExecutionState() {
- return PAUSED;
+ return ON_PAUSE;
}
@Override
public int getPostExecutionState() {
- return RESUMED;
- }
+ return ON_RESUME;
+ }*/
@Override
- public void execute(android.app.ClientTransactionHandler client, IBinder token) {
+ public void execute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityNewIntent");
client.handleNewIntent(token, mIntents, mPause);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
+ // ObjectPoolItem implementation
+
+ private NewIntentItem() {}
+
+ /** Obtain an instance initialized with provided params. */
+ public static NewIntentItem obtain(List<ReferrerIntent> intents, boolean pause) {
+ NewIntentItem instance = ObjectPool.obtain(NewIntentItem.class);
+ if (instance == null) {
+ instance = new NewIntentItem();
+ }
+ instance.mIntents = intents;
+ instance.mPause = pause;
+
+ return instance;
+ }
+
+ @Override
+ public void recycle() {
+ mIntents = null;
+ mPause = false;
+ ObjectPool.recycle(this);
+ }
+
+
// Parcelable implementation
/** Write to Parcel. */
@@ -95,7 +115,7 @@ public class NewIntentItem extends ClientTransactionItem {
return false;
}
final NewIntentItem other = (NewIntentItem) o;
- return mPause == other.mPause && mIntents.equals(other.mIntents);
+ return mPause == other.mPause && Objects.equals(mIntents, other.mIntents);
}
@Override
@@ -105,4 +125,9 @@ public class NewIntentItem extends ClientTransactionItem {
result = 31 * result + mIntents.hashCode();
return result;
}
+
+ @Override
+ public String toString() {
+ return "NewIntentItem{pause=" + mPause + ",intents=" + mIntents + "}";
+ }
}
diff --git a/android/app/servertransaction/ObjectPool.java b/android/app/servertransaction/ObjectPool.java
new file mode 100644
index 00000000..2fec30a0
--- /dev/null
+++ b/android/app/servertransaction/ObjectPool.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.servertransaction;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * An object pool that can provide reused objects if available.
+ * @hide
+ */
+class ObjectPool {
+
+ private static final Object sPoolSync = new Object();
+ private static final Map<Class, ArrayList<? extends ObjectPoolItem>> sPoolMap =
+ new HashMap<>();
+
+ private static final int MAX_POOL_SIZE = 50;
+
+ /**
+ * Obtain an instance of a specific class from the pool
+ * @param itemClass The class of the object we're looking for.
+ * @return An instance or null if there is none.
+ */
+ public static <T extends ObjectPoolItem> T obtain(Class<T> itemClass) {
+ synchronized (sPoolSync) {
+ @SuppressWarnings("unchecked")
+ final ArrayList<T> itemPool = (ArrayList<T>) sPoolMap.get(itemClass);
+ if (itemPool != null && !itemPool.isEmpty()) {
+ return itemPool.remove(itemPool.size() - 1);
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Recycle the object to the pool. The object should be properly cleared before this.
+ * @param item The object to recycle.
+ * @see ObjectPoolItem#recycle()
+ */
+ public static <T extends ObjectPoolItem> void recycle(T item) {
+ synchronized (sPoolSync) {
+ @SuppressWarnings("unchecked")
+ ArrayList<T> itemPool = (ArrayList<T>) sPoolMap.get(item.getClass());
+ if (itemPool == null) {
+ itemPool = new ArrayList<>();
+ sPoolMap.put(item.getClass(), itemPool);
+ }
+ // Check if the item is already in the pool
+ final int size = itemPool.size();
+ for (int i = 0; i < size; i++) {
+ if (itemPool.get(i) == item) {
+ throw new IllegalStateException("Trying to recycle already recycled item");
+ }
+ }
+
+ if (size < MAX_POOL_SIZE) {
+ itemPool.add(item);
+ }
+ }
+ }
+}
diff --git a/android/app/servertransaction/ObjectPoolItem.java b/android/app/servertransaction/ObjectPoolItem.java
new file mode 100644
index 00000000..17bd4f30
--- /dev/null
+++ b/android/app/servertransaction/ObjectPoolItem.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.servertransaction;
+
+/**
+ * Base interface for all lifecycle items that can be put in object pool.
+ * @hide
+ */
+public interface ObjectPoolItem {
+ /**
+ * Clear the contents of the item and putting it to a pool. The implementation should call
+ * {@link ObjectPool#recycle(ObjectPoolItem)} passing itself.
+ */
+ void recycle();
+}
diff --git a/android/app/servertransaction/PauseActivityItem.java b/android/app/servertransaction/PauseActivityItem.java
index e561a4b5..880fef73 100644
--- a/android/app/servertransaction/PauseActivityItem.java
+++ b/android/app/servertransaction/PauseActivityItem.java
@@ -18,11 +18,12 @@ package android.app.servertransaction;
import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
+import android.app.ActivityManager;
import android.app.ClientTransactionHandler;
import android.os.IBinder;
import android.os.Parcel;
+import android.os.RemoteException;
import android.os.Trace;
-import android.util.Slog;
/**
* Request to move an activity to paused state.
@@ -32,43 +33,81 @@ public class PauseActivityItem extends ActivityLifecycleItem {
private static final String TAG = "PauseActivityItem";
- private final boolean mFinished;
- private final boolean mUserLeaving;
- private final int mConfigChanges;
- private final boolean mDontReport;
+ private boolean mFinished;
+ private boolean mUserLeaving;
+ private int mConfigChanges;
+ private boolean mDontReport;
- private int mLifecycleSeq;
+ @Override
+ public void execute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
+ Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
+ client.handlePauseActivity(token, mFinished, mUserLeaving, mConfigChanges, mDontReport,
+ pendingActions);
+ Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
+ }
- public PauseActivityItem(boolean finished, boolean userLeaving, int configChanges,
- boolean dontReport) {
- mFinished = finished;
- mUserLeaving = userLeaving;
- mConfigChanges = configChanges;
- mDontReport = dontReport;
+ @Override
+ public int getTargetState() {
+ return ON_PAUSE;
}
@Override
- public void prepare(ClientTransactionHandler client, IBinder token) {
- mLifecycleSeq = client.getLifecycleSeq();
- if (DEBUG_ORDER) {
- Slog.d(TAG, "Pause transaction for " + client + " received seq: "
- + mLifecycleSeq);
+ public void postExecute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
+ if (mDontReport) {
+ return;
+ }
+ try {
+ // TODO(lifecycler): Use interface callback instead of AMS.
+ ActivityManager.getService().activityPaused(token);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
}
}
- @Override
- public void execute(ClientTransactionHandler client, IBinder token) {
- Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
- client.handlePauseActivity(token, mFinished, mUserLeaving, mConfigChanges, mDontReport,
- mLifecycleSeq);
- Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
+
+ // ObjectPoolItem implementation
+
+ private PauseActivityItem() {}
+
+ /** Obtain an instance initialized with provided params. */
+ public static PauseActivityItem obtain(boolean finished, boolean userLeaving, int configChanges,
+ boolean dontReport) {
+ PauseActivityItem instance = ObjectPool.obtain(PauseActivityItem.class);
+ if (instance == null) {
+ instance = new PauseActivityItem();
+ }
+ instance.mFinished = finished;
+ instance.mUserLeaving = userLeaving;
+ instance.mConfigChanges = configChanges;
+ instance.mDontReport = dontReport;
+
+ return instance;
}
- @Override
- public int getTargetState() {
- return PAUSED;
+ /** Obtain an instance initialized with default params. */
+ public static PauseActivityItem obtain() {
+ PauseActivityItem instance = ObjectPool.obtain(PauseActivityItem.class);
+ if (instance == null) {
+ instance = new PauseActivityItem();
+ }
+ instance.mFinished = false;
+ instance.mUserLeaving = false;
+ instance.mConfigChanges = 0;
+ instance.mDontReport = true;
+
+ return instance;
}
+ @Override
+ public void recycle() {
+ mFinished = false;
+ mUserLeaving = false;
+ mConfigChanges = 0;
+ mDontReport = false;
+ ObjectPool.recycle(this);
+ }
// Parcelable implementation
@@ -122,4 +161,10 @@ public class PauseActivityItem extends ActivityLifecycleItem {
result = 31 * result + (mDontReport ? 1 : 0);
return result;
}
+
+ @Override
+ public String toString() {
+ return "PauseActivityItem{finished=" + mFinished + ",userLeaving=" + mUserLeaving
+ + ",configChanges=" + mConfigChanges + ",dontReport=" + mDontReport + "}";
+ }
}
diff --git a/android/app/servertransaction/PendingTransactionActions.java b/android/app/servertransaction/PendingTransactionActions.java
new file mode 100644
index 00000000..8304c1c5
--- /dev/null
+++ b/android/app/servertransaction/PendingTransactionActions.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.servertransaction;
+
+import static android.app.ActivityThread.DEBUG_MEMORY_TRIM;
+
+import android.app.ActivityManager;
+import android.app.ActivityThread.ActivityClientRecord;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.os.TransactionTooLargeException;
+import android.util.Log;
+import android.util.LogWriter;
+import android.util.Slog;
+
+import com.android.internal.util.IndentingPrintWriter;
+
+/**
+ * Container that has data pending to be used at later stages of
+ * {@link android.app.servertransaction.ClientTransaction}.
+ * An instance of this class is passed to each individual transaction item, so it can use some
+ * information from previous steps or add some for the following steps.
+ *
+ * @hide
+ */
+public class PendingTransactionActions {
+ private boolean mRestoreInstanceState;
+ private boolean mCallOnPostCreate;
+ private Bundle mOldState;
+ private StopInfo mStopInfo;
+
+ public PendingTransactionActions() {
+ clear();
+ }
+
+ /** Reset the state of the instance to default, non-initialized values. */
+ public void clear() {
+ mRestoreInstanceState = false;
+ mCallOnPostCreate = false;
+ mOldState = null;
+ mStopInfo = null;
+ }
+
+ /** Getter */
+ public boolean shouldRestoreInstanceState() {
+ return mRestoreInstanceState;
+ }
+
+ public void setRestoreInstanceState(boolean restoreInstanceState) {
+ mRestoreInstanceState = restoreInstanceState;
+ }
+
+ /** Getter */
+ public boolean shouldCallOnPostCreate() {
+ return mCallOnPostCreate;
+ }
+
+ public void setCallOnPostCreate(boolean callOnPostCreate) {
+ mCallOnPostCreate = callOnPostCreate;
+ }
+
+ public Bundle getOldState() {
+ return mOldState;
+ }
+
+ public void setOldState(Bundle oldState) {
+ mOldState = oldState;
+ }
+
+ public StopInfo getStopInfo() {
+ return mStopInfo;
+ }
+
+ public void setStopInfo(StopInfo stopInfo) {
+ mStopInfo = stopInfo;
+ }
+
+ /** Reports to server about activity stop. */
+ public static class StopInfo implements Runnable {
+ private static final String TAG = "ActivityStopInfo";
+
+ private ActivityClientRecord mActivity;
+ private Bundle mState;
+ private PersistableBundle mPersistentState;
+ private CharSequence mDescription;
+
+ public void setActivity(ActivityClientRecord activity) {
+ mActivity = activity;
+ }
+
+ public void setState(Bundle state) {
+ mState = state;
+ }
+
+ public void setPersistentState(PersistableBundle persistentState) {
+ mPersistentState = persistentState;
+ }
+
+ public void setDescription(CharSequence description) {
+ mDescription = description;
+ }
+
+ @Override
+ public void run() {
+ // Tell activity manager we have been stopped.
+ try {
+ if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Reporting activity stopped: " + mActivity);
+ // TODO(lifecycler): Use interface callback instead of AMS.
+ ActivityManager.getService().activityStopped(
+ mActivity.token, mState, mPersistentState, mDescription);
+ } catch (RemoteException ex) {
+ // Dump statistics about bundle to help developers debug
+ final LogWriter writer = new LogWriter(Log.WARN, TAG);
+ final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
+ pw.println("Bundle stats:");
+ Bundle.dumpStats(pw, mState);
+ pw.println("PersistableBundle stats:");
+ Bundle.dumpStats(pw, mPersistentState);
+
+ if (ex instanceof TransactionTooLargeException
+ && mActivity.loadedApk.getTargetSdkVersion() < Build.VERSION_CODES.N) {
+ Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex);
+ return;
+ }
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+ }
+}
diff --git a/android/app/servertransaction/PipModeChangeItem.java b/android/app/servertransaction/PipModeChangeItem.java
index 923839ee..b999cd7e 100644
--- a/android/app/servertransaction/PipModeChangeItem.java
+++ b/android/app/servertransaction/PipModeChangeItem.java
@@ -16,10 +16,13 @@
package android.app.servertransaction;
+import android.app.ClientTransactionHandler;
import android.content.res.Configuration;
import android.os.IBinder;
import android.os.Parcel;
+import java.util.Objects;
+
/**
* Picture in picture mode change message.
* @hide
@@ -28,17 +31,37 @@ import android.os.Parcel;
// communicate multi-window mode change with WindowConfiguration.
public class PipModeChangeItem extends ClientTransactionItem {
- private final boolean mIsInPipMode;
- private final Configuration mOverrideConfig;
+ private boolean mIsInPipMode;
+ private Configuration mOverrideConfig;
+
+ @Override
+ public void execute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
+ client.handlePictureInPictureModeChanged(token, mIsInPipMode, mOverrideConfig);
+ }
+
+
+ // ObjectPoolItem implementation
- public PipModeChangeItem(boolean isInPipMode, Configuration overrideConfig) {
- mIsInPipMode = isInPipMode;
- mOverrideConfig = overrideConfig;
+ private PipModeChangeItem() {}
+
+ /** Obtain an instance initialized with provided params. */
+ public static PipModeChangeItem obtain(boolean isInPipMode, Configuration overrideConfig) {
+ PipModeChangeItem instance = ObjectPool.obtain(PipModeChangeItem.class);
+ if (instance == null) {
+ instance = new PipModeChangeItem();
+ }
+ instance.mIsInPipMode = isInPipMode;
+ instance.mOverrideConfig = overrideConfig;
+
+ return instance;
}
@Override
- public void execute(android.app.ClientTransactionHandler client, IBinder token) {
- client.handlePictureInPictureModeChanged(token, mIsInPipMode, mOverrideConfig);
+ public void recycle() {
+ mIsInPipMode = false;
+ mOverrideConfig = null;
+ ObjectPool.recycle(this);
}
@@ -76,7 +99,8 @@ public class PipModeChangeItem extends ClientTransactionItem {
return false;
}
final PipModeChangeItem other = (PipModeChangeItem) o;
- return mIsInPipMode == other.mIsInPipMode && mOverrideConfig.equals(other.mOverrideConfig);
+ return mIsInPipMode == other.mIsInPipMode
+ && Objects.equals(mOverrideConfig, other.mOverrideConfig);
}
@Override
@@ -86,4 +110,10 @@ public class PipModeChangeItem extends ClientTransactionItem {
result = 31 * result + mOverrideConfig.hashCode();
return result;
}
+
+ @Override
+ public String toString() {
+ return "PipModeChangeItem{isInPipMode=" + mIsInPipMode
+ + ",overrideConfig=" + mOverrideConfig + "}";
+ }
}
diff --git a/android/app/servertransaction/ResumeActivityItem.java b/android/app/servertransaction/ResumeActivityItem.java
index ea31a461..9249c6e8 100644
--- a/android/app/servertransaction/ResumeActivityItem.java
+++ b/android/app/servertransaction/ResumeActivityItem.java
@@ -18,11 +18,12 @@ package android.app.servertransaction;
import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
+import android.app.ActivityManager;
import android.app.ClientTransactionHandler;
import android.os.IBinder;
import android.os.Parcel;
+import android.os.RemoteException;
import android.os.Trace;
-import android.util.Slog;
/**
* Request to move an activity to resumed state.
@@ -32,37 +33,78 @@ public class ResumeActivityItem extends ActivityLifecycleItem {
private static final String TAG = "ResumeActivityItem";
- private final int mProcState;
- private final boolean mIsForward;
-
- private int mLifecycleSeq;
-
- public ResumeActivityItem(int procState, boolean isForward) {
- mProcState = procState;
- mIsForward = isForward;
- }
+ private int mProcState;
+ private boolean mUpdateProcState;
+ private boolean mIsForward;
@Override
- public void prepare(ClientTransactionHandler client, IBinder token) {
- mLifecycleSeq = client.getLifecycleSeq();
- if (DEBUG_ORDER) {
- Slog.d(TAG, "Resume transaction for " + client + " received seq: "
- + mLifecycleSeq);
+ public void preExecute(ClientTransactionHandler client, IBinder token) {
+ if (mUpdateProcState) {
+ client.updateProcessState(mProcState, false);
}
- client.updateProcessState(mProcState, false);
}
@Override
- public void execute(ClientTransactionHandler client, IBinder token) {
+ public void execute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityResume");
- client.handleResumeActivity(token, true /* clearHide */, mIsForward,
- true /* reallyResume */, mLifecycleSeq, "RESUME_ACTIVITY");
+ client.handleResumeActivity(token, true /* clearHide */, mIsForward, "RESUME_ACTIVITY");
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}
@Override
+ public void postExecute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
+ try {
+ // TODO(lifecycler): Use interface callback instead of AMS.
+ ActivityManager.getService().activityResumed(token);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
public int getTargetState() {
- return RESUMED;
+ return ON_RESUME;
+ }
+
+
+ // ObjectPoolItem implementation
+
+ private ResumeActivityItem() {}
+
+ /** Obtain an instance initialized with provided params. */
+ public static ResumeActivityItem obtain(int procState, boolean isForward) {
+ ResumeActivityItem instance = ObjectPool.obtain(ResumeActivityItem.class);
+ if (instance == null) {
+ instance = new ResumeActivityItem();
+ }
+ instance.mProcState = procState;
+ instance.mUpdateProcState = true;
+ instance.mIsForward = isForward;
+
+ return instance;
+ }
+
+ /** Obtain an instance initialized with provided params. */
+ public static ResumeActivityItem obtain(boolean isForward) {
+ ResumeActivityItem instance = ObjectPool.obtain(ResumeActivityItem.class);
+ if (instance == null) {
+ instance = new ResumeActivityItem();
+ }
+ instance.mProcState = ActivityManager.PROCESS_STATE_UNKNOWN;
+ instance.mUpdateProcState = false;
+ instance.mIsForward = isForward;
+
+ return instance;
+ }
+
+ @Override
+ public void recycle() {
+ mProcState = ActivityManager.PROCESS_STATE_UNKNOWN;
+ mUpdateProcState = false;
+ mIsForward = false;
+ ObjectPool.recycle(this);
}
@@ -72,12 +114,14 @@ public class ResumeActivityItem extends ActivityLifecycleItem {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mProcState);
+ dest.writeBoolean(mUpdateProcState);
dest.writeBoolean(mIsForward);
}
/** Read from Parcel. */
private ResumeActivityItem(Parcel in) {
mProcState = in.readInt();
+ mUpdateProcState = in.readBoolean();
mIsForward = in.readBoolean();
}
@@ -101,14 +145,22 @@ public class ResumeActivityItem extends ActivityLifecycleItem {
return false;
}
final ResumeActivityItem other = (ResumeActivityItem) o;
- return mProcState == other.mProcState && mIsForward == other.mIsForward;
+ return mProcState == other.mProcState && mUpdateProcState == other.mUpdateProcState
+ && mIsForward == other.mIsForward;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + mProcState;
+ result = 31 * result + (mUpdateProcState ? 1 : 0);
result = 31 * result + (mIsForward ? 1 : 0);
return result;
}
+
+ @Override
+ public String toString() {
+ return "ResumeActivityItem{procState=" + mProcState
+ + ",updateProcState=" + mUpdateProcState + ",isForward=" + mIsForward + "}";
+ }
}
diff --git a/android/app/servertransaction/StopActivityItem.java b/android/app/servertransaction/StopActivityItem.java
index d62c5077..5c5c3041 100644
--- a/android/app/servertransaction/StopActivityItem.java
+++ b/android/app/servertransaction/StopActivityItem.java
@@ -22,7 +22,6 @@ import android.app.ClientTransactionHandler;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Trace;
-import android.util.Slog;
/**
* Request to move an activity to stopped state.
@@ -32,35 +31,50 @@ public class StopActivityItem extends ActivityLifecycleItem {
private static final String TAG = "StopActivityItem";
- private final boolean mShowWindow;
- private final int mConfigChanges;
+ private boolean mShowWindow;
+ private int mConfigChanges;
- private int mLifecycleSeq;
-
- public StopActivityItem(boolean showWindow, int configChanges) {
- mShowWindow = showWindow;
- mConfigChanges = configChanges;
+ @Override
+ public void execute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
+ Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStop");
+ client.handleStopActivity(token, mShowWindow, mConfigChanges, pendingActions);
+ Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}
@Override
- public void prepare(ClientTransactionHandler client, IBinder token) {
- mLifecycleSeq = client.getLifecycleSeq();
- if (DEBUG_ORDER) {
- Slog.d(TAG, "Stop transaction for " + client + " received seq: "
- + mLifecycleSeq);
- }
+ public void postExecute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
+ client.reportStop(pendingActions);
}
@Override
- public void execute(ClientTransactionHandler client, IBinder token) {
- Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStop");
- client.handleStopActivity(token, mShowWindow, mConfigChanges, mLifecycleSeq);
- Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
+ public int getTargetState() {
+ return ON_STOP;
+ }
+
+
+ // ObjectPoolItem implementation
+
+ private StopActivityItem() {}
+
+ /** Obtain an instance initialized with provided params. */
+ public static StopActivityItem obtain(boolean showWindow, int configChanges) {
+ StopActivityItem instance = ObjectPool.obtain(StopActivityItem.class);
+ if (instance == null) {
+ instance = new StopActivityItem();
+ }
+ instance.mShowWindow = showWindow;
+ instance.mConfigChanges = configChanges;
+
+ return instance;
}
@Override
- public int getTargetState() {
- return STOPPED;
+ public void recycle() {
+ mShowWindow = false;
+ mConfigChanges = 0;
+ ObjectPool.recycle(this);
}
@@ -109,4 +123,10 @@ public class StopActivityItem extends ActivityLifecycleItem {
result = 31 * result + mConfigChanges;
return result;
}
+
+ @Override
+ public String toString() {
+ return "StopActivityItem{showWindow=" + mShowWindow + ",configChanges=" + mConfigChanges
+ + "}";
+ }
}
diff --git a/android/app/servertransaction/TransactionExecutor.java b/android/app/servertransaction/TransactionExecutor.java
new file mode 100644
index 00000000..5b0ea6b1
--- /dev/null
+++ b/android/app/servertransaction/TransactionExecutor.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.servertransaction;
+
+import static android.app.servertransaction.ActivityLifecycleItem.ON_CREATE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_DESTROY;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_RESTART;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_RESUME;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_START;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
+import static android.app.servertransaction.ActivityLifecycleItem.UNDEFINED;
+
+import android.app.ActivityThread.ActivityClientRecord;
+import android.app.ClientTransactionHandler;
+import android.os.IBinder;
+import android.util.IntArray;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.List;
+
+/**
+ * Class that manages transaction execution in the correct order.
+ * @hide
+ */
+public class TransactionExecutor {
+
+ private static final boolean DEBUG_RESOLVER = false;
+ private static final String TAG = "TransactionExecutor";
+
+ private ClientTransactionHandler mTransactionHandler;
+ private PendingTransactionActions mPendingActions = new PendingTransactionActions();
+
+ // Temp holder for lifecycle path.
+ // No direct transition between two states should take more than one complete cycle of 6 states.
+ @ActivityLifecycleItem.LifecycleState
+ private IntArray mLifecycleSequence = new IntArray(6);
+
+ /** Initialize an instance with transaction handler, that will execute all requested actions. */
+ public TransactionExecutor(ClientTransactionHandler clientTransactionHandler) {
+ mTransactionHandler = clientTransactionHandler;
+ }
+
+ /**
+ * Resolve transaction.
+ * First all callbacks will be executed in the order they appear in the list. If a callback
+ * requires a certain pre- or post-execution state, the client will be transitioned accordingly.
+ * Then the client will cycle to the final lifecycle state if provided. Otherwise, it will
+ * either remain in the initial state, or last state needed by a callback.
+ */
+ public void execute(ClientTransaction transaction) {
+ final IBinder token = transaction.getActivityToken();
+ log("Start resolving transaction for client: " + mTransactionHandler + ", token: " + token);
+
+ executeCallbacks(transaction);
+
+ executeLifecycleState(transaction);
+ mPendingActions.clear();
+ log("End resolving transaction");
+ }
+
+ /** Cycle through all states requested by callbacks and execute them at proper times. */
+ @VisibleForTesting
+ public void executeCallbacks(ClientTransaction transaction) {
+ final List<ClientTransactionItem> callbacks = transaction.getCallbacks();
+ if (callbacks == null) {
+ // No callbacks to execute, return early.
+ return;
+ }
+ log("Resolving callbacks");
+
+ final IBinder token = transaction.getActivityToken();
+ ActivityClientRecord r = mTransactionHandler.getActivityClient(token);
+ final int size = callbacks.size();
+ for (int i = 0; i < size; ++i) {
+ final ClientTransactionItem item = callbacks.get(i);
+ log("Resolving callback: " + item);
+ final int preExecutionState = item.getPreExecutionState();
+ if (preExecutionState != UNDEFINED) {
+ cycleToPath(r, preExecutionState);
+ }
+
+ item.execute(mTransactionHandler, token, mPendingActions);
+ item.postExecute(mTransactionHandler, token, mPendingActions);
+ if (r == null) {
+ // Launch activity request will create an activity record.
+ r = mTransactionHandler.getActivityClient(token);
+ }
+
+ final int postExecutionState = item.getPostExecutionState();
+ if (postExecutionState != UNDEFINED) {
+ cycleToPath(r, postExecutionState);
+ }
+ }
+ }
+
+ /** Transition to the final state if requested by the transaction. */
+ private void executeLifecycleState(ClientTransaction transaction) {
+ final ActivityLifecycleItem lifecycleItem = transaction.getLifecycleStateRequest();
+ if (lifecycleItem == null) {
+ // No lifecycle request, return early.
+ return;
+ }
+ log("Resolving lifecycle state: " + lifecycleItem);
+
+ final IBinder token = transaction.getActivityToken();
+ final ActivityClientRecord r = mTransactionHandler.getActivityClient(token);
+
+ // Cycle to the state right before the final requested state.
+ cycleToPath(r, lifecycleItem.getTargetState(), true /* excludeLastState */);
+
+ // Execute the final transition with proper parameters.
+ lifecycleItem.execute(mTransactionHandler, token, mPendingActions);
+ lifecycleItem.postExecute(mTransactionHandler, token, mPendingActions);
+ }
+
+ /** Transition the client between states. */
+ @VisibleForTesting
+ public void cycleToPath(ActivityClientRecord r, int finish) {
+ cycleToPath(r, finish, false /* excludeLastState */);
+ }
+
+ /**
+ * Transition the client between states with an option not to perform the last hop in the
+ * sequence. This is used when resolving lifecycle state request, when the last transition must
+ * be performed with some specific parameters.
+ */
+ private void cycleToPath(ActivityClientRecord r, int finish,
+ boolean excludeLastState) {
+ final int start = r.getLifecycleState();
+ log("Cycle from: " + start + " to: " + finish + " excludeLastState:" + excludeLastState);
+ initLifecyclePath(start, finish, excludeLastState);
+ performLifecycleSequence(r);
+ }
+
+ /** Transition the client through previously initialized state sequence. */
+ private void performLifecycleSequence(ActivityClientRecord r) {
+ final int size = mLifecycleSequence.size();
+ for (int i = 0, state; i < size; i++) {
+ state = mLifecycleSequence.get(i);
+ log("Transitioning to state: " + state);
+ switch (state) {
+ case ON_CREATE:
+ mTransactionHandler.handleLaunchActivity(r, mPendingActions);
+ break;
+ case ON_START:
+ mTransactionHandler.handleStartActivity(r, mPendingActions);
+ break;
+ case ON_RESUME:
+ mTransactionHandler.handleResumeActivity(r.token, false /* clearHide */,
+ r.isForward, "LIFECYCLER_RESUME_ACTIVITY");
+ break;
+ case ON_PAUSE:
+ mTransactionHandler.handlePauseActivity(r.token, false /* finished */,
+ false /* userLeaving */, 0 /* configChanges */,
+ true /* dontReport */, mPendingActions);
+ break;
+ case ON_STOP:
+ mTransactionHandler.handleStopActivity(r.token, false /* show */,
+ 0 /* configChanges */, mPendingActions);
+ break;
+ case ON_DESTROY:
+ mTransactionHandler.handleDestroyActivity(r.token, false /* finishing */,
+ 0 /* configChanges */, false /* getNonConfigInstance */);
+ break;
+ case ON_RESTART:
+ mTransactionHandler.performRestartActivity(r.token, false /* start */);
+ break;
+ default:
+ throw new IllegalArgumentException("Unexpected lifecycle state: " + state);
+ }
+ }
+ }
+
+ /**
+ * Calculate the path through main lifecycle states for an activity and fill
+ * @link #mLifecycleSequence} with values starting with the state that follows the initial
+ * state.
+ */
+ public void initLifecyclePath(int start, int finish, boolean excludeLastState) {
+ mLifecycleSequence.clear();
+ if (finish >= start) {
+ // just go there
+ for (int i = start + 1; i <= finish; i++) {
+ mLifecycleSequence.add(i);
+ }
+ } else { // finish < start, can't just cycle down
+ if (start == ON_PAUSE && finish == ON_RESUME) {
+ // Special case when we can just directly go to resumed state.
+ mLifecycleSequence.add(ON_RESUME);
+ } else if (start <= ON_STOP && finish >= ON_START) {
+ // Restart and go to required state.
+
+ // Go to stopped state first.
+ for (int i = start + 1; i <= ON_STOP; i++) {
+ mLifecycleSequence.add(i);
+ }
+ // Restart
+ mLifecycleSequence.add(ON_RESTART);
+ // Go to required state
+ for (int i = ON_START; i <= finish; i++) {
+ mLifecycleSequence.add(i);
+ }
+ } else {
+ // Relaunch and go to required state
+
+ // Go to destroyed state first.
+ for (int i = start + 1; i <= ON_DESTROY; i++) {
+ mLifecycleSequence.add(i);
+ }
+ // Go to required state
+ for (int i = ON_CREATE; i <= finish; i++) {
+ mLifecycleSequence.add(i);
+ }
+ }
+ }
+
+ // Remove last transition in case we want to perform it with some specific params.
+ if (excludeLastState && mLifecycleSequence.size() != 0) {
+ mLifecycleSequence.remove(mLifecycleSequence.size() - 1);
+ }
+ }
+
+ @VisibleForTesting
+ public int[] getLifecycleSequence() {
+ return mLifecycleSequence.toArray();
+ }
+
+ private static void log(String message) {
+ if (DEBUG_RESOLVER) Slog.d(TAG, message);
+ }
+}
diff --git a/android/app/servertransaction/WindowVisibilityItem.java b/android/app/servertransaction/WindowVisibilityItem.java
index 8e88b38d..d9956b13 100644
--- a/android/app/servertransaction/WindowVisibilityItem.java
+++ b/android/app/servertransaction/WindowVisibilityItem.java
@@ -18,6 +18,7 @@ package android.app.servertransaction;
import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
+import android.app.ClientTransactionHandler;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Trace;
@@ -28,20 +29,39 @@ import android.os.Trace;
*/
public class WindowVisibilityItem extends ClientTransactionItem {
- private final boolean mShowWindow;
-
- public WindowVisibilityItem(boolean showWindow) {
- mShowWindow = showWindow;
- }
+ private boolean mShowWindow;
@Override
- public void execute(android.app.ClientTransactionHandler client, IBinder token) {
+ public void execute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityShowWindow");
client.handleWindowVisibility(token, mShowWindow);
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}
+ // ObjectPoolItem implementation
+
+ private WindowVisibilityItem() {}
+
+ /** Obtain an instance initialized with provided params. */
+ public static WindowVisibilityItem obtain(boolean showWindow) {
+ WindowVisibilityItem instance = ObjectPool.obtain(WindowVisibilityItem.class);
+ if (instance == null) {
+ instance = new WindowVisibilityItem();
+ }
+ instance.mShowWindow = showWindow;
+
+ return instance;
+ }
+
+ @Override
+ public void recycle() {
+ mShowWindow = false;
+ ObjectPool.recycle(this);
+ }
+
+
// Parcelable implementation
/** Write to Parcel. */
@@ -82,4 +102,9 @@ public class WindowVisibilityItem extends ClientTransactionItem {
public int hashCode() {
return 17 + 31 * (mShowWindow ? 1 : 0);
}
+
+ @Override
+ public String toString() {
+ return "WindowVisibilityItem{showWindow=" + mShowWindow + "}";
+ }
}
diff --git a/android/app/slice/Slice.java b/android/app/slice/Slice.java
index ddc5760a..5c7f6741 100644
--- a/android/app/slice/Slice.java
+++ b/android/app/slice/Slice.java
@@ -37,6 +37,8 @@ import android.os.RemoteException;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -53,9 +55,21 @@ public final class Slice implements Parcelable {
/**
* @hide
*/
- @StringDef({HINT_TITLE, HINT_LIST, HINT_LIST_ITEM, HINT_LARGE, HINT_ACTIONS, HINT_SELECTED,
- HINT_NO_TINT, HINT_PARTIAL})
- public @interface SliceHint{ }
+ @StringDef(prefix = { "HINT_" }, value = {
+ HINT_TITLE,
+ HINT_LIST,
+ HINT_LIST_ITEM,
+ HINT_LARGE,
+ HINT_ACTIONS,
+ HINT_SELECTED,
+ HINT_NO_TINT,
+ HINT_SHORTCUT,
+ HINT_TOGGLE,
+ HINT_HORIZONTAL,
+ HINT_PARTIAL,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SliceHint {}
/**
* The meta-data key that allows an activity to easily be linked directly to a slice.
@@ -104,12 +118,15 @@ public final class Slice implements Parcelable {
*/
public static final String HINT_NO_TINT = "no_tint";
/**
- * Hint to indicate that this content should not be shown in larger renderings
- * of Slices. This content may be used to populate the shortcut/icon
- * format of the slice.
- * @hide
+ * Hint to indicate that this content should only be displayed if the slice is presented
+ * as a shortcut.
+ */
+ public static final String HINT_SHORTCUT = "shortcut";
+ /**
+ * Hint indicating this content should be shown instead of the normal content when the slice
+ * is in small format.
*/
- public static final String HINT_HIDDEN = "hidden";
+ public static final String HINT_SUMMARY = "summary";
/**
* Hint to indicate that this content has a toggle action associated with it. To indicate that
* the toggle is on, use {@link #HINT_SELECTED}. When the toggle state changes, the intent
@@ -129,10 +146,14 @@ public final class Slice implements Parcelable {
* OS and should not be cached by apps.
*/
public static final String HINT_PARTIAL = "partial";
+ /**
+ * A hint representing that this item is the max value possible for the slice containing this.
+ * Used to indicate the maximum integer value for a {@link #SUBTYPE_SLIDER}.
+ */
+ public static final String HINT_MAX = "max";
/**
* Key to retrieve an extra added to an intent when a control is changed.
- * @hide
*/
public static final String EXTRA_TOGGLE_STATE = "android.app.slice.extra.TOGGLE_STATE";
/**
@@ -144,6 +165,25 @@ public final class Slice implements Parcelable {
* Subtype to tag the source (i.e. sender) of a {@link #SUBTYPE_MESSAGE}.
*/
public static final String SUBTYPE_SOURCE = "source";
+ /**
+ * Subtype to tag an item as representing a color.
+ */
+ public static final String SUBTYPE_COLOR = "color";
+ /**
+ * Subtype to tag an item represents a slider.
+ */
+ public static final String SUBTYPE_SLIDER = "slider";
+ /**
+ * Subtype to indicate that this content has a toggle action associated with it. To indicate
+ * that the toggle is on, use {@link #HINT_SELECTED}. When the toggle state changes, the
+ * intent associated with it will be sent along with an extra {@link #EXTRA_TOGGLE_STATE}
+ * which can be retrieved to see the new state of the toggle.
+ */
+ public static final String SUBTYPE_TOGGLE = "toggle";
+ /**
+ * Subtype to tag an item representing priority.
+ */
+ public static final String SUBTYPE_PRIORITY = "priority";
private final SliceItem[] mItems;
private final @SliceHint String[] mHints;
@@ -375,9 +415,10 @@ public final class Slice implements Parcelable {
* Add a color to the slice being constructed
* @param subType Optional template-specific type information
* @see {@link SliceItem#getSubType()}
+ * @deprecated will be removed once supportlib updates
*/
public Builder addColor(int color, @Nullable String subType, @SliceHint String... hints) {
- mItems.add(new SliceItem(color, SliceItem.FORMAT_COLOR, subType, hints));
+ mItems.add(new SliceItem(color, SliceItem.FORMAT_INT, subType, hints));
return this;
}
@@ -385,6 +426,7 @@ public final class Slice implements Parcelable {
* Add a color to the slice being constructed
* @param subType Optional template-specific type information
* @see {@link SliceItem#getSubType()}
+ * @deprecated will be removed once supportlib updates
*/
public Builder addColor(int color, @Nullable String subType,
@SliceHint List<String> hints) {
@@ -392,6 +434,26 @@ public final class Slice implements Parcelable {
}
/**
+ * Add a color to the slice being constructed
+ * @param subType Optional template-specific type information
+ * @see {@link SliceItem#getSubType()}
+ */
+ public Builder addInt(int value, @Nullable String subType, @SliceHint String... hints) {
+ mItems.add(new SliceItem(value, SliceItem.FORMAT_INT, subType, hints));
+ return this;
+ }
+
+ /**
+ * Add a color to the slice being constructed
+ * @param subType Optional template-specific type information
+ * @see {@link SliceItem#getSubType()}
+ */
+ public Builder addInt(int value, @Nullable String subType,
+ @SliceHint List<String> hints) {
+ return addInt(value, subType, hints.toArray(new String[hints.size()]));
+ }
+
+ /**
* Add a timestamp to the slice being constructed
* @param subType Optional template-specific type information
* @see {@link SliceItem#getSubType()}
@@ -414,6 +476,32 @@ public final class Slice implements Parcelable {
}
/**
+ * Add a bundle to the slice being constructed.
+ * <p>Expected to be used for support library extension, should not be used for general
+ * development
+ * @param subType Optional template-specific type information
+ * @see {@link SliceItem#getSubType()}
+ */
+ public Slice.Builder addBundle(Bundle bundle, @Nullable String subType,
+ @SliceHint String... hints) {
+ mItems.add(new SliceItem(bundle, SliceItem.FORMAT_BUNDLE, subType,
+ hints));
+ return this;
+ }
+
+ /**
+ * Add a bundle to the slice being constructed.
+ * <p>Expected to be used for support library extension, should not be used for general
+ * development
+ * @param subType Optional template-specific type information
+ * @see {@link SliceItem#getSubType()}
+ */
+ public Slice.Builder addBundle(Bundle bundle, @Nullable String subType,
+ @SliceHint List<String> hints) {
+ return addBundle(bundle, subType, hints.toArray(new String[hints.size()]));
+ }
+
+ /**
* Construct the slice.
*/
public Slice build() {
diff --git a/android/app/slice/SliceItem.java b/android/app/slice/SliceItem.java
index cdeee357..bcfd413f 100644
--- a/android/app/slice/SliceItem.java
+++ b/android/app/slice/SliceItem.java
@@ -21,6 +21,7 @@ import android.annotation.StringDef;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.graphics.drawable.Icon;
+import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
@@ -29,6 +30,8 @@ import android.widget.RemoteViews;
import com.android.internal.util.ArrayUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.List;
@@ -42,9 +45,10 @@ import java.util.List;
* <li>{@link #FORMAT_TEXT}</li>
* <li>{@link #FORMAT_IMAGE}</li>
* <li>{@link #FORMAT_ACTION}</li>
- * <li>{@link #FORMAT_COLOR}</li>
+ * <li>{@link #FORMAT_INT}</li>
* <li>{@link #FORMAT_TIMESTAMP}</li>
* <li>{@link #FORMAT_REMOTE_INPUT}</li>
+ * <li>{@link #FORMAT_BUNDLE}</li>
*
* The hints that a {@link SliceItem} are a set of strings which annotate
* the content. The hints that are guaranteed to be understood by the system
@@ -52,11 +56,22 @@ import java.util.List;
*/
public final class SliceItem implements Parcelable {
+ private static final String TAG = "SliceItem";
+
/**
* @hide
*/
- @StringDef({FORMAT_SLICE, FORMAT_TEXT, FORMAT_IMAGE, FORMAT_ACTION, FORMAT_COLOR,
- FORMAT_TIMESTAMP, FORMAT_REMOTE_INPUT})
+ @StringDef(prefix = { "FORMAT_" }, value = {
+ FORMAT_SLICE,
+ FORMAT_TEXT,
+ FORMAT_IMAGE,
+ FORMAT_ACTION,
+ FORMAT_INT,
+ FORMAT_TIMESTAMP,
+ FORMAT_REMOTE_INPUT,
+ FORMAT_BUNDLE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
public @interface SliceType {}
/**
@@ -79,7 +94,12 @@ public final class SliceItem implements Parcelable {
*/
public static final String FORMAT_ACTION = "action";
/**
- * A {@link SliceItem} that contains a Color int.
+ * A {@link SliceItem} that contains an int.
+ */
+ public static final String FORMAT_INT = "int";
+ /**
+ * A {@link SliceItem} that contains an int.
+ * @deprecated to be removed
*/
public static final String FORMAT_COLOR = "color";
/**
@@ -90,6 +110,10 @@ public final class SliceItem implements Parcelable {
* A {@link SliceItem} that contains a {@link RemoteInput}.
*/
public static final String FORMAT_REMOTE_INPUT = "input";
+ /**
+ * A {@link SliceItem} that contains a {@link Bundle}.
+ */
+ public static final String FORMAT_BUNDLE = "bundle";
/**
* @hide
@@ -128,20 +152,6 @@ public final class SliceItem implements Parcelable {
}
/**
- * @hide
- */
- public void addHint(@Slice.SliceHint String hint) {
- mHints = ArrayUtils.appendElement(String.class, mHints, hint);
- }
-
- /**
- * @hide
- */
- public void removeHint(String hint) {
- ArrayUtils.removeElement(String.class, mHints, hint);
- }
-
- /**
* Get the format of this SliceItem.
* <p>
* The format will be one of the following types supported by the platform:
@@ -149,9 +159,10 @@ public final class SliceItem implements Parcelable {
* <li>{@link #FORMAT_TEXT}</li>
* <li>{@link #FORMAT_IMAGE}</li>
* <li>{@link #FORMAT_ACTION}</li>
- * <li>{@link #FORMAT_COLOR}</li>
+ * <li>{@link #FORMAT_INT}</li>
* <li>{@link #FORMAT_TIMESTAMP}</li>
* <li>{@link #FORMAT_REMOTE_INPUT}</li>
+ * <li>{@link #FORMAT_BUNDLE}</li>
* @see #getSubType() ()
*/
public String getFormat() {
@@ -178,6 +189,13 @@ public final class SliceItem implements Parcelable {
}
/**
+ * @return The parcelable held by this {@link #FORMAT_BUNDLE} SliceItem
+ */
+ public Bundle getBundle() {
+ return (Bundle) mObj;
+ }
+
+ /**
* @return The icon held by this {@link #FORMAT_IMAGE} SliceItem
*/
public Icon getIcon() {
@@ -206,7 +224,14 @@ public final class SliceItem implements Parcelable {
}
/**
- * @return The color held by this {@link #FORMAT_COLOR} SliceItem
+ * @return The color held by this {@link #FORMAT_INT} SliceItem
+ */
+ public int getInt() {
+ return (Integer) mObj;
+ }
+
+ /**
+ * @deprecated to be removed.
*/
public int getColor() {
return (Integer) mObj;
@@ -299,6 +324,7 @@ public final class SliceItem implements Parcelable {
case FORMAT_SLICE:
case FORMAT_IMAGE:
case FORMAT_REMOTE_INPUT:
+ case FORMAT_BUNDLE:
((Parcelable) obj).writeToParcel(dest, flags);
break;
case FORMAT_ACTION:
@@ -308,7 +334,7 @@ public final class SliceItem implements Parcelable {
case FORMAT_TEXT:
TextUtils.writeToParcel((CharSequence) obj, dest, flags);
break;
- case FORMAT_COLOR:
+ case FORMAT_INT:
dest.writeInt((Integer) obj);
break;
case FORMAT_TIMESTAMP:
@@ -329,12 +355,14 @@ public final class SliceItem implements Parcelable {
return new Pair<>(
PendingIntent.CREATOR.createFromParcel(in),
Slice.CREATOR.createFromParcel(in));
- case FORMAT_COLOR:
+ case FORMAT_INT:
return in.readInt();
case FORMAT_TIMESTAMP:
return in.readLong();
case FORMAT_REMOTE_INPUT:
return RemoteInput.CREATOR.createFromParcel(in);
+ case FORMAT_BUNDLE:
+ return Bundle.CREATOR.createFromParcel(in);
}
throw new RuntimeException("Unsupported type " + type);
}
diff --git a/android/app/slice/SliceManager.java b/android/app/slice/SliceManager.java
new file mode 100644
index 00000000..0c5f225d
--- /dev/null
+++ b/android/app/slice/SliceManager.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.slice;
+
+import android.annotation.NonNull;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.util.ArrayMap;
+import android.util.Pair;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Class to handle interactions with {@link Slice}s.
+ * <p>
+ * The SliceManager manages permissions and pinned state for slices.
+ */
+@SystemService(Context.SLICE_SERVICE)
+public class SliceManager {
+
+ private final ISliceManager mService;
+ private final Context mContext;
+ private final ArrayMap<Pair<Uri, SliceCallback>, ISliceListener> mListenerLookup =
+ new ArrayMap<>();
+
+ /**
+ * @hide
+ */
+ public SliceManager(Context context, Handler handler) throws ServiceNotFoundException {
+ mContext = context;
+ mService = ISliceManager.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.SLICE_SERVICE));
+ }
+
+ /**
+ * Adds a callback to a specific slice uri.
+ * <p>
+ * This is a convenience that performs a few slice actions at once. It will put
+ * the slice in a pinned state since there is a callback attached. It will also
+ * listen for content changes, when a content change observes, the android system
+ * will bind the new slice and provide it to all registered {@link SliceCallback}s.
+ *
+ * @param uri The uri of the slice being listened to.
+ * @param callback The listener that should receive the callbacks.
+ * @param specs The list of supported {@link SliceSpec}s of the callback.
+ * @see SliceProvider#onSlicePinned(Uri)
+ */
+ public void registerSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback,
+ @NonNull List<SliceSpec> specs) {
+ registerSliceCallback(uri, callback, specs, Handler.getMain());
+ }
+
+ /**
+ * Adds a callback to a specific slice uri.
+ * <p>
+ * This is a convenience that performs a few slice actions at once. It will put
+ * the slice in a pinned state since there is a callback attached. It will also
+ * listen for content changes, when a content change observes, the android system
+ * will bind the new slice and provide it to all registered {@link SliceCallback}s.
+ *
+ * @param uri The uri of the slice being listened to.
+ * @param callback The listener that should receive the callbacks.
+ * @param specs The list of supported {@link SliceSpec}s of the callback.
+ * @see SliceProvider#onSlicePinned(Uri)
+ */
+ public void registerSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback,
+ @NonNull List<SliceSpec> specs, Handler handler) {
+ try {
+ mService.addSliceListener(uri, mContext.getPackageName(),
+ getListener(uri, callback, new ISliceListener.Stub() {
+ @Override
+ public void onSliceUpdated(Slice s) throws RemoteException {
+ handler.post(() -> callback.onSliceUpdated(s));
+ }
+ }), specs.toArray(new SliceSpec[specs.size()]));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Adds a callback to a specific slice uri.
+ * <p>
+ * This is a convenience that performs a few slice actions at once. It will put
+ * the slice in a pinned state since there is a callback attached. It will also
+ * listen for content changes, when a content change observes, the android system
+ * will bind the new slice and provide it to all registered {@link SliceCallback}s.
+ *
+ * @param uri The uri of the slice being listened to.
+ * @param callback The listener that should receive the callbacks.
+ * @param specs The list of supported {@link SliceSpec}s of the callback.
+ * @see SliceProvider#onSlicePinned(Uri)
+ */
+ public void registerSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback,
+ @NonNull List<SliceSpec> specs, Executor executor) {
+ try {
+ mService.addSliceListener(uri, mContext.getPackageName(),
+ getListener(uri, callback, new ISliceListener.Stub() {
+ @Override
+ public void onSliceUpdated(Slice s) throws RemoteException {
+ executor.execute(() -> callback.onSliceUpdated(s));
+ }
+ }), specs.toArray(new SliceSpec[specs.size()]));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private ISliceListener getListener(Uri uri, SliceCallback callback,
+ ISliceListener listener) {
+ Pair<Uri, SliceCallback> key = new Pair<>(uri, callback);
+ if (mListenerLookup.containsKey(key)) {
+ try {
+ mService.removeSliceListener(uri, mContext.getPackageName(),
+ mListenerLookup.get(key));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ mListenerLookup.put(key, listener);
+ return listener;
+ }
+
+ /**
+ * Removes a callback for a specific slice uri.
+ * <p>
+ * Removes the app from the pinned state (if there are no other apps/callbacks pinning it)
+ * in addition to removing the callback.
+ *
+ * @param uri The uri of the slice being listened to
+ * @param callback The listener that should no longer receive callbacks.
+ * @see #registerSliceCallback
+ */
+ public void unregisterSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback) {
+ try {
+ mService.removeSliceListener(uri, mContext.getPackageName(),
+ mListenerLookup.remove(new Pair<>(uri, callback)));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Ensures that a slice is in a pinned state.
+ * <p>
+ * Pinned state is not persisted across reboots, so apps are expected to re-pin any slices
+ * they still care about after a reboot.
+ *
+ * @param uri The uri of the slice being pinned.
+ * @param specs The list of supported {@link SliceSpec}s of the callback.
+ * @see SliceProvider#onSlicePinned(Uri)
+ */
+ public void pinSlice(@NonNull Uri uri, @NonNull List<SliceSpec> specs) {
+ try {
+ mService.pinSlice(mContext.getPackageName(), uri,
+ specs.toArray(new SliceSpec[specs.size()]));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Remove a pin for a slice.
+ * <p>
+ * If the slice has no other pins/callbacks then the slice will be unpinned.
+ *
+ * @param uri The uri of the slice being unpinned.
+ * @see #pinSlice
+ * @see SliceProvider#onSliceUnpinned(Uri)
+ */
+ public void unpinSlice(@NonNull Uri uri) {
+ try {
+ mService.unpinSlice(mContext.getPackageName(), uri);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public boolean hasSliceAccess() {
+ try {
+ return mService.hasSliceAccess(mContext.getPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get the current set of specs for a pinned slice.
+ * <p>
+ * This is the set of specs supported for a specific pinned slice. It will take
+ * into account all clients and returns only specs supported by all.
+ * @see SliceSpec
+ */
+ public @NonNull List<SliceSpec> getPinnedSpecs(Uri uri) {
+ try {
+ return Arrays.asList(mService.getPinnedSpecs(uri, mContext.getPackageName()));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Class that listens to changes in {@link Slice}s.
+ */
+ public interface SliceCallback {
+
+ /**
+ * Called when slice is updated.
+ *
+ * @param s The updated slice.
+ * @see #registerSliceCallback
+ */
+ void onSliceUpdated(Slice s);
+ }
+}
diff --git a/android/app/slice/SliceProvider.java b/android/app/slice/SliceProvider.java
index ac5365c3..8483931c 100644
--- a/android/app/slice/SliceProvider.java
+++ b/android/app/slice/SliceProvider.java
@@ -105,6 +105,14 @@ public abstract class SliceProvider extends ContentProvider {
/**
* @hide
*/
+ public static final String METHOD_PIN = "pin";
+ /**
+ * @hide
+ */
+ public static final String METHOD_UNPIN = "unpin";
+ /**
+ * @hide
+ */
public static final String EXTRA_INTENT = "slice_intent";
/**
* @hide
@@ -143,6 +151,38 @@ public abstract class SliceProvider extends ContentProvider {
}
/**
+ * Called to inform an app that a slice has been pinned.
+ * <p>
+ * Pinning is a way that slice hosts use to notify apps of which slices
+ * they care about updates for. When a slice is pinned the content is
+ * expected to be relatively fresh and kept up to date.
+ * <p>
+ * Being pinned does not provide any escalated privileges for the slice
+ * provider. So apps should do things such as turn on syncing or schedule
+ * a job in response to a onSlicePinned.
+ * <p>
+ * Pinned state is not persisted through a reboot, and apps can expect a
+ * new call to onSlicePinned for any slices that should remain pinned
+ * after a reboot occurs.
+ *
+ * @param sliceUri The uri of the slice being unpinned.
+ * @see #onSliceUnpinned(Uri)
+ */
+ public void onSlicePinned(Uri sliceUri) {
+ }
+
+ /**
+ * Called to inform an app that a slices is no longer pinned.
+ * <p>
+ * This means that no other apps on the device care about updates to this
+ * slice anymore and therefore it is not important to be updated. Any syncs
+ * or jobs related to this slice should be cancelled.
+ * @see #onSlicePinned(Uri)
+ */
+ public void onSliceUnpinned(Uri sliceUri) {
+ }
+
+ /**
* This method must be overridden if an {@link IntentFilter} is specified on the SliceProvider.
* In that case, this method can be called and is expected to return a non-null Uri representing
* a slice. Otherwise this will throw {@link UnsupportedOperationException}.
@@ -221,6 +261,7 @@ public abstract class SliceProvider extends ContentProvider {
getContext().enforceCallingPermission(permission.BIND_SLICE,
"Slice binding requires the permission BIND_SLICE");
Intent intent = extras.getParcelable(EXTRA_INTENT);
+ if (intent == null) return null;
Uri uri = onMapIntentToUri(intent);
List<SliceSpec> supportedSpecs = extras.getParcelableArrayList(EXTRA_SUPPORTED_SPECS);
Bundle b = new Bundle();
@@ -231,10 +272,62 @@ public abstract class SliceProvider extends ContentProvider {
b.putParcelable(EXTRA_SLICE, null);
}
return b;
+ } else if (method.equals(METHOD_PIN)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) {
+ getContext().enforceUriPermission(uri, permission.BIND_SLICE,
+ permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(),
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
+ "Slice binding requires the permission BIND_SLICE");
+ }
+ handlePinSlice(uri);
+ } else if (method.equals(METHOD_UNPIN)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) {
+ getContext().enforceUriPermission(uri, permission.BIND_SLICE,
+ permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(),
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
+ "Slice binding requires the permission BIND_SLICE");
+ }
+ handleUnpinSlice(uri);
}
return super.call(method, arg, extras);
}
+ private void handlePinSlice(Uri sliceUri) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ onSlicePinned(sliceUri);
+ } else {
+ CountDownLatch latch = new CountDownLatch(1);
+ Handler.getMain().post(() -> {
+ onSlicePinned(sliceUri);
+ latch.countDown();
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private void handleUnpinSlice(Uri sliceUri) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ onSliceUnpinned(sliceUri);
+ } else {
+ CountDownLatch latch = new CountDownLatch(1);
+ Handler.getMain().post(() -> {
+ onSliceUnpinned(sliceUri);
+ latch.countDown();
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
private Slice handleBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs) {
if (Looper.myLooper() == Looper.getMainLooper()) {
return onBindSliceStrict(sliceUri, supportedSpecs);
diff --git a/android/app/slice/SliceSpec.java b/android/app/slice/SliceSpec.java
index 433b67e9..8cc0384c 100644
--- a/android/app/slice/SliceSpec.java
+++ b/android/app/slice/SliceSpec.java
@@ -103,6 +103,11 @@ public final class SliceSpec implements Parcelable {
return mType.equals(other.mType) && mRevision == other.mRevision;
}
+ @Override
+ public String toString() {
+ return String.format("SliceSpec{%s,%d}", mType, mRevision);
+ }
+
public static final Creator<SliceSpec> CREATOR = new Creator<SliceSpec>() {
@Override
public SliceSpec createFromParcel(Parcel source) {
diff --git a/android/app/timezone/Callback.java b/android/app/timezone/Callback.java
index aea80380..e3840be6 100644
--- a/android/app/timezone/Callback.java
+++ b/android/app/timezone/Callback.java
@@ -30,9 +30,14 @@ import java.lang.annotation.RetentionPolicy;
public abstract class Callback {
@Retention(RetentionPolicy.SOURCE)
- @IntDef({SUCCESS, ERROR_UNKNOWN_FAILURE, ERROR_INSTALL_BAD_DISTRO_STRUCTURE,
- ERROR_INSTALL_BAD_DISTRO_FORMAT_VERSION, ERROR_INSTALL_RULES_TOO_OLD,
- ERROR_INSTALL_VALIDATION_ERROR})
+ @IntDef(prefix = { "SUCCESS", "ERROR_" }, value = {
+ SUCCESS,
+ ERROR_UNKNOWN_FAILURE,
+ ERROR_INSTALL_BAD_DISTRO_STRUCTURE,
+ ERROR_INSTALL_BAD_DISTRO_FORMAT_VERSION,
+ ERROR_INSTALL_RULES_TOO_OLD,
+ ERROR_INSTALL_VALIDATION_ERROR
+ })
public @interface AsyncResultCode {}
/**
diff --git a/android/app/timezone/RulesManager.java b/android/app/timezone/RulesManager.java
index ad9b698a..0a38eb9a 100644
--- a/android/app/timezone/RulesManager.java
+++ b/android/app/timezone/RulesManager.java
@@ -69,7 +69,11 @@ public final class RulesManager {
private static final boolean DEBUG = false;
@Retention(RetentionPolicy.SOURCE)
- @IntDef({SUCCESS, ERROR_UNKNOWN_FAILURE, ERROR_OPERATION_IN_PROGRESS})
+ @IntDef(prefix = { "SUCCESS", "ERROR_" }, value = {
+ SUCCESS,
+ ERROR_UNKNOWN_FAILURE,
+ ERROR_OPERATION_IN_PROGRESS
+ })
public @interface ResultCode {}
/**
@@ -105,9 +109,9 @@ public final class RulesManager {
*/
public RulesState getRulesState() {
try {
- logDebug("sIRulesManager.getRulesState()");
+ logDebug("mIRulesManager.getRulesState()");
RulesState rulesState = mIRulesManager.getRulesState();
- logDebug("sIRulesManager.getRulesState() returned " + rulesState);
+ logDebug("mIRulesManager.getRulesState() returned " + rulesState);
return rulesState;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -131,7 +135,7 @@ public final class RulesManager {
ICallback iCallback = new CallbackWrapper(mContext, callback);
try {
- logDebug("sIRulesManager.requestInstall()");
+ logDebug("mIRulesManager.requestInstall()");
return mIRulesManager.requestInstall(distroFileDescriptor, checkToken, iCallback);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -151,7 +155,7 @@ public final class RulesManager {
public int requestUninstall(byte[] checkToken, Callback callback) {
ICallback iCallback = new CallbackWrapper(mContext, callback);
try {
- logDebug("sIRulesManager.requestUninstall()");
+ logDebug("mIRulesManager.requestUninstall()");
return mIRulesManager.requestUninstall(checkToken, iCallback);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -196,7 +200,7 @@ public final class RulesManager {
*/
public void requestNothing(byte[] checkToken, boolean succeeded) {
try {
- logDebug("sIRulesManager.requestNothing() with token=" + Arrays.toString(checkToken));
+ logDebug("mIRulesManager.requestNothing() with token=" + Arrays.toString(checkToken));
mIRulesManager.requestNothing(checkToken, succeeded);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
diff --git a/android/app/timezone/RulesState.java b/android/app/timezone/RulesState.java
index ec247ebf..16309fab 100644
--- a/android/app/timezone/RulesState.java
+++ b/android/app/timezone/RulesState.java
@@ -63,11 +63,12 @@ import java.lang.annotation.RetentionPolicy;
public final class RulesState implements Parcelable {
@Retention(RetentionPolicy.SOURCE)
- @IntDef({
+ @IntDef(prefix = { "STAGED_OPERATION_" }, value = {
STAGED_OPERATION_UNKNOWN,
STAGED_OPERATION_NONE,
STAGED_OPERATION_UNINSTALL,
- STAGED_OPERATION_INSTALL })
+ STAGED_OPERATION_INSTALL
+ })
private @interface StagedOperationType {}
/** Staged state could not be determined. */
@@ -80,10 +81,11 @@ public final class RulesState implements Parcelable {
public static final int STAGED_OPERATION_INSTALL = 3;
@Retention(RetentionPolicy.SOURCE)
- @IntDef({
+ @IntDef(prefix = { "DISTRO_STATUS_" }, value = {
DISTRO_STATUS_UNKNOWN,
DISTRO_STATUS_NONE,
- DISTRO_STATUS_INSTALLED })
+ DISTRO_STATUS_INSTALLED
+ })
private @interface DistroStatus {}
/** The current distro status could not be determined. */
diff --git a/android/app/usage/AppStandby.java b/android/app/usage/AppStandby.java
deleted file mode 100644
index 6f9fc2fa..00000000
--- a/android/app/usage/AppStandby.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.app.usage;
-
-import android.annotation.IntDef;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Set of constants for app standby buckets and reasons. Apps will be moved into different buckets
- * that affect how frequently they can run in the background or perform other battery-consuming
- * actions. Buckets will be assigned based on how frequently or when the system thinks the user
- * is likely to use the app.
- * @hide
- */
-public class AppStandby {
-
- /** The app was used very recently, currently in use or likely to be used very soon. */
- public static final int STANDBY_BUCKET_ACTIVE = 0;
-
- // Leave some gap in case we want to increase the number of buckets
-
- /** The app was used recently and/or likely to be used in the next few hours */
- public static final int STANDBY_BUCKET_WORKING_SET = 3;
-
- // Leave some gap in case we want to increase the number of buckets
-
- /** The app was used in the last few days and/or likely to be used in the next few days */
- public static final int STANDBY_BUCKET_FREQUENT = 6;
-
- // Leave some gap in case we want to increase the number of buckets
-
- /** The app has not be used for several days and/or is unlikely to be used for several days */
- public static final int STANDBY_BUCKET_RARE = 9;
-
- // Leave some gap in case we want to increase the number of buckets
-
- /** The app has never been used. */
- public static final int STANDBY_BUCKET_NEVER = 12;
-
- /** Reason for bucketing -- default initial state */
- public static final String REASON_DEFAULT = "default";
-
- /** Reason for bucketing -- timeout */
- public static final String REASON_TIMEOUT = "timeout";
-
- /** Reason for bucketing -- usage */
- public static final String REASON_USAGE = "usage";
-
- /** Reason for bucketing -- forced by user / shell command */
- public static final String REASON_FORCED = "forced";
-
- /**
- * Reason for bucketing -- predicted. This is a prefix and the UID of the bucketeer will
- * be appended.
- */
- public static final String REASON_PREDICTED = "predicted";
-
- @IntDef(flag = false, value = {
- STANDBY_BUCKET_ACTIVE,
- STANDBY_BUCKET_WORKING_SET,
- STANDBY_BUCKET_FREQUENT,
- STANDBY_BUCKET_RARE,
- STANDBY_BUCKET_NEVER,
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface StandbyBuckets {}
-}
diff --git a/android/app/usage/NetworkStats.java b/android/app/usage/NetworkStats.java
index 222e9a0e..2e44a630 100644
--- a/android/app/usage/NetworkStats.java
+++ b/android/app/usage/NetworkStats.java
@@ -129,7 +129,11 @@ public final class NetworkStats implements AutoCloseable {
*/
public static class Bucket {
/** @hide */
- @IntDef({STATE_ALL, STATE_DEFAULT, STATE_FOREGROUND})
+ @IntDef(prefix = { "STATE_" }, value = {
+ STATE_ALL,
+ STATE_DEFAULT,
+ STATE_FOREGROUND
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface State {}
@@ -164,7 +168,11 @@ public final class NetworkStats implements AutoCloseable {
public static final int UID_TETHERING = TrafficStats.UID_TETHERING;
/** @hide */
- @IntDef({METERED_ALL, METERED_NO, METERED_YES})
+ @IntDef(prefix = { "METERED_" }, value = {
+ METERED_ALL,
+ METERED_NO,
+ METERED_YES
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface Metered {}
@@ -187,7 +195,11 @@ public final class NetworkStats implements AutoCloseable {
public static final int METERED_YES = 0x2;
/** @hide */
- @IntDef({ROAMING_ALL, ROAMING_NO, ROAMING_YES})
+ @IntDef(prefix = { "ROAMING_" }, value = {
+ ROAMING_ALL,
+ ROAMING_NO,
+ ROAMING_YES
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface Roaming {}
diff --git a/android/app/usage/StorageStatsManager.java b/android/app/usage/StorageStatsManager.java
index 3d187ec7..a86c27a0 100644
--- a/android/app/usage/StorageStatsManager.java
+++ b/android/app/usage/StorageStatsManager.java
@@ -78,6 +78,16 @@ public class StorageStatsManager {
return isQuotaSupported(convert(uuid));
}
+ /** {@hide} */
+ @TestApi
+ public boolean isReservedSupported(@NonNull UUID storageUuid) {
+ try {
+ return mService.isReservedSupported(convert(storageUuid), mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
/**
* Return the total size of the underlying physical media that is hosting
* this storage volume.
diff --git a/android/app/usage/UsageEvents.java b/android/app/usage/UsageEvents.java
index 8200414f..f04e9074 100644
--- a/android/app/usage/UsageEvents.java
+++ b/android/app/usage/UsageEvents.java
@@ -110,10 +110,9 @@ public final class UsageEvents implements Parcelable {
public static final int FLAG_IS_PACKAGE_INSTANT_APP = 1 << 0;
/** @hide */
- @IntDef(flag = true,
- value = {
- FLAG_IS_PACKAGE_INSTANT_APP,
- })
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
+ FLAG_IS_PACKAGE_INSTANT_APP,
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface EventFlags {}
diff --git a/android/app/usage/UsageStatsManager.java b/android/app/usage/UsageStatsManager.java
index 3a3e16e0..edb6a74b 100644
--- a/android/app/usage/UsageStatsManager.java
+++ b/android/app/usage/UsageStatsManager.java
@@ -16,16 +16,18 @@
package android.app.usage;
+import android.annotation.IntDef;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
-import android.app.usage.AppStandby.StandbyBuckets;
import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -90,6 +92,76 @@ public final class UsageStatsManager {
*/
public static final int INTERVAL_COUNT = 4;
+
+ /**
+ * The app is whitelisted for some reason and the bucket cannot be changed.
+ * {@hide}
+ */
+ @SystemApi
+ public static final int STANDBY_BUCKET_EXEMPTED = 5;
+
+ /**
+ * The app was used very recently, currently in use or likely to be used very soon.
+ * @see #getAppStandbyBucket()
+ */
+ public static final int STANDBY_BUCKET_ACTIVE = 10;
+
+ /**
+ * The app was used recently and/or likely to be used in the next few hours.
+ * @see #getAppStandbyBucket()
+ */
+ public static final int STANDBY_BUCKET_WORKING_SET = 20;
+
+ /**
+ * The app was used in the last few days and/or likely to be used in the next few days.
+ * @see #getAppStandbyBucket()
+ */
+ public static final int STANDBY_BUCKET_FREQUENT = 30;
+
+ /**
+ * The app has not be used for several days and/or is unlikely to be used for several days.
+ * @see #getAppStandbyBucket()
+ */
+ public static final int STANDBY_BUCKET_RARE = 40;
+
+ /**
+ * The app has never been used.
+ * {@hide}
+ */
+ @SystemApi
+ public static final int STANDBY_BUCKET_NEVER = 50;
+
+ /** {@hide} Reason for bucketing -- default initial state */
+ public static final String REASON_DEFAULT = "default";
+
+ /** {@hide} Reason for bucketing -- timeout */
+ public static final String REASON_TIMEOUT = "timeout";
+
+ /** {@hide} Reason for bucketing -- usage */
+ public static final String REASON_USAGE = "usage";
+
+ /** {@hide} Reason for bucketing -- forced by user / shell command */
+ public static final String REASON_FORCED = "forced";
+
+ /**
+ * {@hide}
+ * Reason for bucketing -- predicted. This is a prefix and the UID of the bucketeer will
+ * be appended.
+ */
+ public static final String REASON_PREDICTED = "predicted";
+
+ /** @hide */
+ @IntDef(flag = false, prefix = { "STANDBY_BUCKET_" }, value = {
+ STANDBY_BUCKET_EXEMPTED,
+ STANDBY_BUCKET_ACTIVE,
+ STANDBY_BUCKET_WORKING_SET,
+ STANDBY_BUCKET_FREQUENT,
+ STANDBY_BUCKET_RARE,
+ STANDBY_BUCKET_NEVER,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StandbyBuckets {}
+
private static final UsageEvents sEmptyResults = new UsageEvents();
private final Context mContext;
@@ -237,7 +309,7 @@ public final class UsageStatsManager {
}
/**
- * @hide
+ * {@hide}
*/
public void setAppInactive(String packageName, boolean inactive) {
try {
@@ -248,20 +320,52 @@ public final class UsageStatsManager {
}
/**
- * @hide
+ * Returns the current standby bucket of the calling app. The system determines the standby
+ * state of the app based on app usage patterns. Standby buckets determine how much an app will
+ * be restricted from running background tasks such as jobs, alarms and certain PendingIntent
+ * callbacks.
+ * <p>Restrictions increase progressively from {@link #STANDBY_BUCKET_ACTIVE} to
+ * {@link #STANDBY_BUCKET_RARE}, with {@link #STANDBY_BUCKET_ACTIVE} being the least
+ * restrictive. The battery level of the device might also affect the restrictions.
+ *
+ * @return the current standby bucket of the calling app. One of STANDBY_BUCKET_* constants.
*/
+ public @StandbyBuckets int getAppStandbyBucket() {
+ try {
+ return mService.getAppStandbyBucket(mContext.getOpPackageName(),
+ mContext.getOpPackageName(),
+ mContext.getUserId());
+ } catch (RemoteException e) {
+ }
+ return STANDBY_BUCKET_ACTIVE;
+ }
+
+ /**
+ * {@hide}
+ * Returns the current standby bucket of the specified app. The caller must hold the permission
+ * android.permission.PACKAGE_USAGE_STATS.
+ * @param packageName the package for which to fetch the current standby bucket.
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)
public @StandbyBuckets int getAppStandbyBucket(String packageName) {
try {
return mService.getAppStandbyBucket(packageName, mContext.getOpPackageName(),
mContext.getUserId());
} catch (RemoteException e) {
}
- return AppStandby.STANDBY_BUCKET_ACTIVE;
+ return STANDBY_BUCKET_ACTIVE;
}
/**
- * @hide
- * Changes the app standby state to the provided bucket.
+ * {@hide}
+ * Changes an app's standby bucket to the provided value. The caller can only set the standby
+ * bucket for a different app than itself.
+ * @param packageName the package name of the app to set the bucket for. A SecurityException
+ * will be thrown if the package name is that of the caller.
+ * @param bucket the standby bucket to set it to, which should be one of STANDBY_BUCKET_*.
+ * Setting a standby bucket outside of the range of STANDBY_BUCKET_ACTIVE to
+ * STANDBY_BUCKET_NEVER will result in a SecurityException.
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.CHANGE_APP_IDLE_STATE)
@@ -275,6 +379,39 @@ public final class UsageStatsManager {
/**
* {@hide}
+ * Returns the current standby bucket of every app that has a bucket assigned to it.
+ * The caller must hold the permission android.permission.PACKAGE_USAGE_STATS. The key of the
+ * returned Map is the package name and the value is the bucket assigned to the package.
+ * @see #getAppStandbyBucket()
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)
+ public Map<String, Integer> getAppStandbyBuckets() {
+ try {
+ return (Map<String, Integer>) mService.getAppStandbyBuckets(
+ mContext.getOpPackageName(), mContext.getUserId());
+ } catch (RemoteException e) {
+ }
+ return Collections.EMPTY_MAP;
+ }
+
+ /**
+ * {@hide}
+ * Changes the app standby bucket for multiple apps at once. The Map is keyed by the package
+ * name and the value is one of STANDBY_BUCKET_*.
+ * @param appBuckets a map of package name to bucket value.
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.CHANGE_APP_IDLE_STATE)
+ public void setAppStandbyBuckets(Map<String, Integer> appBuckets) {
+ try {
+ mService.setAppStandbyBuckets(appBuckets, mContext.getUserId());
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * {@hide}
* Temporarily whitelist the specified app for a short duration. This is to allow an app
* receiving a high priority message to be able to access the network and acquire wakelocks
* even if the device is in power-save mode or the app is currently considered inactive.
diff --git a/android/app/usage/UsageStatsManagerInternal.java b/android/app/usage/UsageStatsManagerInternal.java
index 9954484f..4b4fe72f 100644
--- a/android/app/usage/UsageStatsManagerInternal.java
+++ b/android/app/usage/UsageStatsManagerInternal.java
@@ -16,7 +16,7 @@
package android.app.usage;
-import android.app.usage.AppStandby.StandbyBuckets;
+import android.app.usage.UsageStatsManager.StandbyBuckets;
import android.content.ComponentName;
import android.content.res.Configuration;
diff --git a/android/appwidget/AppWidgetManagerInternal.java b/android/appwidget/AppWidgetManagerInternal.java
new file mode 100644
index 00000000..7ab3d8bd
--- /dev/null
+++ b/android/appwidget/AppWidgetManagerInternal.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.appwidget;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArraySet;
+
+import java.util.Set;
+
+/**
+ * App widget manager local system service interface.
+ *
+ * @hide Only for use within the system server.
+ */
+public abstract class AppWidgetManagerInternal {
+
+ /**
+ * Gets the packages from which the uid hosts widgets.
+ *
+ * @param uid The potential host UID.
+ * @return Whether the UID hosts widgets from the package.
+ */
+ public abstract @Nullable ArraySet<String> getHostedWidgetPackages(int uid);
+}
diff --git a/android/appwidget/AppWidgetProviderInfo.java b/android/appwidget/AppWidgetProviderInfo.java
index fd1b0e02..75ce4fbb 100644
--- a/android/appwidget/AppWidgetProviderInfo.java
+++ b/android/appwidget/AppWidgetProviderInfo.java
@@ -17,15 +17,17 @@
package android.appwidget;
import android.annotation.NonNull;
+import android.app.PendingIntent;
+import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.ResourceId;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
+import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
-import android.content.ComponentName;
import android.os.UserHandle;
import android.util.DisplayMetrics;
import android.util.TypedValue;
@@ -69,6 +71,23 @@ public class AppWidgetProviderInfo implements Parcelable {
public static final int WIDGET_CATEGORY_SEARCHBOX = 4;
/**
+ * The widget can be reconfigured anytime after it is bound by starting the
+ * {@link #configure} activity.
+ *
+ * @see #widgetFeatures
+ */
+ public static final int WIDGET_FEATURE_RECONFIGURABLE = 1;
+
+ /**
+ * The widget is added directly by the app, and the host may hide this widget when providing
+ * the user with the list of available widgets to choose from.
+ *
+ * @see AppWidgetManager#requestPinAppWidget(ComponentName, Bundle, PendingIntent)
+ * @see #widgetFeatures
+ */
+ public static final int WIDGET_FEATURE_HIDE_FROM_PICKER = 2;
+
+ /**
* Identity of this AppWidget component. This component should be a {@link
* android.content.BroadcastReceiver}, and it will be sent the AppWidget intents
* {@link android.appwidget as described in the AppWidget package documentation}.
@@ -209,6 +228,15 @@ public class AppWidgetProviderInfo implements Parcelable {
*/
public int widgetCategory;
+ /**
+ * Flags indicating various features supported by the widget. These are hints to the widget
+ * host, and do not actually change the behavior of the widget.
+ *
+ * @see #WIDGET_FEATURE_RECONFIGURABLE
+ * @see #WIDGET_FEATURE_HIDE_FROM_PICKER
+ */
+ public int widgetFeatures;
+
/** @hide */
public ActivityInfo providerInfo;
@@ -221,9 +249,7 @@ public class AppWidgetProviderInfo implements Parcelable {
*/
@SuppressWarnings("deprecation")
public AppWidgetProviderInfo(Parcel in) {
- if (0 != in.readInt()) {
- this.provider = new ComponentName(in);
- }
+ this.provider = in.readTypedObject(ComponentName.CREATOR);
this.minWidth = in.readInt();
this.minHeight = in.readInt();
this.minResizeWidth = in.readInt();
@@ -231,16 +257,15 @@ public class AppWidgetProviderInfo implements Parcelable {
this.updatePeriodMillis = in.readInt();
this.initialLayout = in.readInt();
this.initialKeyguardLayout = in.readInt();
- if (0 != in.readInt()) {
- this.configure = new ComponentName(in);
- }
+ this.configure = in.readTypedObject(ComponentName.CREATOR);
this.label = in.readString();
this.icon = in.readInt();
this.previewImage = in.readInt();
this.autoAdvanceViewId = in.readInt();
this.resizeMode = in.readInt();
this.widgetCategory = in.readInt();
- this.providerInfo = in.readParcelable(null);
+ this.providerInfo = in.readTypedObject(ActivityInfo.CREATOR);
+ this.widgetFeatures = in.readInt();
}
/**
@@ -308,13 +333,8 @@ public class AppWidgetProviderInfo implements Parcelable {
@Override
@SuppressWarnings("deprecation")
- public void writeToParcel(android.os.Parcel out, int flags) {
- if (this.provider != null) {
- out.writeInt(1);
- this.provider.writeToParcel(out, flags);
- } else {
- out.writeInt(0);
- }
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeTypedObject(this.provider, flags);
out.writeInt(this.minWidth);
out.writeInt(this.minHeight);
out.writeInt(this.minResizeWidth);
@@ -322,19 +342,15 @@ public class AppWidgetProviderInfo implements Parcelable {
out.writeInt(this.updatePeriodMillis);
out.writeInt(this.initialLayout);
out.writeInt(this.initialKeyguardLayout);
- if (this.configure != null) {
- out.writeInt(1);
- this.configure.writeToParcel(out, flags);
- } else {
- out.writeInt(0);
- }
+ out.writeTypedObject(this.configure, flags);
out.writeString(this.label);
out.writeInt(this.icon);
out.writeInt(this.previewImage);
out.writeInt(this.autoAdvanceViewId);
out.writeInt(this.resizeMode);
out.writeInt(this.widgetCategory);
- out.writeParcelable(this.providerInfo, flags);
+ out.writeTypedObject(this.providerInfo, flags);
+ out.writeInt(this.widgetFeatures);
}
@Override
@@ -357,6 +373,7 @@ public class AppWidgetProviderInfo implements Parcelable {
that.resizeMode = this.resizeMode;
that.widgetCategory = this.widgetCategory;
that.providerInfo = this.providerInfo;
+ that.widgetFeatures = this.widgetFeatures;
return that;
}
diff --git a/android/arch/core/executor/ArchTaskExecutor.java b/android/arch/core/executor/ArchTaskExecutor.java
index 2401a730..6276ee34 100644
--- a/android/arch/core/executor/ArchTaskExecutor.java
+++ b/android/arch/core/executor/ArchTaskExecutor.java
@@ -64,6 +64,7 @@ public class ArchTaskExecutor extends TaskExecutor {
*
* @return The singleton ArchTaskExecutor.
*/
+ @NonNull
public static ArchTaskExecutor getInstance() {
if (sInstance != null) {
return sInstance;
diff --git a/android/arch/core/executor/TaskExecutor.java b/android/arch/core/executor/TaskExecutor.java
index 055b4763..71758019 100644
--- a/android/arch/core/executor/TaskExecutor.java
+++ b/android/arch/core/executor/TaskExecutor.java
@@ -16,6 +16,7 @@
package android.arch.core.executor;
+import android.support.annotation.NonNull;
import android.support.annotation.RestrictTo;
/**
@@ -33,14 +34,14 @@ public abstract class TaskExecutor {
*
* @param runnable The runnable to run in the disk IO thread pool.
*/
- public abstract void executeOnDiskIO(Runnable runnable);
+ public abstract void executeOnDiskIO(@NonNull Runnable runnable);
/**
* Posts the given task to the main thread.
*
* @param runnable The runnable to run on the main thread.
*/
- public abstract void postToMainThread(Runnable runnable);
+ public abstract void postToMainThread(@NonNull Runnable runnable);
/**
* Executes the given task on the main thread.
@@ -49,7 +50,7 @@ public abstract class TaskExecutor {
*
* @param runnable The runnable to run on the main thread.
*/
- public void executeOnMainThread(Runnable runnable) {
+ public void executeOnMainThread(@NonNull Runnable runnable) {
if (isMainThread()) {
runnable.run();
} else {
diff --git a/android/arch/lifecycle/ComputableLiveData.java b/android/arch/lifecycle/ComputableLiveData.java
index f1352446..1ddcb1a9 100644
--- a/android/arch/lifecycle/ComputableLiveData.java
+++ b/android/arch/lifecycle/ComputableLiveData.java
@@ -1,9 +1,136 @@
-//ComputableLiveData interface for tests
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package android.arch.lifecycle;
-import android.arch.lifecycle.LiveData;
+
+import android.arch.core.executor.ArchTaskExecutor;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A LiveData class that can be invalidated & computed on demand.
+ * <p>
+ * This is an internal class for now, might be public if we see the necessity.
+ *
+ * @param <T> The type of the live data
+ * @hide internal
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract class ComputableLiveData<T> {
- public ComputableLiveData(){}
- abstract protected T compute();
- public LiveData<T> getLiveData() {return null;}
- public void invalidate() {}
+
+ private final LiveData<T> mLiveData;
+
+ private AtomicBoolean mInvalid = new AtomicBoolean(true);
+ private AtomicBoolean mComputing = new AtomicBoolean(false);
+
+ /**
+ * Creates a computable live data which is computed when there are active observers.
+ * <p>
+ * It can also be invalidated via {@link #invalidate()} which will result in a call to
+ * {@link #compute()} if there are active observers (or when they start observing)
+ */
+ @SuppressWarnings("WeakerAccess")
+ public ComputableLiveData() {
+ mLiveData = new LiveData<T>() {
+ @Override
+ protected void onActive() {
+ // TODO if we make this class public, we should accept an executor
+ ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable);
+ }
+ };
+ }
+
+ /**
+ * Returns the LiveData managed by this class.
+ *
+ * @return A LiveData that is controlled by ComputableLiveData.
+ */
+ @SuppressWarnings("WeakerAccess")
+ @NonNull
+ public LiveData<T> getLiveData() {
+ return mLiveData;
+ }
+
+ @VisibleForTesting
+ final Runnable mRefreshRunnable = new Runnable() {
+ @WorkerThread
+ @Override
+ public void run() {
+ boolean computed;
+ do {
+ computed = false;
+ // compute can happen only in 1 thread but no reason to lock others.
+ if (mComputing.compareAndSet(false, true)) {
+ // as long as it is invalid, keep computing.
+ try {
+ T value = null;
+ while (mInvalid.compareAndSet(true, false)) {
+ computed = true;
+ value = compute();
+ }
+ if (computed) {
+ mLiveData.postValue(value);
+ }
+ } finally {
+ // release compute lock
+ mComputing.set(false);
+ }
+ }
+ // check invalid after releasing compute lock to avoid the following scenario.
+ // Thread A runs compute()
+ // Thread A checks invalid, it is false
+ // Main thread sets invalid to true
+ // Thread B runs, fails to acquire compute lock and skips
+ // Thread A releases compute lock
+ // We've left invalid in set state. The check below recovers.
+ } while (computed && mInvalid.get());
+ }
+ };
+
+ // invalidation check always happens on the main thread
+ @VisibleForTesting
+ final Runnable mInvalidationRunnable = new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ boolean isActive = mLiveData.hasActiveObservers();
+ if (mInvalid.compareAndSet(false, true)) {
+ if (isActive) {
+ // TODO if we make this class public, we should accept an executor.
+ ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable);
+ }
+ }
+ }
+ };
+
+ /**
+ * Invalidates the LiveData.
+ * <p>
+ * When there are active observers, this will trigger a call to {@link #compute()}.
+ */
+ public void invalidate() {
+ ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @WorkerThread
+ protected abstract T compute();
}
diff --git a/android/arch/lifecycle/Lifecycle.java b/android/arch/lifecycle/Lifecycle.java
index c0a2090c..e04cdb4c 100644
--- a/android/arch/lifecycle/Lifecycle.java
+++ b/android/arch/lifecycle/Lifecycle.java
@@ -108,6 +108,7 @@ public abstract class Lifecycle {
* @return The current state of the Lifecycle.
*/
@MainThread
+ @NonNull
public abstract State getCurrentState();
@SuppressWarnings("WeakerAccess")
diff --git a/android/arch/lifecycle/LifecycleRegistry.java b/android/arch/lifecycle/LifecycleRegistry.java
index bf8aff79..eff946b2 100644
--- a/android/arch/lifecycle/LifecycleRegistry.java
+++ b/android/arch/lifecycle/LifecycleRegistry.java
@@ -225,6 +225,7 @@ public class LifecycleRegistry extends Lifecycle {
return mObserverMap.size();
}
+ @NonNull
@Override
public State getCurrentState() {
return mState;
diff --git a/android/arch/lifecycle/LiveData.java b/android/arch/lifecycle/LiveData.java
index 3aea6acb..5b09c32f 100644
--- a/android/arch/lifecycle/LiveData.java
+++ b/android/arch/lifecycle/LiveData.java
@@ -1,4 +1,410 @@
-//LiveData interface for tests
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package android.arch.lifecycle;
-public class LiveData<T> {
+
+import static android.arch.lifecycle.Lifecycle.State.DESTROYED;
+import static android.arch.lifecycle.Lifecycle.State.STARTED;
+
+import android.arch.core.executor.ArchTaskExecutor;
+import android.arch.core.internal.SafeIterableMap;
+import android.arch.lifecycle.Lifecycle.State;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * LiveData is a data holder class that can be observed within a given lifecycle.
+ * This means that an {@link Observer} can be added in a pair with a {@link LifecycleOwner}, and
+ * this observer will be notified about modifications of the wrapped data only if the paired
+ * LifecycleOwner is in active state. LifecycleOwner is considered as active, if its state is
+ * {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED}. An observer added via
+ * {@link #observeForever(Observer)} is considered as always active and thus will be always notified
+ * about modifications. For those observers, you should manually call
+ * {@link #removeObserver(Observer)}.
+ *
+ * <p> An observer added with a Lifecycle will be automatically removed if the corresponding
+ * Lifecycle moves to {@link Lifecycle.State#DESTROYED} state. This is especially useful for
+ * activities and fragments where they can safely observe LiveData and not worry about leaks:
+ * they will be instantly unsubscribed when they are destroyed.
+ *
+ * <p>
+ * In addition, LiveData has {@link LiveData#onActive()} and {@link LiveData#onInactive()} methods
+ * to get notified when number of active {@link Observer}s change between 0 and 1.
+ * This allows LiveData to release any heavy resources when it does not have any Observers that
+ * are actively observing.
+ * <p>
+ * This class is designed to hold individual data fields of {@link ViewModel},
+ * but can also be used for sharing data between different modules in your application
+ * in a decoupled fashion.
+ *
+ * @param <T> The type of data held by this instance
+ * @see ViewModel
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+// TODO: Thread checks are too strict right now, we may consider automatically moving them to main
+// thread.
+public abstract class LiveData<T> {
+ private final Object mDataLock = new Object();
+ static final int START_VERSION = -1;
+ private static final Object NOT_SET = new Object();
+
+ private static final LifecycleOwner ALWAYS_ON = new LifecycleOwner() {
+
+ private LifecycleRegistry mRegistry = init();
+
+ private LifecycleRegistry init() {
+ LifecycleRegistry registry = new LifecycleRegistry(this);
+ registry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
+ registry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
+ return registry;
+ }
+
+ @Override
+ public Lifecycle getLifecycle() {
+ return mRegistry;
+ }
+ };
+
+ private SafeIterableMap<Observer<T>, LifecycleBoundObserver> mObservers =
+ new SafeIterableMap<>();
+
+ // how many observers are in active state
+ private int mActiveCount = 0;
+ private volatile Object mData = NOT_SET;
+ // when setData is called, we set the pending data and actual data swap happens on the main
+ // thread
+ private volatile Object mPendingData = NOT_SET;
+ private int mVersion = START_VERSION;
+
+ private boolean mDispatchingValue;
+ @SuppressWarnings("FieldCanBeLocal")
+ private boolean mDispatchInvalidated;
+ private final Runnable mPostValueRunnable = new Runnable() {
+ @Override
+ public void run() {
+ Object newValue;
+ synchronized (mDataLock) {
+ newValue = mPendingData;
+ mPendingData = NOT_SET;
+ }
+ //noinspection unchecked
+ setValue((T) newValue);
+ }
+ };
+
+ private void considerNotify(LifecycleBoundObserver observer) {
+ if (!observer.active) {
+ return;
+ }
+ // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
+ //
+ // we still first check observer.active to keep it as the entrance for events. So even if
+ // the observer moved to an active state, if we've not received that event, we better not
+ // notify for a more predictable notification order.
+ if (!isActiveState(observer.owner.getLifecycle().getCurrentState())) {
+ observer.activeStateChanged(false);
+ return;
+ }
+ if (observer.lastVersion >= mVersion) {
+ return;
+ }
+ observer.lastVersion = mVersion;
+ //noinspection unchecked
+ observer.observer.onChanged((T) mData);
+ }
+
+ private void dispatchingValue(@Nullable LifecycleBoundObserver initiator) {
+ if (mDispatchingValue) {
+ mDispatchInvalidated = true;
+ return;
+ }
+ mDispatchingValue = true;
+ do {
+ mDispatchInvalidated = false;
+ if (initiator != null) {
+ considerNotify(initiator);
+ initiator = null;
+ } else {
+ for (Iterator<Map.Entry<Observer<T>, LifecycleBoundObserver>> iterator =
+ mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
+ considerNotify(iterator.next().getValue());
+ if (mDispatchInvalidated) {
+ break;
+ }
+ }
+ }
+ } while (mDispatchInvalidated);
+ mDispatchingValue = false;
+ }
+
+ /**
+ * Adds the given observer to the observers list within the lifespan of the given
+ * owner. The events are dispatched on the main thread. If LiveData already has data
+ * set, it will be delivered to the observer.
+ * <p>
+ * The observer will only receive events if the owner is in {@link Lifecycle.State#STARTED}
+ * or {@link Lifecycle.State#RESUMED} state (active).
+ * <p>
+ * If the owner moves to the {@link Lifecycle.State#DESTROYED} state, the observer will
+ * automatically be removed.
+ * <p>
+ * When data changes while the {@code owner} is not active, it will not receive any updates.
+ * If it becomes active again, it will receive the last available data automatically.
+ * <p>
+ * LiveData keeps a strong reference to the observer and the owner as long as the
+ * given LifecycleOwner is not destroyed. When it is destroyed, LiveData removes references to
+ * the observer &amp; the owner.
+ * <p>
+ * If the given owner is already in {@link Lifecycle.State#DESTROYED} state, LiveData
+ * ignores the call.
+ * <p>
+ * If the given owner, observer tuple is already in the list, the call is ignored.
+ * If the observer is already in the list with another owner, LiveData throws an
+ * {@link IllegalArgumentException}.
+ *
+ * @param owner The LifecycleOwner which controls the observer
+ * @param observer The observer that will receive the events
+ */
+ @MainThread
+ public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) {
+ if (owner.getLifecycle().getCurrentState() == DESTROYED) {
+ // ignore
+ return;
+ }
+ LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
+ LifecycleBoundObserver existing = mObservers.putIfAbsent(observer, wrapper);
+ if (existing != null && existing.owner != wrapper.owner) {
+ throw new IllegalArgumentException("Cannot add the same observer"
+ + " with different lifecycles");
+ }
+ if (existing != null) {
+ return;
+ }
+ owner.getLifecycle().addObserver(wrapper);
+ }
+
+ /**
+ * Adds the given observer to the observers list. This call is similar to
+ * {@link LiveData#observe(LifecycleOwner, Observer)} with a LifecycleOwner, which
+ * is always active. This means that the given observer will receive all events and will never
+ * be automatically removed. You should manually call {@link #removeObserver(Observer)} to stop
+ * observing this LiveData.
+ * While LiveData has one of such observers, it will be considered
+ * as active.
+ * <p>
+ * If the observer was already added with an owner to this LiveData, LiveData throws an
+ * {@link IllegalArgumentException}.
+ *
+ * @param observer The observer that will receive the events
+ */
+ @MainThread
+ public void observeForever(@NonNull Observer<T> observer) {
+ observe(ALWAYS_ON, observer);
+ }
+
+ /**
+ * Removes the given observer from the observers list.
+ *
+ * @param observer The Observer to receive events.
+ */
+ @MainThread
+ public void removeObserver(@NonNull final Observer<T> observer) {
+ assertMainThread("removeObserver");
+ LifecycleBoundObserver removed = mObservers.remove(observer);
+ if (removed == null) {
+ return;
+ }
+ removed.owner.getLifecycle().removeObserver(removed);
+ removed.activeStateChanged(false);
+ }
+
+ /**
+ * Removes all observers that are tied to the given {@link LifecycleOwner}.
+ *
+ * @param owner The {@code LifecycleOwner} scope for the observers to be removed.
+ */
+ @MainThread
+ public void removeObservers(@NonNull final LifecycleOwner owner) {
+ assertMainThread("removeObservers");
+ for (Map.Entry<Observer<T>, LifecycleBoundObserver> entry : mObservers) {
+ if (entry.getValue().owner == owner) {
+ removeObserver(entry.getKey());
+ }
+ }
+ }
+
+ /**
+ * Posts a task to a main thread to set the given value. So if you have a following code
+ * executed in the main thread:
+ * <pre class="prettyprint">
+ * liveData.postValue("a");
+ * liveData.setValue("b");
+ * </pre>
+ * The value "b" would be set at first and later the main thread would override it with
+ * the value "a".
+ * <p>
+ * If you called this method multiple times before a main thread executed a posted task, only
+ * the last value would be dispatched.
+ *
+ * @param value The new value
+ */
+ protected void postValue(T value) {
+ boolean postTask;
+ synchronized (mDataLock) {
+ postTask = mPendingData == NOT_SET;
+ mPendingData = value;
+ }
+ if (!postTask) {
+ return;
+ }
+ ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
+ }
+
+ /**
+ * Sets the value. If there are active observers, the value will be dispatched to them.
+ * <p>
+ * This method must be called from the main thread. If you need set a value from a background
+ * thread, you can use {@link #postValue(Object)}
+ *
+ * @param value The new value
+ */
+ @MainThread
+ protected void setValue(T value) {
+ assertMainThread("setValue");
+ mVersion++;
+ mData = value;
+ dispatchingValue(null);
+ }
+
+ /**
+ * Returns the current value.
+ * Note that calling this method on a background thread does not guarantee that the latest
+ * value set will be received.
+ *
+ * @return the current value
+ */
+ @Nullable
+ public T getValue() {
+ Object data = mData;
+ if (data != NOT_SET) {
+ //noinspection unchecked
+ return (T) data;
+ }
+ return null;
+ }
+
+ int getVersion() {
+ return mVersion;
+ }
+
+ /**
+ * Called when the number of active observers change to 1 from 0.
+ * <p>
+ * This callback can be used to know that this LiveData is being used thus should be kept
+ * up to date.
+ */
+ protected void onActive() {
+
+ }
+
+ /**
+ * Called when the number of active observers change from 1 to 0.
+ * <p>
+ * This does not mean that there are no observers left, there may still be observers but their
+ * lifecycle states aren't {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED}
+ * (like an Activity in the back stack).
+ * <p>
+ * You can check if there are observers via {@link #hasObservers()}.
+ */
+ protected void onInactive() {
+
+ }
+
+ /**
+ * Returns true if this LiveData has observers.
+ *
+ * @return true if this LiveData has observers
+ */
+ public boolean hasObservers() {
+ return mObservers.size() > 0;
+ }
+
+ /**
+ * Returns true if this LiveData has active observers.
+ *
+ * @return true if this LiveData has active observers
+ */
+ public boolean hasActiveObservers() {
+ return mActiveCount > 0;
+ }
+
+ class LifecycleBoundObserver implements GenericLifecycleObserver {
+ public final LifecycleOwner owner;
+ public final Observer<T> observer;
+ public boolean active;
+ public int lastVersion = START_VERSION;
+
+ LifecycleBoundObserver(LifecycleOwner owner, Observer<T> observer) {
+ this.owner = owner;
+ this.observer = observer;
+ }
+
+ @Override
+ public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
+ if (owner.getLifecycle().getCurrentState() == DESTROYED) {
+ removeObserver(observer);
+ return;
+ }
+ // immediately set active state, so we'd never dispatch anything to inactive
+ // owner
+ activeStateChanged(isActiveState(owner.getLifecycle().getCurrentState()));
+ }
+
+ void activeStateChanged(boolean newActive) {
+ if (newActive == active) {
+ return;
+ }
+ active = newActive;
+ boolean wasInactive = LiveData.this.mActiveCount == 0;
+ LiveData.this.mActiveCount += active ? 1 : -1;
+ if (wasInactive && active) {
+ onActive();
+ }
+ if (LiveData.this.mActiveCount == 0 && !active) {
+ onInactive();
+ }
+ if (active) {
+ dispatchingValue(this);
+ }
+ }
+ }
+
+ static boolean isActiveState(State state) {
+ return state.isAtLeast(STARTED);
+ }
+
+ private void assertMainThread(String methodName) {
+ if (!ArchTaskExecutor.getInstance().isMainThread()) {
+ throw new IllegalStateException("Cannot invoke " + methodName + " on a background"
+ + " thread");
+ }
+ }
}
diff --git a/android/arch/lifecycle/LiveDataReactiveStreams.java b/android/arch/lifecycle/LiveDataReactiveStreams.java
index ba76f8e8..ed3c57c3 100644
--- a/android/arch/lifecycle/LiveDataReactiveStreams.java
+++ b/android/arch/lifecycle/LiveDataReactiveStreams.java
@@ -50,8 +50,9 @@ public final class LiveDataReactiveStreams {
* will buffer the latest item and emit it to the subscriber when data is again requested. Any
* other items emitted during the time there was no backpressure requested will be dropped.
*/
+ @NonNull
public static <T> Publisher<T> toPublisher(
- final LifecycleOwner lifecycle, final LiveData<T> liveData) {
+ @NonNull LifecycleOwner lifecycle, @NonNull LiveData<T> liveData) {
return new LiveDataPublisher<>(lifecycle, liveData);
}
@@ -60,7 +61,7 @@ public final class LiveDataReactiveStreams {
final LifecycleOwner mLifecycle;
final LiveData<T> mLiveData;
- LiveDataPublisher(final LifecycleOwner lifecycle, final LiveData<T> liveData) {
+ LiveDataPublisher(LifecycleOwner lifecycle, LiveData<T> liveData) {
this.mLifecycle = lifecycle;
this.mLiveData = liveData;
}
@@ -91,7 +92,7 @@ public final class LiveDataReactiveStreams {
}
@Override
- public void onChanged(T t) {
+ public void onChanged(@Nullable T t) {
if (mCanceled) {
return;
}
@@ -183,7 +184,8 @@ public final class LiveDataReactiveStreams {
*
* @param <T> The type of data hold by this instance.
*/
- public static <T> LiveData<T> fromPublisher(final Publisher<T> publisher) {
+ @NonNull
+ public static <T> LiveData<T> fromPublisher(@NonNull Publisher<T> publisher) {
return new PublisherLiveData<>(publisher);
}
@@ -209,10 +211,10 @@ public final class LiveDataReactiveStreams {
* @param <T> The type of data hold by this instance.
*/
private static class PublisherLiveData<T> extends LiveData<T> {
- private final Publisher mPublisher;
+ private final Publisher<T> mPublisher;
final AtomicReference<LiveDataSubscriber> mSubscriber;
- PublisherLiveData(@NonNull final Publisher publisher) {
+ PublisherLiveData(@NonNull Publisher<T> publisher) {
mPublisher = publisher;
mSubscriber = new AtomicReference<>();
}
diff --git a/android/arch/lifecycle/LiveDataReactiveStreamsTest.java b/android/arch/lifecycle/LiveDataReactiveStreamsTest.java
index 83e543c3..163cff00 100644
--- a/android/arch/lifecycle/LiveDataReactiveStreamsTest.java
+++ b/android/arch/lifecycle/LiveDataReactiveStreamsTest.java
@@ -16,11 +16,10 @@
package android.arch.lifecycle;
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.fail;
-
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
import android.arch.core.executor.ArchTaskExecutor;
import android.arch.core.executor.TaskExecutor;
diff --git a/android/arch/lifecycle/ViewModelProviderTest.java b/android/arch/lifecycle/ViewModelProviderTest.java
index 8877357a..37d2020a 100644
--- a/android/arch/lifecycle/ViewModelProviderTest.java
+++ b/android/arch/lifecycle/ViewModelProviderTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,8 +21,6 @@ import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import android.arch.lifecycle.ViewModelProvider.NewInstanceFactory;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentActivity;
import org.junit.Assert;
import org.junit.Before;
@@ -84,18 +82,6 @@ public class ViewModelProviderTest {
assertThat(viewModel, is(provider.get(ViewModel1.class)));
}
- @Test(expected = IllegalStateException.class)
- public void testNotAttachedActivity() {
- // This is similar to call ViewModelProviders.of in Activity's constructor
- ViewModelProviders.of(new FragmentActivity());
- }
-
- @Test(expected = IllegalStateException.class)
- public void testNotAttachedFragment() {
- // This is similar to call ViewModelProviders.of in Activity's constructor
- ViewModelProviders.of(new Fragment());
- }
-
public static class ViewModel1 extends ViewModel {
boolean mCleared;
diff --git a/android/arch/lifecycle/ViewModelProvidersTest.java b/android/arch/lifecycle/ViewModelProvidersTest.java
new file mode 100644
index 00000000..f37c9a29
--- /dev/null
+++ b/android/arch/lifecycle/ViewModelProvidersTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.lifecycle;
+
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ViewModelProvidersTest {
+
+ @Test(expected = IllegalStateException.class)
+ public void testNotAttachedActivity() {
+ // This is similar to call ViewModelProviders.of in Activity's constructor
+ ViewModelProviders.of(new FragmentActivity());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testNotAttachedFragment() {
+ // This is similar to call ViewModelProviders.of in Activity's constructor
+ ViewModelProviders.of(new Fragment());
+ }
+}
diff --git a/android/arch/lifecycle/ViewModelStores.java b/android/arch/lifecycle/ViewModelStores.java
index d7d769d6..e79c934a 100644
--- a/android/arch/lifecycle/ViewModelStores.java
+++ b/android/arch/lifecycle/ViewModelStores.java
@@ -40,6 +40,9 @@ public class ViewModelStores {
*/
@MainThread
public static ViewModelStore of(@NonNull FragmentActivity activity) {
+ if (activity instanceof ViewModelStoreOwner) {
+ return ((ViewModelStoreOwner) activity).getViewModelStore();
+ }
return holderFragmentFor(activity).getViewModelStore();
}
@@ -51,6 +54,9 @@ public class ViewModelStores {
*/
@MainThread
public static ViewModelStore of(@NonNull Fragment fragment) {
+ if (fragment instanceof ViewModelStoreOwner) {
+ return ((ViewModelStoreOwner) fragment).getViewModelStore();
+ }
return holderFragmentFor(fragment).getViewModelStore();
}
}
diff --git a/android/arch/paging/ContiguousDataSource.java b/android/arch/paging/ContiguousDataSource.java
index 38b7cc04..b2e389ff 100644
--- a/android/arch/paging/ContiguousDataSource.java
+++ b/android/arch/paging/ContiguousDataSource.java
@@ -27,15 +27,27 @@ abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, Value> {
return true;
}
- abstract void loadInitial(@Nullable Key key, int initialLoadSize,
- int pageSize, boolean enablePlaceholders,
- @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<Value> receiver);
-
- abstract void loadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
- @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<Value> receiver);
-
- abstract void loadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize,
- @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<Value> receiver);
+ abstract void dispatchLoadInitial(
+ @Nullable Key key,
+ int initialLoadSize,
+ int pageSize,
+ boolean enablePlaceholders,
+ @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver);
+
+ abstract void dispatchLoadAfter(
+ int currentEndIndex,
+ @NonNull Value currentEndItem,
+ int pageSize,
+ @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver);
+
+ abstract void dispatchLoadBefore(
+ int currentBeginIndex,
+ @NonNull Value currentBeginItem,
+ int pageSize,
+ @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver);
/**
* Get the key from either the position, or item, or null if position/item invalid.
diff --git a/android/arch/paging/ContiguousPagedList.java b/android/arch/paging/ContiguousPagedList.java
index a134e440..42eb320d 100644
--- a/android/arch/paging/ContiguousPagedList.java
+++ b/android/arch/paging/ContiguousPagedList.java
@@ -87,7 +87,7 @@ class ContiguousPagedList<K, V> extends PagedList<V> implements PagedStorage.Cal
if (mDataSource.isInvalid()) {
detach();
} else {
- mDataSource.loadInitial(key,
+ mDataSource.dispatchLoadInitial(key,
mConfig.initialLoadSizeHint,
mConfig.pageSize,
mConfig.enablePlaceholders,
@@ -184,7 +184,7 @@ class ContiguousPagedList<K, V> extends PagedList<V> implements PagedStorage.Cal
if (mDataSource.isInvalid()) {
detach();
} else {
- mDataSource.loadBefore(position, item, mConfig.pageSize,
+ mDataSource.dispatchLoadBefore(position, item, mConfig.pageSize,
mMainThreadExecutor, mReceiver);
}
@@ -213,7 +213,7 @@ class ContiguousPagedList<K, V> extends PagedList<V> implements PagedStorage.Cal
if (mDataSource.isInvalid()) {
detach();
} else {
- mDataSource.loadAfter(position, item, mConfig.pageSize,
+ mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize,
mMainThreadExecutor, mReceiver);
}
}
diff --git a/android/arch/paging/DataSource.java b/android/arch/paging/DataSource.java
index b82d4e6d..bbf7ccb3 100644
--- a/android/arch/paging/DataSource.java
+++ b/android/arch/paging/DataSource.java
@@ -30,11 +30,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
* Base class for loading pages of snapshot data into a {@link PagedList}.
* <p>
* DataSource is queried to load pages of content into a {@link PagedList}. A PagedList can grow as
- * it loads more data, but the data loaded cannot be updated.
- * <p>
- * A PagedList / DataSource pair serve as a snapshot of the data set being loaded. If the
- * underlying data set is modified, a new PagedList / DataSource pair must be created to represent
- * the new data.
+ * it loads more data, but the data loaded cannot be updated. If the underlying data set is
+ * modified, a new PagedList / DataSource pair must be created to represent the new data.
* <h4>Loading Pages</h4>
* PagedList queries data from its DataSource in response to loading hints. {@link PagedListAdapter}
* calls {@link PagedList#loadAround(int)} to load content as the user scrolls in a RecyclerView.
@@ -68,18 +65,23 @@ import java.util.concurrent.atomic.AtomicBoolean;
* copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the
* snapshot can be created.
* <h4>Implementing a DataSource</h4>
- * To implement, extend either the {@link KeyedDataSource}, or {@link PositionalDataSource}
- * subclass. Choose based on whether each load operation is based on the position of the data in the
- * list.
+ * To implement, extend one of the subclasses: {@link PageKeyedDataSource},
+ * {@link ItemKeyedDataSource}, or {@link PositionalDataSource}.
+ * <p>
+ * Use {@link PageKeyedDataSource} if pages you load embed keys for loading adjacent pages. For
+ * example a network response that returns some items, and a next/previous page links.
* <p>
- * Use {@link KeyedDataSource} if you need to use data from item {@code N-1} to load item
+ * Use {@link ItemKeyedDataSource} if you need to use data from item {@code N-1} to load item
* {@code N}. For example, if requesting the backend for the next comments in the list
* requires the ID or timestamp of the most recent loaded comment, or if querying the next users
* from a name-sorted database query requires the name and unique ID of the previous.
* <p>
- * Use {@link PositionalDataSource} if you can load arbitrary pages based solely on position
- * information, and can provide a fixed item count. PositionalDataSource supports querying pages at
- * arbitrary positions, so can provide data to PagedLists in arbitrary order.
+ * Use {@link PositionalDataSource} if you can load pages of a requested size at arbitrary
+ * positions, and provide a fixed item count. PositionalDataSource supports querying pages at
+ * arbitrary positions, so can provide data to PagedLists in arbitrary order. Note that
+ * PositionalDataSource is required to respect page size for efficient tiling. If you want to
+ * override page size (e.g. when network page size constraints are only known at runtime), use one
+ * of the other DataSource classes.
* <p>
* Because a {@code null} item indicates a placeholder in {@link PagedList}, DataSource may not
* return {@code null} items in lists that it loads. This is so that users of the PagedList
@@ -115,8 +117,13 @@ public abstract class DataSource<Key, Value> {
/**
* Create a DataSource.
* <p>
- * The DataSource should invalidate itself if the snapshot is no longer valid, and a new
- * DataSource should be queried from the Factory.
+ * The DataSource should invalidate itself if the snapshot is no longer valid. If a
+ * DataSource becomes invalid, the only way to query more data is to create a new DataSource
+ * from the Factory.
+ * <p>
+ * {@link LivePagedListBuilder} for example will construct a new PagedList and DataSource
+ * when the current DataSource is invalidated, and pass the new PagedList through the
+ * {@code LiveData<PagedList>} to observers.
*
* @return the new DataSource.
*/
@@ -159,11 +166,11 @@ public abstract class DataSource<Key, Value> {
private Executor mPostExecutor = null;
private boolean mHasSignalled = false;
- BaseLoadCallback(@PageResult.ResultType int resultType, @NonNull DataSource dataSource,
+ BaseLoadCallback(@NonNull DataSource dataSource, @PageResult.ResultType int resultType,
@Nullable Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
+ mDataSource = dataSource;
mResultType = resultType;
mPostExecutor = mainThreadExecutor;
- mDataSource = dataSource;
mReceiver = receiver;
}
@@ -173,20 +180,30 @@ public abstract class DataSource<Key, Value> {
}
}
+ /**
+ * Call before verifying args, or dispatching actul results
+ *
+ * @return true if DataSource was invalid, and invalid result dispatched
+ */
+ boolean dispatchInvalidResultIfInvalid() {
+ if (mDataSource.isInvalid()) {
+ dispatchResultToReceiver(PageResult.<T>getInvalidResult());
+ return true;
+ }
+ return false;
+ }
+
void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
Executor executor;
synchronized (mSignalLock) {
if (mHasSignalled) {
throw new IllegalStateException(
- "LoadCallback already dispatched, cannot dispatch again.");
+ "callback.onResult already called, cannot call again.");
}
mHasSignalled = true;
executor = mPostExecutor;
}
- final PageResult<T> resolvedResult =
- mDataSource.isInvalid() ? PageResult.<T>getInvalidResult() : result;
-
if (executor != null) {
executor.execute(new Runnable() {
@Override
diff --git a/android/arch/paging/ItemKeyedDataSource.java b/android/arch/paging/ItemKeyedDataSource.java
new file mode 100644
index 00000000..cb8247bd
--- /dev/null
+++ b/android/arch/paging/ItemKeyedDataSource.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.paging;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Incremental data loader for paging keyed content, where loaded content uses previously loaded
+ * items as input to future loads.
+ * <p>
+ * Implement a DataSource using ItemKeyedDataSource if you need to use data from item {@code N - 1}
+ * to load item {@code N}. This is common, for example, in sorted database queries where
+ * attributes of the item such just before the next query define how to execute it.
+ * <p>
+ * The {@code InMemoryByItemRepository} in the
+ * <a href="https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md">PagingWithNetworkSample</a>
+ * shows how to implement a network ItemKeyedDataSource using
+ * <a href="https://square.github.io/retrofit/">Retrofit</a>, while
+ * handling swipe-to-refresh, network errors, and retry.
+ *
+ * @param <Key> Type of data used to query Value types out of the DataSource.
+ * @param <Value> Type of items being loaded by the DataSource.
+ */
+public abstract class ItemKeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> {
+
+ /**
+ * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
+ *
+ * @param <Key> Type of data used to query Value types out of the DataSource.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static class LoadInitialParams<Key> {
+ /**
+ * Load items around this key, or at the beginning of the data set if {@code null} is
+ * passed.
+ * <p>
+ * Note that this key is generally a hint, and may be ignored if you want to always load
+ * from the beginning.
+ */
+ @Nullable
+ public final Key requestedInitialKey;
+
+ /**
+ * Requested number of items to load.
+ * <p>
+ * Note that this may be larger than available data.
+ */
+ public final int requestedLoadSize;
+
+ /**
+ * Defines whether placeholders are enabled, and whether the total count passed to
+ * {@link LoadInitialCallback#onResult(List, int, int)} will be ignored.
+ */
+ public final boolean placeholdersEnabled;
+
+
+ LoadInitialParams(@Nullable Key requestedInitialKey, int requestedLoadSize,
+ boolean placeholdersEnabled) {
+ this.requestedInitialKey = requestedInitialKey;
+ this.requestedLoadSize = requestedLoadSize;
+ this.placeholdersEnabled = placeholdersEnabled;
+ }
+ }
+
+ /**
+ * Holder object for inputs to {@link #loadBefore(LoadParams, LoadCallback)}
+ * and {@link #loadAfter(LoadParams, LoadCallback)}.
+ *
+ * @param <Key> Type of data used to query Value types out of the DataSource.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static class LoadParams<Key> {
+ /**
+ * Load items before/after this key.
+ * <p>
+ * Returned data must begin directly adjacent to this position.
+ */
+ public final Key key;
+ /**
+ * Requested number of items to load.
+ * <p>
+ * Returned page can be of this size, but it may be altered if that is easier, e.g. a
+ * network data source where the backend defines page size.
+ */
+ public final int requestedLoadSize;
+
+ LoadParams(Key key, int requestedLoadSize) {
+ this.key = key;
+ this.requestedLoadSize = requestedLoadSize;
+ }
+ }
+
+ /**
+ * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}
+ * to return data and, optionally, position/count information.
+ * <p>
+ * A callback can be called only once, and will throw if called again.
+ * <p>
+ * If you can compute the number of items in the data set before and after the loaded range,
+ * call the three parameter {@link #onResult(List, int, int)} to pass that information. You
+ * can skip passing this information by calling the single parameter {@link #onResult(List)},
+ * either if it's difficult to compute, or if {@link LoadInitialParams#placeholdersEnabled} is
+ * {@code false}, so the positioning information will be ignored.
+ * <p>
+ * It is always valid for a DataSource loading method that takes a callback to stash the
+ * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
+ * temporary, recoverable error states (such as a network error that can be retried).
+ *
+ * @param <Value> Type of items being loaded.
+ */
+ public static class LoadInitialCallback<Value> extends LoadCallback<Value> {
+ private final boolean mCountingEnabled;
+ LoadInitialCallback(@NonNull ItemKeyedDataSource dataSource, boolean countingEnabled,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ super(dataSource, PageResult.INIT, null, receiver);
+ mCountingEnabled = countingEnabled;
+ }
+
+ /**
+ * Called to pass initial load state from a DataSource.
+ * <p>
+ * Call this method from your DataSource's {@code loadInitial} function to return data,
+ * and inform how many placeholders should be shown before and after. If counting is cheap
+ * to compute (for example, if a network load returns the information regardless), it's
+ * recommended to pass data back through this method.
+ * <p>
+ * It is always valid to pass a different amount of data than what is requested. Pass an
+ * empty list if there is no more data to load.
+ *
+ * @param data List of items loaded from the DataSource. If this is empty, the DataSource
+ * is treated as empty, and no further loads will occur.
+ * @param position Position of the item at the front of the list. If there are {@code N}
+ * items before the items in data that can be loaded from this DataSource,
+ * pass {@code N}.
+ * @param totalCount Total number of items that may be returned from this DataSource.
+ * Includes the number in the initial {@code data} parameter
+ * as well as any items that can be loaded in front or behind of
+ * {@code data}.
+ */
+ public void onResult(@NonNull List<Value> data, int position, int totalCount) {
+ if (!dispatchInvalidResultIfInvalid()) {
+ validateInitialLoadParams(data, position, totalCount);
+
+ int trailingUnloadedCount = totalCount - position - data.size();
+ if (mCountingEnabled) {
+ dispatchResultToReceiver(new PageResult<>(
+ data, position, trailingUnloadedCount, 0));
+ } else {
+ dispatchResultToReceiver(new PageResult<>(data, position));
+ }
+ }
+ }
+ }
+
+ /**
+ * Callback for ItemKeyedDataSource {@link #loadBefore(LoadParams, LoadCallback)}
+ * and {@link #loadAfter(LoadParams, LoadCallback)} to return data.
+ * <p>
+ * A callback can be called only once, and will throw if called again.
+ * <p>
+ * It is always valid for a DataSource loading method that takes a callback to stash the
+ * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
+ * temporary, recoverable error states (such as a network error that can be retried).
+ *
+ * @param <Value> Type of items being loaded.
+ */
+ public static class LoadCallback<Value> extends BaseLoadCallback<Value> {
+ LoadCallback(@NonNull ItemKeyedDataSource dataSource, @PageResult.ResultType int type,
+ @Nullable Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ super(dataSource, type, mainThreadExecutor, receiver);
+ }
+
+ /**
+ * Called to pass loaded data from a DataSource.
+ * <p>
+ * Call this method from your ItemKeyedDataSource's
+ * {@link #loadBefore(LoadParams, LoadCallback)} and
+ * {@link #loadAfter(LoadParams, LoadCallback)} methods to return data.
+ * <p>
+ * Call this from {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} to
+ * initialize without counting available data, or supporting placeholders.
+ * <p>
+ * It is always valid to pass a different amount of data than what is requested. Pass an
+ * empty list if there is no more data to load.
+ *
+ * @param data List of items loaded from the ItemKeyedDataSource.
+ */
+ public void onResult(@NonNull List<Value> data) {
+ if (!dispatchInvalidResultIfInvalid()) {
+ dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
+ }
+ }
+ }
+
+ @Nullable
+ @Override
+ final Key getKey(int position, Value item) {
+ if (item == null) {
+ return null;
+ }
+
+ return getKey(item);
+ }
+
+ @Override
+ final void dispatchLoadInitial(@Nullable Key key, int initialLoadSize, int pageSize,
+ boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ LoadInitialCallback<Value> callback =
+ new LoadInitialCallback<>(this, enablePlaceholders, receiver);
+ loadInitial(new LoadInitialParams<>(key, initialLoadSize, enablePlaceholders), callback);
+
+ // If initialLoad's callback is not called within the body, we force any following calls
+ // to post to the UI thread. This constructor may be run on a background thread, but
+ // after constructor, mutation must happen on UI thread.
+ callback.setPostExecutor(mainThreadExecutor);
+ }
+
+ @Override
+ final void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem,
+ int pageSize, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ loadAfter(new LoadParams<>(getKey(currentEndItem), pageSize),
+ new LoadCallback<>(this, PageResult.APPEND, mainThreadExecutor, receiver));
+ }
+
+ @Override
+ final void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
+ int pageSize, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ loadBefore(new LoadParams<>(getKey(currentBeginItem), pageSize),
+ new LoadCallback<>(this, PageResult.PREPEND, mainThreadExecutor, receiver));
+ }
+
+ /**
+ * Load initial data.
+ * <p>
+ * This method is called first to initialize a PagedList with data. If it's possible to count
+ * the items that can be loaded by the DataSource, it's recommended to pass the loaded data to
+ * the callback via the three-parameter
+ * {@link LoadInitialCallback#onResult(List, int, int)}. This enables PagedLists
+ * presenting data from this source to display placeholders to represent unloaded items.
+ * <p>
+ * {@link LoadInitialParams#requestedInitialKey} and {@link LoadInitialParams#requestedLoadSize}
+ * are hints, not requirements, so they may be altered or ignored. Note that ignoring the
+ * {@code requestedInitialKey} can prevent subsequent PagedList/DataSource pairs from
+ * initializing at the same location. If your data source never invalidates (for example,
+ * loading from the network without the network ever signalling that old data must be reloaded),
+ * it's fine to ignore the {@code initialLoadKey} and always start from the beginning of the
+ * data set.
+ *
+ * @param params Parameters for initial load, including initial key and requested size.
+ * @param callback Callback that receives initial load data.
+ */
+ public abstract void loadInitial(@NonNull LoadInitialParams<Key> params,
+ @NonNull LoadInitialCallback<Value> callback);
+
+ /**
+ * Load list data after the key specified in {@link LoadParams#key LoadParams.key}.
+ * <p>
+ * It's valid to return a different list size than the page size if it's easier, e.g. if your
+ * backend defines page sizes. It is generally safer to increase the number loaded than reduce.
+ * <p>
+ * Data may be passed synchronously during the loadAfter method, or deferred and called at a
+ * later time. Further loads going down will be blocked until the callback is called.
+ * <p>
+ * If data cannot be loaded (for example, if the request is invalid, or the data would be stale
+ * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
+ * and prevent further loading.
+ *
+ * @param params Parameters for the load, including the key to load after, and requested size.
+ * @param callback Callback that receives loaded data.
+ */
+ public abstract void loadAfter(@NonNull LoadParams<Key> params,
+ @NonNull LoadCallback<Value> callback);
+
+ /**
+ * Load list data before the key specified in {@link LoadParams#key LoadParams.key}.
+ * <p>
+ * It's valid to return a different list size than the page size if it's easier, e.g. if your
+ * backend defines page sizes. It is generally safer to increase the number loaded than reduce.
+ * <p>
+ * <p class="note"><strong>Note:</strong> Data returned will be prepended just before the key
+ * passed, so if you vary size, ensure that the last item is adjacent to the passed key.
+ * <p>
+ * Data may be passed synchronously during the loadBefore method, or deferred and called at a
+ * later time. Further loads going up will be blocked until the callback is called.
+ * <p>
+ * If data cannot be loaded (for example, if the request is invalid, or the data would be stale
+ * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
+ * and prevent further loading.
+ *
+ * @param params Parameters for the load, including the key to load before, and requested size.
+ * @param callback Callback that receives loaded data.
+ */
+ public abstract void loadBefore(@NonNull LoadParams<Key> params,
+ @NonNull LoadCallback<Value> callback);
+
+ /**
+ * Return a key associated with the given item.
+ * <p>
+ * If your ItemKeyedDataSource is loading from a source that is sorted and loaded by a unique
+ * integer ID, you would return {@code item.getID()} here. This key can then be passed to
+ * {@link #loadBefore(LoadParams, LoadCallback)} or
+ * {@link #loadAfter(LoadParams, LoadCallback)} to load additional items adjacent to the item
+ * passed to this function.
+ * <p>
+ * If your key is more complex, such as when you're sorting by name, then resolving collisions
+ * with integer ID, you'll need to return both. In such a case you would use a wrapper class,
+ * such as {@code Pair<String, Integer>} or, in Kotlin,
+ * {@code data class Key(val name: String, val id: Int)}
+ *
+ * @param item Item to get the key from.
+ * @return Key associated with given item.
+ */
+ @NonNull
+ public abstract Key getKey(@NonNull Value item);
+}
diff --git a/android/arch/paging/KeyedDataSource.java b/android/arch/paging/KeyedDataSource.java
deleted file mode 100644
index 4f62692b..00000000
--- a/android/arch/paging/KeyedDataSource.java
+++ /dev/null
@@ -1,260 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.arch.paging;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import java.util.List;
-import java.util.concurrent.Executor;
-
-/**
- * Incremental data loader for paging keyed content, where loaded content uses previously loaded
- * items as input to future loads.
- * <p>
- * Implement a DataSource using KeyedDataSource if you need to use data from item {@code N - 1}
- * to load item {@code N}. This is common, for example, in sorted database queries where
- * attributes of the item such just before the next query define how to execute it.
- *
- * @param <Key> Type of data used to query Value types out of the DataSource.
- * @param <Value> Type of items being loaded by the DataSource.
- */
-public abstract class KeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> {
-
- /**
- * Callback for KeyedDataSource initial loading methods to return data and (optionally)
- * position/count information.
- * <p>
- * A callback can be called only once, and will throw if called again.
- * <p>
- * It is always valid for a DataSource loading method that takes a callback to stash the
- * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
- * temporary, recoverable error states (such as a network error that can be retried).
- *
- * @param <T> Type of items being loaded.
- */
- public static class InitialLoadCallback<T> extends LoadCallback<T> {
- private final boolean mCountingEnabled;
- InitialLoadCallback(@NonNull KeyedDataSource dataSource, boolean countingEnabled,
- @NonNull PageResult.Receiver<T> receiver) {
- super(dataSource, PageResult.INIT, null, receiver);
- mCountingEnabled = countingEnabled;
- }
-
- /**
- * Called to pass initial load state from a DataSource.
- * <p>
- * Call this method from your DataSource's {@code loadInitial} function to return data,
- * and inform how many placeholders should be shown before and after. If counting is cheap
- * to compute (for example, if a network load returns the information regardless), it's
- * recommended to pass data back through this method.
- * <p>
- * It is always valid to pass a different amount of data than what is requested. Pass an
- * empty list if there is no more data to load.
- *
- * @param data List of items loaded from the DataSource. If this is empty, the DataSource
- * is treated as empty, and no further loads will occur.
- * @param position Position of the item at the front of the list. If there are {@code N}
- * items before the items in data that can be loaded from this DataSource,
- * pass {@code N}.
- * @param totalCount Total number of items that may be returned from this DataSource.
- * Includes the number in the initial {@code data} parameter
- * as well as any items that can be loaded in front or behind of
- * {@code data}.
- */
- public void onResult(@NonNull List<T> data, int position, int totalCount) {
- validateInitialLoadParams(data, position, totalCount);
-
- int trailingUnloadedCount = totalCount - position - data.size();
- if (mCountingEnabled) {
- dispatchResultToReceiver(new PageResult<>(
- data, position, trailingUnloadedCount, 0));
- } else {
- dispatchResultToReceiver(new PageResult<>(data, position));
- }
- }
- }
-
- /**
- * Callback for KeyedDataSource {@link #loadBefore(Object, int, LoadCallback)}
- * and {@link #loadAfter(Object, int, LoadCallback)} methods to return data.
- * <p>
- * A callback can be called only once, and will throw if called again.
- * <p>
- * It is always valid for a DataSource loading method that takes a callback to stash the
- * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
- * temporary, recoverable error states (such as a network error that can be retried).
- *
- * @param <T> Type of items being loaded.
- */
- public static class LoadCallback<T> extends BaseLoadCallback<T> {
- LoadCallback(@NonNull KeyedDataSource dataSource, @PageResult.ResultType int type,
- @Nullable Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
- super(type, dataSource, mainThreadExecutor, receiver);
- }
-
- /**
- * Called to pass loaded data from a DataSource.
- * <p>
- * Call this method from your KeyedDataSource's
- * {@link #loadBefore(Object, int, LoadCallback)} and
- * {@link #loadAfter(Object, int, LoadCallback)} methods to return data.
- * <p>
- * Call this from {@link #loadInitial(Object, int, boolean, InitialLoadCallback)} to
- * initialize without counting available data, or supporting placeholders.
- * <p>
- * It is always valid to pass a different amount of data than what is requested. Pass an
- * empty list if there is no more data to load.
- *
- * @param data List of items loaded from the KeyedDataSource.
- */
- public void onResult(@NonNull List<T> data) {
- dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
- }
- }
-
- @Nullable
- @Override
- final Key getKey(int position, Value item) {
- if (item == null) {
- return null;
- }
-
- return getKey(item);
- }
-
- @Override
- public void loadInitial(@Nullable Key key, int initialLoadSize, int pageSize,
- boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver) {
- InitialLoadCallback<Value> callback =
- new InitialLoadCallback<>(this, enablePlaceholders, receiver);
- loadInitial(key, initialLoadSize, enablePlaceholders, callback);
-
- // If initialLoad's callback is not called within the body, we force any following calls
- // to post to the UI thread. This constructor may be run on a background thread, but
- // after constructor, mutation must happen on UI thread.
- callback.setPostExecutor(mainThreadExecutor);
- }
-
- @Override
- void loadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
- @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<Value> receiver) {
- loadAfter(getKey(currentEndItem), pageSize,
- new LoadCallback<>(this, PageResult.APPEND, mainThreadExecutor, receiver));
- }
-
- @Override
- void loadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize,
- @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<Value> receiver) {
- loadBefore(getKey(currentBeginItem), pageSize,
- new LoadCallback<>(this, PageResult.PREPEND, mainThreadExecutor, receiver));
- }
-
- /**
- * Load initial data.
- * <p>
- * This method is called first to initialize a PagedList with data. If it's possible to count
- * the items that can be loaded by the DataSource, it's recommended to pass the loaded data to
- * the callback via the three-parameter
- * {@link InitialLoadCallback#onResult(List, int, int)}. This enables PagedLists
- * presenting data from this source to display placeholders to represent unloaded items.
- * <p>
- * {@code initialLoadKey} and {@code requestedLoadSize} are hints, not requirements, so if it is
- * difficult or impossible to respect them, they may be altered. Note that ignoring the
- * {@code initialLoadKey} can prevent subsequent PagedList/DataSource pairs from initializing at
- * the same location. If your data source never invalidates (for example, loading from the
- * network without the network ever signalling that old data must be reloaded), it's fine to
- * ignore the {@code initialLoadKey} and always start from the beginning of the data set.
- *
- * @param initialLoadKey Load items around this key, or at the beginning of the data set if null
- * is passed.
- * @param requestedLoadSize Suggested number of items to load.
- * @param enablePlaceholders Signals whether counting is requested. If false, you can
- * potentially save work by calling the single-parameter variant of
- * {@link LoadCallback#onResult(List)} and not counting the
- * number of items in the data set.
- * @param callback DataSource.LoadCallback that receives initial load data.
- */
- public abstract void loadInitial(@Nullable Key initialLoadKey, int requestedLoadSize,
- boolean enablePlaceholders, @NonNull InitialLoadCallback<Value> callback);
-
- /**
- * Load list data after the specified item.
- * <p>
- * It's valid to return a different list size than the page size, if it's easier for this data
- * source. It is generally safer to increase the number loaded than reduce.
- * <p>
- * Data may be passed synchronously during the loadAfter method, or deferred and called at a
- * later time. Further loads going down will be blocked until the callback is called.
- * <p>
- * If data cannot be loaded (for example, if the request is invalid, or the data would be stale
- * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
- * and prevent further loading.
- *
- * @param currentEndKey Load items after this key. May be null on initial load, to indicate load
- * from beginning.
- * @param pageSize Suggested number of items to load.
- * @param callback DataSource.LoadCallback that receives loaded data.
- */
- public abstract void loadAfter(@NonNull Key currentEndKey, int pageSize,
- @NonNull LoadCallback<Value> callback);
-
- /**
- * Load data before the currently loaded content.
- * <p>
- * It's valid to return a different list size than the page size, if it's easier for this data
- * source. It is generally safer to increase the number loaded than reduce. Note that the last
- * item returned must be directly adjacent to the key passed, so varying size from the pageSize
- * requested should effectively grow or shrink the list by modifying the beginning, not the end.
- * <p>
- * Data may be passed synchronously during the loadBefore method, or deferred and called at a
- * later time. Further loads going up will be blocked until the callback is called.
- * <p>
- * If data cannot be loaded (for example, if the request is invalid, or the data would be stale
- * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
- * and prevent further loading.
- * <p class="note"><strong>Note:</strong> Data must be returned in the order it will be
- * presented in the list.
- *
- * @param currentBeginKey Load items before this key.
- * @param pageSize Suggested number of items to load.
- * @param callback DataSource.LoadCallback that receives loaded data.
- */
- public abstract void loadBefore(@NonNull Key currentBeginKey, int pageSize,
- @NonNull LoadCallback<Value> callback);
-
- /**
- * Return a key associated with the given item.
- * <p>
- * If your KeyedDataSource is loading from a source that is sorted and loaded by a unique
- * integer ID, you would return {@code item.getID()} here. This key can then be passed to
- * {@link #loadBefore(Object, int, LoadCallback)} or
- * {@link #loadAfter(Object, int, LoadCallback)} to load additional items adjacent to the item
- * passed to this function.
- * <p>
- * If your key is more complex, such as when you're sorting by name, then resolving collisions
- * with integer ID, you'll need to return both. In such a case you would use a wrapper class,
- * such as {@code Pair<String, Integer>} or, in Kotlin,
- * {@code data class Key(val name: String, val id: Int)}
- *
- * @param item Item to get the key from.
- * @return Key associated with given item.
- */
- @NonNull
- public abstract Key getKey(@NonNull Value item);
-}
diff --git a/android/arch/paging/ListDataSource.java b/android/arch/paging/ListDataSource.java
index b6f366a3..1482a91f 100644
--- a/android/arch/paging/ListDataSource.java
+++ b/android/arch/paging/ListDataSource.java
@@ -29,22 +29,23 @@ class ListDataSource<T> extends PositionalDataSource<T> {
}
@Override
- public void loadInitial(int requestedStartPosition, int requestedLoadSize, int pageSize,
- @NonNull InitialLoadCallback<T> callback) {
+ public void loadInitial(@NonNull LoadInitialParams params,
+ @NonNull LoadInitialCallback<T> callback) {
final int totalCount = mList.size();
- final int firstLoadPosition = computeFirstLoadPosition(
- requestedStartPosition, requestedLoadSize, pageSize, totalCount);
- final int firstLoadSize = Math.min(totalCount - firstLoadPosition, requestedLoadSize);
+ final int position = computeInitialLoadPosition(params, totalCount);
+ final int loadSize = computeInitialLoadSize(params, position, totalCount);
// for simplicity, we could return everything immediately,
// but we tile here since it's expected behavior
- List<T> sublist = mList.subList(firstLoadPosition, firstLoadPosition + firstLoadSize);
- callback.onResult(sublist, firstLoadPosition, totalCount);
+ List<T> sublist = mList.subList(position, position + loadSize);
+ callback.onResult(sublist, position, totalCount);
}
@Override
- public void loadRange(int startPosition, int count, @NonNull LoadCallback<T> callback) {
- callback.onResult(mList.subList(startPosition, startPosition + count));
+ public void loadRange(@NonNull LoadRangeParams params,
+ @NonNull LoadRangeCallback<T> callback) {
+ callback.onResult(mList.subList(params.startPosition,
+ params.startPosition + params.loadSize));
}
}
diff --git a/android/arch/paging/LivePagedListBuilder.java b/android/arch/paging/LivePagedListBuilder.java
index b0fddba2..f2d09cc7 100644
--- a/android/arch/paging/LivePagedListBuilder.java
+++ b/android/arch/paging/LivePagedListBuilder.java
@@ -88,9 +88,21 @@ public class LivePagedListBuilder<Key, Value> {
}
/**
- * Sets a {@link PagedList.BoundaryCallback} on each PagedList created.
+ * Sets a {@link PagedList.BoundaryCallback} on each PagedList created, typically used to load
+ * additional data from network when paging from local storage.
* <p>
- * This can be used to
+ * Pass a BoundaryCallback to listen to when the PagedList runs out of data to load. If this
+ * method is not called, or {@code null} is passed, you will not be notified when each
+ * DataSource runs out of data to provide to its PagedList.
+ * <p>
+ * If you are paging from a DataSource.Factory backed by local storage, you can set a
+ * BoundaryCallback to know when there is no more information to page from local storage.
+ * This is useful to page from the network when local storage is a cache of network data.
+ * <p>
+ * Note that when using a BoundaryCallback with a {@code LiveData<PagedList>}, method calls
+ * on the callback may be dispatched multiple times - one for each PagedList/DataSource
+ * pair. If loading network data from a BoundaryCallback, you should prevent multiple
+ * dispatches of the same method from triggering multiple simultaneous network loads.
*
* @param boundaryCallback The boundary callback for listening to PagedList load state.
* @return this
@@ -106,6 +118,8 @@ public class LivePagedListBuilder<Key, Value> {
/**
* Sets executor which will be used for background loading of pages.
* <p>
+ * If not set, defaults to the Arch components I/O thread.
+ * <p>
* Does not affect initial load, which will be always be done on done on the Arch components
* I/O thread.
*
diff --git a/android/arch/paging/LivePagedListProvider.java b/android/arch/paging/LivePagedListProvider.java
index b7c68dd6..44b71a82 100644
--- a/android/arch/paging/LivePagedListProvider.java
+++ b/android/arch/paging/LivePagedListProvider.java
@@ -16,5 +16,91 @@
package android.arch.paging;
-abstract public class LivePagedListProvider<K, T> {
-} \ No newline at end of file
+import android.arch.lifecycle.LiveData;
+import android.support.annotation.AnyThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+
+// NOTE: Room 1.0 depends on this class, so it should not be removed
+// until Room switches to using DataSource.Factory directly
+/**
+ * Provides a {@code LiveData<PagedList>}, given a means to construct a DataSource.
+ * <p>
+ * Return type for data-loading system of an application or library to produce a
+ * {@code LiveData<PagedList>}, while leaving the details of the paging mechanism up to the
+ * consumer.
+ *
+ * @param <Key> Type of input valued used to load data from the DataSource. Must be integer if
+ * you're using PositionalDataSource.
+ * @param <Value> Data type produced by the DataSource, and held by the PagedLists.
+ *
+ * @see PagedListAdapter
+ * @see DataSource
+ * @see PagedList
+ *
+ * @deprecated use {@link LivePagedListBuilder} to construct a {@code LiveData<PagedList>}. It
+ * provides the same construction capability with more customization, and simpler defaults. The role
+ * of DataSource construction has been separated out to {@link DataSource.Factory} to access or
+ * provide a self-invalidating sequence of DataSources. If you were acquiring this from Room, you
+ * can switch to having your Dao return a {@link DataSource.Factory} instead, and create a
+ * {@code LiveData<PagedList>} with a {@link LivePagedListBuilder}.
+ */
+@Deprecated
+public abstract class LivePagedListProvider<Key, Value> implements DataSource.Factory<Key, Value> {
+
+ @Override
+ public DataSource<Key, Value> create() {
+ return createDataSource();
+ }
+
+ /**
+ * Construct a new data source to be wrapped in a new PagedList, which will be returned
+ * through the LiveData.
+ *
+ * @return The data source.
+ */
+ @WorkerThread
+ protected abstract DataSource<Key, Value> createDataSource();
+
+ /**
+ * Creates a LiveData of PagedLists, given the page size.
+ * <p>
+ * This LiveData can be passed to a {@link PagedListAdapter} to be displayed with a
+ * {@link android.support.v7.widget.RecyclerView}.
+ *
+ * @param initialLoadKey Initial key used to load initial data from the data source.
+ * @param pageSize Page size defining how many items are loaded from a data source at a time.
+ * Recommended to be multiple times the size of item displayed at once.
+ *
+ * @return The LiveData of PagedLists.
+ */
+ @AnyThread
+ @NonNull
+ public LiveData<PagedList<Value>> create(@Nullable Key initialLoadKey, int pageSize) {
+ return new LivePagedListBuilder<>(this, pageSize)
+ .setInitialLoadKey(initialLoadKey)
+ .build();
+ }
+
+ /**
+ * Creates a LiveData of PagedLists, given the PagedList.Config.
+ * <p>
+ * This LiveData can be passed to a {@link PagedListAdapter} to be displayed with a
+ * {@link android.support.v7.widget.RecyclerView}.
+ *
+ * @param initialLoadKey Initial key to pass to the data source to initialize data with.
+ * @param config PagedList.Config to use with created PagedLists. This specifies how the
+ * lists will load data.
+ *
+ * @return The LiveData of PagedLists.
+ */
+ @AnyThread
+ @NonNull
+ public LiveData<PagedList<Value>> create(@Nullable Key initialLoadKey,
+ @NonNull PagedList.Config config) {
+ return new LivePagedListBuilder<>(this, config)
+ .setInitialLoadKey(initialLoadKey)
+ .build();
+ }
+}
diff --git a/android/arch/paging/PageKeyedDataSource.java b/android/arch/paging/PageKeyedDataSource.java
new file mode 100644
index 00000000..a10eceeb
--- /dev/null
+++ b/android/arch/paging/PageKeyedDataSource.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.paging;
+
+import android.support.annotation.GuardedBy;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Incremental data loader for page-keyed content, where requests return keys for next/previous
+ * pages.
+ * <p>
+ * Implement a DataSource using PageKeyedDataSource if you need to use data from page {@code N - 1}
+ * to load page {@code N}. This is common, for example, in network APIs that include a next/previous
+ * link or key with each page load.
+ * <p>
+ * The {@code InMemoryByPageRepository} in the
+ * <a href="https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md">PagingWithNetworkSample</a>
+ * shows how to implement a network PageKeyedDataSource using
+ * <a href="https://square.github.io/retrofit/">Retrofit</a>, while
+ * handling swipe-to-refresh, network errors, and retry.
+ *
+ * @param <Key> Type of data used to query Value types out of the DataSource.
+ * @param <Value> Type of items being loaded by the DataSource.
+ */
+public abstract class PageKeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> {
+ private final Object mKeyLock = new Object();
+
+ @Nullable
+ @GuardedBy("mKeyLock")
+ private Key mNextKey = null;
+
+ @Nullable
+ @GuardedBy("mKeyLock")
+ private Key mPreviousKey = null;
+
+ private void initKeys(@Nullable Key previousKey, @Nullable Key nextKey) {
+ synchronized (mKeyLock) {
+ mPreviousKey = previousKey;
+ mNextKey = nextKey;
+ }
+ }
+
+ private void setPreviousKey(@Nullable Key previousKey) {
+ synchronized (mKeyLock) {
+ mPreviousKey = previousKey;
+ }
+ }
+
+ private void setNextKey(@Nullable Key nextKey) {
+ synchronized (mKeyLock) {
+ mNextKey = nextKey;
+ }
+ }
+
+ private @Nullable Key getPreviousKey() {
+ synchronized (mKeyLock) {
+ return mPreviousKey;
+ }
+ }
+
+ private @Nullable Key getNextKey() {
+ synchronized (mKeyLock) {
+ return mNextKey;
+ }
+ }
+
+ /**
+ * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
+ *
+ * @param <Key> Type of data used to query pages.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static class LoadInitialParams<Key> {
+ /**
+ * Requested number of items to load.
+ * <p>
+ * Note that this may be larger than available data.
+ */
+ public final int requestedLoadSize;
+
+ /**
+ * Defines whether placeholders are enabled, and whether the total count passed to
+ * {@link LoadInitialCallback#onResult(List, int, int, Key, Key)} will be ignored.
+ */
+ public final boolean placeholdersEnabled;
+
+
+ LoadInitialParams(int requestedLoadSize,
+ boolean placeholdersEnabled) {
+ this.requestedLoadSize = requestedLoadSize;
+ this.placeholdersEnabled = placeholdersEnabled;
+ }
+ }
+
+ /**
+ * Holder object for inputs to {@link #loadBefore(LoadParams, LoadCallback)} and
+ * {@link #loadAfter(LoadParams, LoadCallback)}.
+ *
+ * @param <Key> Type of data used to query pages.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static class LoadParams<Key> {
+ /**
+ * Load items before/after this key.
+ * <p>
+ * Returned data must begin directly adjacent to this position.
+ */
+ public final Key key;
+
+ /**
+ * Requested number of items to load.
+ * <p>
+ * Returned page can be of this size, but it may be altered if that is easier, e.g. a
+ * network data source where the backend defines page size.
+ */
+ public final int requestedLoadSize;
+
+ LoadParams(Key key, int requestedLoadSize) {
+ this.key = key;
+ this.requestedLoadSize = requestedLoadSize;
+ }
+ }
+
+ /**
+ * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}
+ * to return data and, optionally, position/count information.
+ * <p>
+ * A callback can be called only once, and will throw if called again.
+ * <p>
+ * If you can compute the number of items in the data set before and after the loaded range,
+ * call the five parameter {@link #onResult(List, int, int, Object, Object)} to pass that
+ * information. You can skip passing this information by calling the three parameter
+ * {@link #onResult(List, Object, Object)}, either if it's difficult to compute, or if
+ * {@link LoadInitialParams#placeholdersEnabled} is {@code false}, so the positioning
+ * information will be ignored.
+ * <p>
+ * It is always valid for a DataSource loading method that takes a callback to stash the
+ * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
+ * temporary, recoverable error states (such as a network error that can be retried).
+ *
+ * @param <Key> Type of data used to query pages.
+ * @param <Value> Type of items being loaded.
+ */
+ public static class LoadInitialCallback<Key, Value> extends BaseLoadCallback<Value> {
+ private final PageKeyedDataSource<Key, Value> mDataSource;
+ private final boolean mCountingEnabled;
+ LoadInitialCallback(@NonNull PageKeyedDataSource<Key, Value> dataSource,
+ boolean countingEnabled, @NonNull PageResult.Receiver<Value> receiver) {
+ super(dataSource, PageResult.INIT, null, receiver);
+ mDataSource = dataSource;
+ mCountingEnabled = countingEnabled;
+ }
+
+ /**
+ * Called to pass initial load state from a DataSource.
+ * <p>
+ * Call this method from your DataSource's {@code loadInitial} function to return data,
+ * and inform how many placeholders should be shown before and after. If counting is cheap
+ * to compute (for example, if a network load returns the information regardless), it's
+ * recommended to pass data back through this method.
+ * <p>
+ * It is always valid to pass a different amount of data than what is requested. Pass an
+ * empty list if there is no more data to load.
+ *
+ * @param data List of items loaded from the DataSource. If this is empty, the DataSource
+ * is treated as empty, and no further loads will occur.
+ * @param position Position of the item at the front of the list. If there are {@code N}
+ * items before the items in data that can be loaded from this DataSource,
+ * pass {@code N}.
+ * @param totalCount Total number of items that may be returned from this DataSource.
+ * Includes the number in the initial {@code data} parameter
+ * as well as any items that can be loaded in front or behind of
+ * {@code data}.
+ */
+ public void onResult(@NonNull List<Value> data, int position, int totalCount,
+ @Nullable Key previousPageKey, @Nullable Key nextPageKey) {
+ if (!dispatchInvalidResultIfInvalid()) {
+ validateInitialLoadParams(data, position, totalCount);
+
+ // setup keys before dispatching data, so guaranteed to be ready
+ mDataSource.initKeys(previousPageKey, nextPageKey);
+
+ int trailingUnloadedCount = totalCount - position - data.size();
+ if (mCountingEnabled) {
+ dispatchResultToReceiver(new PageResult<>(
+ data, position, trailingUnloadedCount, 0));
+ } else {
+ dispatchResultToReceiver(new PageResult<>(data, position));
+ }
+ }
+ }
+
+ /**
+ * Called to pass loaded data from a DataSource.
+ * <p>
+ * Call this from {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} to
+ * initialize without counting available data, or supporting placeholders.
+ * <p>
+ * It is always valid to pass a different amount of data than what is requested. Pass an
+ * empty list if there is no more data to load.
+ *
+ * @param data List of items loaded from the PageKeyedDataSource.
+ * @param previousPageKey Key for page before the initial load result, or {@code null} if no
+ * more data can be loaded before.
+ * @param nextPageKey Key for page after the initial load result, or {@code null} if no
+ * more data can be loaded after.
+ */
+ public void onResult(@NonNull List<Value> data, @Nullable Key previousPageKey,
+ @Nullable Key nextPageKey) {
+ if (!dispatchInvalidResultIfInvalid()) {
+ mDataSource.initKeys(previousPageKey, nextPageKey);
+ dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
+ }
+ }
+ }
+
+ /**
+ * Callback for PageKeyedDataSource {@link #loadBefore(LoadParams, LoadCallback)} and
+ * {@link #loadAfter(LoadParams, LoadCallback)} to return data.
+ * <p>
+ * A callback can be called only once, and will throw if called again.
+ * <p>
+ * It is always valid for a DataSource loading method that takes a callback to stash the
+ * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
+ * temporary, recoverable error states (such as a network error that can be retried).
+ *
+ * @param <Key> Type of data used to query pages.
+ * @param <Value> Type of items being loaded.
+ */
+ public static class LoadCallback<Key, Value> extends BaseLoadCallback<Value> {
+ private final PageKeyedDataSource<Key, Value> mDataSource;
+ LoadCallback(@NonNull PageKeyedDataSource<Key, Value> dataSource,
+ @PageResult.ResultType int type, @Nullable Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ super(dataSource, type, mainThreadExecutor, receiver);
+ mDataSource = dataSource;
+ }
+
+ /**
+ * Called to pass loaded data from a DataSource.
+ * <p>
+ * Call this method from your PageKeyedDataSource's
+ * {@link #loadBefore(LoadParams, LoadCallback)} and
+ * {@link #loadAfter(LoadParams, LoadCallback)} methods to return data.
+ * <p>
+ * It is always valid to pass a different amount of data than what is requested. Pass an
+ * empty list if there is no more data to load.
+ * <p>
+ * Pass the key for the subsequent page to load to adjacentPageKey. For example, if you've
+ * loaded a page in {@link #loadBefore(LoadParams, LoadCallback)}, pass the key for the
+ * previous page, or {@code null} if the loaded page is the first. If in
+ * {@link #loadAfter(LoadParams, LoadCallback)}, pass the key for the next page, or
+ * {@code null} if the loaded page is the last.
+ *
+ * @param data List of items loaded from the PageKeyedDataSource.
+ * @param adjacentPageKey Key for subsequent page load (previous page in {@link #loadBefore}
+ * / next page in {@link #loadAfter}), or {@code null} if there are
+ * no more pages to load in the current load direction.
+ */
+ public void onResult(@NonNull List<Value> data, @Nullable Key adjacentPageKey) {
+ if (!dispatchInvalidResultIfInvalid()) {
+ if (mResultType == PageResult.APPEND) {
+ mDataSource.setNextKey(adjacentPageKey);
+ } else {
+ mDataSource.setPreviousKey(adjacentPageKey);
+ }
+ dispatchResultToReceiver(new PageResult<>(data, 0, 0, 0));
+ }
+ }
+ }
+
+ @Nullable
+ @Override
+ final Key getKey(int position, Value item) {
+ // don't attempt to persist keys, since we currently don't pass them to initial load
+ return null;
+ }
+
+ @Override
+ final void dispatchLoadInitial(@Nullable Key key, int initialLoadSize, int pageSize,
+ boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ LoadInitialCallback<Key, Value> callback =
+ new LoadInitialCallback<>(this, enablePlaceholders, receiver);
+ loadInitial(new LoadInitialParams<Key>(initialLoadSize, enablePlaceholders), callback);
+
+ // If initialLoad's callback is not called within the body, we force any following calls
+ // to post to the UI thread. This constructor may be run on a background thread, but
+ // after constructor, mutation must happen on UI thread.
+ callback.setPostExecutor(mainThreadExecutor);
+ }
+
+
+ @Override
+ final void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem,
+ int pageSize, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ @Nullable Key key = getNextKey();
+ if (key != null) {
+ loadAfter(new LoadParams<>(key, pageSize),
+ new LoadCallback<>(this, PageResult.APPEND, mainThreadExecutor, receiver));
+ }
+ }
+
+ @Override
+ final void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
+ int pageSize, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<Value> receiver) {
+ @Nullable Key key = getPreviousKey();
+ if (key != null) {
+ loadBefore(new LoadParams<>(key, pageSize),
+ new LoadCallback<>(this, PageResult.PREPEND, mainThreadExecutor, receiver));
+ }
+ }
+
+ /**
+ * Load initial data.
+ * <p>
+ * This method is called first to initialize a PagedList with data. If it's possible to count
+ * the items that can be loaded by the DataSource, it's recommended to pass the loaded data to
+ * the callback via the three-parameter
+ * {@link LoadInitialCallback#onResult(List, int, int, Object, Object)}. This enables PagedLists
+ * presenting data from this source to display placeholders to represent unloaded items.
+ * <p>
+ * {@link LoadInitialParams#requestedLoadSize} is a hint, not a requirement, so it may be may be
+ * altered or ignored.
+ *
+ * @param params Parameters for initial load, including requested load size.
+ * @param callback Callback that receives initial load data.
+ */
+ public abstract void loadInitial(@NonNull LoadInitialParams<Key> params,
+ @NonNull LoadInitialCallback<Key, Value> callback);
+
+ /**
+ * Prepend page with the key specified by {@link LoadParams#key LoadParams.key}.
+ * <p>
+ * It's valid to return a different list size than the page size if it's easier, e.g. if your
+ * backend defines page sizes. It is generally safer to increase the number loaded than reduce.
+ * <p>
+ * Data may be passed synchronously during the load method, or deferred and called at a
+ * later time. Further loads going down will be blocked until the callback is called.
+ * <p>
+ * If data cannot be loaded (for example, if the request is invalid, or the data would be stale
+ * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
+ * and prevent further loading.
+ *
+ * @param params Parameters for the load, including the key for the new page, and requested load
+ * size.
+ * @param callback Callback that receives loaded data.
+ */
+ public abstract void loadBefore(@NonNull LoadParams<Key> params,
+ @NonNull LoadCallback<Key, Value> callback);
+
+ /**
+ * Append page with the key specified by {@link LoadParams#key LoadParams.key}.
+ * <p>
+ * It's valid to return a different list size than the page size if it's easier, e.g. if your
+ * backend defines page sizes. It is generally safer to increase the number loaded than reduce.
+ * <p>
+ * Data may be passed synchronously during the load method, or deferred and called at a
+ * later time. Further loads going down will be blocked until the callback is called.
+ * <p>
+ * If data cannot be loaded (for example, if the request is invalid, or the data would be stale
+ * and inconsistent, it is valid to call {@link #invalidate()} to invalidate the data source,
+ * and prevent further loading.
+ *
+ * @param params Parameters for the load, including the key for the new page, and requested load
+ * size.
+ * @param callback Callback that receives loaded data.
+ */
+ public abstract void loadAfter(@NonNull LoadParams<Key> params,
+ @NonNull LoadCallback<Key, Value> callback);
+}
diff --git a/android/arch/paging/PagedList.java b/android/arch/paging/PagedList.java
index 4e17a151..c6de5c52 100644
--- a/android/arch/paging/PagedList.java
+++ b/android/arch/paging/PagedList.java
@@ -578,7 +578,8 @@ public abstract class PagedList<T> extends AbstractList<T> {
* If data is supplied by a {@link PositionalDataSource}, the item returned from
* <code>get(i)</code> has a position of <code>i + getPositionOffset()</code>.
* <p>
- * If the DataSource is a {@link KeyedDataSource}, and thus doesn't use positions, returns 0.
+ * If the DataSource is a {@link ItemKeyedDataSource} or {@link PageKeyedDataSource}, it
+ * doesn't use positions, returns 0.
*/
public int getPositionOffset() {
return mStorage.getPositionOffset();
@@ -602,7 +603,8 @@ public abstract class PagedList<T> extends AbstractList<T> {
* GC'd.
*
* @param previousSnapshot Snapshot previously captured from this List, or null.
- * @param callback LoadCallback to dispatch to.
+ * @param callback Callback to dispatch to.
+ *
* @see #removeWeakCallback(Callback)
*/
@SuppressWarnings("WeakerAccess")
@@ -637,7 +639,7 @@ public abstract class PagedList<T> extends AbstractList<T> {
/**
* Removes a previously added callback.
*
- * @param callback LoadCallback, previously added.
+ * @param callback Callback, previously added.
* @see #addWeakCallback(List, Callback)
*/
@SuppressWarnings("WeakerAccess")
@@ -680,7 +682,7 @@ public abstract class PagedList<T> extends AbstractList<T> {
* Dispatch updates since the non-empty snapshot was taken.
*
* @param snapshot Non-empty snapshot.
- * @param callback LoadCallback for updates that have occurred since snapshot.
+ * @param callback Callback for updates that have occurred since snapshot.
*/
abstract void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> snapshot,
@NonNull Callback callback);
@@ -857,12 +859,14 @@ public abstract class PagedList<T> extends AbstractList<T> {
}
/**
- * Defines how many items to load when first load occurs, if you are using a
- * {@link KeyedDataSource}.
+ * Defines how many items to load when first load occurs.
* <p>
* This value is typically larger than page size, so on first load data there's a large
* enough range of content loaded to cover small scrolls.
* <p>
+ * When using a {@link PositionalDataSource}, the initial load size will be coerced to
+ * an integer multiple of pageSize, to enable efficient tiling.
+ * <p>
* If not set, defaults to three times page size.
*
* @param initialLoadSizeHint Number of items to load while initializing the PagedList.
@@ -874,7 +878,6 @@ public abstract class PagedList<T> extends AbstractList<T> {
return this;
}
-
/**
* Creates a {@link Config} with the given parameters.
*
@@ -905,13 +908,32 @@ public abstract class PagedList<T> extends AbstractList<T> {
/**
* Signals when a PagedList has reached the end of available data.
* <p>
- * This can be used to implement paging from the network into a local database - when the
- * database has no more data to present, a BoundaryCallback can be used to fetch more data.
+ * When local storage is a cache of network data, it's common to set up a streaming pipeline:
+ * Network data is paged into the database, database is paged into UI. Paging from the database
+ * to UI can be done with a {@code LiveData<PagedList>}, but it's still necessary to know when
+ * to trigger network loads.
* <p>
- * If an instance is shared across multiple PagedLists (e.g. when passed to
+ * BoundaryCallback does this signaling - when a DataSource runs out of data at the end of
+ * the list, {@link #onItemAtEndLoaded(Object)} is called, and you can start an async network
+ * load that will write the result directly to the database. Because the database is being
+ * observed, the UI bound to the {@code LiveData<PagedList>} will update automatically to
+ * account for the new items.
+ * <p>
+ * Note that a BoundaryCallback instance shared across multiple PagedLists (e.g. when passed to
* {@link LivePagedListBuilder#setBoundaryCallback}), the callbacks may be issued multiple
* times. If for example {@link #onItemAtEndLoaded(Object)} triggers a network load, it should
* avoid triggering it again while the load is ongoing.
+ * <p>
+ * BoundaryCallback only passes the item at front or end of the list. Number of items is not
+ * passed, since it may not be fully computed by the DataSource if placeholders are not
+ * supplied. Keys are not known because the BoundaryCallback is independent of the
+ * DataSource-specific keys, which may be different for local vs remote storage.
+ * <p>
+ * The database + network Repository in the
+ * <a href="https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md">PagingWithNetworkSample</a>
+ * shows how to implement a network BoundaryCallback using
+ * <a href="https://square.github.io/retrofit/">Retrofit</a>, while
+ * handling swipe-to-refresh, network errors, and retry.
*
* @param <T> Type loaded by the PagedList.
*/
diff --git a/android/arch/paging/PositionalDataSource.java b/android/arch/paging/PositionalDataSource.java
index d3946370..780bcf6d 100644
--- a/android/arch/paging/PositionalDataSource.java
+++ b/android/arch/paging/PositionalDataSource.java
@@ -25,15 +25,19 @@ import java.util.List;
import java.util.concurrent.Executor;
/**
- * Position-based data loader for a fixed-size, countable data set, supporting loads at arbitrary
- * positions.
+ * Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at
+ * arbitrary page positions.
* <p>
- * Extend PositionalDataSource if you can support counting your data set, and loading based on
- * position information.
+ * Extend PositionalDataSource if you can load pages of a requested size at arbitrary
+ * positions, and provide a fixed item count. If your data source can't support loading arbitrary
+ * requested page sizes (e.g. when network page size constraints are only known at runtime), use
+ * either {@link PageKeyedDataSource} or {@link ItemKeyedDataSource} instead.
* <p>
* Note that unless {@link PagedList.Config#enablePlaceholders placeholders are disabled}
- * PositionalDataSource requires counting the size of the dataset. This allows pages to be tiled in
+ * PositionalDataSource requires counting the size of the data set. This allows pages to be tiled in
* at arbitrary, non-contiguous locations based upon what the user observes in a {@link PagedList}.
+ * If placeholders are disabled, initialize with the two parameter
+ * {@link LoadInitialCallback#onResult(List, int)}.
* <p>
* Room can generate a Factory of PositionalDataSources for you:
* <pre>
@@ -46,9 +50,79 @@ import java.util.concurrent.Executor;
* @param <T> Type of items being loaded by the PositionalDataSource.
*/
public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
+
+ /**
+ * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static class LoadInitialParams {
+ /**
+ * Initial load position requested.
+ * <p>
+ * Note that this may not be within the bounds of your data set, it may need to be adjusted
+ * before you execute your load.
+ */
+ public final int requestedStartPosition;
+
+ /**
+ * Requested number of items to load.
+ * <p>
+ * Note that this may be larger than available data.
+ */
+ public final int requestedLoadSize;
+
+ /**
+ * Defines page size acceptable for return values.
+ * <p>
+ * List of items passed to the callback must be an integer multiple of page size.
+ */
+ public final int pageSize;
+
+ /**
+ * Defines whether placeholders are enabled, and whether the total count passed to
+ * {@link LoadInitialCallback#onResult(List, int, int)} will be ignored.
+ */
+ public final boolean placeholdersEnabled;
+
+ LoadInitialParams(
+ int requestedStartPosition,
+ int requestedLoadSize,
+ int pageSize,
+ boolean placeholdersEnabled) {
+ this.requestedStartPosition = requestedStartPosition;
+ this.requestedLoadSize = requestedLoadSize;
+ this.pageSize = pageSize;
+ this.placeholdersEnabled = placeholdersEnabled;
+ }
+ }
+
+ /**
+ * Holder object for inputs to {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static class LoadRangeParams {
+ /**
+ * Start position of data to load.
+ * <p>
+ * Returned data must start at this position.
+ */
+ public final int startPosition;
+ /**
+ * Number of items to load.
+ * <p>
+ * Returned data must be of this size, unless at end of the list.
+ */
+ public final int loadSize;
+
+ LoadRangeParams(int startPosition, int loadSize) {
+ this.startPosition = startPosition;
+ this.loadSize = loadSize;
+ }
+ }
+
/**
- * Callback for PositionalDataSource initial loading methods to return data, position, and
- * (optionally) count information.
+ * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}
+ * to return data, position, and count.
* <p>
* A callback can be called only once, and will throw if called again.
* <p>
@@ -58,13 +132,13 @@ public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
*
* @param <T> Type of items being loaded.
*/
- public static class InitialLoadCallback<T> extends BaseLoadCallback<T> {
+ public static class LoadInitialCallback<T> extends BaseLoadCallback<T> {
private final boolean mCountingEnabled;
private final int mPageSize;
- InitialLoadCallback(@NonNull PositionalDataSource dataSource, boolean countingEnabled,
+ LoadInitialCallback(@NonNull PositionalDataSource dataSource, boolean countingEnabled,
int pageSize, PageResult.Receiver<T> receiver) {
- super(PageResult.INIT, dataSource, null, receiver);
+ super(dataSource, PageResult.INIT, null, receiver);
mCountingEnabled = countingEnabled;
mPageSize = pageSize;
if (mPageSize < 1) {
@@ -78,7 +152,9 @@ public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
* Call this method from your DataSource's {@code loadInitial} function to return data,
* and inform how many placeholders should be shown before and after. If counting is cheap
* to compute (for example, if a network load returns the information regardless), it's
- * recommended to pass data back through this method.
+ * recommended to pass the total size to the totalCount parameter. If placeholders are not
+ * requested (when {@link LoadInitialParams#placeholdersEnabled} is false), you can instead
+ * call {@link #onResult(List, int)}.
*
* @param data List of items loaded from the DataSource. If this is empty, the DataSource
* is treated as empty, and no further loads will occur.
@@ -91,29 +167,34 @@ public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
* {@code data}.
*/
public void onResult(@NonNull List<T> data, int position, int totalCount) {
- validateInitialLoadParams(data, position, totalCount);
- if (position + data.size() != totalCount
- && data.size() % mPageSize != 0) {
- throw new IllegalArgumentException("PositionalDataSource requires initial load size"
- + " to be a multiple of page size to support internal tiling.");
- }
+ if (!dispatchInvalidResultIfInvalid()) {
+ validateInitialLoadParams(data, position, totalCount);
+ if (position + data.size() != totalCount
+ && data.size() % mPageSize != 0) {
+ throw new IllegalArgumentException("PositionalDataSource requires initial load"
+ + " size to be a multiple of page size to support internal tiling.");
+ }
- if (mCountingEnabled) {
- int trailingUnloadedCount = totalCount - position - data.size();
- dispatchResultToReceiver(
- new PageResult<>(data, position, trailingUnloadedCount, 0));
- } else {
- // Only occurs when wrapped as contiguous
- dispatchResultToReceiver(new PageResult<>(data, position));
+ if (mCountingEnabled) {
+ int trailingUnloadedCount = totalCount - position - data.size();
+ dispatchResultToReceiver(
+ new PageResult<>(data, position, trailingUnloadedCount, 0));
+ } else {
+ // Only occurs when wrapped as contiguous
+ dispatchResultToReceiver(new PageResult<>(data, position));
+ }
}
}
/**
- * Called to pass initial load state from a DataSource without supporting placeholders.
+ * Called to pass initial load state from a DataSource without total count,
+ * when placeholders aren't requested.
+ * <p class="note"><strong>Note:</strong> This method can only be called when placeholders
+ * are disabled ({@link LoadInitialParams#placeholdersEnabled} is false).
* <p>
* Call this method from your DataSource's {@code loadInitial} function to return data,
- * if position is known but total size is not. If counting is not expensive, consider
- * calling the three parameter variant: {@link #onResult(List, int, int)}.
+ * if position is known but total size is not. If placeholders are requested, call the three
+ * parameter variant: {@link #onResult(List, int, int)}.
*
* @param data List of items loaded from the DataSource. If this is empty, the DataSource
* is treated as empty, and no further loads will occur.
@@ -121,15 +202,28 @@ public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
* items before the items in data that can be provided by this DataSource,
* pass {@code N}.
*/
- void onResult(@NonNull List<T> data, int position) {
- // not counting, don't need to check mAcceptCount
- dispatchResultToReceiver(new PageResult<>(
- data, 0, 0, position));
+ @SuppressWarnings("WeakerAccess")
+ public void onResult(@NonNull List<T> data, int position) {
+ if (!dispatchInvalidResultIfInvalid()) {
+ if (position < 0) {
+ throw new IllegalArgumentException("Position must be non-negative");
+ }
+ if (data.isEmpty() && position != 0) {
+ throw new IllegalArgumentException(
+ "Initial result cannot be empty if items are present in data set.");
+ }
+ if (mCountingEnabled) {
+ throw new IllegalStateException("Placeholders requested, but totalCount not"
+ + " provided. Please call the three-parameter onResult method, or"
+ + " disable placeholders in the PagedList.Config");
+ }
+ dispatchResultToReceiver(new PageResult<>(data, position));
+ }
}
}
/**
- * Callback for PositionalDataSource {@link #loadRange(int, int, LoadCallback)} methods
+ * Callback for PositionalDataSource {@link #loadRange(LoadRangeParams, LoadRangeCallback)}
* to return data.
* <p>
* A callback can be called only once, and will throw if called again.
@@ -140,33 +234,37 @@ public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
*
* @param <T> Type of items being loaded.
*/
- public static class LoadCallback<T> extends BaseLoadCallback<T> {
+ public static class LoadRangeCallback<T> extends BaseLoadCallback<T> {
private final int mPositionOffset;
- LoadCallback(@NonNull PositionalDataSource dataSource, int positionOffset,
+ LoadRangeCallback(@NonNull PositionalDataSource dataSource, int positionOffset,
Executor mainThreadExecutor, PageResult.Receiver<T> receiver) {
- super(PageResult.TILE, dataSource, mainThreadExecutor, receiver);
+ super(dataSource, PageResult.TILE, mainThreadExecutor, receiver);
mPositionOffset = positionOffset;
}
/**
- * Called to pass loaded data from a DataSource.
- * <p>
- * Call this method from your DataSource's {@code load} methods to return data.
+ * Called to pass loaded data from {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
*
- * @param data List of items loaded from the DataSource.
+ * @param data List of items loaded from the DataSource. Must be same size as requested,
+ * unless at end of list.
*/
public void onResult(@NonNull List<T> data) {
- dispatchResultToReceiver(new PageResult<>(
- data, 0, 0, mPositionOffset));
+ if (!dispatchInvalidResultIfInvalid()) {
+ dispatchResultToReceiver(new PageResult<>(
+ data, 0, 0, mPositionOffset));
+ }
}
}
- void loadInitial(boolean acceptCount,
+ final void dispatchLoadInitial(boolean acceptCount,
int requestedStartPosition, int requestedLoadSize, int pageSize,
@NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
- InitialLoadCallback<T> callback =
- new InitialLoadCallback<>(this, acceptCount, pageSize, receiver);
- loadInitial(requestedStartPosition, requestedLoadSize, pageSize, callback);
+ LoadInitialCallback<T> callback =
+ new LoadInitialCallback<>(this, acceptCount, pageSize, receiver);
+
+ LoadInitialParams params = new LoadInitialParams(
+ requestedStartPosition, requestedLoadSize, pageSize, acceptCount);
+ loadInitial(params, callback);
// If initialLoad's callback is not called within the body, we force any following calls
// to post to the UI thread. This constructor may be run on a background thread, but
@@ -174,14 +272,14 @@ public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
callback.setPostExecutor(mainThreadExecutor);
}
- void loadRange(int startPosition, int count,
+ final void dispatchLoadRange(int startPosition, int count,
@NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
- LoadCallback<T> callback =
- new LoadCallback<>(this, startPosition, mainThreadExecutor, receiver);
+ LoadRangeCallback<T> callback =
+ new LoadRangeCallback<>(this, startPosition, mainThreadExecutor, receiver);
if (count == 0) {
callback.onResult(Collections.<T>emptyList());
} else {
- loadRange(startPosition, count, callback);
+ loadRange(new LoadRangeParams(startPosition, count), callback);
}
}
@@ -192,52 +290,94 @@ public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
* <p>
* Result list must be a multiple of pageSize to enable efficient tiling.
*
- * @param requestedStartPosition Initial load position requested. Note that this may not be
- * within the bounds of your data set, it should be corrected
- * before you make your query.
- * @param requestedLoadSize Requested number of items to load. Note that this may be larger than
- * available data.
- * @param pageSize Defines page size acceptable for return values. List of items passed to the
- * callback must be an integer multiple of page size.
- * @param callback DataSource.InitialLoadCallback that receives initial load data, including
+ * @param params Parameters for initial load, including requested start position, load size, and
+ * page size.
+ * @param callback Callback that receives initial load data, including
* position and total data set size.
*/
@WorkerThread
- public abstract void loadInitial(int requestedStartPosition, int requestedLoadSize,
- int pageSize, @NonNull InitialLoadCallback<T> callback);
+ public abstract void loadInitial(
+ @NonNull LoadInitialParams params,
+ @NonNull LoadInitialCallback<T> callback);
/**
* Called to load a range of data from the DataSource.
* <p>
* This method is called to load additional pages from the DataSource after the
- * InitialLoadCallback passed to loadInitial has initialized a PagedList.
+ * LoadInitialCallback passed to dispatchLoadInitial has initialized a PagedList.
* <p>
- * Unlike {@link #loadInitial(int, int, int, InitialLoadCallback)}, this method must return the
- * number of items requested, at the position requested.
+ * Unlike {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, this method must return
+ * the number of items requested, at the position requested.
*
- * @param startPosition Initial load position.
- * @param count Number of items to load.
- * @param callback DataSource.LoadCallback that receives loaded data.
+ * @param params Parameters for load, including start position and load size.
+ * @param callback Callback that receives loaded data.
*/
@WorkerThread
- public abstract void loadRange(int startPosition, int count, @NonNull LoadCallback<T> callback);
+ public abstract void loadRange(@NonNull LoadRangeParams params,
+ @NonNull LoadRangeCallback<T> callback);
@Override
boolean isContiguous() {
return false;
}
-
@NonNull
ContiguousDataSource<Integer, T> wrapAsContiguousWithoutPlaceholders() {
return new ContiguousWithoutPlaceholdersWrapper<>(this);
}
- static int computeFirstLoadPosition(int position, int firstLoadSize, int pageSize, int size) {
+ /**
+ * Helper for computing an initial position in
+ * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be
+ * computed ahead of loading.
+ * <p>
+ * The value computed by this function will do bounds checking, page alignment, and positioning
+ * based on initial load size requested.
+ * <p>
+ * Example usage in a PositionalDataSource subclass:
+ * <pre>
+ * class ItemDataSource extends PositionalDataSource&lt;Item> {
+ * private int computeCount() {
+ * // actual count code here
+ * }
+ *
+ * private List&lt;Item> loadRangeInternal(int startPosition, int loadCount) {
+ * // actual load code here
+ * }
+ *
+ * {@literal @}Override
+ * public void loadInitial({@literal @}NonNull LoadInitialParams params,
+ * {@literal @}NonNull LoadInitialCallback&lt;Item> callback) {
+ * int totalCount = computeCount();
+ * int position = computeInitialLoadPosition(params, totalCount);
+ * int loadSize = computeInitialLoadSize(params, position, totalCount);
+ * callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
+ * }
+ *
+ * {@literal @}Override
+ * public void loadRange({@literal @}NonNull LoadRangeParams params,
+ * {@literal @}NonNull LoadRangeCallback&lt;Item> callback) {
+ * callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
+ * }
+ * }</pre>
+ *
+ * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
+ * including page size, and requested start/loadSize.
+ * @param totalCount Total size of the data set.
+ * @return Position to start loading at.
+ *
+ * @see #computeInitialLoadSize(LoadInitialParams, int, int)
+ */
+ public static int computeInitialLoadPosition(@NonNull LoadInitialParams params,
+ int totalCount) {
+ int position = params.requestedStartPosition;
+ int initialLoadSize = params.requestedLoadSize;
+ int pageSize = params.pageSize;
+
int roundedPageStart = Math.round(position / pageSize) * pageSize;
// maximum start pos is that which will encompass end of list
- int maximumLoadPage = ((size - firstLoadSize + pageSize - 1) / pageSize) * pageSize;
+ int maximumLoadPage = ((totalCount - initialLoadSize + pageSize - 1) / pageSize) * pageSize;
roundedPageStart = Math.min(maximumLoadPage, roundedPageStart);
// minimum start position is 0
@@ -246,6 +386,56 @@ public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
return roundedPageStart;
}
+ /**
+ * Helper for computing an initial load size in
+ * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be
+ * computed ahead of loading.
+ * <p>
+ * This function takes the requested load size, and bounds checks it against the value returned
+ * by {@link #computeInitialLoadPosition(LoadInitialParams, int)}.
+ * <p>
+ * Example usage in a PositionalDataSource subclass:
+ * <pre>
+ * class ItemDataSource extends PositionalDataSource&lt;Item> {
+ * private int computeCount() {
+ * // actual count code here
+ * }
+ *
+ * private List&lt;Item> loadRangeInternal(int startPosition, int loadCount) {
+ * // actual load code here
+ * }
+ *
+ * {@literal @}Override
+ * public void loadInitial({@literal @}NonNull LoadInitialParams params,
+ * {@literal @}NonNull LoadInitialCallback&lt;Item> callback) {
+ * int totalCount = computeCount();
+ * int position = computeInitialLoadPosition(params, totalCount);
+ * int loadSize = computeInitialLoadSize(params, position, totalCount);
+ * callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
+ * }
+ *
+ * {@literal @}Override
+ * public void loadRange({@literal @}NonNull LoadRangeParams params,
+ * {@literal @}NonNull LoadRangeCallback&lt;Item> callback) {
+ * callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
+ * }
+ * }</pre>
+ *
+ * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
+ * including page size, and requested start/loadSize.
+ * @param initialLoadPosition Value returned by
+ * {@link #computeInitialLoadPosition(LoadInitialParams, int)}
+ * @param totalCount Total size of the data set.
+ * @return Number of items to load.
+ *
+ * @see #computeInitialLoadPosition(LoadInitialParams, int)
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static int computeInitialLoadSize(@NonNull LoadInitialParams params,
+ int initialLoadPosition, int totalCount) {
+ return Math.min(totalCount - initialLoadPosition, params.requestedLoadSize);
+ }
+
@SuppressWarnings("deprecation")
static class ContiguousWithoutPlaceholdersWrapper<Value>
extends ContiguousDataSource<Integer, Value> {
@@ -259,39 +449,42 @@ public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
}
@Override
- void loadInitial(@Nullable Integer position, int initialLoadSize, int pageSize,
+ void dispatchLoadInitial(@Nullable Integer position, int initialLoadSize, int pageSize,
boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver) {
final int convertPosition = position == null ? 0 : position;
// Note enablePlaceholders will be false here, but we don't have a way to communicate
// this to PositionalDataSource. This is fine, because only the list and its position
- // offset will be consumed by the InitialLoadCallback.
- mPositionalDataSource.loadInitial(false, convertPosition, initialLoadSize,
+ // offset will be consumed by the LoadInitialCallback.
+ mPositionalDataSource.dispatchLoadInitial(false, convertPosition, initialLoadSize,
pageSize, mainThreadExecutor, receiver);
}
@Override
- void loadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
+ void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
@NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver) {
int startIndex = currentEndIndex + 1;
- mPositionalDataSource.loadRange(startIndex, pageSize, mainThreadExecutor, receiver);
+ mPositionalDataSource.dispatchLoadRange(
+ startIndex, pageSize, mainThreadExecutor, receiver);
}
@Override
- void loadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, int pageSize,
- @NonNull Executor mainThreadExecutor,
+ void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
+ int pageSize, @NonNull Executor mainThreadExecutor,
@NonNull PageResult.Receiver<Value> receiver) {
int startIndex = currentBeginIndex - 1;
if (startIndex < 0) {
// trigger empty list load
- mPositionalDataSource.loadRange(startIndex, 0, mainThreadExecutor, receiver);
+ mPositionalDataSource.dispatchLoadRange(
+ startIndex, 0, mainThreadExecutor, receiver);
} else {
int loadSize = Math.min(pageSize, startIndex + 1);
startIndex = startIndex - loadSize + 1;
- mPositionalDataSource.loadRange(startIndex, loadSize, mainThreadExecutor, receiver);
+ mPositionalDataSource.dispatchLoadRange(
+ startIndex, loadSize, mainThreadExecutor, receiver);
}
}
diff --git a/android/arch/paging/TiledDataSource.java b/android/arch/paging/TiledDataSource.java
index 34d0091e..77695e5c 100644
--- a/android/arch/paging/TiledDataSource.java
+++ b/android/arch/paging/TiledDataSource.java
@@ -46,8 +46,8 @@ public abstract class TiledDataSource<T> extends PositionalDataSource<T> {
public abstract List<T> loadRange(int startPosition, int count);
@Override
- public void loadInitial(int requestedStartPosition, int requestedLoadSize, int pageSize,
- @NonNull InitialLoadCallback callback) {
+ public void loadInitial(@NonNull LoadInitialParams params,
+ @NonNull LoadInitialCallback<T> callback) {
int totalCount = countItems();
if (totalCount == 0) {
callback.onResult(Collections.<T>emptyList(), 0, 0);
@@ -55,9 +55,8 @@ public abstract class TiledDataSource<T> extends PositionalDataSource<T> {
}
// bound the size requested, based on known count
- final int firstLoadPosition = computeFirstLoadPosition(
- requestedStartPosition, requestedLoadSize, pageSize, totalCount);
- final int firstLoadSize = Math.min(totalCount - firstLoadPosition, requestedLoadSize);
+ final int firstLoadPosition = computeInitialLoadPosition(params, totalCount);
+ final int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount);
// convert from legacy behavior
List<T> list = loadRange(firstLoadPosition, firstLoadSize);
@@ -69,8 +68,9 @@ public abstract class TiledDataSource<T> extends PositionalDataSource<T> {
}
@Override
- public void loadRange(int startPosition, int count, @NonNull LoadCallback callback) {
- List<T> list = loadRange(startPosition, count);
+ public void loadRange(@NonNull LoadRangeParams params,
+ @NonNull LoadRangeCallback<T> callback) {
+ List<T> list = loadRange(params.startPosition, params.loadSize);
if (list != null) {
callback.onResult(list);
} else {
diff --git a/android/arch/paging/TiledPagedList.java b/android/arch/paging/TiledPagedList.java
index 6c189cdb..f7aae980 100644
--- a/android/arch/paging/TiledPagedList.java
+++ b/android/arch/paging/TiledPagedList.java
@@ -92,7 +92,7 @@ class TiledPagedList<T> extends PagedList<T>
final int idealStart = position - firstLoadSize / 2;
final int roundedPageStart = Math.max(0, Math.round(idealStart / pageSize) * pageSize);
- mDataSource.loadInitial(true, roundedPageStart, firstLoadSize,
+ mDataSource.dispatchLoadInitial(true, roundedPageStart, firstLoadSize,
pageSize, mMainThreadExecutor, mReceiver);
}
}
@@ -178,7 +178,8 @@ class TiledPagedList<T> extends PagedList<T>
} else {
int startPosition = pageIndex * pageSize;
int count = Math.min(pageSize, mStorage.size() - startPosition);
- mDataSource.loadRange(startPosition, count, mMainThreadExecutor, mReceiver);
+ mDataSource.dispatchLoadRange(
+ startPosition, count, mMainThreadExecutor, mReceiver);
}
}
});
diff --git a/android/arch/paging/integration/testapp/ItemDataSource.java b/android/arch/paging/integration/testapp/ItemDataSource.java
index bbbfabb8..c53d3614 100644
--- a/android/arch/paging/integration/testapp/ItemDataSource.java
+++ b/android/arch/paging/integration/testapp/ItemDataSource.java
@@ -56,35 +56,19 @@ class ItemDataSource extends PositionalDataSource<Item> {
return items;
}
- // TODO: open up this API in PositionalDataSource?
- private static int computeFirstLoadPosition(int position, int firstLoadSize,
- int pageSize, int size) {
- int roundedPageStart = Math.round(position / pageSize) * pageSize;
-
- // minimum start position is 0
- roundedPageStart = Math.max(0, roundedPageStart);
-
- // maximum start pos is that which will encompass end of list
- int maximumLoadPage = ((size - firstLoadSize + pageSize - 1) / pageSize) * pageSize;
- roundedPageStart = Math.min(maximumLoadPage, roundedPageStart);
-
- return roundedPageStart;
- }
-
@Override
- public void loadInitial(int requestedStartPosition, int requestedLoadSize,
- int pageSize, @NonNull InitialLoadCallback<Item> callback) {
- requestedStartPosition = computeFirstLoadPosition(
- requestedStartPosition, requestedLoadSize, pageSize, COUNT);
-
- requestedLoadSize = Math.min(COUNT - requestedStartPosition, requestedLoadSize);
- List<Item> data = loadRangeInternal(requestedStartPosition, requestedLoadSize);
- callback.onResult(data, requestedStartPosition, COUNT);
+ public void loadInitial(@NonNull LoadInitialParams params,
+ @NonNull LoadInitialCallback<Item> callback) {
+ int position = computeInitialLoadPosition(params, COUNT);
+ int loadSize = computeInitialLoadSize(params, position, COUNT);
+ List<Item> data = loadRangeInternal(position, loadSize);
+ callback.onResult(data, position, COUNT);
}
@Override
- public void loadRange(int startPosition, int count, @NonNull LoadCallback<Item> callback) {
- List<Item> data = loadRangeInternal(startPosition, count);
+ public void loadRange(@NonNull LoadRangeParams params,
+ @NonNull LoadRangeCallback<Item> callback) {
+ List<Item> data = loadRangeInternal(params.startPosition, params.loadSize);
callback.onResult(data);
}
}
diff --git a/android/arch/persistence/db/SupportSQLiteProgram.java b/android/arch/persistence/db/SupportSQLiteProgram.java
index c6d43cca..38c1ac16 100644
--- a/android/arch/persistence/db/SupportSQLiteProgram.java
+++ b/android/arch/persistence/db/SupportSQLiteProgram.java
@@ -16,16 +16,14 @@
package android.arch.persistence.db;
-import android.annotation.TargetApi;
-import android.os.Build;
+import java.io.Closeable;
/**
* An interface to map the behavior of {@link android.database.sqlite.SQLiteProgram}.
*/
-@TargetApi(Build.VERSION_CODES.KITKAT)
@SuppressWarnings("unused")
-public interface SupportSQLiteProgram extends AutoCloseable {
+public interface SupportSQLiteProgram extends Closeable {
/**
* Bind a NULL value to this statement. The value remains bound until
* {@link #clearBindings} is called.
diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java b/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java
index e9c2b741..d564a031 100644
--- a/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java
+++ b/android/arch/persistence/db/framework/FrameworkSQLiteDatabase.java
@@ -16,6 +16,8 @@
package android.arch.persistence.db.framework;
+import static android.text.TextUtils.isEmpty;
+
import android.arch.persistence.db.SimpleSQLiteQuery;
import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.db.SupportSQLiteQuery;
@@ -312,8 +314,4 @@ class FrameworkSQLiteDatabase implements SupportSQLiteDatabase {
public void close() throws IOException {
mDelegate.close();
}
-
- private static boolean isEmpty(String input) {
- return input == null || input.length() == 0;
- }
}
diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteProgram.java b/android/arch/persistence/db/framework/FrameworkSQLiteProgram.java
index 6c2bb721..73c98c61 100644
--- a/android/arch/persistence/db/framework/FrameworkSQLiteProgram.java
+++ b/android/arch/persistence/db/framework/FrameworkSQLiteProgram.java
@@ -60,7 +60,7 @@ class FrameworkSQLiteProgram implements SupportSQLiteProgram {
}
@Override
- public void close() throws Exception {
+ public void close() {
mDelegate.close();
}
}
diff --git a/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java b/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java
index 53a04bd6..7f07865d 100644
--- a/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java
+++ b/android/arch/persistence/db/framework/FrameworkSQLiteStatement.java
@@ -22,7 +22,7 @@ import android.database.sqlite.SQLiteStatement;
/**
* Delegates all calls to a {@link SQLiteStatement}.
*/
-class FrameworkSQLiteStatement implements SupportSQLiteStatement {
+class FrameworkSQLiteStatement extends FrameworkSQLiteProgram implements SupportSQLiteStatement {
private final SQLiteStatement mDelegate;
/**
@@ -31,40 +31,11 @@ class FrameworkSQLiteStatement implements SupportSQLiteStatement {
* @param delegate The SQLiteStatement to delegate calls to.
*/
FrameworkSQLiteStatement(SQLiteStatement delegate) {
+ super(delegate);
mDelegate = delegate;
}
@Override
- public void bindNull(int index) {
- mDelegate.bindNull(index);
- }
-
- @Override
- public void bindLong(int index, long value) {
- mDelegate.bindLong(index, value);
- }
-
- @Override
- public void bindDouble(int index, double value) {
- mDelegate.bindDouble(index, value);
- }
-
- @Override
- public void bindString(int index, String value) {
- mDelegate.bindString(index, value);
- }
-
- @Override
- public void bindBlob(int index, byte[] value) {
- mDelegate.bindBlob(index, value);
- }
-
- @Override
- public void clearBindings() {
- mDelegate.clearBindings();
- }
-
- @Override
public void execute() {
mDelegate.execute();
}
@@ -88,9 +59,4 @@ class FrameworkSQLiteStatement implements SupportSQLiteStatement {
public String simpleQueryForString() {
return mDelegate.simpleQueryForString();
}
-
- @Override
- public void close() throws Exception {
- mDelegate.close();
- }
}
diff --git a/android/arch/persistence/room/Database.java b/android/arch/persistence/room/Database.java
index f12d1b94..14e722fd 100644
--- a/android/arch/persistence/room/Database.java
+++ b/android/arch/persistence/room/Database.java
@@ -34,7 +34,7 @@ import java.lang.annotation.Target;
* <pre>
* // User and Book are classes annotated with {@literal @}Entity.
* {@literal @}Database(version = 1, entities = {User.class, Book.class})
- * abstract class AppDatabase extends RoomDatabase() {
+ * abstract class AppDatabase extends RoomDatabase {
* // BookDao is a class annotated with {@literal @}Dao.
* abstract public BookDao bookDao();
* // UserDao is a class annotated with {@literal @}Dao.
diff --git a/android/arch/persistence/room/RoomDatabase.java b/android/arch/persistence/room/RoomDatabase.java
index 8c940246..70d832b0 100644
--- a/android/arch/persistence/room/RoomDatabase.java
+++ b/android/arch/persistence/room/RoomDatabase.java
@@ -52,6 +52,13 @@ import java.util.concurrent.locks.ReentrantLock;
//@SuppressWarnings({"unused", "WeakerAccess"})
public abstract class RoomDatabase {
private static final String DB_IMPL_SUFFIX = "_Impl";
+ /**
+ * Unfortunately, we cannot read this value so we are only setting it to the SQLite default.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final int MAX_BIND_PARAMETER_CNT = 999;
// set by the generated open helper.
protected volatile SupportSQLiteDatabase mDatabase;
private SupportSQLiteOpenHelper mOpenHelper;
@@ -90,7 +97,7 @@ public abstract class RoomDatabase {
* @param configuration The database configuration.
*/
@CallSuper
- public void init(DatabaseConfiguration configuration) {
+ public void init(@NonNull DatabaseConfiguration configuration) {
mOpenHelper = createOpenHelper(configuration);
mCallbacks = configuration.callbacks;
mAllowMainThreadQueries = configuration.allowMainThreadQueries;
@@ -101,6 +108,7 @@ public abstract class RoomDatabase {
*
* @return The SQLite open helper used by this database.
*/
+ @NonNull
public SupportSQLiteOpenHelper getOpenHelper() {
return mOpenHelper;
}
@@ -113,6 +121,7 @@ public abstract class RoomDatabase {
* @param config The configuration of the Room database.
* @return A new SupportSQLiteOpenHelper to be used while connecting to the database.
*/
+ @NonNull
protected abstract SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config);
/**
@@ -122,6 +131,7 @@ public abstract class RoomDatabase {
*
* @return Creates a new InvalidationTracker.
*/
+ @NonNull
protected abstract InvalidationTracker createInvalidationTracker();
/**
@@ -199,7 +209,7 @@ public abstract class RoomDatabase {
* @param sql The query to compile.
* @return The compiled query.
*/
- public SupportSQLiteStatement compileStatement(String sql) {
+ public SupportSQLiteStatement compileStatement(@NonNull String sql) {
assertNotMainThread();
return mOpenHelper.getWritableDatabase().compileStatement(sql);
}
@@ -238,7 +248,7 @@ public abstract class RoomDatabase {
*
* @param body The piece of code to execute.
*/
- public void runInTransaction(Runnable body) {
+ public void runInTransaction(@NonNull Runnable body) {
beginTransaction();
try {
body.run();
@@ -256,7 +266,7 @@ public abstract class RoomDatabase {
* @param <V> The type of the return value.
* @return The value returned from the {@link Callable}.
*/
- public <V> V runInTransaction(Callable<V> body) {
+ public <V> V runInTransaction(@NonNull Callable<V> body) {
beginTransaction();
try {
V result = body.call();
@@ -278,7 +288,7 @@ public abstract class RoomDatabase {
*
* @param db The database instance.
*/
- protected void internalInitInvalidationTracker(SupportSQLiteDatabase db) {
+ protected void internalInitInvalidationTracker(@NonNull SupportSQLiteDatabase db) {
mInvalidationTracker.internalInit(db);
}
@@ -290,6 +300,7 @@ public abstract class RoomDatabase {
*
* @return The invalidation tracker for the database.
*/
+ @NonNull
public InvalidationTracker getInvalidationTracker() {
return mInvalidationTracker;
}
@@ -365,7 +376,7 @@ public abstract class RoomDatabase {
* @return this
*/
@NonNull
- public Builder<T> addMigrations(Migration... migrations) {
+ public Builder<T> addMigrations(@NonNull Migration... migrations) {
mMigrationContainer.addMigrations(migrations);
return this;
}
@@ -471,7 +482,7 @@ public abstract class RoomDatabase {
*
* @param migrations List of available migrations.
*/
- public void addMigrations(Migration... migrations) {
+ public void addMigrations(@NonNull Migration... migrations) {
for (Migration migration : migrations) {
addMigration(migration);
}
diff --git a/android/arch/persistence/room/RoomSQLiteQuery.java b/android/arch/persistence/room/RoomSQLiteQuery.java
index a8defd48..a10cc525 100644
--- a/android/arch/persistence/room/RoomSQLiteQuery.java
+++ b/android/arch/persistence/room/RoomSQLiteQuery.java
@@ -209,7 +209,7 @@ public class RoomSQLiteQuery implements SupportSQLiteQuery, SupportSQLiteProgram
}
@Override
- public void close() throws Exception {
+ public void close() {
// no-op. not calling release because it is internal API.
}
diff --git a/android/arch/persistence/room/integration/testapp/TestDatabase.java b/android/arch/persistence/room/integration/testapp/TestDatabase.java
index 2fad7b1f..610afb26 100644
--- a/android/arch/persistence/room/integration/testapp/TestDatabase.java
+++ b/android/arch/persistence/room/integration/testapp/TestDatabase.java
@@ -32,6 +32,7 @@ import android.arch.persistence.room.integration.testapp.dao.UserDao;
import android.arch.persistence.room.integration.testapp.dao.UserPetDao;
import android.arch.persistence.room.integration.testapp.dao.WithClauseDao;
import android.arch.persistence.room.integration.testapp.vo.BlobEntity;
+import android.arch.persistence.room.integration.testapp.vo.Day;
import android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity;
import android.arch.persistence.room.integration.testapp.vo.Pet;
import android.arch.persistence.room.integration.testapp.vo.PetCouple;
@@ -41,6 +42,8 @@ import android.arch.persistence.room.integration.testapp.vo.Toy;
import android.arch.persistence.room.integration.testapp.vo.User;
import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
@Database(entities = {User.class, Pet.class, School.class, PetCouple.class, Toy.class,
BlobEntity.class, Product.class, FunnyNamedEntity.class},
@@ -74,5 +77,25 @@ public abstract class TestDatabase extends RoomDatabase {
return date.getTime();
}
}
+
+ @TypeConverter
+ public Set<Day> decomposeDays(int flags) {
+ Set<Day> result = new HashSet<>();
+ for (Day day : Day.values()) {
+ if ((flags & (1 << day.ordinal())) != 0) {
+ result.add(day);
+ }
+ }
+ return result;
+ }
+
+ @TypeConverter
+ public int composeDays(Set<Day> days) {
+ int result = 0;
+ for (Day day : days) {
+ result |= 1 << day.ordinal();
+ }
+ return result;
+ }
}
}
diff --git a/android/arch/persistence/room/integration/testapp/dao/PetDao.java b/android/arch/persistence/room/integration/testapp/dao/PetDao.java
index 5179655a..5d060f41 100644
--- a/android/arch/persistence/room/integration/testapp/dao/PetDao.java
+++ b/android/arch/persistence/room/integration/testapp/dao/PetDao.java
@@ -21,6 +21,9 @@ import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.integration.testapp.vo.Pet;
+import android.arch.persistence.room.integration.testapp.vo.PetWithToyIds;
+
+import java.util.List;
@Dao
public interface PetDao {
@@ -32,4 +35,7 @@ public interface PetDao {
@Query("SELECT COUNT(*) FROM Pet")
int count();
+
+ @Query("SELECT * FROM Pet ORDER BY Pet.mPetId ASC")
+ List<PetWithToyIds> allPetsWithToyIds();
}
diff --git a/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/android/arch/persistence/room/integration/testapp/dao/UserDao.java
index 0b184a9c..1a2a4689 100644
--- a/android/arch/persistence/room/integration/testapp/dao/UserDao.java
+++ b/android/arch/persistence/room/integration/testapp/dao/UserDao.java
@@ -29,6 +29,8 @@ import android.arch.persistence.room.Transaction;
import android.arch.persistence.room.Update;
import android.arch.persistence.room.integration.testapp.TestDatabase;
import android.arch.persistence.room.integration.testapp.vo.AvgWeightByAge;
+import android.arch.persistence.room.integration.testapp.vo.Day;
+import android.arch.persistence.room.integration.testapp.vo.NameAndLastName;
import android.arch.persistence.room.integration.testapp.vo.User;
import android.database.Cursor;
@@ -36,6 +38,7 @@ import org.reactivestreams.Publisher;
import java.util.Date;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.Callable;
import io.reactivex.Flowable;
@@ -203,11 +206,17 @@ public abstract class UserDao {
@Query("UPDATE User set mWeight = :weight WHERE mId IN (:ids) AND mAge == :age")
public abstract int updateByAgeAndIds(float weight, int age, List<Integer> ids);
+ @Query("SELECT * FROM user WHERE (mWorkDays & :days) != 0")
+ public abstract List<User> findUsersByWorkDays(Set<Day> days);
+
// QueryLoader
@Query("SELECT COUNT(*) from user")
public abstract Integer getUserCount();
+ @Query("SELECT u.mName, u.mLastName from user u where mId = :id")
+ public abstract NameAndLastName getNameAndLastName(int id);
+
@Transaction
public void insertBothByAnnotation(final User a, final User b) {
insert(a);
diff --git a/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java b/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java
index eb159014..263417e4 100644
--- a/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java
+++ b/android/arch/persistence/room/integration/testapp/dao/UserPetDao.java
@@ -27,6 +27,7 @@ import android.arch.persistence.room.integration.testapp.vo.Pet;
import android.arch.persistence.room.integration.testapp.vo.User;
import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets;
import android.arch.persistence.room.integration.testapp.vo.UserAndPet;
+import android.arch.persistence.room.integration.testapp.vo.UserAndPetAdoptionDates;
import android.arch.persistence.room.integration.testapp.vo.UserAndPetNonNull;
import android.arch.persistence.room.integration.testapp.vo.UserIdAndPetNames;
import android.arch.persistence.room.integration.testapp.vo.UserWithPetsAndToys;
@@ -61,6 +62,9 @@ public interface UserPetDao {
@Query("SELECT * FROM User UNION ALL SELECT * FROM USER")
List<UserAndAllPets> unionByItself();
+ @Query("SELECT * FROM User")
+ List<UserAndPetAdoptionDates> loadUserWithPetAdoptionDates();
+
@Query("SELECT * FROM User u where u.mId = :userId")
LiveData<UserAndAllPets> liveUserWithPets(int userId);
diff --git a/android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java b/android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java
index 2db543b3..6278bc28 100644
--- a/android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java
+++ b/android/arch/persistence/room/integration/testapp/database/LastNameAscCustomerDataSource.java
@@ -16,10 +16,9 @@
package android.arch.persistence.room.integration.testapp.database;
import android.arch.paging.DataSource;
-import android.arch.paging.KeyedDataSource;
+import android.arch.paging.ItemKeyedDataSource;
import android.arch.persistence.room.InvalidationTracker;
import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
import java.util.Collections;
import java.util.List;
@@ -28,7 +27,7 @@ import java.util.Set;
/**
* Sample Room keyed data source.
*/
-public class LastNameAscCustomerDataSource extends KeyedDataSource<String, Customer> {
+public class LastNameAscCustomerDataSource extends ItemKeyedDataSource<String, Customer> {
private final CustomerDao mCustomerDao;
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationTracker.Observer mObserver;
@@ -76,13 +75,14 @@ public class LastNameAscCustomerDataSource extends KeyedDataSource<String, Custo
}
@Override
- public void loadInitial(@Nullable String customerName, int initialLoadSize,
- boolean enablePlaceholders, @NonNull InitialLoadCallback<Customer> callback) {
+ public void loadInitial(@NonNull LoadInitialParams<String> params,
+ @NonNull LoadInitialCallback<Customer> callback) {
+ String customerName = params.requestedInitialKey;
List<Customer> list;
if (customerName != null) {
// initial keyed load - load before 'customerName',
// and load after last item in before list
- int pageSize = initialLoadSize / 2;
+ int pageSize = params.requestedLoadSize / 2;
String key = customerName;
list = mCustomerDao.customerNameLoadBefore(key, pageSize);
Collections.reverse(list);
@@ -91,10 +91,10 @@ public class LastNameAscCustomerDataSource extends KeyedDataSource<String, Custo
}
list.addAll(mCustomerDao.customerNameLoadAfter(key, pageSize));
} else {
- list = mCustomerDao.customerNameInitial(initialLoadSize);
+ list = mCustomerDao.customerNameInitial(params.requestedLoadSize);
}
- if (enablePlaceholders && !list.isEmpty()) {
+ if (params.placeholdersEnabled && !list.isEmpty()) {
String firstKey = getKey(list.get(0));
String lastKey = getKey(list.get(list.size() - 1));
@@ -108,16 +108,18 @@ public class LastNameAscCustomerDataSource extends KeyedDataSource<String, Custo
}
@Override
- public void loadAfter(@NonNull String currentEndKey, int pageSize,
+ public void loadAfter(@NonNull LoadParams<String> params,
@NonNull LoadCallback<Customer> callback) {
- callback.onResult(mCustomerDao.customerNameLoadAfter(currentEndKey, pageSize));
+ callback.onResult(mCustomerDao.customerNameLoadAfter(params.key, params.requestedLoadSize));
}
@Override
- public void loadBefore(@NonNull String currentBeginKey, int pageSize,
+ public void loadBefore(@NonNull LoadParams<String> params,
@NonNull LoadCallback<Customer> callback) {
- List<Customer> list = mCustomerDao.customerNameLoadBefore(currentBeginKey, pageSize);
+ List<Customer> list = mCustomerDao.customerNameLoadBefore(
+ params.key, params.requestedLoadSize);
Collections.reverse(list);
callback.onResult(list);
}
}
+
diff --git a/android/arch/persistence/room/integration/testapp/test/ConstructorTest.java b/android/arch/persistence/room/integration/testapp/test/ConstructorTest.java
index 7a21d989..46c875ce 100644
--- a/android/arch/persistence/room/integration/testapp/test/ConstructorTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/ConstructorTest.java
@@ -28,6 +28,8 @@ import android.arch.persistence.room.PrimaryKey;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Room;
import android.arch.persistence.room.RoomDatabase;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -40,7 +42,8 @@ import org.junit.runner.RunWith;
@SuppressWarnings("SqlNoDataSourceInspection")
@SmallTest
public class ConstructorTest {
- @Database(version = 1, entities = {FullConstructor.class, PartialConstructor.class},
+ @Database(version = 1, entities = {FullConstructor.class, PartialConstructor.class,
+ EntityWithAnnotations.class},
exportSchema = false)
abstract static class MyDb extends RoomDatabase {
abstract MyDao dao();
@@ -59,6 +62,12 @@ public class ConstructorTest {
@Query("SELECT * FROM pc WHERE a = :a")
PartialConstructor loadPartial(int a);
+
+ @Insert
+ void insertEntityWithAnnotations(EntityWithAnnotations... items);
+
+ @Query("SELECT * FROM EntityWithAnnotations")
+ EntityWithAnnotations getEntitiWithAnnotations();
}
@SuppressWarnings("WeakerAccess")
@@ -167,6 +176,50 @@ public class ConstructorTest {
}
}
+ @SuppressWarnings("WeakerAccess")
+ @Entity
+ static class EntityWithAnnotations {
+ @PrimaryKey
+ @NonNull
+ public final String id;
+
+ @NonNull
+ public final String username;
+
+ @Nullable
+ public final String displayName;
+
+ EntityWithAnnotations(
+ @NonNull String id,
+ @NonNull String username,
+ @Nullable String displayName) {
+ this.id = id;
+ this.username = username;
+ this.displayName = displayName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ EntityWithAnnotations that = (EntityWithAnnotations) o;
+
+ if (!id.equals(that.id)) return false;
+ if (!username.equals(that.username)) return false;
+ return displayName != null ? displayName.equals(that.displayName)
+ : that.displayName == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id.hashCode();
+ result = 31 * result + username.hashCode();
+ result = 31 * result + (displayName != null ? displayName.hashCode() : 0);
+ return result;
+ }
+ }
+
private MyDb mDb;
private MyDao mDao;
@@ -193,4 +246,11 @@ public class ConstructorTest {
PartialConstructor load = mDao.loadPartial(3);
assertThat(load, is(item));
}
+
+ @Test // for bug b/69562125
+ public void entityWithAnnotations() {
+ EntityWithAnnotations item = new EntityWithAnnotations("a", "b", null);
+ mDao.insertEntityWithAnnotations(item);
+ assertThat(mDao.getEntitiWithAnnotations(), is(item));
+ }
}
diff --git a/android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java b/android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java
index 45233f37..c3ebfe94 100644
--- a/android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/PojoWithRelationTest.java
@@ -22,9 +22,11 @@ import static org.hamcrest.MatcherAssert.assertThat;
import android.arch.persistence.room.integration.testapp.vo.EmbeddedUserAndAllPets;
import android.arch.persistence.room.integration.testapp.vo.Pet;
+import android.arch.persistence.room.integration.testapp.vo.PetWithToyIds;
import android.arch.persistence.room.integration.testapp.vo.Toy;
import android.arch.persistence.room.integration.testapp.vo.User;
import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets;
+import android.arch.persistence.room.integration.testapp.vo.UserAndPetAdoptionDates;
import android.arch.persistence.room.integration.testapp.vo.UserIdAndPetNames;
import android.arch.persistence.room.integration.testapp.vo.UserWithPetsAndToys;
import android.support.test.filters.SmallTest;
@@ -33,8 +35,10 @@ import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.Date;
import java.util.List;
@RunWith(AndroidJUnit4.class)
@@ -141,4 +145,90 @@ public class PojoWithRelationTest extends TestDatabaseTest {
assertThat(relationContainer.getUserAndAllPets().user.getId(), is(1));
assertThat(relationContainer.getUserAndAllPets().pets.size(), is(2));
}
+
+ @Test
+ public void boxedPrimitiveList() {
+ Pet pet1 = TestUtil.createPet(3);
+ Pet pet2 = TestUtil.createPet(5);
+
+ Toy pet1_toy1 = TestUtil.createToyForPet(pet1, 10);
+ Toy pet1_toy2 = TestUtil.createToyForPet(pet1, 20);
+ Toy pet2_toy1 = TestUtil.createToyForPet(pet2, 30);
+
+ mPetDao.insertOrReplace(pet1, pet2);
+ mToyDao.insert(pet1_toy1, pet1_toy2, pet2_toy1);
+
+ List<PetWithToyIds> petWithToyIds = mPetDao.allPetsWithToyIds();
+ //noinspection ArraysAsListWithZeroOrOneArgument
+ assertThat(petWithToyIds, is(
+ Arrays.asList(
+ new PetWithToyIds(pet1, Arrays.asList(10, 20)),
+ new PetWithToyIds(pet2, Arrays.asList(30)))
+ ));
+ }
+
+ @Test
+ public void viaTypeConverter() {
+ User user = TestUtil.createUser(3);
+ Pet pet1 = TestUtil.createPet(3);
+ Date date1 = new Date(300);
+ pet1.setAdoptionDate(date1);
+ Pet pet2 = TestUtil.createPet(5);
+ Date date2 = new Date(700);
+ pet2.setAdoptionDate(date2);
+
+ pet1.setUserId(3);
+ pet2.setUserId(3);
+ mUserDao.insert(user);
+ mPetDao.insertOrReplace(pet1, pet2);
+
+ List<UserAndPetAdoptionDates> adoptions =
+ mUserPetDao.loadUserWithPetAdoptionDates();
+
+ assertThat(adoptions, is(Arrays.asList(
+ new UserAndPetAdoptionDates(user, Arrays.asList(new Date(300), new Date(700)))
+ )));
+ }
+
+ @Test
+ public void largeRelation_child() {
+ User user = TestUtil.createUser(3);
+ List<Pet> pets = new ArrayList<>();
+ for (int i = 0; i < 2000; i++) {
+ Pet pet = TestUtil.createPet(i + 1);
+ pet.setUserId(3);
+ }
+ mUserDao.insert(user);
+ mPetDao.insertAll(pets.toArray(new Pet[pets.size()]));
+ List<UserAndAllPets> result = mUserPetDao.loadAllUsersWithTheirPets();
+ assertThat(result.size(), is(1));
+ assertThat(result.get(0).user, is(user));
+ assertThat(result.get(0).pets, is(pets));
+ }
+
+ @Test
+ public void largeRelation_parent() {
+ final List<User> users = new ArrayList<>();
+ final List<Pet> pets = new ArrayList<>();
+ for (int i = 0; i < 2000; i++) {
+ User user = TestUtil.createUser(i + 1);
+ users.add(user);
+ Pet pet = TestUtil.createPet(i + 1);
+ pet.setUserId(user.getId());
+ pets.add(pet);
+ }
+ mDatabase.runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ mUserDao.insertAll(users.toArray(new User[users.size()]));
+ mPetDao.insertAll(pets.toArray(new Pet[pets.size()]));
+ }
+ });
+ List<UserAndAllPets> result = mUserPetDao.loadAllUsersWithTheirPets();
+ assertThat(result.size(), is(2000));
+ for (int i = 0; i < 2000; i++) {
+ assertThat(result.get(i).user, is(users.get(i)));
+ assertThat(result.get(i).pets, is(Collections.singletonList(pets.get(i))));
+ }
+ }
}
diff --git a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
index f8049f35..de45ebb3 100644
--- a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
@@ -35,6 +35,8 @@ import android.arch.persistence.room.integration.testapp.dao.ProductDao;
import android.arch.persistence.room.integration.testapp.dao.UserDao;
import android.arch.persistence.room.integration.testapp.dao.UserPetDao;
import android.arch.persistence.room.integration.testapp.vo.BlobEntity;
+import android.arch.persistence.room.integration.testapp.vo.Day;
+import android.arch.persistence.room.integration.testapp.vo.NameAndLastName;
import android.arch.persistence.room.integration.testapp.vo.Pet;
import android.arch.persistence.room.integration.testapp.vo.Product;
import android.arch.persistence.room.integration.testapp.vo.User;
@@ -47,8 +49,6 @@ import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
-import junit.framework.AssertionFailedError;
-
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -57,7 +57,9 @@ import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
@SmallTest
@@ -158,7 +160,7 @@ public class SimpleEntityReadWriteTest {
User user2 = TestUtil.createUser(3);
try {
mUserDao.insert(user2);
- throw new AssertionFailedError("didn't throw in conflicting insertion");
+ throw new AssertionError("didn't throw in conflicting insertion");
} catch (SQLiteException ignored) {
}
}
@@ -524,4 +526,44 @@ public class SimpleEntityReadWriteTest {
assertTrue("SQLiteConstraintException expected", caught);
assertThat(mUserDao.count(), is(0));
}
+
+ @Test
+ public void tablePrefix_simpleSelect() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ NameAndLastName result = mUserDao.getNameAndLastName(3);
+ assertThat(result.getName(), is(user.getName()));
+ assertThat(result.getLastName(), is(user.getLastName()));
+ }
+
+ @Test
+ public void enumSet_simpleLoad() {
+ User a = TestUtil.createUser(3);
+ Set<Day> expected = toSet(Day.MONDAY, Day.TUESDAY);
+ a.setWorkDays(expected);
+ mUserDao.insert(a);
+ User loaded = mUserDao.load(3);
+ assertThat(loaded.getWorkDays(), is(expected));
+ }
+
+ @Test
+ public void enumSet_query() {
+ User user1 = TestUtil.createUser(3);
+ user1.setWorkDays(toSet(Day.MONDAY, Day.FRIDAY));
+ User user2 = TestUtil.createUser(5);
+ user2.setWorkDays(toSet(Day.MONDAY, Day.THURSDAY));
+ mUserDao.insert(user1);
+ mUserDao.insert(user2);
+ List<User> empty = mUserDao.findUsersByWorkDays(toSet(Day.WEDNESDAY));
+ assertThat(empty.size(), is(0));
+ List<User> friday = mUserDao.findUsersByWorkDays(toSet(Day.FRIDAY));
+ assertThat(friday, is(Arrays.asList(user1)));
+ List<User> monday = mUserDao.findUsersByWorkDays(toSet(Day.MONDAY));
+ assertThat(monday, is(Arrays.asList(user1, user2)));
+
+ }
+
+ private Set<Day> toSet(Day... days) {
+ return new HashSet<>(Arrays.asList(days));
+ }
}
diff --git a/android/arch/persistence/room/integration/testapp/test/TestUtil.java b/android/arch/persistence/room/integration/testapp/test/TestUtil.java
index 0a35b2f0..d5309b3b 100644
--- a/android/arch/persistence/room/integration/testapp/test/TestUtil.java
+++ b/android/arch/persistence/room/integration/testapp/test/TestUtil.java
@@ -20,6 +20,7 @@ import android.arch.persistence.room.integration.testapp.vo.Address;
import android.arch.persistence.room.integration.testapp.vo.Coordinates;
import android.arch.persistence.room.integration.testapp.vo.Pet;
import android.arch.persistence.room.integration.testapp.vo.School;
+import android.arch.persistence.room.integration.testapp.vo.Toy;
import android.arch.persistence.room.integration.testapp.vo.User;
import java.util.ArrayList;
@@ -59,9 +60,18 @@ public class TestUtil {
Pet pet = new Pet();
pet.setPetId(id);
pet.setName(UUID.randomUUID().toString());
+ pet.setAdoptionDate(new Date());
return pet;
}
+ public static Toy createToyForPet(Pet pet, int toyId) {
+ Toy toy = new Toy();
+ toy.setName("toy " + toyId);
+ toy.setId(toyId);
+ toy.setPetId(pet.getPetId());
+ return toy;
+ }
+
public static Pet[] createPetsForUser(int uid, int petStartId, int count) {
Pet[] pets = new Pet[count];
for (int i = 0; i < count; i++) {
diff --git a/android/arch/persistence/room/integration/testapp/vo/Day.java b/android/arch/persistence/room/integration/testapp/vo/Day.java
new file mode 100644
index 00000000..e02b91c4
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/vo/Day.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.integration.testapp.vo;
+
+public enum Day {
+ MONDAY,
+ TUESDAY,
+ WEDNESDAY,
+ THURSDAY,
+ FRIDAY,
+ SATURDAY,
+ SUNDAY
+}
diff --git a/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java b/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java
new file mode 100644
index 00000000..29e25548
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.integration.testapp.vo;
+
+
+public class NameAndLastName {
+ private String mName;
+ private String mLastName;
+
+ public NameAndLastName(String name, String lastName) {
+ mName = name;
+ mLastName = lastName;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getLastName() {
+ return mLastName;
+ }
+}
diff --git a/android/arch/persistence/room/integration/testapp/vo/Pet.java b/android/arch/persistence/room/integration/testapp/vo/Pet.java
index 8806e10c..cc7549fa 100644
--- a/android/arch/persistence/room/integration/testapp/vo/Pet.java
+++ b/android/arch/persistence/room/integration/testapp/vo/Pet.java
@@ -20,6 +20,8 @@ import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.PrimaryKey;
+import java.util.Date;
+
@Entity
public class Pet {
@PrimaryKey
@@ -28,6 +30,8 @@ public class Pet {
@ColumnInfo(name = "mPetName")
private String mName;
+ private Date mAdoptionDate;
+
public int getPetId() {
return mPetId;
}
@@ -52,6 +56,14 @@ public class Pet {
mUserId = userId;
}
+ public Date getAdoptionDate() {
+ return mAdoptionDate;
+ }
+
+ public void setAdoptionDate(Date adoptionDate) {
+ mAdoptionDate = adoptionDate;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -61,7 +73,9 @@ public class Pet {
if (mPetId != pet.mPetId) return false;
if (mUserId != pet.mUserId) return false;
- return mName != null ? mName.equals(pet.mName) : pet.mName == null;
+ if (mName != null ? !mName.equals(pet.mName) : pet.mName != null) return false;
+ return mAdoptionDate != null ? mAdoptionDate.equals(pet.mAdoptionDate)
+ : pet.mAdoptionDate == null;
}
@Override
@@ -69,6 +83,17 @@ public class Pet {
int result = mPetId;
result = 31 * result + mUserId;
result = 31 * result + (mName != null ? mName.hashCode() : 0);
+ result = 31 * result + (mAdoptionDate != null ? mAdoptionDate.hashCode() : 0);
return result;
}
+
+ @Override
+ public String toString() {
+ return "Pet{"
+ + "mPetId=" + mPetId
+ + ", mUserId=" + mUserId
+ + ", mName='" + mName + '\''
+ + ", mAdoptionDate=" + mAdoptionDate
+ + '}';
+ }
}
diff --git a/android/arch/persistence/room/integration/testapp/vo/PetWithToyIds.java b/android/arch/persistence/room/integration/testapp/vo/PetWithToyIds.java
new file mode 100644
index 00000000..005afb1c
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/vo/PetWithToyIds.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.integration.testapp.vo;
+
+import android.arch.persistence.room.Embedded;
+import android.arch.persistence.room.Ignore;
+import android.arch.persistence.room.Relation;
+
+import java.util.List;
+
+public class PetWithToyIds {
+ @Embedded
+ public final Pet pet;
+ @Relation(parentColumn = "mPetId", entityColumn = "mPetId", projection = "mId",
+ entity = Toy.class)
+ public List<Integer> toyIds;
+
+ // for the relation
+ public PetWithToyIds(Pet pet) {
+ this.pet = pet;
+ }
+
+ @Ignore
+ public PetWithToyIds(Pet pet, List<Integer> toyIds) {
+ this.pet = pet;
+ this.toyIds = toyIds;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PetWithToyIds that = (PetWithToyIds) o;
+
+ if (pet != null ? !pet.equals(that.pet) : that.pet != null) return false;
+ return toyIds != null ? toyIds.equals(that.toyIds) : that.toyIds == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = pet != null ? pet.hashCode() : 0;
+ result = 31 * result + (toyIds != null ? toyIds.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "PetWithToyIds{"
+ + "pet=" + pet
+ + ", toyIds=" + toyIds
+ + '}';
+ }
+}
diff --git a/android/arch/persistence/room/integration/testapp/vo/User.java b/android/arch/persistence/room/integration/testapp/vo/User.java
index a5b88394..a615819b 100644
--- a/android/arch/persistence/room/integration/testapp/vo/User.java
+++ b/android/arch/persistence/room/integration/testapp/vo/User.java
@@ -23,6 +23,8 @@ import android.arch.persistence.room.TypeConverters;
import android.arch.persistence.room.integration.testapp.TestDatabase;
import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
@Entity
@TypeConverters({TestDatabase.Converters.class})
@@ -46,6 +48,9 @@ public class User {
@ColumnInfo(name = "custommm", collate = ColumnInfo.NOCASE)
private String mCustomField;
+ // bit flags
+ private Set<Day> mWorkDays = new HashSet<>();
+
public int getId() {
return mId;
}
@@ -110,6 +115,15 @@ public class User {
mCustomField = customField;
}
+ public Set<Day> getWorkDays() {
+ return mWorkDays;
+ }
+
+ public void setWorkDays(
+ Set<Day> workDays) {
+ mWorkDays = workDays;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -128,8 +142,11 @@ public class User {
if (mBirthday != null ? !mBirthday.equals(user.mBirthday) : user.mBirthday != null) {
return false;
}
- return mCustomField != null ? mCustomField.equals(user.mCustomField)
- : user.mCustomField == null;
+ if (mCustomField != null ? !mCustomField.equals(user.mCustomField)
+ : user.mCustomField != null) {
+ return false;
+ }
+ return mWorkDays != null ? mWorkDays.equals(user.mWorkDays) : user.mWorkDays == null;
}
@Override
@@ -142,6 +159,7 @@ public class User {
result = 31 * result + (mWeight != +0.0f ? Float.floatToIntBits(mWeight) : 0);
result = 31 * result + (mBirthday != null ? mBirthday.hashCode() : 0);
result = 31 * result + (mCustomField != null ? mCustomField.hashCode() : 0);
+ result = 31 * result + (mWorkDays != null ? mWorkDays.hashCode() : 0);
return result;
}
@@ -155,7 +173,8 @@ public class User {
+ ", mAdmin=" + mAdmin
+ ", mWeight=" + mWeight
+ ", mBirthday=" + mBirthday
- + ", mCustom=" + mCustomField
+ + ", mCustomField='" + mCustomField + '\''
+ + ", mWorkDays=" + mWorkDays
+ '}';
}
}
diff --git a/android/arch/persistence/room/integration/testapp/vo/UserAndPetAdoptionDates.java b/android/arch/persistence/room/integration/testapp/vo/UserAndPetAdoptionDates.java
new file mode 100644
index 00000000..28d24959
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/vo/UserAndPetAdoptionDates.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.arch.persistence.room.integration.testapp.vo;
+
+import android.arch.persistence.room.Embedded;
+import android.arch.persistence.room.Ignore;
+import android.arch.persistence.room.Relation;
+
+import java.util.Date;
+import java.util.List;
+
+public class UserAndPetAdoptionDates {
+ @Embedded
+ public final User user;
+ @Relation(entity = Pet.class,
+ parentColumn = "mId",
+ entityColumn = "mUserId",
+ projection = "mAdoptionDate")
+ public List<Date> petAdoptionDates;
+
+ public UserAndPetAdoptionDates(User user) {
+ this.user = user;
+ }
+
+ @Ignore
+ public UserAndPetAdoptionDates(User user, List<Date> petAdoptionDates) {
+ this.user = user;
+ this.petAdoptionDates = petAdoptionDates;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ UserAndPetAdoptionDates that = (UserAndPetAdoptionDates) o;
+
+ if (user != null ? !user.equals(that.user) : that.user != null) return false;
+ return petAdoptionDates != null ? petAdoptionDates.equals(that.petAdoptionDates)
+ : that.petAdoptionDates == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = user != null ? user.hashCode() : 0;
+ result = 31 * result + (petAdoptionDates != null ? petAdoptionDates.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "UserAndPetAdoptionDates{"
+ + "user=" + user
+ + ", petAdoptionDates=" + petAdoptionDates
+ + '}';
+ }
+}
diff --git a/android/bluetooth/BluetoothAdapter.java b/android/bluetooth/BluetoothAdapter.java
index 578a5b8b..3290d57f 100644
--- a/android/bluetooth/BluetoothAdapter.java
+++ b/android/bluetooth/BluetoothAdapter.java
@@ -1,6 +1,6 @@
/*
- * Copyright (C) 2009-2016 The Android Open Source Project
- * Copyright (C) 2015 Samsung LSI
+ * Copyright 2009-2016 The Android Open Source Project
+ * Copyright 2015 Samsung LSI
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -132,9 +132,8 @@ public final class BluetoothAdapter {
* respectively.
* <p>Requires {@link android.Manifest.permission#BLUETOOTH} to receive.
*/
- @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
- public static final String ACTION_STATE_CHANGED =
- "android.bluetooth.adapter.action.STATE_CHANGED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_STATE_CHANGED = "android.bluetooth.adapter.action.STATE_CHANGED";
/**
* Used as an int extra field in {@link #ACTION_STATE_CHANGED}
@@ -144,8 +143,7 @@ public final class BluetoothAdapter {
* {@link #STATE_ON},
* {@link #STATE_TURNING_OFF},
*/
- public static final String EXTRA_STATE =
- "android.bluetooth.adapter.extra.STATE";
+ public static final String EXTRA_STATE = "android.bluetooth.adapter.extra.STATE";
/**
* Used as an int extra field in {@link #ACTION_STATE_CHANGED}
* intents to request the previous power state. Possible values are:
@@ -158,11 +156,17 @@ public final class BluetoothAdapter {
"android.bluetooth.adapter.extra.PREVIOUS_STATE";
/** @hide */
- @IntDef({STATE_OFF, STATE_TURNING_ON, STATE_ON, STATE_TURNING_OFF, STATE_BLE_TURNING_ON,
- STATE_BLE_ON, STATE_BLE_TURNING_OFF})
+ @IntDef(prefix = { "STATE_" }, value = {
+ STATE_OFF,
+ STATE_TURNING_ON,
+ STATE_ON,
+ STATE_TURNING_OFF,
+ STATE_BLE_TURNING_ON,
+ STATE_BLE_ON,
+ STATE_BLE_TURNING_OFF
+ })
@Retention(RetentionPolicy.SOURCE)
- public @interface AdapterState {
- }
+ public @interface AdapterState {}
/**
* Indicates the local Bluetooth adapter is off.
@@ -254,9 +258,8 @@ public final class BluetoothAdapter {
* application can be notified when the device has ended discoverability.
* <p>Requires {@link android.Manifest.permission#BLUETOOTH}
*/
- @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
- public static final String ACTION_REQUEST_DISCOVERABLE =
- "android.bluetooth.adapter.action.REQUEST_DISCOVERABLE";
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String
+ ACTION_REQUEST_DISCOVERABLE = "android.bluetooth.adapter.action.REQUEST_DISCOVERABLE";
/**
* Used as an optional int extra field in {@link
@@ -282,9 +285,8 @@ public final class BluetoothAdapter {
* for global notification whenever Bluetooth is turned on or off.
* <p>Requires {@link android.Manifest.permission#BLUETOOTH}
*/
- @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
- public static final String ACTION_REQUEST_ENABLE =
- "android.bluetooth.adapter.action.REQUEST_ENABLE";
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String
+ ACTION_REQUEST_ENABLE = "android.bluetooth.adapter.action.REQUEST_ENABLE";
/**
* Activity Action: Show a system activity that allows the user to turn off
@@ -305,9 +307,8 @@ public final class BluetoothAdapter {
*
* @hide
*/
- @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
- public static final String ACTION_REQUEST_DISABLE =
- "android.bluetooth.adapter.action.REQUEST_DISABLE";
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String
+ ACTION_REQUEST_DISABLE = "android.bluetooth.adapter.action.REQUEST_DISABLE";
/**
* Activity Action: Show a system activity that allows user to enable BLE scans even when
@@ -334,9 +335,8 @@ public final class BluetoothAdapter {
* respectively.
* <p>Requires {@link android.Manifest.permission#BLUETOOTH}
*/
- @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
- public static final String ACTION_SCAN_MODE_CHANGED =
- "android.bluetooth.adapter.action.SCAN_MODE_CHANGED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_SCAN_MODE_CHANGED = "android.bluetooth.adapter.action.SCAN_MODE_CHANGED";
/**
* Used as an int extra field in {@link #ACTION_SCAN_MODE_CHANGED}
@@ -357,10 +357,13 @@ public final class BluetoothAdapter {
"android.bluetooth.adapter.extra.PREVIOUS_SCAN_MODE";
/** @hide */
- @IntDef({SCAN_MODE_NONE, SCAN_MODE_CONNECTABLE, SCAN_MODE_CONNECTABLE_DISCOVERABLE})
+ @IntDef(prefix = { "SCAN_" }, value = {
+ SCAN_MODE_NONE,
+ SCAN_MODE_CONNECTABLE,
+ SCAN_MODE_CONNECTABLE_DISCOVERABLE
+ })
@Retention(RetentionPolicy.SOURCE)
- public @interface ScanMode {
- }
+ public @interface ScanMode {}
/**
* Indicates that both inquiry scan and page scan are disabled on the local
@@ -396,17 +399,15 @@ public final class BluetoothAdapter {
* discovery.
* <p>Requires {@link android.Manifest.permission#BLUETOOTH} to receive.
*/
- @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
- public static final String ACTION_DISCOVERY_STARTED =
- "android.bluetooth.adapter.action.DISCOVERY_STARTED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_DISCOVERY_STARTED = "android.bluetooth.adapter.action.DISCOVERY_STARTED";
/**
* Broadcast Action: The local Bluetooth adapter has finished the device
* discovery process.
* <p>Requires {@link android.Manifest.permission#BLUETOOTH} to receive.
*/
- @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
- public static final String ACTION_DISCOVERY_FINISHED =
- "android.bluetooth.adapter.action.DISCOVERY_FINISHED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_DISCOVERY_FINISHED = "android.bluetooth.adapter.action.DISCOVERY_FINISHED";
/**
* Broadcast Action: The local Bluetooth adapter has changed its friendly
@@ -416,9 +417,8 @@ public final class BluetoothAdapter {
* the name.
* <p>Requires {@link android.Manifest.permission#BLUETOOTH} to receive.
*/
- @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
- public static final String ACTION_LOCAL_NAME_CHANGED =
- "android.bluetooth.adapter.action.LOCAL_NAME_CHANGED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_LOCAL_NAME_CHANGED = "android.bluetooth.adapter.action.LOCAL_NAME_CHANGED";
/**
* Used as a String extra field in {@link #ACTION_LOCAL_NAME_CHANGED}
* intents to request the local Bluetooth name.
@@ -451,8 +451,8 @@ public final class BluetoothAdapter {
*
* <p>Requires {@link android.Manifest.permission#BLUETOOTH} to receive.
*/
- @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
- public static final String ACTION_CONNECTION_STATE_CHANGED =
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_CONNECTION_STATE_CHANGED =
"android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED";
/**
@@ -476,8 +476,7 @@ public final class BluetoothAdapter {
*
* @hide
*/
- @SystemApi
- public static final String ACTION_BLE_STATE_CHANGED =
+ @SystemApi public static final String ACTION_BLE_STATE_CHANGED =
"android.bluetooth.adapter.action.BLE_STATE_CHANGED";
/**
@@ -574,8 +573,7 @@ public final class BluetoothAdapter {
private final IBluetoothManager mManagerService;
private IBluetooth mService;
- private final ReentrantReadWriteLock mServiceLock =
- new ReentrantReadWriteLock();
+ private final ReentrantReadWriteLock mServiceLock = new ReentrantReadWriteLock();
private final Object mLock = new Object();
private final Map<LeScanCallback, ScanCallback> mLeScanClients;
@@ -655,8 +653,9 @@ public final class BluetoothAdapter {
if (address == null || address.length != 6) {
throw new IllegalArgumentException("Bluetooth address must have 6 bytes");
}
- return new BluetoothDevice(String.format(Locale.US, "%02X:%02X:%02X:%02X:%02X:%02X",
- address[0], address[1], address[2], address[3], address[4], address[5]));
+ return new BluetoothDevice(
+ String.format(Locale.US, "%02X:%02X:%02X:%02X:%02X:%02X", address[0], address[1],
+ address[2], address[3], address[4], address[5]));
}
/**
@@ -668,7 +667,9 @@ public final class BluetoothAdapter {
* on this device before calling this method.
*/
public BluetoothLeAdvertiser getBluetoothLeAdvertiser() {
- if (!getLeAccess()) return null;
+ if (!getLeAccess()) {
+ return null;
+ }
synchronized (mLock) {
if (sBluetoothLeAdvertiser == null) {
sBluetoothLeAdvertiser = new BluetoothLeAdvertiser(mManagerService);
@@ -698,8 +699,7 @@ public final class BluetoothAdapter {
synchronized (mLock) {
if (sPeriodicAdvertisingManager == null) {
- sPeriodicAdvertisingManager =
- new PeriodicAdvertisingManager(mManagerService);
+ sPeriodicAdvertisingManager = new PeriodicAdvertisingManager(mManagerService);
}
}
return sPeriodicAdvertisingManager;
@@ -709,7 +709,9 @@ public final class BluetoothAdapter {
* Returns a {@link BluetoothLeScanner} object for Bluetooth LE scan operations.
*/
public BluetoothLeScanner getBluetoothLeScanner() {
- if (!getLeAccess()) return null;
+ if (!getLeAccess()) {
+ return null;
+ }
synchronized (mLock) {
if (sBluetoothLeScanner == null) {
sBluetoothLeScanner = new BluetoothLeScanner(mManagerService);
@@ -729,7 +731,9 @@ public final class BluetoothAdapter {
public boolean isEnabled() {
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.isEnabled();
+ if (mService != null) {
+ return mService.isEnabled();
+ }
} catch (RemoteException e) {
Log.e(TAG, "", e);
} finally {
@@ -750,7 +754,9 @@ public final class BluetoothAdapter {
@SystemApi
public boolean isLeEnabled() {
final int state = getLeState();
- if (DBG) Log.d(TAG, "isLeEnabled(): " + BluetoothAdapter.nameForState(state));
+ if (DBG) {
+ Log.d(TAG, "isLeEnabled(): " + BluetoothAdapter.nameForState(state));
+ }
return (state == BluetoothAdapter.STATE_ON || state == BluetoothAdapter.STATE_BLE_ON);
}
@@ -781,12 +787,16 @@ public final class BluetoothAdapter {
*/
@SystemApi
public boolean disableBLE() {
- if (!isBleScanAlwaysAvailable()) return false;
+ if (!isBleScanAlwaysAvailable()) {
+ return false;
+ }
int state = getLeState();
if (state == BluetoothAdapter.STATE_ON || state == BluetoothAdapter.STATE_BLE_ON) {
String packageName = ActivityThread.currentPackageName();
- if (DBG) Log.d(TAG, "disableBLE(): de-registering " + packageName);
+ if (DBG) {
+ Log.d(TAG, "disableBLE(): de-registering " + packageName);
+ }
try {
mManagerService.updateBleAppCount(mToken, false, packageName);
} catch (RemoteException e) {
@@ -795,7 +805,9 @@ public final class BluetoothAdapter {
return true;
}
- if (DBG) Log.d(TAG, "disableBLE(): Already disabled");
+ if (DBG) {
+ Log.d(TAG, "disableBLE(): Already disabled");
+ }
return false;
}
@@ -832,16 +844,22 @@ public final class BluetoothAdapter {
*/
@SystemApi
public boolean enableBLE() {
- if (!isBleScanAlwaysAvailable()) return false;
+ if (!isBleScanAlwaysAvailable()) {
+ return false;
+ }
try {
String packageName = ActivityThread.currentPackageName();
mManagerService.updateBleAppCount(mToken, true, packageName);
if (isLeEnabled()) {
- if (DBG) Log.d(TAG, "enableBLE(): Bluetooth already enabled");
+ if (DBG) {
+ Log.d(TAG, "enableBLE(): Bluetooth already enabled");
+ }
return true;
}
- if (DBG) Log.d(TAG, "enableBLE(): Calling enable");
+ if (DBG) {
+ Log.d(TAG, "enableBLE(): Calling enable");
+ }
return mManagerService.enable(packageName);
} catch (RemoteException e) {
Log.e(TAG, "", e);
@@ -877,19 +895,16 @@ public final class BluetoothAdapter {
}
// Consider all internal states as OFF
- if (state == BluetoothAdapter.STATE_BLE_ON
- || state == BluetoothAdapter.STATE_BLE_TURNING_ON
+ if (state == BluetoothAdapter.STATE_BLE_ON || state == BluetoothAdapter.STATE_BLE_TURNING_ON
|| state == BluetoothAdapter.STATE_BLE_TURNING_OFF) {
if (VDBG) {
- Log.d(TAG,
- "Consider " + BluetoothAdapter.nameForState(state) + " state as OFF");
+ Log.d(TAG, "Consider " + BluetoothAdapter.nameForState(state) + " state as OFF");
}
state = BluetoothAdapter.STATE_OFF;
}
if (VDBG) {
- Log.d(TAG,
- "" + hashCode() + ": getState(). Returning " + BluetoothAdapter.nameForState(
- state));
+ Log.d(TAG, "" + hashCode() + ": getState(). Returning " + BluetoothAdapter.nameForState(
+ state));
}
return state;
}
@@ -926,7 +941,9 @@ public final class BluetoothAdapter {
mServiceLock.readLock().unlock();
}
- if (VDBG) Log.d(TAG, "getLeState() returning " + BluetoothAdapter.nameForState(state));
+ if (VDBG) {
+ Log.d(TAG, "getLeState() returning " + BluetoothAdapter.nameForState(state));
+ }
return state;
}
@@ -967,7 +984,9 @@ public final class BluetoothAdapter {
@RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
public boolean enable() {
if (isEnabled()) {
- if (DBG) Log.d(TAG, "enable(): BT already enabled!");
+ if (DBG) {
+ Log.d(TAG, "enable(): BT already enabled!");
+ }
return true;
}
try {
@@ -1093,10 +1112,14 @@ public final class BluetoothAdapter {
* @hide
*/
public ParcelUuid[] getUuids() {
- if (getState() != STATE_ON) return null;
+ if (getState() != STATE_ON) {
+ return null;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.getUuids();
+ if (mService != null) {
+ return mService.getUuids();
+ }
} catch (RemoteException e) {
Log.e(TAG, "", e);
} finally {
@@ -1121,10 +1144,14 @@ public final class BluetoothAdapter {
*/
@RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
public boolean setName(String name) {
- if (getState() != STATE_ON) return false;
+ if (getState() != STATE_ON) {
+ return false;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.setName(name);
+ if (mService != null) {
+ return mService.setName(name);
+ }
} catch (RemoteException e) {
Log.e(TAG, "", e);
} finally {
@@ -1143,10 +1170,14 @@ public final class BluetoothAdapter {
*/
@RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
public BluetoothClass getBluetoothClass() {
- if (getState() != STATE_ON) return null;
+ if (getState() != STATE_ON) {
+ return null;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.getBluetoothClass();
+ if (mService != null) {
+ return mService.getBluetoothClass();
+ }
} catch (RemoteException e) {
Log.e(TAG, "", e);
} finally {
@@ -1168,10 +1199,14 @@ public final class BluetoothAdapter {
*/
@RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
public boolean setBluetoothClass(BluetoothClass bluetoothClass) {
- if (getState() != STATE_ON) return false;
+ if (getState() != STATE_ON) {
+ return false;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.setBluetoothClass(bluetoothClass);
+ if (mService != null) {
+ return mService.setBluetoothClass(bluetoothClass);
+ }
} catch (RemoteException e) {
Log.e(TAG, "", e);
} finally {
@@ -1198,10 +1233,14 @@ public final class BluetoothAdapter {
@RequiresPermission(Manifest.permission.BLUETOOTH)
@ScanMode
public int getScanMode() {
- if (getState() != STATE_ON) return SCAN_MODE_NONE;
+ if (getState() != STATE_ON) {
+ return SCAN_MODE_NONE;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.getScanMode();
+ if (mService != null) {
+ return mService.getScanMode();
+ }
} catch (RemoteException e) {
Log.e(TAG, "", e);
} finally {
@@ -1239,10 +1278,14 @@ public final class BluetoothAdapter {
* @hide
*/
public boolean setScanMode(@ScanMode int mode, int duration) {
- if (getState() != STATE_ON) return false;
+ if (getState() != STATE_ON) {
+ return false;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.setScanMode(mode, duration);
+ if (mService != null) {
+ return mService.setScanMode(mode, duration);
+ }
} catch (RemoteException e) {
Log.e(TAG, "", e);
} finally {
@@ -1253,17 +1296,23 @@ public final class BluetoothAdapter {
/** @hide */
public boolean setScanMode(int mode) {
- if (getState() != STATE_ON) return false;
+ if (getState() != STATE_ON) {
+ return false;
+ }
/* getDiscoverableTimeout() to use the latest from NV than use 0 */
return setScanMode(mode, getDiscoverableTimeout());
}
/** @hide */
public int getDiscoverableTimeout() {
- if (getState() != STATE_ON) return -1;
+ if (getState() != STATE_ON) {
+ return -1;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.getDiscoverableTimeout();
+ if (mService != null) {
+ return mService.getDiscoverableTimeout();
+ }
} catch (RemoteException e) {
Log.e(TAG, "", e);
} finally {
@@ -1274,10 +1323,14 @@ public final class BluetoothAdapter {
/** @hide */
public void setDiscoverableTimeout(int timeout) {
- if (getState() != STATE_ON) return;
+ if (getState() != STATE_ON) {
+ return;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) mService.setDiscoverableTimeout(timeout);
+ if (mService != null) {
+ mService.setDiscoverableTimeout(timeout);
+ }
} catch (RemoteException e) {
Log.e(TAG, "", e);
} finally {
@@ -1296,7 +1349,9 @@ public final class BluetoothAdapter {
public long getDiscoveryEndMillis() {
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.getDiscoveryEndMillis();
+ if (mService != null) {
+ return mService.getDiscoveryEndMillis();
+ }
} catch (RemoteException e) {
Log.e(TAG, "", e);
} finally {
@@ -1336,10 +1391,14 @@ public final class BluetoothAdapter {
*/
@RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
public boolean startDiscovery() {
- if (getState() != STATE_ON) return false;
+ if (getState() != STATE_ON) {
+ return false;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.startDiscovery();
+ if (mService != null) {
+ return mService.startDiscovery();
+ }
} catch (RemoteException e) {
Log.e(TAG, "", e);
} finally {
@@ -1366,10 +1425,14 @@ public final class BluetoothAdapter {
*/
@RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
public boolean cancelDiscovery() {
- if (getState() != STATE_ON) return false;
+ if (getState() != STATE_ON) {
+ return false;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.cancelDiscovery();
+ if (mService != null) {
+ return mService.cancelDiscovery();
+ }
} catch (RemoteException e) {
Log.e(TAG, "", e);
} finally {
@@ -1398,10 +1461,14 @@ public final class BluetoothAdapter {
*/
@RequiresPermission(Manifest.permission.BLUETOOTH)
public boolean isDiscovering() {
- if (getState() != STATE_ON) return false;
+ if (getState() != STATE_ON) {
+ return false;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.isDiscovering();
+ if (mService != null) {
+ return mService.isDiscovering();
+ }
} catch (RemoteException e) {
Log.e(TAG, "", e);
} finally {
@@ -1416,10 +1483,14 @@ public final class BluetoothAdapter {
* @return true if Multiple Advertisement feature is supported
*/
public boolean isMultipleAdvertisementSupported() {
- if (getState() != STATE_ON) return false;
+ if (getState() != STATE_ON) {
+ return false;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.isMultiAdvertisementSupported();
+ if (mService != null) {
+ return mService.isMultiAdvertisementSupported();
+ }
} catch (RemoteException e) {
Log.e(TAG, "failed to get isMultipleAdvertisementSupported, error: ", e);
} finally {
@@ -1454,10 +1525,14 @@ public final class BluetoothAdapter {
* @return true if chipset supports on-chip filtering
*/
public boolean isOffloadedFilteringSupported() {
- if (!getLeAccess()) return false;
+ if (!getLeAccess()) {
+ return false;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.isOffloadedFilteringSupported();
+ if (mService != null) {
+ return mService.isOffloadedFilteringSupported();
+ }
} catch (RemoteException e) {
Log.e(TAG, "failed to get isOffloadedFilteringSupported, error: ", e);
} finally {
@@ -1472,10 +1547,14 @@ public final class BluetoothAdapter {
* @return true if chipset supports on-chip scan batching
*/
public boolean isOffloadedScanBatchingSupported() {
- if (!getLeAccess()) return false;
+ if (!getLeAccess()) {
+ return false;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.isOffloadedScanBatchingSupported();
+ if (mService != null) {
+ return mService.isOffloadedScanBatchingSupported();
+ }
} catch (RemoteException e) {
Log.e(TAG, "failed to get isOffloadedScanBatchingSupported, error: ", e);
} finally {
@@ -1490,10 +1569,14 @@ public final class BluetoothAdapter {
* @return true if chipset supports LE 2M PHY feature
*/
public boolean isLe2MPhySupported() {
- if (!getLeAccess()) return false;
+ if (!getLeAccess()) {
+ return false;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.isLe2MPhySupported();
+ if (mService != null) {
+ return mService.isLe2MPhySupported();
+ }
} catch (RemoteException e) {
Log.e(TAG, "failed to get isExtendedAdvertisingSupported, error: ", e);
} finally {
@@ -1508,10 +1591,14 @@ public final class BluetoothAdapter {
* @return true if chipset supports LE Coded PHY feature
*/
public boolean isLeCodedPhySupported() {
- if (!getLeAccess()) return false;
+ if (!getLeAccess()) {
+ return false;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.isLeCodedPhySupported();
+ if (mService != null) {
+ return mService.isLeCodedPhySupported();
+ }
} catch (RemoteException e) {
Log.e(TAG, "failed to get isLeCodedPhySupported, error: ", e);
} finally {
@@ -1526,10 +1613,14 @@ public final class BluetoothAdapter {
* @return true if chipset supports LE Extended Advertising feature
*/
public boolean isLeExtendedAdvertisingSupported() {
- if (!getLeAccess()) return false;
+ if (!getLeAccess()) {
+ return false;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.isLeExtendedAdvertisingSupported();
+ if (mService != null) {
+ return mService.isLeExtendedAdvertisingSupported();
+ }
} catch (RemoteException e) {
Log.e(TAG, "failed to get isLeExtendedAdvertisingSupported, error: ", e);
} finally {
@@ -1544,10 +1635,14 @@ public final class BluetoothAdapter {
* @return true if chipset supports LE Periodic Advertising feature
*/
public boolean isLePeriodicAdvertisingSupported() {
- if (!getLeAccess()) return false;
+ if (!getLeAccess()) {
+ return false;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.isLePeriodicAdvertisingSupported();
+ if (mService != null) {
+ return mService.isLePeriodicAdvertisingSupported();
+ }
} catch (RemoteException e) {
Log.e(TAG, "failed to get isLePeriodicAdvertisingSupported, error: ", e);
} finally {
@@ -1563,10 +1658,14 @@ public final class BluetoothAdapter {
* @return the maximum LE advertising data length.
*/
public int getLeMaximumAdvertisingDataLength() {
- if (!getLeAccess()) return 0;
+ if (!getLeAccess()) {
+ return 0;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.getLeMaximumAdvertisingDataLength();
+ if (mService != null) {
+ return mService.getLeMaximumAdvertisingDataLength();
+ }
} catch (RemoteException e) {
Log.e(TAG, "failed to get getLeMaximumAdvertisingDataLength, error: ", e);
} finally {
@@ -1582,7 +1681,9 @@ public final class BluetoothAdapter {
* @hide
*/
public boolean isHardwareTrackingFiltersAvailable() {
- if (!getLeAccess()) return false;
+ if (!getLeAccess()) {
+ return false;
+ }
try {
IBluetoothGatt iGatt = mManagerService.getBluetoothGatt();
if (iGatt == null) {
@@ -1669,7 +1770,9 @@ public final class BluetoothAdapter {
}
try {
mServiceLock.readLock().lock();
- if (mService != null) return toDeviceSet(mService.getBondedDevices());
+ if (mService != null) {
+ return toDeviceSet(mService.getBondedDevices());
+ }
return toDeviceSet(new BluetoothDevice[0]);
} catch (RemoteException e) {
Log.e(TAG, "", e);
@@ -1723,10 +1826,14 @@ public final class BluetoothAdapter {
* @hide
*/
public int getConnectionState() {
- if (getState() != STATE_ON) return BluetoothAdapter.STATE_DISCONNECTED;
+ if (getState() != STATE_ON) {
+ return BluetoothAdapter.STATE_DISCONNECTED;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.getAdapterConnectionState();
+ if (mService != null) {
+ return mService.getAdapterConnectionState();
+ }
} catch (RemoteException e) {
Log.e(TAG, "getConnectionState:", e);
} finally {
@@ -1750,10 +1857,14 @@ public final class BluetoothAdapter {
*/
@RequiresPermission(Manifest.permission.BLUETOOTH)
public int getProfileConnectionState(int profile) {
- if (getState() != STATE_ON) return BluetoothProfile.STATE_DISCONNECTED;
+ if (getState() != STATE_ON) {
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
try {
mServiceLock.readLock().lock();
- if (mService != null) return mService.getProfileConnectionState(profile);
+ if (mService != null) {
+ return mService.getProfileConnectionState(profile);
+ }
} catch (RemoteException e) {
Log.e(TAG, "getProfileConnectionState:", e);
} finally {
@@ -1790,7 +1901,7 @@ public final class BluetoothAdapter {
* <p>Valid RFCOMM channels are in range 1 to 30.
* <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
* <p>To auto assign a channel without creating a SDP record use
- * {@link SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as channel number.
+ * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as channel number.
*
* @param channel RFCOMM channel to listen on
* @param mitm enforce man-in-the-middle protection for authentication.
@@ -1802,10 +1913,10 @@ public final class BluetoothAdapter {
* @hide
*/
public BluetoothServerSocket listenUsingRfcommOn(int channel, boolean mitm,
- boolean min16DigitPin)
- throws IOException {
- BluetoothServerSocket socket = new BluetoothServerSocket(
- BluetoothSocket.TYPE_RFCOMM, true, true, channel, mitm, min16DigitPin);
+ boolean min16DigitPin) throws IOException {
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, true, true, channel, mitm,
+ min16DigitPin);
int errno = socket.mSocket.bindListen();
if (channel == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
socket.setChannel(socket.mSocket.getPort());
@@ -1913,8 +2024,8 @@ public final class BluetoothAdapter {
* permissions, or channel in use.
* @hide
*/
- public BluetoothServerSocket listenUsingEncryptedRfcommWithServiceRecord(
- String name, UUID uuid) throws IOException {
+ public BluetoothServerSocket listenUsingEncryptedRfcommWithServiceRecord(String name, UUID uuid)
+ throws IOException {
return createNewRfcommSocketAndRecord(name, uuid, false, true);
}
@@ -1922,8 +2033,8 @@ public final class BluetoothAdapter {
private BluetoothServerSocket createNewRfcommSocketAndRecord(String name, UUID uuid,
boolean auth, boolean encrypt) throws IOException {
BluetoothServerSocket socket;
- socket = new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, auth,
- encrypt, new ParcelUuid(uuid));
+ socket = new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, auth, encrypt,
+ new ParcelUuid(uuid));
socket.setServiceName(name);
int errno = socket.mSocket.bindListen();
if (errno != 0) {
@@ -1945,8 +2056,8 @@ public final class BluetoothAdapter {
* @hide
*/
public BluetoothServerSocket listenUsingInsecureRfcommOn(int port) throws IOException {
- BluetoothServerSocket socket = new BluetoothServerSocket(
- BluetoothSocket.TYPE_RFCOMM, false, false, port);
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, false, false, port);
int errno = socket.mSocket.bindListen();
if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
socket.setChannel(socket.mSocket.getPort());
@@ -1969,10 +2080,9 @@ public final class BluetoothAdapter {
* permissions.
* @hide
*/
- public BluetoothServerSocket listenUsingEncryptedRfcommOn(int port)
- throws IOException {
- BluetoothServerSocket socket = new BluetoothServerSocket(
- BluetoothSocket.TYPE_RFCOMM, false, true, port);
+ public BluetoothServerSocket listenUsingEncryptedRfcommOn(int port) throws IOException {
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, false, true, port);
int errno = socket.mSocket.bindListen();
if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
socket.setChannel(socket.mSocket.getPort());
@@ -1996,8 +2106,8 @@ public final class BluetoothAdapter {
* @hide
*/
public static BluetoothServerSocket listenUsingScoOn() throws IOException {
- BluetoothServerSocket socket = new BluetoothServerSocket(
- BluetoothSocket.TYPE_SCO, false, false, -1);
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_SCO, false, false, -1);
int errno = socket.mSocket.bindListen();
if (errno < 0) {
//TODO(BT): Throw the same exception error code
@@ -2011,7 +2121,7 @@ public final class BluetoothAdapter {
* Construct an encrypted, authenticated, L2CAP server socket.
* Call #accept to retrieve connections to this socket.
* <p>To auto assign a port without creating a SDP record use
- * {@link SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number.
+ * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number.
*
* @param port the PSM to listen on
* @param mitm enforce man-in-the-middle protection for authentication.
@@ -2024,8 +2134,9 @@ public final class BluetoothAdapter {
*/
public BluetoothServerSocket listenUsingL2capOn(int port, boolean mitm, boolean min16DigitPin)
throws IOException {
- BluetoothServerSocket socket = new BluetoothServerSocket(
- BluetoothSocket.TYPE_L2CAP, true, true, port, mitm, min16DigitPin);
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP, true, true, port, mitm,
+ min16DigitPin);
int errno = socket.mSocket.bindListen();
if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
socket.setChannel(socket.mSocket.getPort());
@@ -2043,7 +2154,7 @@ public final class BluetoothAdapter {
* Construct an encrypted, authenticated, L2CAP server socket.
* Call #accept to retrieve connections to this socket.
* <p>To auto assign a port without creating a SDP record use
- * {@link SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number.
+ * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number.
*
* @param port the PSM to listen on
* @return An L2CAP BluetoothServerSocket
@@ -2060,7 +2171,7 @@ public final class BluetoothAdapter {
* Construct an insecure L2CAP server socket.
* Call #accept to retrieve connections to this socket.
* <p>To auto assign a port without creating a SDP record use
- * {@link SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number.
+ * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number.
*
* @param port the PSM to listen on
* @return An L2CAP BluetoothServerSocket
@@ -2069,8 +2180,9 @@ public final class BluetoothAdapter {
* @hide
*/
public BluetoothServerSocket listenUsingInsecureL2capOn(int port) throws IOException {
- BluetoothServerSocket socket = new BluetoothServerSocket(
- BluetoothSocket.TYPE_L2CAP, false, false, port, false, false);
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP, false, false, port, false,
+ false);
int errno = socket.mSocket.bindListen();
if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
socket.setChannel(socket.mSocket.getPort());
@@ -2114,7 +2226,9 @@ public final class BluetoothAdapter {
*/
public boolean getProfileProxy(Context context, BluetoothProfile.ServiceListener listener,
int profile) {
- if (context == null || listener == null) return false;
+ if (context == null || listener == null) {
+ return false;
+ }
if (profile == BluetoothProfile.HEADSET) {
BluetoothHeadset headset = new BluetoothHeadset(context, listener);
@@ -2172,7 +2286,9 @@ public final class BluetoothAdapter {
* @param proxy Profile proxy object
*/
public void closeProfileProxy(int profile, BluetoothProfile proxy) {
- if (proxy == null) return;
+ if (proxy == null) {
+ return;
+ }
switch (profile) {
case BluetoothProfile.HEADSET:
@@ -2241,7 +2357,9 @@ public final class BluetoothAdapter {
private final IBluetoothManagerCallback mManagerCallback =
new IBluetoothManagerCallback.Stub() {
public void onBluetoothServiceUp(IBluetooth bluetoothService) {
- if (DBG) Log.d(TAG, "onBluetoothServiceUp: " + bluetoothService);
+ if (DBG) {
+ Log.d(TAG, "onBluetoothServiceUp: " + bluetoothService);
+ }
mServiceLock.writeLock().lock();
mService = bluetoothService;
@@ -2263,14 +2381,22 @@ public final class BluetoothAdapter {
}
public void onBluetoothServiceDown() {
- if (DBG) Log.d(TAG, "onBluetoothServiceDown: " + mService);
+ if (DBG) {
+ Log.d(TAG, "onBluetoothServiceDown: " + mService);
+ }
try {
mServiceLock.writeLock().lock();
mService = null;
- if (mLeScanClients != null) mLeScanClients.clear();
- if (sBluetoothLeAdvertiser != null) sBluetoothLeAdvertiser.cleanup();
- if (sBluetoothLeScanner != null) sBluetoothLeScanner.cleanup();
+ if (mLeScanClients != null) {
+ mLeScanClients.clear();
+ }
+ if (sBluetoothLeAdvertiser != null) {
+ sBluetoothLeAdvertiser.cleanup();
+ }
+ if (sBluetoothLeScanner != null) {
+ sBluetoothLeScanner.cleanup();
+ }
} finally {
mServiceLock.writeLock().unlock();
}
@@ -2291,7 +2417,9 @@ public final class BluetoothAdapter {
}
public void onBrEdrDown() {
- if (VDBG) Log.i(TAG, "onBrEdrDown: " + mService);
+ if (VDBG) {
+ Log.i(TAG, "onBrEdrDown: " + mService);
+ }
}
};
@@ -2305,7 +2433,9 @@ public final class BluetoothAdapter {
@RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN)
public boolean enableNoAutoConnect() {
if (isEnabled()) {
- if (DBG) Log.d(TAG, "enableNoAutoConnect(): BT already enabled!");
+ if (DBG) {
+ Log.d(TAG, "enableNoAutoConnect(): BT already enabled!");
+ }
return true;
}
try {
@@ -2354,7 +2484,10 @@ public final class BluetoothAdapter {
* @hide
*/
public interface BluetoothStateChangeCallback {
- public void onBluetoothStateChange(boolean on);
+ /**
+ * @hide
+ */
+ void onBluetoothStateChange(boolean on);
}
/**
@@ -2363,8 +2496,7 @@ public final class BluetoothAdapter {
public class StateChangeCallbackWrapper extends IBluetoothStateChangeCallback.Stub {
private BluetoothStateChangeCallback mCallback;
- StateChangeCallbackWrapper(BluetoothStateChangeCallback
- callback) {
+ StateChangeCallbackWrapper(BluetoothStateChangeCallback callback) {
mCallback = callback;
}
@@ -2461,7 +2593,7 @@ public final class BluetoothAdapter {
* if no RSSI value is available.
* @param scanRecord The content of the advertisement record offered by the remote device.
*/
- public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord);
+ void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord);
}
/**
@@ -2497,20 +2629,28 @@ public final class BluetoothAdapter {
@Deprecated
@RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
public boolean startLeScan(final UUID[] serviceUuids, final LeScanCallback callback) {
- if (DBG) Log.d(TAG, "startLeScan(): " + Arrays.toString(serviceUuids));
+ if (DBG) {
+ Log.d(TAG, "startLeScan(): " + Arrays.toString(serviceUuids));
+ }
if (callback == null) {
- if (DBG) Log.e(TAG, "startLeScan: null callback");
+ if (DBG) {
+ Log.e(TAG, "startLeScan: null callback");
+ }
return false;
}
BluetoothLeScanner scanner = getBluetoothLeScanner();
if (scanner == null) {
- if (DBG) Log.e(TAG, "startLeScan: cannot get BluetoothLeScanner");
+ if (DBG) {
+ Log.e(TAG, "startLeScan: cannot get BluetoothLeScanner");
+ }
return false;
}
synchronized (mLeScanClients) {
if (mLeScanClients.containsKey(callback)) {
- if (DBG) Log.e(TAG, "LE Scan has already started");
+ if (DBG) {
+ Log.e(TAG, "LE Scan has already started");
+ }
return false;
}
@@ -2540,7 +2680,9 @@ public final class BluetoothAdapter {
}
List<ParcelUuid> scanServiceUuids = scanRecord.getServiceUuids();
if (scanServiceUuids == null || !scanServiceUuids.containsAll(uuids)) {
- if (DBG) Log.d(TAG, "uuids does not match");
+ if (DBG) {
+ Log.d(TAG, "uuids does not match");
+ }
return;
}
}
@@ -2548,16 +2690,18 @@ public final class BluetoothAdapter {
scanRecord.getBytes());
}
};
- ScanSettings settings = new ScanSettings.Builder()
- .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
- .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
+ ScanSettings settings = new ScanSettings.Builder().setCallbackType(
+ ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+ .build();
List<ScanFilter> filters = new ArrayList<ScanFilter>();
if (serviceUuids != null && serviceUuids.length > 0) {
// Note scan filter does not support matching an UUID array so we put one
// UUID to hardware and match the whole array in callback.
- ScanFilter filter = new ScanFilter.Builder().setServiceUuid(
- new ParcelUuid(serviceUuids[0])).build();
+ ScanFilter filter =
+ new ScanFilter.Builder().setServiceUuid(new ParcelUuid(serviceUuids[0]))
+ .build();
filters.add(filter);
}
scanner.startScan(filters, settings, scanCallback);
@@ -2582,7 +2726,9 @@ public final class BluetoothAdapter {
@Deprecated
@RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
public void stopLeScan(LeScanCallback callback) {
- if (DBG) Log.d(TAG, "stopLeScan()");
+ if (DBG) {
+ Log.d(TAG, "stopLeScan()");
+ }
BluetoothLeScanner scanner = getBluetoothLeScanner();
if (scanner == null) {
return;
@@ -2590,7 +2736,9 @@ public final class BluetoothAdapter {
synchronized (mLeScanClients) {
ScanCallback scanCallback = mLeScanClients.remove(callback);
if (scanCallback == null) {
- if (DBG) Log.d(TAG, "scan not started yet");
+ if (DBG) {
+ Log.d(TAG, "scan not started yet");
+ }
return;
}
scanner.stopScan(scanCallback);
diff --git a/android/bluetooth/BluetoothHeadset.java b/android/bluetooth/BluetoothHeadset.java
index 1241f230..838d3153 100644
--- a/android/bluetooth/BluetoothHeadset.java
+++ b/android/bluetooth/BluetoothHeadset.java
@@ -678,33 +678,6 @@ public final class BluetoothHeadset implements BluetoothProfile {
}
/**
- * Get battery usage hint for Bluetooth Headset service.
- * This is a monotonically increasing integer. Wraps to 0 at
- * Integer.MAX_INT, and at boot.
- * Current implementation returns the number of AT commands handled since
- * boot. This is a good indicator for spammy headset/handsfree units that
- * can keep the device awake by polling for cellular status updates. As a
- * rule of thumb, each AT command prevents the CPU from sleeping for 500 ms
- *
- * @param device the bluetooth headset.
- * @return monotonically increasing battery usage hint, or a negative error code on error
- * @hide
- */
- public int getBatteryUsageHint(BluetoothDevice device) {
- if (VDBG) log("getBatteryUsageHint()");
- final IBluetoothHeadset service = mService;
- if (service != null && isEnabled() && isValidDevice(device)) {
- try {
- return service.getBatteryUsageHint(device);
- } catch (RemoteException e) {
- Log.e(TAG, Log.getStackTraceString(new Throwable()));
- }
- }
- if (service == null) Log.w(TAG, "Proxy not attached to service");
- return -1;
- }
-
- /**
* Indicates if current platform supports voice dialing over bluetooth SCO.
*
* @return true if voice dialing over bluetooth is supported, false otherwise.
@@ -716,49 +689,6 @@ public final class BluetoothHeadset implements BluetoothProfile {
}
/**
- * Accept the incoming connection.
- * Note: This is an internal function and shouldn't be exposed
- *
- * @hide
- */
- public boolean acceptIncomingConnect(BluetoothDevice device) {
- if (DBG) log("acceptIncomingConnect");
- final IBluetoothHeadset service = mService;
- if (service != null && isEnabled()) {
- try {
- return service.acceptIncomingConnect(device);
- } catch (RemoteException e) {
- Log.e(TAG, e.toString());
- }
- } else {
- Log.w(TAG, "Proxy not attached to service");
- if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
- }
- return false;
- }
-
- /**
- * Reject the incoming connection.
- *
- * @hide
- */
- public boolean rejectIncomingConnect(BluetoothDevice device) {
- if (DBG) log("rejectIncomingConnect");
- final IBluetoothHeadset service = mService;
- if (service != null) {
- try {
- return service.rejectIncomingConnect(device);
- } catch (RemoteException e) {
- Log.e(TAG, e.toString());
- }
- } else {
- Log.w(TAG, "Proxy not attached to service");
- if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
- }
- return false;
- }
-
- /**
* Get the current audio state of the Headset.
* Note: This is an internal function and shouldn't be exposed
*
@@ -1053,50 +983,6 @@ public final class BluetoothHeadset implements BluetoothProfile {
}
/**
- * enable WBS codec setting.
- *
- * @return true if successful false if there was some error such as there is no connected
- * headset
- * @hide
- */
- public boolean enableWBS() {
- final IBluetoothHeadset service = mService;
- if (service != null && isEnabled()) {
- try {
- return service.enableWBS();
- } catch (RemoteException e) {
- Log.e(TAG, e.toString());
- }
- } else {
- Log.w(TAG, "Proxy not attached to service");
- if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
- }
- return false;
- }
-
- /**
- * disable WBS codec settting. It set NBS codec.
- *
- * @return true if successful false if there was some error such as there is no connected
- * headset
- * @hide
- */
- public boolean disableWBS() {
- final IBluetoothHeadset service = mService;
- if (service != null && isEnabled()) {
- try {
- return service.disableWBS();
- } catch (RemoteException e) {
- Log.e(TAG, e.toString());
- }
- } else {
- Log.w(TAG, "Proxy not attached to service");
- if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
- }
- return false;
- }
-
- /**
* check if in-band ringing is supported for this platform.
*
* @return true if in-band ringing is supported false if in-band ringing is not supported
@@ -1107,30 +993,6 @@ public final class BluetoothHeadset implements BluetoothProfile {
com.android.internal.R.bool.config_bluetooth_hfp_inband_ringing_support);
}
- /**
- * Send Headset the BIND response from AG to report change in the status of the
- * HF indicators to the headset
- *
- * @param indId Assigned Number of the indicator (defined by SIG)
- * @param indStatus possible values- false-Indicator is disabled, no value changes shall be
- * sent for this indicator true-Indicator is enabled, value changes may be sent for this
- * indicator
- * @hide
- */
- public void bindResponse(int indId, boolean indStatus) {
- final IBluetoothHeadset service = mService;
- if (service != null && isEnabled()) {
- try {
- service.bindResponse(indId, indStatus);
- } catch (RemoteException e) {
- Log.e(TAG, e.toString());
- }
- } else {
- Log.w(TAG, "Proxy not attached to service");
- if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
- }
- }
-
private final IBluetoothProfileServiceConnection mConnection =
new IBluetoothProfileServiceConnection.Stub() {
@Override
diff --git a/android/bluetooth/BluetoothHidDevice.java b/android/bluetooth/BluetoothHidDevice.java
index 6692e137..2fab305b 100644
--- a/android/bluetooth/BluetoothHidDevice.java
+++ b/android/bluetooth/BluetoothHidDevice.java
@@ -31,36 +31,31 @@ import java.util.Arrays;
import java.util.List;
/**
- * Provides the public APIs to control the Bluetooth HID Device
- * profile.
+ * Provides the public APIs to control the Bluetooth HID Device profile.
*
- * BluetoothHidDevice is a proxy object for controlling the Bluetooth HID
- * Device Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
- * the BluetoothHidDevice proxy object.
- *
- * {@hide}
+ * <p>BluetoothHidDevice is a proxy object for controlling the Bluetooth HID Device Service via IPC.
+ * Use {@link BluetoothAdapter#getProfileProxy} to get the BluetoothHidDevice proxy object.
*/
public final class BluetoothHidDevice implements BluetoothProfile {
private static final String TAG = BluetoothHidDevice.class.getSimpleName();
/**
- * Intent used to broadcast the change in connection state of the Input
- * Host profile.
+ * Intent used to broadcast the change in connection state of the Input Host 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>
+ * <li>{@link #EXTRA_STATE} - The current state of the profile.
+ * <li>{@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.
+ * <li>{@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
* </ul>
*
- * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
- * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
- * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
+ * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of {@link
+ * #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, {@link #STATE_CONNECTED}, {@link
+ * #STATE_DISCONNECTING}.
*
- * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
- * receive.
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to receive.
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
public static final String ACTION_CONNECTION_STATE_CHANGED =
@@ -69,9 +64,8 @@ public final class BluetoothHidDevice implements BluetoothProfile {
/**
* Constants representing device subclass.
*
- * @see #registerApp
- * (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings,
- * BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceCallback)
+ * @see #registerApp (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings,
+ * BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceCallback)
*/
public static final byte SUBCLASS1_NONE = (byte) 0x00;
public static final byte SUBCLASS1_KEYBOARD = (byte) 0x40;
@@ -91,7 +85,7 @@ public final class BluetoothHidDevice implements BluetoothProfile {
*
* @see BluetoothHidDeviceCallback#onGetReport(BluetoothDevice, byte, byte, int)
* @see BluetoothHidDeviceCallback#onSetReport(BluetoothDevice, byte, byte, byte[])
- * @see BluetoothHidDeviceCallback#onIntrData(BluetoothDevice, byte, byte[])
+ * @see BluetoothHidDeviceCallback#onInterruptData(BluetoothDevice, byte, byte[])
*/
public static final byte REPORT_TYPE_INPUT = (byte) 1;
public static final byte REPORT_TYPE_OUTPUT = (byte) 2;
@@ -110,8 +104,8 @@ public final class BluetoothHidDevice implements BluetoothProfile {
public static final byte ERROR_RSP_UNKNOWN = (byte) 14;
/**
- * Constants representing protocol mode used set by host. Default is always
- * {@link #PROTOCOL_REPORT_MODE} unless notified otherwise.
+ * Constants representing protocol mode used set by host. Default is always {@link
+ * #PROTOCOL_REPORT_MODE} unless notified otherwise.
*
* @see BluetoothHidDeviceCallback#onSetProtocol(BluetoothDevice, byte)
*/
@@ -126,8 +120,8 @@ public final class BluetoothHidDevice implements BluetoothProfile {
private BluetoothAdapter mAdapter;
- private static class BluetoothHidDeviceCallbackWrapper extends
- IBluetoothHidDeviceCallback.Stub {
+ private static class BluetoothHidDeviceCallbackWrapper
+ extends IBluetoothHidDeviceCallback.Stub {
private BluetoothHidDeviceCallback mCallback;
@@ -136,9 +130,8 @@ public final class BluetoothHidDevice implements BluetoothProfile {
}
@Override
- public void onAppStatusChanged(BluetoothDevice pluggedDevice,
- BluetoothHidDeviceAppConfiguration config, boolean registered) {
- mCallback.onAppStatusChanged(pluggedDevice, config, registered);
+ public void onAppStatusChanged(BluetoothDevice pluggedDevice, boolean registered) {
+ mCallback.onAppStatusChanged(pluggedDevice, registered);
}
@Override
@@ -162,8 +155,8 @@ public final class BluetoothHidDevice implements BluetoothProfile {
}
@Override
- public void onIntrData(BluetoothDevice device, byte reportId, byte[] data) {
- mCallback.onIntrData(device, reportId, data);
+ public void onInterruptData(BluetoothDevice device, byte reportId, byte[] data) {
+ mCallback.onInterruptData(device, reportId, data);
}
@Override
@@ -185,13 +178,11 @@ public final class BluetoothHidDevice implements BluetoothProfile {
doBind();
}
} catch (IllegalStateException e) {
- Log.e(TAG,
- "onBluetoothStateChange: could not bind to HID Dev "
- + "service: ", e);
+ Log.e(TAG, "onBluetoothStateChange: could not bind to HID Dev "
+ + "service: ", e);
} catch (SecurityException e) {
- Log.e(TAG,
- "onBluetoothStateChange: could not bind to HID Dev "
- + "service: ", e);
+ Log.e(TAG, "onBluetoothStateChange: could not bind to HID Dev "
+ + "service: ", e);
}
} else {
Log.d(TAG, "Unbinding service...");
@@ -201,23 +192,25 @@ public final class BluetoothHidDevice implements BluetoothProfile {
}
};
- private final ServiceConnection mConnection = new ServiceConnection() {
- public void onServiceConnected(ComponentName className, IBinder service) {
- Log.d(TAG, "onServiceConnected()");
- mService = IBluetoothHidDevice.Stub.asInterface(service);
- if (mServiceListener != null) {
- mServiceListener.onServiceConnected(BluetoothProfile.HID_DEVICE,
- BluetoothHidDevice.this);
- }
- }
- public void onServiceDisconnected(ComponentName className) {
- Log.d(TAG, "onServiceDisconnected()");
- mService = null;
- if (mServiceListener != null) {
- mServiceListener.onServiceDisconnected(BluetoothProfile.HID_DEVICE);
- }
- }
- };
+ private final ServiceConnection mConnection =
+ new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ Log.d(TAG, "onServiceConnected()");
+ mService = IBluetoothHidDevice.Stub.asInterface(service);
+ if (mServiceListener != null) {
+ mServiceListener.onServiceConnected(
+ BluetoothProfile.HID_DEVICE, BluetoothHidDevice.this);
+ }
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ Log.d(TAG, "onServiceDisconnected()");
+ mService = null;
+ if (mServiceListener != null) {
+ mServiceListener.onServiceDisconnected(BluetoothProfile.HID_DEVICE);
+ }
+ }
+ };
BluetoothHidDevice(Context context, ServiceListener listener) {
Log.v(TAG, "BluetoothHidDevice");
@@ -281,9 +274,7 @@ public final class BluetoothHidDevice implements BluetoothProfile {
mServiceListener = null;
}
- /**
- * {@inheritDoc}
- */
+ /** {@inheritDoc} */
@Override
public List<BluetoothDevice> getConnectedDevices() {
Log.v(TAG, "getConnectedDevices()");
@@ -302,9 +293,7 @@ public final class BluetoothHidDevice implements BluetoothProfile {
return new ArrayList<BluetoothDevice>();
}
- /**
- * {@inheritDoc}
- */
+ /** {@inheritDoc} */
@Override
public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
Log.v(TAG, "getDevicesMatchingConnectionStates(): states=" + Arrays.toString(states));
@@ -323,9 +312,7 @@ public final class BluetoothHidDevice implements BluetoothProfile {
return new ArrayList<BluetoothDevice>();
}
- /**
- * {@inheritDoc}
- */
+ /** {@inheritDoc} */
@Override
public int getConnectionState(BluetoothDevice device) {
Log.v(TAG, "getConnectionState(): device=" + device);
@@ -345,33 +332,31 @@ public final class BluetoothHidDevice implements BluetoothProfile {
}
/**
- * Registers application to be used for HID device. Connections to HID
- * Device are only possible when application is registered. Only one
- * application can be registered at time. When no longer used, application
- * should be unregistered using
- * {@link #unregisterApp(BluetoothHidDeviceAppConfiguration)}.
- * The registration status should be tracked by the application by handling callback from
- * BluetoothHidDeviceCallback#onAppStatusChanged. The app registration status is not related
- * to the return value of this method.
+ * Registers application to be used for HID device. Connections to HID Device are only possible
+ * when application is registered. Only one application can be registered at one time. When an
+ * application is registered, the HID Host service will be disabled until it is unregistered.
+ * When no longer used, application should be unregistered using {@link #unregisterApp()}. The
+ * registration status should be tracked by the application by handling callback from
+ * BluetoothHidDeviceCallback#onAppStatusChanged. The app registration status is not related to
+ * the return value of this method.
*
- * @param sdp {@link BluetoothHidDeviceAppSdpSettings} object of HID Device SDP record.
- * The HID Device SDP record is required.
- * @param inQos {@link BluetoothHidDeviceAppQosSettings} object of Incoming QoS Settings.
- * The Incoming QoS Settings is not required. Use null or default
- * BluetoothHidDeviceAppQosSettings.Builder for default values.
- * @param outQos {@link BluetoothHidDeviceAppQosSettings} object of Outgoing QoS Settings.
- * The Outgoing QoS Settings is not required. Use null or default
- * BluetoothHidDeviceAppQosSettings.Builder for default values.
+ * @param sdp {@link BluetoothHidDeviceAppSdpSettings} object of HID Device SDP record. The HID
+ * Device SDP record is required.
+ * @param inQos {@link BluetoothHidDeviceAppQosSettings} object of Incoming QoS Settings. The
+ * Incoming QoS Settings is not required. Use null or default
+ * BluetoothHidDeviceAppQosSettings.Builder for default values.
+ * @param outQos {@link BluetoothHidDeviceAppQosSettings} object of Outgoing QoS Settings. The
+ * Outgoing QoS Settings is not required. Use null or default
+ * BluetoothHidDeviceAppQosSettings.Builder for default values.
* @param callback {@link BluetoothHidDeviceCallback} object to which callback messages will be
- * sent.
- * The BluetoothHidDeviceCallback object is required.
+ * sent. The BluetoothHidDeviceCallback object is required.
* @return true if the command is successfully sent; otherwise false.
*/
public boolean registerApp(BluetoothHidDeviceAppSdpSettings sdp,
BluetoothHidDeviceAppQosSettings inQos, BluetoothHidDeviceAppQosSettings outQos,
BluetoothHidDeviceCallback callback) {
Log.v(TAG, "registerApp(): sdp=" + sdp + " inQos=" + inQos + " outQos=" + outQos
- + " callback=" + callback);
+ + " callback=" + callback);
boolean result = false;
@@ -382,11 +367,9 @@ public final class BluetoothHidDevice implements BluetoothProfile {
final IBluetoothHidDevice service = mService;
if (service != null) {
try {
- BluetoothHidDeviceAppConfiguration config =
- new BluetoothHidDeviceAppConfiguration();
BluetoothHidDeviceCallbackWrapper cbw =
new BluetoothHidDeviceCallbackWrapper(callback);
- result = service.registerApp(config, sdp, inQos, outQos, cbw);
+ result = service.registerApp(sdp, inQos, outQos, cbw);
} catch (RemoteException e) {
Log.e(TAG, e.toString());
}
@@ -398,22 +381,17 @@ public final class BluetoothHidDevice implements BluetoothProfile {
}
/**
- * Unregisters application. Active connection will be disconnected and no
- * new connections will be allowed until registered again using
- * {@link #registerApp
+ * Unregisters application. Active connection will be disconnected and no new connections will
+ * be allowed until registered again using {@link #registerApp
* (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings,
- * BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceCallback)}
- * The registration status should be tracked by the application by handling callback from
- * BluetoothHidDeviceCallback#onAppStatusChanged. The app registration status is not related
- * to the return value of this method.
+ * BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceCallback)} The registration status should
+ * be tracked by the application by handling callback from
+ * BluetoothHidDeviceCallback#onAppStatusChanged. The app registration status is not related to
+ * the return value of this method.
*
- * @param config {@link BluetoothHidDeviceAppConfiguration} object as obtained from {@link
- * BluetoothHidDeviceCallback#onAppStatusChanged(BluetoothDevice,
- * BluetoothHidDeviceAppConfiguration,
- * boolean)}
* @return true if the command is successfully sent; otherwise false.
*/
- public boolean unregisterApp(BluetoothHidDeviceAppConfiguration config) {
+ public boolean unregisterApp() {
Log.v(TAG, "unregisterApp()");
boolean result = false;
@@ -421,7 +399,7 @@ public final class BluetoothHidDevice implements BluetoothProfile {
final IBluetoothHidDevice service = mService;
if (service != null) {
try {
- result = service.unregisterApp(config);
+ result = service.unregisterApp();
} catch (RemoteException e) {
Log.e(TAG, e.toString());
}
@@ -436,7 +414,7 @@ public final class BluetoothHidDevice implements BluetoothProfile {
* Sends report to remote host using interrupt channel.
*
* @param id Report Id, as defined in descriptor. Can be 0 in case Report Id are not defined in
- * descriptor.
+ * descriptor.
* @param data Report data, not including Report Id.
* @return true if the command is successfully sent; otherwise false.
*/
@@ -458,8 +436,8 @@ public final class BluetoothHidDevice implements BluetoothProfile {
}
/**
- * Sends report to remote host as reply for GET_REPORT request from
- * {@link BluetoothHidDeviceCallback#onGetReport(BluetoothDevice, byte, byte, int)}.
+ * Sends report to remote host as reply for GET_REPORT request from {@link
+ * BluetoothHidDeviceCallback#onGetReport(BluetoothDevice, byte, byte, int)}.
*
* @param type Report Type, as in request.
* @param id Report Id, as in request.
@@ -486,8 +464,8 @@ public final class BluetoothHidDevice implements BluetoothProfile {
}
/**
- * Sends error handshake message as reply for invalid SET_REPORT request
- * from {@link BluetoothHidDeviceCallback#onSetReport(BluetoothDevice, byte, byte, byte[])}.
+ * Sends error handshake message as reply for invalid SET_REPORT request from {@link
+ * BluetoothHidDeviceCallback#onSetReport(BluetoothDevice, byte, byte, byte[])}.
*
* @param error Error to be sent for SET_REPORT via HANDSHAKE.
* @return true if the command is successfully sent; otherwise false.
@@ -515,6 +493,7 @@ public final class BluetoothHidDevice implements BluetoothProfile {
* Sends Virtual Cable Unplug to currently connected host.
*
* @return
+ * {@hide}
*/
public boolean unplug(BluetoothDevice device) {
Log.v(TAG, "unplug(): device=" + device);
@@ -536,11 +515,11 @@ public final class BluetoothHidDevice implements BluetoothProfile {
}
/**
- * Initiates connection to host which is currently paired with this device.
- * If the application is not registered, #connect(BluetoothDevice) will fail.
- * The connection state should be tracked by the application by handling callback from
- * BluetoothHidDeviceCallback#onConnectionStateChanged. The connection state is not related
- * to the return value of this method.
+ * Initiates connection to host which is currently paired with this device. If the application
+ * is not registered, #connect(BluetoothDevice) will fail. The connection state should be
+ * tracked by the application by handling callback from
+ * BluetoothHidDeviceCallback#onConnectionStateChanged. The connection state is not related to
+ * the return value of this method.
*
* @return true if the command is successfully sent; otherwise false.
*/
@@ -564,10 +543,9 @@ public final class BluetoothHidDevice implements BluetoothProfile {
}
/**
- * Disconnects from currently connected host.
- * The connection state should be tracked by the application by handling callback from
- * BluetoothHidDeviceCallback#onConnectionStateChanged. The connection state is not related
- * to the return value of this method.
+ * Disconnects from currently connected host. The connection state should be tracked by the
+ * application by handling callback from BluetoothHidDeviceCallback#onConnectionStateChanged.
+ * The connection state is not related to the return value of this method.
*
* @return true if the command is successfully sent; otherwise false.
*/
diff --git a/android/bluetooth/BluetoothHidDeviceAppConfiguration.java b/android/bluetooth/BluetoothHidDeviceAppConfiguration.java
deleted file mode 100644
index d1efa2d6..00000000
--- a/android/bluetooth/BluetoothHidDeviceAppConfiguration.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2016 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.bluetooth;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import java.util.Random;
-
-/**
- * Represents the app configuration for a Bluetooth HID Device application.
- *
- * The app needs a BluetoothHidDeviceAppConfiguration token to unregister
- * the Bluetooth HID Device service.
- *
- * {@see BluetoothHidDevice}
- *
- * {@hide}
- */
-public final class BluetoothHidDeviceAppConfiguration implements Parcelable {
- private final long mHash;
-
- BluetoothHidDeviceAppConfiguration() {
- Random rnd = new Random();
- mHash = rnd.nextLong();
- }
-
- BluetoothHidDeviceAppConfiguration(long hash) {
- mHash = hash;
- }
-
- @Override
- public boolean equals(Object o) {
- if (o instanceof BluetoothHidDeviceAppConfiguration) {
- BluetoothHidDeviceAppConfiguration config = (BluetoothHidDeviceAppConfiguration) o;
- return mHash == config.mHash;
- }
- return false;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- public static final Parcelable.Creator<BluetoothHidDeviceAppConfiguration> CREATOR =
- new Parcelable.Creator<BluetoothHidDeviceAppConfiguration>() {
-
- @Override
- public BluetoothHidDeviceAppConfiguration createFromParcel(Parcel in) {
- long hash = in.readLong();
- return new BluetoothHidDeviceAppConfiguration(hash);
- }
-
- @Override
- public BluetoothHidDeviceAppConfiguration[] newArray(int size) {
- return new BluetoothHidDeviceAppConfiguration[size];
- }
- };
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- out.writeLong(mHash);
- }
-}
diff --git a/android/bluetooth/BluetoothHidDeviceAppQosSettings.java b/android/bluetooth/BluetoothHidDeviceAppQosSettings.java
index 881ae98d..c05df2d2 100644
--- a/android/bluetooth/BluetoothHidDeviceAppQosSettings.java
+++ b/android/bluetooth/BluetoothHidDeviceAppQosSettings.java
@@ -20,15 +20,12 @@ import android.os.Parcel;
import android.os.Parcelable;
/**
- * Represents the Quality of Service (QoS) settings for a Bluetooth HID Device
- * application.
+ * Represents the Quality of Service (QoS) settings for a Bluetooth HID Device application.
*
- * The BluetoothHidDevice framework will update the L2CAP QoS settings for the
- * app during registration.
+ * <p>The BluetoothHidDevice framework will update the L2CAP QoS settings for the app during
+ * registration.
*
- * {@see BluetoothHidDevice}
- *
- * {@hide}
+ * <p>{@see BluetoothHidDevice}
*/
public final class BluetoothHidDeviceAppQosSettings implements Parcelable {
@@ -46,13 +43,10 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable {
public static final int MAX = (int) 0xffffffff;
/**
- * Create a BluetoothHidDeviceAppQosSettings object for the Bluetooth L2CAP channel.
- * The QoS Settings is optional.
- * Recommended to use BluetoothHidDeviceAppQosSettings.Builder.
- * {@see <a href="https://www.bluetooth.com/specifications/profiles-overview">
- * https://www.bluetooth.com/specifications/profiles-overview
- * </a>
- * Bluetooth HID Specfication v1.1.1 Section 5.2 and Appendix D }
+ * Create a BluetoothHidDeviceAppQosSettings object for the Bluetooth L2CAP channel. The QoS
+ * Settings is optional. Recommended to use BluetoothHidDeviceAppQosSettings.Builder.
+ * Please refer to Bluetooth HID Specfication v1.1.1 Section 5.2 and Appendix D for parameters.
+ *
* @param serviceType L2CAP service type
* @param tokenRate L2CAP token rate
* @param tokenBucketSize L2CAP token bucket size
@@ -123,13 +117,11 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable {
/** @return an int array representation of this instance */
public int[] toArray() {
return new int[] {
- serviceType, tokenRate, tokenBucketSize, peakBandwidth, latency, delayVariation
+ serviceType, tokenRate, tokenBucketSize, peakBandwidth, latency, delayVariation
};
}
- /**
- * A helper to build the BluetoothHidDeviceAppQosSettings object.
- */
+ /** A helper to build the BluetoothHidDeviceAppQosSettings object. */
public static class Builder {
// Optional parameters - initialized to default values
private int mServiceType = SERVICE_BEST_EFFORT;
@@ -141,8 +133,9 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable {
/**
* Set the service type.
+ *
* @param val service type. Should be one of {SERVICE_NO_TRAFFIC, SERVICE_BEST_EFFORT,
- * SERVICE_GUARANTEED}, with SERVICE_BEST_EFFORT being the default one.
+ * SERVICE_GUARANTEED}, with SERVICE_BEST_EFFORT being the default one.
* @return BluetoothHidDeviceAppQosSettings Builder with specified service type.
*/
public Builder serviceType(int val) {
@@ -151,6 +144,7 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable {
}
/**
* Set the token rate.
+ *
* @param val token rate
* @return BluetoothHidDeviceAppQosSettings Builder with specified token rate.
*/
@@ -161,6 +155,7 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable {
/**
* Set the bucket size.
+ *
* @param val bucket size
* @return BluetoothHidDeviceAppQosSettings Builder with specified bucket size.
*/
@@ -171,6 +166,7 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable {
/**
* Set the peak bandwidth.
+ *
* @param val peak bandwidth
* @return BluetoothHidDeviceAppQosSettings Builder with specified peak bandwidth.
*/
@@ -180,6 +176,7 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable {
}
/**
* Set the latency.
+ *
* @param val latency
* @return BluetoothHidDeviceAppQosSettings Builder with specified latency.
*/
@@ -190,6 +187,7 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable {
/**
* Set the delay variation.
+ *
* @param val delay variation
* @return BluetoothHidDeviceAppQosSettings Builder with specified delay variation.
*/
@@ -200,6 +198,7 @@ public final class BluetoothHidDeviceAppQosSettings implements Parcelable {
/**
* Build the BluetoothHidDeviceAppQosSettings object.
+ *
* @return BluetoothHidDeviceAppQosSettings object with current settings.
*/
public BluetoothHidDeviceAppQosSettings build() {
diff --git a/android/bluetooth/BluetoothHidDeviceAppSdpSettings.java b/android/bluetooth/BluetoothHidDeviceAppSdpSettings.java
index 46696370..562c559e 100644
--- a/android/bluetooth/BluetoothHidDeviceAppSdpSettings.java
+++ b/android/bluetooth/BluetoothHidDeviceAppSdpSettings.java
@@ -22,16 +22,12 @@ import android.os.Parcelable;
import java.util.Arrays;
/**
- * Represents the Service Discovery Protocol (SDP) settings for a Bluetooth
- * HID Device application.
+ * Represents the Service Discovery Protocol (SDP) settings for a Bluetooth HID Device application.
*
- * The BluetoothHidDevice framework adds the SDP record during app
- * registration, so that the Android device can be discovered as a Bluetooth
- * HID Device.
+ * <p>The BluetoothHidDevice framework adds the SDP record during app registration, so that the
+ * Android device can be discovered as a Bluetooth HID Device.
*
- * {@see BluetoothHidDevice}
- *
- * {@hide}
+ * <p>{@see BluetoothHidDevice}
*/
public final class BluetoothHidDeviceAppSdpSettings implements Parcelable {
@@ -43,18 +39,19 @@ public final class BluetoothHidDeviceAppSdpSettings implements Parcelable {
/**
* Create a BluetoothHidDeviceAppSdpSettings object for the Bluetooth SDP record.
+ *
* @param name Name of this Bluetooth HID device. Maximum length is 50 bytes.
* @param description Description for this Bluetooth HID device. Maximum length is 50 bytes.
* @param provider Provider of this Bluetooth HID device. Maximum length is 50 bytes.
- * @param subclass Subclass of this Bluetooth HID device.
- * See <a href="www.usb.org/developers/hidpage/HID1_11.pdf">
+ * @param subclass Subclass of this Bluetooth HID device. See <a
+ * href="www.usb.org/developers/hidpage/HID1_11.pdf">
* www.usb.org/developers/hidpage/HID1_11.pdf Section 4.2</a>
- * @param descriptors Descriptors of this Bluetooth HID device.
- * See <a href="www.usb.org/developers/hidpage/HID1_11.pdf">
+ * @param descriptors Descriptors of this Bluetooth HID device. See <a
+ * href="www.usb.org/developers/hidpage/HID1_11.pdf">
* www.usb.org/developers/hidpage/HID1_11.pdf Chapter 6</a> Maximum length is 2048 bytes.
*/
- public BluetoothHidDeviceAppSdpSettings(String name, String description, String provider,
- byte subclass, byte[] descriptors) {
+ public BluetoothHidDeviceAppSdpSettings(
+ String name, String description, String provider, byte subclass, byte[] descriptors) {
this.name = name;
this.description = description;
this.provider = provider;
diff --git a/android/bluetooth/BluetoothHidDeviceCallback.java b/android/bluetooth/BluetoothHidDeviceCallback.java
index 5ccda0dc..e71b00f2 100644
--- a/android/bluetooth/BluetoothHidDeviceCallback.java
+++ b/android/bluetooth/BluetoothHidDeviceCallback.java
@@ -19,50 +19,41 @@ package android.bluetooth;
import android.util.Log;
/**
- * The template class that applications use to call callback functions on
- * events from the HID host. Callback functions are wrapped in this class and
- * registered to the Android system during app registration.
+ * The template class that applications use to call callback functions on events from the HID host.
+ * Callback functions are wrapped in this class and registered to the Android system during app
+ * registration.
*
- * {@see BluetoothHidDevice}
- *
- * {@hide}
+ * <p>{@see BluetoothHidDevice}
*/
public abstract class BluetoothHidDeviceCallback {
private static final String TAG = "BluetoothHidDevCallback";
/**
- * Callback called when application registration state changes. Usually it's
- * called due to either
- * {@link BluetoothHidDevice#registerApp
- * (String, String, String, byte, byte[], BluetoothHidDeviceCallback)}
- * or
- * {@link BluetoothHidDevice#unregisterApp(BluetoothHidDeviceAppConfiguration)}
- * , but can be also unsolicited in case e.g. Bluetooth was turned off in
- * which case application is unregistered automatically.
+ * Callback called when application registration state changes. Usually it's called due to
+ * either {@link BluetoothHidDevice#registerApp (String, String, String, byte, byte[],
+ * BluetoothHidDeviceCallback)} or {@link BluetoothHidDevice#unregisterApp()} , but can be also
+ * unsolicited in case e.g. Bluetooth was turned off in which case application is unregistered
+ * automatically.
*
* @param pluggedDevice {@link BluetoothDevice} object which represents host that currently has
- * Virtual Cable established with device. Only valid when application is registered, can be
- * <code>null</code>.
- * @param config {@link BluetoothHidDeviceAppConfiguration} object which represents token
- * required to unregister application using
- * {@link BluetoothHidDevice#unregisterApp(BluetoothHidDeviceAppConfiguration)}.
+ * Virtual Cable established with device. Only valid when application is registered, can be
+ * <code>null</code>.
* @param registered <code>true</code> if application is registered, <code>false</code>
- * otherwise.
+ * otherwise.
*/
- public void onAppStatusChanged(BluetoothDevice pluggedDevice,
- BluetoothHidDeviceAppConfiguration config, boolean registered) {
- Log.d(TAG, "onAppStatusChanged: pluggedDevice=" + pluggedDevice + " registered="
- + registered);
+ public void onAppStatusChanged(BluetoothDevice pluggedDevice, boolean registered) {
+ Log.d(TAG,
+ "onAppStatusChanged: pluggedDevice=" + pluggedDevice + " registered=" + registered);
}
/**
- * Callback called when connection state with remote host was changed.
- * Application can assume than Virtual Cable is established when called with
- * {@link BluetoothProfile#STATE_CONNECTED} <code>state</code>.
+ * Callback called when connection state with remote host was changed. Application can assume
+ * than Virtual Cable is established when called with {@link BluetoothProfile#STATE_CONNECTED}
+ * <code>state</code>.
*
* @param device {@link BluetoothDevice} object representing host device which connection state
- * was changed.
+ * was changed.
* @param state Connection state as defined in {@link BluetoothProfile}.
*/
public void onConnectionStateChanged(BluetoothDevice device, int state) {
@@ -70,14 +61,14 @@ public abstract class BluetoothHidDeviceCallback {
}
/**
- * Callback called when GET_REPORT is received from remote host. Should be
- * replied by application using
- * {@link BluetoothHidDevice#replyReport(BluetoothDevice, byte, byte, byte[])}.
+ * Callback called when GET_REPORT is received from remote host. Should be replied by
+ * application using {@link BluetoothHidDevice#replyReport(BluetoothDevice, byte, byte,
+ * byte[])}.
*
* @param type Requested Report Type.
* @param id Requested Report Id, can be 0 if no Report Id are defined in descriptor.
* @param bufferSize Requested buffer size, application shall respond with at least given number
- * of bytes.
+ * of bytes.
*/
public void onGetReport(BluetoothDevice device, byte type, byte id, int bufferSize) {
Log.d(TAG, "onGetReport: device=" + device + " type=" + type + " id=" + id + " bufferSize="
@@ -85,9 +76,9 @@ public abstract class BluetoothHidDeviceCallback {
}
/**
- * Callback called when SET_REPORT is received from remote host. In case
- * received data are invalid, application shall respond with
- * {@link BluetoothHidDevice#reportError(BluetoothDevice, byte)}.
+ * Callback called when SET_REPORT is received from remote host. In case received data are
+ * invalid, application shall respond with {@link
+ * BluetoothHidDevice#reportError(BluetoothDevice, byte)}.
*
* @param type Report Type.
* @param id Report Id.
@@ -98,10 +89,9 @@ public abstract class BluetoothHidDeviceCallback {
}
/**
- * Callback called when SET_PROTOCOL is received from remote host.
- * Application shall use this information to send only reports valid for
- * given protocol mode. By default,
- * {@link BluetoothHidDevice#PROTOCOL_REPORT_MODE} shall be assumed.
+ * Callback called when SET_PROTOCOL is received from remote host. Application shall use this
+ * information to send only reports valid for given protocol mode. By default, {@link
+ * BluetoothHidDevice#PROTOCOL_REPORT_MODE} shall be assumed.
*
* @param protocol Protocol Mode.
*/
@@ -110,22 +100,19 @@ public abstract class BluetoothHidDeviceCallback {
}
/**
- * Callback called when report data is received over interrupt channel.
- * Report Type is assumed to be
- * {@link BluetoothHidDevice#REPORT_TYPE_OUTPUT}.
+ * Callback called when report data is received over interrupt channel. Report Type is assumed
+ * to be {@link BluetoothHidDevice#REPORT_TYPE_OUTPUT}.
*
* @param reportId Report Id.
* @param data Report data.
*/
- public void onIntrData(BluetoothDevice device, byte reportId, byte[] data) {
- Log.d(TAG, "onIntrData: device=" + device + " reportId=" + reportId);
+ public void onInterruptData(BluetoothDevice device, byte reportId, byte[] data) {
+ Log.d(TAG, "onInterruptData: device=" + device + " reportId=" + reportId);
}
/**
- * Callback called when Virtual Cable is removed. This can be either due to
- * {@link BluetoothHidDevice#unplug(BluetoothDevice)} or request from remote
- * side. After this callback is received connection will be disconnected
- * automatically.
+ * Callback called when Virtual Cable is removed. After this callback is
+ * received connection will be disconnected automatically.
*/
public void onVirtualCableUnplug(BluetoothDevice device) {
Log.d(TAG, "onVirtualCableUnplug: device=" + device);
diff --git a/android/bluetooth/BluetoothPbap.java b/android/bluetooth/BluetoothPbap.java
index a1a9347d..79443545 100644
--- a/android/bluetooth/BluetoothPbap.java
+++ b/android/bluetooth/BluetoothPbap.java
@@ -25,6 +25,10 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
/**
* The Android Bluetooth API is not finalized, and *will* change. Use at your
* own risk.
@@ -48,11 +52,10 @@ import android.util.Log;
*
* @hide
*/
-public class BluetoothPbap {
+public class BluetoothPbap implements BluetoothProfile {
private static final String TAG = "BluetoothPbap";
- private static final boolean DBG = true;
- private static final boolean VDBG = false;
+ private static final boolean DBG = false;
/**
* Intent used to broadcast the change in connection state of the PBAP
@@ -111,9 +114,9 @@ public class BluetoothPbap {
private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
new IBluetoothStateChangeCallback.Stub() {
public void onBluetoothStateChange(boolean up) {
- if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up);
+ log("onBluetoothStateChange: up=" + up);
if (!up) {
- if (VDBG) Log.d(TAG, "Unbinding service...");
+ log("Unbinding service...");
synchronized (mConnection) {
try {
mService = null;
@@ -126,7 +129,7 @@ public class BluetoothPbap {
synchronized (mConnection) {
try {
if (mService == null) {
- if (VDBG) Log.d(TAG, "Binding service...");
+ log("Binding service...");
doBind();
}
} catch (Exception re) {
@@ -205,47 +208,60 @@ public class BluetoothPbap {
}
/**
- * Get the current state of the BluetoothPbap service.
- *
- * @return One of the STATE_ return codes, or {@link BluetoothProfile#STATE_DISCONNECTED}
- * if this proxy object is currently not connected to the Pbap service.
+ * {@inheritDoc}
*/
- public int getState() {
- if (VDBG) log("getState()");
+ @Override
+ public List<BluetoothDevice> getConnectedDevices() {
+ log("getConnectedDevices()");
final IBluetoothPbap service = mService;
- if (service != null) {
- try {
- return service.getState();
- } catch (RemoteException e) {
- Log.e(TAG, e.toString());
- }
- } else {
+ if (service == null) {
Log.w(TAG, "Proxy not attached to service");
- if (DBG) log(Log.getStackTraceString(new Throwable()));
+ return new ArrayList<BluetoothDevice>();
+ }
+ try {
+ return service.getConnectedDevices();
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
+ }
+ return new ArrayList<BluetoothDevice>();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getConnectionState(BluetoothDevice device) {
+ log("getConnectionState: device=" + device);
+ final IBluetoothPbap service = mService;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+ try {
+ return service.getConnectionState(device);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
}
return BluetoothProfile.STATE_DISCONNECTED;
}
/**
- * Get the currently connected remote Bluetooth device (PCE).
- *
- * @return The remote Bluetooth device, or null if not in connected or connecting state, or if
- * this proxy object is not connected to the Pbap service.
+ * {@inheritDoc}
*/
- public BluetoothDevice getClient() {
- if (VDBG) log("getClient()");
+ @Override
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ log("getDevicesMatchingConnectionStates: states=" + Arrays.toString(states));
final IBluetoothPbap service = mService;
- if (service != null) {
- try {
- return service.getClient();
- } catch (RemoteException e) {
- Log.e(TAG, e.toString());
- }
- } else {
+ if (service == null) {
Log.w(TAG, "Proxy not attached to service");
- if (DBG) log(Log.getStackTraceString(new Throwable()));
+ return new ArrayList<BluetoothDevice>();
+ }
+ try {
+ return service.getDevicesMatchingConnectionStates(states);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
}
- return null;
+ return new ArrayList<BluetoothDevice>();
}
/**
@@ -253,20 +269,9 @@ public class BluetoothPbap {
* include connecting). Returns false if not connected, or if this proxy
* object is not currently connected to the Pbap service.
*/
+ // TODO: This is currently being used by SettingsLib and internal app.
public boolean isConnected(BluetoothDevice device) {
- if (VDBG) log("isConnected(" + device + ")");
- final IBluetoothPbap service = mService;
- if (service != null) {
- try {
- return service.isConnected(device);
- } catch (RemoteException e) {
- Log.e(TAG, e.toString());
- }
- } else {
- Log.w(TAG, "Proxy not attached to service");
- if (DBG) log(Log.getStackTraceString(new Throwable()));
- }
- return false;
+ return getConnectionState(device) == BluetoothAdapter.STATE_CONNECTED;
}
/**
@@ -274,47 +279,27 @@ public class BluetoothPbap {
* it may soon be made asynchronous. Returns false if this proxy object is
* not currently connected to the Pbap service.
*/
- public boolean disconnect() {
- if (DBG) log("disconnect()");
+ // TODO: This is currently being used by SettingsLib and will be used in the future.
+ // TODO: Must specify target device. Implement this in the service.
+ public boolean disconnect(BluetoothDevice device) {
+ log("disconnect()");
final IBluetoothPbap service = mService;
- if (service != null) {
- try {
- service.disconnect();
- return true;
- } catch (RemoteException e) {
- Log.e(TAG, e.toString());
- }
- } else {
+ if (service == null) {
Log.w(TAG, "Proxy not attached to service");
- if (DBG) log(Log.getStackTraceString(new Throwable()));
+ return false;
}
- return false;
- }
-
- /**
- * Check class bits for possible PBAP support.
- * This is a simple heuristic that tries to guess if a device with the
- * given class bits might support PBAP. It is not accurate for all
- * devices. It tries to err on the side of false positives.
- *
- * @return True if this device might support PBAP.
- */
- public static boolean doesClassMatchSink(BluetoothClass btClass) {
- // TODO optimize the rule
- switch (btClass.getDeviceClass()) {
- case BluetoothClass.Device.COMPUTER_DESKTOP:
- case BluetoothClass.Device.COMPUTER_LAPTOP:
- case BluetoothClass.Device.COMPUTER_SERVER:
- case BluetoothClass.Device.COMPUTER_UNCATEGORIZED:
- return true;
- default:
- return false;
+ try {
+ service.disconnect(device);
+ return true;
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
}
+ return false;
}
private final ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
- if (DBG) log("Proxy object connected");
+ log("Proxy object connected");
mService = IBluetoothPbap.Stub.asInterface(service);
if (mServiceListener != null) {
mServiceListener.onServiceConnected(BluetoothPbap.this);
@@ -322,7 +307,7 @@ public class BluetoothPbap {
}
public void onServiceDisconnected(ComponentName className) {
- if (DBG) log("Proxy object disconnected");
+ log("Proxy object disconnected");
mService = null;
if (mServiceListener != null) {
mServiceListener.onServiceDisconnected();
@@ -331,6 +316,8 @@ public class BluetoothPbap {
};
private static void log(String msg) {
- Log.d(TAG, msg);
+ if (DBG) {
+ Log.d(TAG, msg);
+ }
}
}
diff --git a/android/bluetooth/BluetoothProfile.java b/android/bluetooth/BluetoothProfile.java
index 46a230b5..df2028a5 100644
--- a/android/bluetooth/BluetoothProfile.java
+++ b/android/bluetooth/BluetoothProfile.java
@@ -153,8 +153,6 @@ public interface BluetoothProfile {
/**
* HID Device
- *
- * @hide
*/
public static final int HID_DEVICE = 19;
@@ -254,4 +252,28 @@ public interface BluetoothProfile {
*/
public void onServiceDisconnected(int profile);
}
+
+ /**
+ * Convert an integer value of connection state into human readable string
+ *
+ * @param connectionState - One of {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
+ * {@link #STATE_CONNECTED}, or {@link #STATE_DISCONNECTED}
+ * @return a string representation of the connection state, STATE_UNKNOWN if the state
+ * is not defined
+ * @hide
+ */
+ static String getConnectionStateName(int connectionState) {
+ switch (connectionState) {
+ case STATE_DISCONNECTED:
+ return "STATE_DISCONNECTED";
+ case STATE_CONNECTING:
+ return "STATE_CONNECTING";
+ case STATE_CONNECTED:
+ return "STATE_CONNECTED";
+ case STATE_DISCONNECTING:
+ return "STATE_DISCONNECTING";
+ default:
+ return "STATE_UNKNOWN";
+ }
+ }
}
diff --git a/android/bluetooth/le/PeriodicAdvertisingReport.java b/android/bluetooth/le/PeriodicAdvertisingReport.java
index 55c3a730..73a2e74d 100644
--- a/android/bluetooth/le/PeriodicAdvertisingReport.java
+++ b/android/bluetooth/le/PeriodicAdvertisingReport.java
@@ -71,7 +71,7 @@ public final class PeriodicAdvertisingReport implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mSyncHandle);
- dest.writeLong(mTxPower);
+ dest.writeInt(mTxPower);
dest.writeInt(mRssi);
dest.writeInt(mDataStatus);
if (mData != null) {
diff --git a/android/companion/DeviceFilter.java b/android/companion/DeviceFilter.java
index 9b4fdfdf..10135a45 100644
--- a/android/companion/DeviceFilter.java
+++ b/android/companion/DeviceFilter.java
@@ -62,7 +62,11 @@ public interface DeviceFilter<D extends Parcelable> extends Parcelable {
}
/** @hide */
- @IntDef({MEDIUM_TYPE_BLUETOOTH, MEDIUM_TYPE_BLUETOOTH_LE, MEDIUM_TYPE_WIFI})
+ @IntDef(prefix = { "MEDIUM_TYPE_" }, value = {
+ MEDIUM_TYPE_BLUETOOTH,
+ MEDIUM_TYPE_BLUETOOTH_LE,
+ MEDIUM_TYPE_WIFI
+ })
@Retention(RetentionPolicy.SOURCE)
@interface MediumType {}
}
diff --git a/android/content/AsyncTaskLoader.java b/android/content/AsyncTaskLoader.java
index 6e9f09cb..c44e3568 100644
--- a/android/content/AsyncTaskLoader.java
+++ b/android/content/AsyncTaskLoader.java
@@ -50,7 +50,8 @@ import java.util.concurrent.Executor;
*
* @param <D> the data type to be loaded.
*
- * @deprecated Use {@link android.support.v4.content.AsyncTaskLoader}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link android.support.v4.content.AsyncTaskLoader}
*/
@Deprecated
public abstract class AsyncTaskLoader<D> extends Loader<D> {
diff --git a/android/content/ContentResolver.java b/android/content/ContentResolver.java
index 9ccc552f..8d2e141a 100644
--- a/android/content/ContentResolver.java
+++ b/android/content/ContentResolver.java
@@ -335,7 +335,7 @@ public abstract class ContentResolver {
public static final String EXTRA_HONORED_ARGS = "android.content.extra.HONORED_ARGS";
/** @hide */
- @IntDef(flag = false, value = {
+ @IntDef(flag = false, prefix = { "QUERY_SORT_DIRECTION_" }, value = {
QUERY_SORT_DIRECTION_ASCENDING,
QUERY_SORT_DIRECTION_DESCENDING
})
@@ -482,11 +482,10 @@ public abstract class ContentResolver {
public static final int SYNC_OBSERVER_TYPE_ALL = 0x7fffffff;
/** @hide */
- @IntDef(flag = true,
- value = {
- NOTIFY_SYNC_TO_NETWORK,
- NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS
- })
+ @IntDef(flag = true, prefix = { "NOTIFY_" }, value = {
+ NOTIFY_SYNC_TO_NETWORK,
+ NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface NotifyFlags {}
diff --git a/android/content/Context.java b/android/content/Context.java
index 19e24ad5..4cedeaa0 100644
--- a/android/content/Context.java
+++ b/android/content/Context.java
@@ -51,6 +51,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.Looper;
import android.os.StatFs;
@@ -75,6 +76,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
/**
* Interface to global information about an application environment. This is
@@ -219,17 +221,16 @@ public abstract class Context {
public static final int MODE_NO_LOCALIZED_COLLATORS = 0x0010;
/** @hide */
- @IntDef(flag = true,
- value = {
- BIND_AUTO_CREATE,
- BIND_DEBUG_UNBIND,
- BIND_NOT_FOREGROUND,
- BIND_ABOVE_CLIENT,
- BIND_ALLOW_OOM_MANAGEMENT,
- BIND_WAIVE_PRIORITY,
- BIND_IMPORTANT,
- BIND_ADJUST_WITH_ACTIVITY
- })
+ @IntDef(flag = true, prefix = { "BIND_" }, value = {
+ BIND_AUTO_CREATE,
+ BIND_DEBUG_UNBIND,
+ BIND_NOT_FOREGROUND,
+ BIND_ABOVE_CLIENT,
+ BIND_ALLOW_OOM_MANAGEMENT,
+ BIND_WAIVE_PRIORITY,
+ BIND_IMPORTANT,
+ BIND_ADJUST_WITH_ACTIVITY
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface BindServiceFlags {}
@@ -323,6 +324,15 @@ public abstract class Context {
public static final int BIND_ADJUST_WITH_ACTIVITY = 0x0080;
/**
+ * @hide Flag for {@link #bindService}: allows binding to a service provided
+ * by an instant app. Note that the caller may not have access to the instant
+ * app providing the service which is a violation of the instant app sandbox.
+ * This flag is intended ONLY for development/testing and should be used with
+ * great care. Only the system is allowed to use this flag.
+ */
+ public static final int BIND_ALLOW_INSTANT = 0x00400000;
+
+ /**
* @hide Flag for {@link #bindService}: like {@link #BIND_NOT_FOREGROUND}, but puts it
* up in to the important background state (instead of transient).
*/
@@ -404,10 +414,9 @@ public abstract class Context {
public static final int BIND_EXTERNAL_SERVICE = 0x80000000;
/** @hide */
- @IntDef(flag = true,
- value = {
- RECEIVER_VISIBLE_TO_INSTANT_APPS
- })
+ @IntDef(flag = true, prefix = { "RECEIVER_VISIBLE_" }, value = {
+ RECEIVER_VISIBLE_TO_INSTANT_APPS
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface RegisterReceiverFlags {}
@@ -462,6 +471,16 @@ public abstract class Context {
public abstract Looper getMainLooper();
/**
+ * Return an {@link Executor} that will run enqueued tasks on the main
+ * thread associated with this context. This is the thread used to dispatch
+ * calls to application components (activities, services, etc).
+ */
+ public Executor getMainExecutor() {
+ // This is pretty inefficient, which is why ContextImpl overrides it
+ return new HandlerExecutor(new Handler(getMainLooper()));
+ }
+
+ /**
* Return the context of the single, global Application object of the
* current process. This generally should only be used if you need a
* Context whose lifecycle is separate from the current context, that is
@@ -754,6 +773,8 @@ public abstract class Context {
* to any callers for the same name, meaning they will see each other's
* edits as soon as they are made.
*
+ * This method is thead-safe.
+ *
* @param name Desired preferences file. If a preferences file by this name
* does not exist, it will be created when you retrieve an
* editor (SharedPreferences.edit()) and then commit changes (Editor.commit()).
@@ -2799,10 +2820,17 @@ public abstract class Context {
* example, if this Context is an Activity that is stopped, the service will
* not be required to continue running until the Activity is resumed.
*
- * <p>This function will throw {@link SecurityException} if you do not
+ * <p>If the service does not support binding, it may return {@code null} from
+ * its {@link android.app.Service#onBind(Intent) onBind()} method. If it does, then
+ * the ServiceConnection's
+ * {@link ServiceConnection#onNullBinding(ComponentName) onNullBinding()} method
+ * will be invoked instead of
+ * {@link ServiceConnection#onServiceConnected(ComponentName, IBinder) onServiceConnected()}.
+ *
+ * <p>This method will throw {@link SecurityException} if the calling app does not
* have permission to bind to the given service.
*
- * <p class="note">Note: this method <em>can not be called from a
+ * <p class="note">Note: this method <em>cannot be called from a
* {@link BroadcastReceiver} component</em>. A pattern you can use to
* communicate from a BroadcastReceiver to a Service is to call
* {@link #startService} with the arguments containing the command to be
@@ -2825,8 +2853,8 @@ public abstract class Context {
* {@link #BIND_WAIVE_PRIORITY}.
* @return If you have successfully bound to the service, {@code true} is returned;
* {@code false} is returned if the connection is not made so you will not
- * receive the service object. However, you should still call
- * {@link #unbindService} to release the connection.
+ * receive the service object. You should still call {@link #unbindService}
+ * to release the connection even if this method returned {@code false}.
*
* @throws SecurityException If the caller does not have permission to access the service
* or the service can not be found.
@@ -2904,7 +2932,7 @@ public abstract class Context {
@Nullable String profileFile, @Nullable Bundle arguments);
/** @hide */
- @StringDef({
+ @StringDef(suffix = { "_SERVICE" }, value = {
POWER_SERVICE,
WINDOW_SERVICE,
LAYOUT_INFLATER_SERVICE,
@@ -2938,7 +2966,7 @@ public abstract class Context {
//@hide: LOWPAN_SERVICE,
//@hide: WIFI_RTT_SERVICE,
//@hide: ETHERNET_SERVICE,
- WIFI_RTT_SERVICE,
+ WIFI_RTT_RANGING_SERVICE,
NSD_SERVICE,
AUDIO_SERVICE,
FINGERPRINT_SERVICE,
@@ -2993,7 +3021,8 @@ public abstract class Context {
SYSTEM_HEALTH_SERVICE,
//@hide: INCIDENT_SERVICE,
//@hide: STATS_COMPANION_SERVICE,
- COMPANION_DEVICE_SERVICE
+ COMPANION_DEVICE_SERVICE,
+ CROSS_PROFILE_APPS_SERVICE
})
@Retention(RetentionPolicy.SOURCE)
public @interface ServiceName {}
@@ -3072,6 +3101,14 @@ public abstract class Context {
* service objects between various different contexts (Activities, Applications,
* Services, Providers, etc.)
*
+ * <p>Note: Instant apps, for which {@link PackageManager#isInstantApp()} returns true,
+ * don't have access to the following system services: {@link #DEVICE_POLICY_SERVICE},
+ * {@link #FINGERPRINT_SERVICE}, {@link #SHORTCUT_SERVICE}, {@link #USB_SERVICE},
+ * {@link #WALLPAPER_SERVICE}, {@link #WIFI_P2P_SERVICE}, {@link #WIFI_SERVICE},
+ * {@link #WIFI_AWARE_SERVICE}. For these services this method will return <code>null</code>.
+ * Generally, if you are running as an instant app you should always check whether the result
+ * of this method is null.
+ *
* @param name The name of the desired service.
*
* @return The service or null if the name does not exist.
@@ -3155,6 +3192,14 @@ public abstract class Context {
* Services, Providers, etc.)
* </p>
*
+ * <p>Note: Instant apps, for which {@link PackageManager#isInstantApp()} returns true,
+ * don't have access to the following system services: {@link #DEVICE_POLICY_SERVICE},
+ * {@link #FINGERPRINT_SERVICE}, {@link #SHORTCUT_SERVICE}, {@link #USB_SERVICE},
+ * {@link #WALLPAPER_SERVICE}, {@link #WIFI_P2P_SERVICE}, {@link #WIFI_SERVICE},
+ * {@link #WIFI_AWARE_SERVICE}. For these services this method will return <code>null</code>.
+ * Generally, if you are running as an instant app you should always check whether the result
+ * of this method is null.
+ *
* @param serviceClass The class of the desired service.
* @return The service or null if the class is not a supported system service.
*/
@@ -3404,6 +3449,14 @@ public abstract class Context {
public static final String NETWORKMANAGEMENT_SERVICE = "network_management";
/**
+ * Use with {@link #getSystemService} to retrieve a
+ * {@link com.android.server.slice.SliceManagerService} for managing slices.
+ * @hide
+ * @see #getSystemService
+ */
+ public static final String SLICE_SERVICE = "slice";
+
+ /**
* Use with {@link #getSystemService} to retrieve a {@link
* android.app.usage.NetworkStatsManager} for querying network usage stats.
*
@@ -3479,7 +3532,7 @@ public abstract class Context {
* @see android.net.wifi.rtt.WifiRttManager
* @hide
*/
- public static final String WIFI_RTT2_SERVICE = "rttmanager2";
+ public static final String WIFI_RTT_RANGING_SERVICE = "rttmanager2";
/**
* Use with {@link #getSystemService} to retrieve a {@link
diff --git a/android/content/ContextWrapper.java b/android/content/ContextWrapper.java
index 85acdc6b..67de4fe6 100644
--- a/android/content/ContextWrapper.java
+++ b/android/content/ContextWrapper.java
@@ -45,6 +45,7 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.util.concurrent.Executor;
/**
* Proxying implementation of Context that simply delegates all of its calls to
@@ -103,7 +104,12 @@ public class ContextWrapper extends Context {
public Looper getMainLooper() {
return mBase.getMainLooper();
}
-
+
+ @Override
+ public Executor getMainExecutor() {
+ return mBase.getMainExecutor();
+ }
+
@Override
public Context getApplicationContext() {
return mBase.getApplicationContext();
diff --git a/android/content/CursorLoader.java b/android/content/CursorLoader.java
index 7f24c51d..5a08636c 100644
--- a/android/content/CursorLoader.java
+++ b/android/content/CursorLoader.java
@@ -39,7 +39,8 @@ import java.util.Arrays;
* {@link #setSelectionArgs(String[])}, {@link #setSortOrder(String)},
* and {@link #setProjection(String[])}.
*
- * @deprecated Use {@link android.support.v4.content.CursorLoader}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link android.support.v4.content.CursorLoader}
*/
@Deprecated
public class CursorLoader extends AsyncTaskLoader<Cursor> {
diff --git a/android/content/Intent.java b/android/content/Intent.java
index bad452ce..e940769a 100644
--- a/android/content/Intent.java
+++ b/android/content/Intent.java
@@ -4455,12 +4455,36 @@ public class Intent implements Parcelable, Cloneable {
public static final String EXTRA_EPHEMERAL_TOKEN = "android.intent.extra.EPHEMERAL_TOKEN";
/**
+ * The action that triggered an instant application resolution.
+ * @hide
+ */
+ public static final String EXTRA_INSTANT_APP_ACTION = "android.intent.extra.INSTANT_APP_ACTION";
+
+ /**
+ * A {@link Bundle} of metadata that describes the instanta application that needs to be
+ * installed. This data is populated from the response to
+ * {@link android.content.pm.InstantAppResolveInfo#getExtras()} as provided by the registered
+ * instant application resolver.
+ * @hide
+ */
+ public static final String EXTRA_INSTANT_APP_EXTRAS =
+ "android.intent.extra.INSTANT_APP_EXTRAS";
+
+ /**
* The version code of the app to install components from.
+ * @deprecated Use {@link #EXTRA_LONG_VERSION_CODE).
* @hide
*/
+ @Deprecated
public static final String EXTRA_VERSION_CODE = "android.intent.extra.VERSION_CODE";
/**
+ * The version code of the app to install components from.
+ * @hide
+ */
+ public static final String EXTRA_LONG_VERSION_CODE = "android.intent.extra.LONG_VERSION_CODE";
+
+ /**
* The app that triggered the ephemeral installation.
* @hide
*/
@@ -4891,8 +4915,9 @@ public class Intent implements Parcelable, Cloneable {
* <li>Enumeration of features here is not meant to restrict capabilities of the quick viewer.
* Quick viewer can implement features not listed below.
* <li>Features included at this time are: {@link QuickViewConstants#FEATURE_VIEW},
- * {@link QuickViewConstants#FEATURE_EDIT}, {@link QuickViewConstants#FEATURE_DOWNLOAD},
- * {@link QuickViewConstants#FEATURE_SEND}, {@link QuickViewConstants#FEATURE_PRINT}.
+ * {@link QuickViewConstants#FEATURE_EDIT}, {@link QuickViewConstants#FEATURE_DELETE},
+ * {@link QuickViewConstants#FEATURE_DOWNLOAD}, {@link QuickViewConstants#FEATURE_SEND},
+ * {@link QuickViewConstants#FEATURE_PRINT}.
* <p>
* Requirements:
* <li>Quick viewer shouldn't show a feature if the feature is absent in
@@ -5665,11 +5690,14 @@ public class Intent implements Parcelable, Cloneable {
private static final int COPY_MODE_HISTORY = 2;
/** @hide */
- @IntDef(value = {COPY_MODE_ALL, COPY_MODE_FILTER, COPY_MODE_HISTORY})
+ @IntDef(prefix = { "COPY_MODE_" }, value = {
+ COPY_MODE_ALL,
+ COPY_MODE_FILTER,
+ COPY_MODE_HISTORY
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface CopyMode {}
-
/**
* Create an empty intent.
*/
@@ -8941,17 +8969,16 @@ public class Intent implements Parcelable, Cloneable {
}
/** @hide */
- @IntDef(flag = true,
- value = {
- FILL_IN_ACTION,
- FILL_IN_DATA,
- FILL_IN_CATEGORIES,
- FILL_IN_COMPONENT,
- FILL_IN_PACKAGE,
- FILL_IN_SOURCE_BOUNDS,
- FILL_IN_SELECTOR,
- FILL_IN_CLIP_DATA
- })
+ @IntDef(flag = true, prefix = { "FILL_IN_" }, value = {
+ FILL_IN_ACTION,
+ FILL_IN_DATA,
+ FILL_IN_CATEGORIES,
+ FILL_IN_COMPONENT,
+ FILL_IN_PACKAGE,
+ FILL_IN_SOURCE_BOUNDS,
+ FILL_IN_SELECTOR,
+ FILL_IN_CLIP_DATA
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface FillInFlags {}
diff --git a/android/content/Loader.java b/android/content/Loader.java
index 80f9a14c..b0555d4c 100644
--- a/android/content/Loader.java
+++ b/android/content/Loader.java
@@ -49,7 +49,8 @@ import java.io.PrintWriter;
*
* @param <D> The result returned when the load is complete
*
- * @deprecated Use {@link android.support.v4.content.Loader}
+ * @deprecated Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link android.support.v4.content.Loader}
*/
@Deprecated
public class Loader<D> {
@@ -561,4 +562,4 @@ public class Loader<D> {
writer.print(" mReset="); writer.println(mReset);
}
}
-} \ No newline at end of file
+}
diff --git a/android/content/QuickViewConstants.java b/android/content/QuickViewConstants.java
index 7455d0cb..a25513de 100644
--- a/android/content/QuickViewConstants.java
+++ b/android/content/QuickViewConstants.java
@@ -33,7 +33,7 @@ public class QuickViewConstants {
public static final String FEATURE_VIEW = "android:view";
/**
- * Feature to view a document using system standard editing mechanism, like
+ * Feature to edit a document using system standard editing mechanism, like
* {@link Intent#ACTION_EDIT}.
*
* @see Intent#EXTRA_QUICK_VIEW_FEATURES
@@ -42,6 +42,15 @@ public class QuickViewConstants {
public static final String FEATURE_EDIT = "android:edit";
/**
+ * Feature to delete an individual document. Quick viewer implementations must use
+ * Storage Access Framework to both verify delete permission and to delete content.
+ *
+ * @see DocumentsContract#Document#FLAG_SUPPORTS_DELETE
+ * @see DocumentsContract#deleteDocument(ContentResolver resolver, Uri documentUri)
+ */
+ public static final String FEATURE_DELETE = "android:delete";
+
+ /**
* Feature to view a document using system standard sending mechanism, like
* {@link Intent#ACTION_SEND}.
*
diff --git a/android/content/ServiceConnection.java b/android/content/ServiceConnection.java
index 6ff49002..c16dbbe3 100644
--- a/android/content/ServiceConnection.java
+++ b/android/content/ServiceConnection.java
@@ -63,4 +63,21 @@ public interface ServiceConnection {
*/
default void onBindingDied(ComponentName name) {
}
+
+ /**
+ * Called when the service being bound has returned {@code null} from its
+ * {@link android.app.Service#onBind(Intent) onBind()} method. This indicates
+ * that the attempting service binding represented by this ServiceConnection
+ * will never become usable.
+ *
+ * <p class="note">The app which requested the binding must still call
+ * {@link Context#unbindService(ServiceConnection)} to release the tracking
+ * resources associated with this ServiceConnection even if this callback was
+ * invoked following {@link Context#bindService Context.bindService() bindService()}.
+ *
+ * @param name The concrete component name of the service whose binding
+ * has been rejected by the Service implementation.
+ */
+ default void onNullBinding(ComponentName name) {
+ }
}
diff --git a/android/content/pm/ActivityInfo.java b/android/content/pm/ActivityInfo.java
index f8cdce64..14617116 100644
--- a/android/content/pm/ActivityInfo.java
+++ b/android/content/pm/ActivityInfo.java
@@ -262,10 +262,10 @@ public class ActivityInfo extends ComponentInfo implements Parcelable {
public static final int COLOR_MODE_HDR = 2;
/** @hide */
- @IntDef({
- COLOR_MODE_DEFAULT,
- COLOR_MODE_WIDE_COLOR_GAMUT,
- COLOR_MODE_HDR,
+ @IntDef(prefix = { "COLOR_MODE_" }, value = {
+ COLOR_MODE_DEFAULT,
+ COLOR_MODE_WIDE_COLOR_GAMUT,
+ COLOR_MODE_HDR,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ColorMode {}
@@ -492,7 +492,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable {
public int flags;
/** @hide */
- @IntDef({
+ @IntDef(prefix = { "SCREEN_ORIENTATION_" }, value = {
SCREEN_ORIENTATION_UNSET,
SCREEN_ORIENTATION_UNSPECIFIED,
SCREEN_ORIENTATION_LANDSCAPE,
@@ -638,25 +638,24 @@ public class ActivityInfo extends ComponentInfo implements Parcelable {
public int screenOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
/** @hide */
- @IntDef(flag = true,
- value = {
- CONFIG_MCC,
- CONFIG_MNC,
- CONFIG_LOCALE,
- CONFIG_TOUCHSCREEN,
- CONFIG_KEYBOARD,
- CONFIG_KEYBOARD_HIDDEN,
- CONFIG_NAVIGATION,
- CONFIG_ORIENTATION,
- CONFIG_SCREEN_LAYOUT,
- CONFIG_UI_MODE,
- CONFIG_SCREEN_SIZE,
- CONFIG_SMALLEST_SCREEN_SIZE,
- CONFIG_DENSITY,
- CONFIG_LAYOUT_DIRECTION,
- CONFIG_COLOR_MODE,
- CONFIG_FONT_SCALE,
- })
+ @IntDef(flag = true, prefix = { "CONFIG_" }, value = {
+ CONFIG_MCC,
+ CONFIG_MNC,
+ CONFIG_LOCALE,
+ CONFIG_TOUCHSCREEN,
+ CONFIG_KEYBOARD,
+ CONFIG_KEYBOARD_HIDDEN,
+ CONFIG_NAVIGATION,
+ CONFIG_ORIENTATION,
+ CONFIG_SCREEN_LAYOUT,
+ CONFIG_UI_MODE,
+ CONFIG_SCREEN_SIZE,
+ CONFIG_SMALLEST_SCREEN_SIZE,
+ CONFIG_DENSITY,
+ CONFIG_LAYOUT_DIRECTION,
+ CONFIG_COLOR_MODE,
+ CONFIG_FONT_SCALE,
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface Config {}
diff --git a/android/content/pm/ApplicationInfo.java b/android/content/pm/ApplicationInfo.java
index edb27cd4..15e119b2 100644
--- a/android/content/pm/ApplicationInfo.java
+++ b/android/content/pm/ApplicationInfo.java
@@ -26,6 +26,7 @@ import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
+import android.os.Build;
import android.os.Environment;
import android.os.Parcel;
import android.os.Parcelable;
@@ -594,6 +595,13 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
*/
public static final int PRIVATE_FLAG_OEM = 1 << 17;
+ /**
+ * Value for {@linl #privateFlags}: whether this app is pre-installed on the
+ * vendor partition of the system image.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_VENDOR = 1 << 18;
+
/** @hide */
@IntDef(flag = true, prefix = { "PRIVATE_FLAG_" }, value = {
PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE,
@@ -613,6 +621,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
PRIVATE_FLAG_PRIVILEGED,
PRIVATE_FLAG_REQUIRED_FOR_SYSTEM_USER,
PRIVATE_FLAG_STATIC_SHARED_LIBRARY,
+ PRIVATE_FLAG_VENDOR,
PRIVATE_FLAG_VIRTUAL_PRELOAD,
})
@Retention(RetentionPolicy.SOURCE)
@@ -888,7 +897,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
* The app's declared version code.
* @hide
*/
- public int versionCode;
+ public long versionCode;
/**
* The user-visible SDK version (ex. 26) of the framework against which the application claims
@@ -944,6 +953,13 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
public int targetSandboxVersion;
/**
+ * The factory of this package, as specified by the &lt;manifest&gt;
+ * tag's {@link android.R.styleable#AndroidManifestApplication_appComponentFactory}
+ * attribute.
+ */
+ public String appComponentFactory;
+
+ /**
* The category of this app. Categories are used to cluster multiple apps
* together into meaningful groups, such as when summarizing battery,
* network, or disk usage. Apps should only define this value when they fit
@@ -1259,6 +1275,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
targetSandboxVersion = orig.targetSandboxVersion;
classLoaderName = orig.classLoaderName;
splitClassLoaderNames = orig.splitClassLoaderNames;
+ appComponentFactory = orig.appComponentFactory;
}
public String toString() {
@@ -1315,7 +1332,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
dest.writeInt(uid);
dest.writeInt(minSdkVersion);
dest.writeInt(targetSdkVersion);
- dest.writeInt(versionCode);
+ dest.writeLong(versionCode);
dest.writeInt(enabled ? 1 : 0);
dest.writeInt(enabledSetting);
dest.writeInt(installLocation);
@@ -1331,6 +1348,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
dest.writeStringArray(splitClassLoaderNames);
dest.writeInt(compileSdkVersion);
dest.writeString(compileSdkVersionCodename);
+ dest.writeString(appComponentFactory);
}
public static final Parcelable.Creator<ApplicationInfo> CREATOR
@@ -1384,7 +1402,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
uid = source.readInt();
minSdkVersion = source.readInt();
targetSdkVersion = source.readInt();
- versionCode = source.readInt();
+ versionCode = source.readLong();
enabled = source.readInt() != 0;
enabledSetting = source.readInt();
installLocation = source.readInt();
@@ -1400,6 +1418,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
splitClassLoaderNames = source.readStringArray();
compileSdkVersion = source.readInt();
compileSdkVersionCodename = source.readString();
+ appComponentFactory = source.readString();
}
/**
@@ -1569,6 +1588,16 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
return (flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
}
+ /** @hide */
+ public boolean isVendor() {
+ return (privateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0;
+ }
+
+ /** @hide */
+ public boolean isTargetingDeprecatedSdkVersion() {
+ return targetSdkVersion < Build.VERSION.MIN_SUPPORTED_TARGET_SDK_INT;
+ }
+
/**
* Returns whether or not this application was installed as a virtual preload.
*/
diff --git a/android/content/pm/AuxiliaryResolveInfo.java b/android/content/pm/AuxiliaryResolveInfo.java
index 067363d4..6bdcefbe 100644
--- a/android/content/pm/AuxiliaryResolveInfo.java
+++ b/android/content/pm/AuxiliaryResolveInfo.java
@@ -45,7 +45,7 @@ public final class AuxiliaryResolveInfo extends IntentFilter {
/** Opaque token to track the instant application resolution */
public final String token;
/** The version code of the package */
- public final int versionCode;
+ public final long versionCode;
/** An intent to start upon failure to install */
public final Intent failureIntent;
@@ -71,7 +71,7 @@ public final class AuxiliaryResolveInfo extends IntentFilter {
public AuxiliaryResolveInfo(@NonNull String packageName,
@Nullable String splitName,
@Nullable ComponentName failureActivity,
- int versionCode,
+ long versionCode,
@Nullable Intent failureIntent) {
super();
this.packageName = packageName;
diff --git a/android/content/pm/InstantAppResolveInfo.java b/android/content/pm/InstantAppResolveInfo.java
index 22e994f4..19cb9323 100644
--- a/android/content/pm/InstantAppResolveInfo.java
+++ b/android/content/pm/InstantAppResolveInfo.java
@@ -19,8 +19,7 @@ package android.content.pm;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
-import android.content.IntentFilter;
-import android.net.Uri;
+import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@@ -44,10 +43,18 @@ public final class InstantAppResolveInfo implements Parcelable {
/** The filters used to match domain */
private final List<InstantAppIntentFilter> mFilters;
/** The version code of the app that this class resolves to */
- private final int mVersionCode;
+ private final long mVersionCode;
+ /** Data about the app that should be passed along to the Instant App installer on resolve */
+ private final Bundle mExtras;
public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName,
@Nullable List<InstantAppIntentFilter> filters, int versionCode) {
+ this(digest, packageName, filters, (long) versionCode, null /* extras */);
+ }
+
+ public InstantAppResolveInfo(@NonNull InstantAppDigest digest, @Nullable String packageName,
+ @Nullable List<InstantAppIntentFilter> filters, long versionCode,
+ @Nullable Bundle extras) {
// validate arguments
if ((packageName == null && (filters != null && filters.size() != 0))
|| (packageName != null && (filters == null || filters.size() == 0))) {
@@ -62,11 +69,13 @@ public final class InstantAppResolveInfo implements Parcelable {
}
mPackageName = packageName;
mVersionCode = versionCode;
+ mExtras = extras;
}
public InstantAppResolveInfo(@NonNull String hostName, @Nullable String packageName,
@Nullable List<InstantAppIntentFilter> filters) {
- this(new InstantAppDigest(hostName), packageName, filters, -1 /*versionCode*/);
+ this(new InstantAppDigest(hostName), packageName, filters, -1 /*versionCode*/,
+ null /* extras */);
}
InstantAppResolveInfo(Parcel in) {
@@ -74,7 +83,8 @@ public final class InstantAppResolveInfo implements Parcelable {
mPackageName = in.readString();
mFilters = new ArrayList<InstantAppIntentFilter>();
in.readList(mFilters, null /*loader*/);
- mVersionCode = in.readInt();
+ mVersionCode = in.readLong();
+ mExtras = in.readBundle();
}
public byte[] getDigestBytes() {
@@ -93,10 +103,23 @@ public final class InstantAppResolveInfo implements Parcelable {
return mFilters;
}
+ /**
+ * @deprecated Use {@link #getLongVersionCode} instead.
+ */
+ @Deprecated
public int getVersionCode() {
+ return (int) (mVersionCode & 0xffffffff);
+ }
+
+ public long getLongVersionCode() {
return mVersionCode;
}
+ @Nullable
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -107,7 +130,8 @@ public final class InstantAppResolveInfo implements Parcelable {
out.writeParcelable(mDigest, flags);
out.writeString(mPackageName);
out.writeList(mFilters);
- out.writeInt(mVersionCode);
+ out.writeLong(mVersionCode);
+ out.writeBundle(mExtras);
}
public static final Parcelable.Creator<InstantAppResolveInfo> CREATOR
diff --git a/android/content/pm/LauncherApps.java b/android/content/pm/LauncherApps.java
index 9e54e235..b4a7eec0 100644
--- a/android/content/pm/LauncherApps.java
+++ b/android/content/pm/LauncherApps.java
@@ -265,6 +265,14 @@ public class LauncherApps {
/**
* Include pinned shortcuts in the result.
+ *
+ * <p>If you are the selected assistant app, and wishes to fetch all shortcuts that the
+ * user owns on the launcher (or by other launchers, in case the user has multiple), use
+ * {@link #FLAG_MATCH_PINNED_BY_ANY_LAUNCHER} instead.
+ *
+ * <p>If you're a regular launcher app, there's no way to get shortcuts pinned by other
+ * launchers, and {@link #FLAG_MATCH_PINNED_BY_ANY_LAUNCHER} will be ignored. So use this
+ * flag to get own pinned shortcuts.
*/
public static final int FLAG_MATCH_PINNED = 1 << 1;
@@ -285,8 +293,15 @@ public class LauncherApps {
* Include all pinned shortcuts by any launchers, not just by the caller,
* in the result.
*
- * The caller must be the selected assistant app to use this flag, or have the system
+ * <p>The caller must be the selected assistant app to use this flag, or have the system
* {@code ACCESS_SHORTCUTS} permission.
+ *
+ * <p>If you are the selected assistant app, and wishes to fetch all shortcuts that the
+ * user owns on the launcher (or by other launchers, in case the user has multiple), use
+ * {@link #FLAG_MATCH_PINNED_BY_ANY_LAUNCHER} instead.
+ *
+ * <p>If you're a regular launcher app (or any app that's not the selected assistant app)
+ * then this flag will be ignored.
*/
public static final int FLAG_MATCH_PINNED_BY_ANY_LAUNCHER = 1 << 10;
@@ -328,14 +343,13 @@ public class LauncherApps {
public static final int FLAG_GET_KEY_FIELDS_ONLY = 1 << 2;
/** @hide */
- @IntDef(flag = true,
- value = {
- FLAG_MATCH_DYNAMIC,
- FLAG_MATCH_PINNED,
- FLAG_MATCH_MANIFEST,
- FLAG_GET_KEY_FIELDS_ONLY,
- FLAG_MATCH_MANIFEST,
- })
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
+ FLAG_MATCH_DYNAMIC,
+ FLAG_MATCH_PINNED,
+ FLAG_MATCH_MANIFEST,
+ FLAG_GET_KEY_FIELDS_ONLY,
+ FLAG_MATCH_MANIFEST,
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface QueryFlags {}
@@ -1365,7 +1379,10 @@ public class LauncherApps {
public static final int REQUEST_TYPE_APPWIDGET = 2;
/** @hide */
- @IntDef(value = {REQUEST_TYPE_SHORTCUT})
+ @IntDef(prefix = { "REQUEST_TYPE_" }, value = {
+ REQUEST_TYPE_SHORTCUT,
+ REQUEST_TYPE_APPWIDGET
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface RequestType {}
diff --git a/android/content/pm/PackageInfo.java b/android/content/pm/PackageInfo.java
index f8889b68..5a91e947 100644
--- a/android/content/pm/PackageInfo.java
+++ b/android/content/pm/PackageInfo.java
@@ -16,10 +16,14 @@
package android.content.pm;
+import android.annotation.IntDef;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Overall information about the contents of a package. This corresponds
* to all of the information collected from AndroidManifest.xml.
@@ -37,13 +41,56 @@ public class PackageInfo implements Parcelable {
public String[] splitNames;
/**
+ * @deprecated Use {@link #getLongVersionCode()} instead, which includes both
+ * this and the additional
+ * {@link android.R.styleable#AndroidManifest_versionCodeMajor versionCodeMajor} attribute.
* The version number of this package, as specified by the &lt;manifest&gt;
* tag's {@link android.R.styleable#AndroidManifest_versionCode versionCode}
* attribute.
+ * @see #getLongVersionCode()
*/
+ @Deprecated
public int versionCode;
/**
+ * @hide
+ * The major version number of this package, as specified by the &lt;manifest&gt;
+ * tag's {@link android.R.styleable#AndroidManifest_versionCode versionCodeMajor}
+ * attribute.
+ * @see #getLongVersionCode()
+ */
+ public int versionCodeMajor;
+
+ /**
+ * Return {@link android.R.styleable#AndroidManifest_versionCode versionCode} and
+ * {@link android.R.styleable#AndroidManifest_versionCodeMajor versionCodeMajor} combined
+ * together as a single long value. The
+ * {@link android.R.styleable#AndroidManifest_versionCodeMajor versionCodeMajor} is placed in
+ * the upper 32 bits.
+ */
+ public long getLongVersionCode() {
+ return composeLongVersionCode(versionCodeMajor, versionCode);
+ }
+
+ /**
+ * Set the full version code in this PackageInfo, updating {@link #versionCode}
+ * with the lower bits.
+ * @see #getLongVersionCode()
+ */
+ public void setLongVersionCode(long longVersionCode) {
+ versionCodeMajor = (int) (longVersionCode>>32);
+ versionCode = (int) longVersionCode;
+ }
+
+ /**
+ * @hide Internal implementation for composing a minor and major version code in to
+ * a single long version code.
+ */
+ public static long composeLongVersionCode(int major, int minor) {
+ return (((long) major) << 32) | (((long) minor) & 0xffffffffL);
+ }
+
+ /**
* The version name of this package, as specified by the &lt;manifest&gt;
* tag's {@link android.R.styleable#AndroidManifest_versionName versionName}
* attribute.
@@ -287,8 +334,29 @@ public class PackageInfo implements Parcelable {
/** @hide */
public int overlayPriority;
- /** @hide */
- public boolean isStaticOverlay;
+ /**
+ * Flag for use with {@link #mOverlayFlags}. Marks the overlay as static, meaning it cannot
+ * be enabled/disabled at runtime.
+ */
+ static final int FLAG_OVERLAY_STATIC = 1 << 1;
+
+ /**
+ * Flag for use with {@link #mOverlayFlags}. Marks the overlay as trusted (not 3rd party).
+ */
+ static final int FLAG_OVERLAY_TRUSTED = 1 << 2;
+
+ @IntDef(flag = true, prefix = "FLAG_OVERLAY_", value = {
+ FLAG_OVERLAY_STATIC,
+ FLAG_OVERLAY_TRUSTED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface OverlayFlags {}
+
+ /**
+ * Modifiers that affect the state of this overlay. See {@link #FLAG_OVERLAY_STATIC},
+ * {@link #FLAG_OVERLAY_TRUSTED}.
+ */
+ @OverlayFlags int mOverlayFlags;
/**
* The user-visible SDK version (ex. 26) of the framework against which the application claims
@@ -316,6 +384,23 @@ public class PackageInfo implements Parcelable {
public PackageInfo() {
}
+ /**
+ * Returns true if the package is a valid Runtime Overlay package.
+ * @hide
+ */
+ public boolean isOverlayPackage() {
+ return overlayTarget != null && (mOverlayFlags & FLAG_OVERLAY_TRUSTED) != 0;
+ }
+
+ /**
+ * Returns true if the package is a valid static Runtime Overlay package. Static overlays
+ * are not updatable outside of a system update and are safe to load in the system process.
+ * @hide
+ */
+ public boolean isStaticOverlayPackage() {
+ return overlayTarget != null && (mOverlayFlags & FLAG_OVERLAY_STATIC) != 0;
+ }
+
@Override
public String toString() {
return "PackageInfo{"
@@ -333,6 +418,7 @@ public class PackageInfo implements Parcelable {
dest.writeString(packageName);
dest.writeStringArray(splitNames);
dest.writeInt(versionCode);
+ dest.writeInt(versionCodeMajor);
dest.writeString(versionName);
dest.writeInt(baseRevisionCode);
dest.writeIntArray(splitRevisionCodes);
@@ -366,8 +452,8 @@ public class PackageInfo implements Parcelable {
dest.writeString(restrictedAccountType);
dest.writeString(requiredAccountType);
dest.writeString(overlayTarget);
- dest.writeInt(isStaticOverlay ? 1 : 0);
dest.writeInt(overlayPriority);
+ dest.writeInt(mOverlayFlags);
dest.writeInt(compileSdkVersion);
dest.writeString(compileSdkVersionCodename);
}
@@ -389,6 +475,7 @@ public class PackageInfo implements Parcelable {
packageName = source.readString();
splitNames = source.createStringArray();
versionCode = source.readInt();
+ versionCodeMajor = source.readInt();
versionName = source.readString();
baseRevisionCode = source.readInt();
splitRevisionCodes = source.createIntArray();
@@ -420,8 +507,8 @@ public class PackageInfo implements Parcelable {
restrictedAccountType = source.readString();
requiredAccountType = source.readString();
overlayTarget = source.readString();
- isStaticOverlay = source.readInt() != 0;
overlayPriority = source.readInt();
+ mOverlayFlags = source.readInt();
compileSdkVersion = source.readInt();
compileSdkVersionCodename = source.readString();
diff --git a/android/content/pm/PackageInfoLite.java b/android/content/pm/PackageInfoLite.java
index 1efe082b..bbf020d7 100644
--- a/android/content/pm/PackageInfoLite.java
+++ b/android/content/pm/PackageInfoLite.java
@@ -38,9 +38,27 @@ public class PackageInfoLite implements Parcelable {
/**
* The android:versionCode of the package.
+ * @deprecated Use {@link #getLongVersionCode()} instead, which includes both
+ * this and the additional
+ * {@link android.R.styleable#AndroidManifest_versionCode versionCodeMajor} attribute.
*/
+ @Deprecated
public int versionCode;
+ /**
+ * @hide
+ * The android:versionCodeMajor of the package.
+ */
+ public int versionCodeMajor;
+
+ /**
+ * Return {@link #versionCode} and {@link #versionCodeMajor} combined together as a
+ * single long value. The {@link #versionCodeMajor} is placed in the upper 32 bits.
+ */
+ public long getLongVersionCode() {
+ return PackageInfo.composeLongVersionCode(versionCodeMajor, versionCode);
+ }
+
/** Revision code of base APK */
public int baseRevisionCode;
/** Revision codes of any split APKs, ordered by parsed splitName */
@@ -55,10 +73,10 @@ public class PackageInfoLite implements Parcelable {
/**
* Specifies the recommended install location. Can be one of
- * {@link #PackageHelper.RECOMMEND_INSTALL_INTERNAL} to install on internal storage
- * {@link #PackageHelper.RECOMMEND_INSTALL_EXTERNAL} to install on external media
- * {@link PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE} for storage errors
- * {@link PackageHelper.RECOMMEND_FAILED_INVALID_APK} for parse errors.
+ * {@link PackageHelper#RECOMMEND_INSTALL_INTERNAL} to install on internal storage,
+ * {@link PackageHelper#RECOMMEND_INSTALL_EXTERNAL} to install on external media,
+ * {@link PackageHelper#RECOMMEND_FAILED_INSUFFICIENT_STORAGE} for storage errors,
+ * or {@link PackageHelper#RECOMMEND_FAILED_INVALID_APK} for parse errors.
*/
public int recommendedInstallLocation;
public int installLocation;
@@ -82,6 +100,7 @@ public class PackageInfoLite implements Parcelable {
dest.writeString(packageName);
dest.writeStringArray(splitNames);
dest.writeInt(versionCode);
+ dest.writeInt(versionCodeMajor);
dest.writeInt(baseRevisionCode);
dest.writeIntArray(splitRevisionCodes);
dest.writeInt(recommendedInstallLocation);
@@ -111,6 +130,7 @@ public class PackageInfoLite implements Parcelable {
packageName = source.readString();
splitNames = source.createStringArray();
versionCode = source.readInt();
+ versionCodeMajor = source.readInt();
baseRevisionCode = source.readInt();
splitRevisionCodes = source.createIntArray();
recommendedInstallLocation = source.readInt();
diff --git a/android/content/pm/PackageList.java b/android/content/pm/PackageList.java
new file mode 100644
index 00000000..cfd99abc
--- /dev/null
+++ b/android/content/pm/PackageList.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.PackageManagerInternal.PackageListObserver;
+
+import com.android.server.LocalServices;
+
+import java.util.List;
+
+/**
+ * All of the package name installed on the system.
+ * <p>A self observable list that automatically removes the listener when it goes out of scope.
+ *
+ * @hide Only for use within the system server.
+ */
+public class PackageList implements PackageListObserver, AutoCloseable {
+ private final PackageListObserver mWrappedObserver;
+ private final List<String> mPackageNames;
+
+ /**
+ * Create a new object.
+ * <p>Ownership of the given {@link List} transfers to this object and should not
+ * be modified by the caller.
+ */
+ public PackageList(@NonNull List<String> packageNames, @Nullable PackageListObserver observer) {
+ mPackageNames = packageNames;
+ mWrappedObserver = observer;
+ }
+
+ @Override
+ public void onPackageAdded(String packageName) {
+ if (mWrappedObserver != null) {
+ mWrappedObserver.onPackageAdded(packageName);
+ }
+ }
+
+ @Override
+ public void onPackageRemoved(String packageName) {
+ if (mWrappedObserver != null) {
+ mWrappedObserver.onPackageRemoved(packageName);
+ }
+ }
+
+ @Override
+ public void close() throws Exception {
+ LocalServices.getService(PackageManagerInternal.class).removePackageListObserver(this);
+ }
+
+ /**
+ * Returns the names of packages installed on the system.
+ * <p>The list is a copy-in-time and the actual set of installed packages may differ. Real
+ * time updates to the package list are sent via the {@link PackageListObserver} callback.
+ */
+ public @NonNull List<String> getPackageNames() {
+ return mPackageNames;
+ }
+}
diff --git a/android/content/pm/PackageManager.java b/android/content/pm/PackageManager.java
index f796aabe..2d726329 100644
--- a/android/content/pm/PackageManager.java
+++ b/android/content/pm/PackageManager.java
@@ -42,6 +42,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.pm.PackageParser.PackageParserException;
+import android.content.pm.dex.ArtManager;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.Rect;
@@ -2074,6 +2075,13 @@ public abstract class PackageManager {
public static final String FEATURE_TELEPHONY_EUICC = "android.hardware.telephony.euicc";
/**
+ * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: The device
+ * supports cell-broadcast reception using the MBMS APIs.
+ */
+ @SdkConstant(SdkConstantType.FEATURE)
+ public static final String FEATURE_TELEPHONY_MBMS = "android.hardware.telephony.mbms";
+
+ /**
* Feature for {@link #getSystemAvailableFeatures} and
* {@link #hasSystemFeature}: The device supports connecting to USB devices
* as the USB host.
@@ -2634,13 +2642,22 @@ public abstract class PackageManager {
/**
* Extra field name for the version code of a package pending verification.
- *
+ * @deprecated Use {@link #EXTRA_VERIFICATION_LONG_VERSION_CODE} instead.
* @hide
*/
+ @Deprecated
public static final String EXTRA_VERIFICATION_VERSION_CODE
= "android.content.pm.extra.VERIFICATION_VERSION_CODE";
/**
+ * Extra field name for the long version code of a package pending verification.
+ *
+ * @hide
+ */
+ public static final String EXTRA_VERIFICATION_LONG_VERSION_CODE =
+ "android.content.pm.extra.VERIFICATION_LONG_VERSION_CODE";
+
+ /**
* Extra field name for the ID of a intent filter pending verification.
* Passed to an intent filter verifier and is used to call back to
* {@link #verifyIntentFilter}
@@ -5842,4 +5859,14 @@ public abstract class PackageManager {
@SystemApi
public abstract void registerDexModule(String dexModulePath,
@Nullable DexModuleRegisterCallback callback);
+
+ /**
+ * Returns the {@link ArtManager} associated with this package manager.
+ *
+ * @hide
+ */
+ @SystemApi
+ public @NonNull ArtManager getArtManager() {
+ throw new UnsupportedOperationException("getArtManager not implemented in subclass");
+ }
}
diff --git a/android/content/pm/PackageManagerInternal.java b/android/content/pm/PackageManagerInternal.java
index 713cd109..8ee8e102 100644
--- a/android/content/pm/PackageManagerInternal.java
+++ b/android/content/pm/PackageManagerInternal.java
@@ -53,6 +53,14 @@ public abstract class PackageManagerInternal {
@Retention(RetentionPolicy.SOURCE)
public @interface KnownPackage {}
+ /** Observer called whenever the list of packages changes */
+ public interface PackageListObserver {
+ /** A package was added to the system. */
+ void onPackageAdded(@NonNull String packageName);
+ /** A package was removed from the system. */
+ void onPackageRemoved(@NonNull String packageName);
+ }
+
/**
* Provider for package names.
*/
@@ -435,6 +443,35 @@ public abstract class PackageManagerInternal {
public abstract @Nullable PackageParser.Package getPackage(@NonNull String packageName);
/**
+ * Returns a list without a change observer.
+ *
+ * {@see #getPackageList(PackageListObserver)}
+ */
+ public @NonNull PackageList getPackageList() {
+ return getPackageList(null);
+ }
+
+ /**
+ * Returns the list of packages installed at the time of the method call.
+ * <p>The given observer is notified when the list of installed packages
+ * changes [eg. a package was installed or uninstalled]. It will not be
+ * notified if a package is updated.
+ * <p>The package list will not be updated automatically as packages are
+ * installed / uninstalled. Any changes must be handled within the observer.
+ */
+ public abstract @NonNull PackageList getPackageList(@Nullable PackageListObserver observer);
+
+ /**
+ * Removes the observer.
+ * <p>Generally not needed. {@link #getPackageList(PackageListObserver)} will automatically
+ * remove the observer.
+ * <p>Does nothing if the observer isn't currently registered.
+ * <p>Observers are notified asynchronously and it's possible for an observer to be
+ * invoked after its been removed.
+ */
+ public abstract void removePackageListObserver(@NonNull PackageListObserver observer);
+
+ /**
* Returns a package object for the disabled system package name.
*/
public abstract @Nullable PackageParser.Package getDisabledPackage(@NonNull String packageName);
diff --git a/android/content/pm/PackageParser.java b/android/content/pm/PackageParser.java
index ebeaad78..77eb57f2 100644
--- a/android/content/pm/PackageParser.java
+++ b/android/content/pm/PackageParser.java
@@ -32,7 +32,6 @@ import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_ACTIVITIES_RESIZE_
import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_UNRESIZEABLE;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME;
-import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NOT_APK;
@@ -87,7 +86,7 @@ import android.util.Slog;
import android.util.SparseArray;
import android.util.TypedValue;
import android.util.apk.ApkSignatureSchemeV2Verifier;
-import android.util.jar.StrictJarFile;
+import android.util.apk.ApkSignatureVerifier;
import android.view.Gravity;
import com.android.internal.R;
@@ -106,12 +105,10 @@ import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Constructor;
-import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
@@ -129,8 +126,6 @@ import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.zip.ZipEntry;
/**
* Parser for package files (APKs) on disk. This supports apps packaged either
@@ -173,7 +168,7 @@ public class PackageParser {
// TODO: refactor "codePath" to "apkPath"
/** File name in an APK for the Android manifest. */
- private static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml";
+ public static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml";
/** Path prefix for apps on expanded storage */
private static final String MNT_EXPAND = "/mnt/expand/";
@@ -394,6 +389,7 @@ public class PackageParser {
public static class PackageLite {
public final String packageName;
public final int versionCode;
+ public final int versionCodeMajor;
public final int installLocation;
public final VerifierInfo[] verifiers;
@@ -436,6 +432,7 @@ public class PackageParser {
String[] splitCodePaths, int[] splitRevisionCodes) {
this.packageName = baseApk.packageName;
this.versionCode = baseApk.versionCode;
+ this.versionCodeMajor = baseApk.versionCodeMajor;
this.installLocation = baseApk.installLocation;
this.verifiers = baseApk.verifiers;
this.splitNames = splitNames;
@@ -476,6 +473,7 @@ public class PackageParser {
public final String configForSplit;
public final String usesSplitName;
public final int versionCode;
+ public final int versionCodeMajor;
public final int revisionCode;
public final int installLocation;
public final VerifierInfo[] verifiers;
@@ -489,11 +487,11 @@ public class PackageParser {
public final boolean isolatedSplits;
public ApkLite(String codePath, String packageName, String splitName, boolean isFeatureSplit,
- String configForSplit, String usesSplitName, int versionCode, int revisionCode,
- int installLocation, List<VerifierInfo> verifiers, Signature[] signatures,
- Certificate[][] certificates, boolean coreApp, boolean debuggable,
- boolean multiArch, boolean use32bitAbi, boolean extractNativeLibs,
- boolean isolatedSplits) {
+ String configForSplit, String usesSplitName, int versionCode, int versionCodeMajor,
+ int revisionCode, int installLocation, List<VerifierInfo> verifiers,
+ Signature[] signatures, Certificate[][] certificates, boolean coreApp,
+ boolean debuggable, boolean multiArch, boolean use32bitAbi,
+ boolean extractNativeLibs, boolean isolatedSplits) {
this.codePath = codePath;
this.packageName = packageName;
this.splitName = splitName;
@@ -501,6 +499,7 @@ public class PackageParser {
this.configForSplit = configForSplit;
this.usesSplitName = usesSplitName;
this.versionCode = versionCode;
+ this.versionCodeMajor = versionCodeMajor;
this.revisionCode = revisionCode;
this.installLocation = installLocation;
this.verifiers = verifiers.toArray(new VerifierInfo[verifiers.size()]);
@@ -513,6 +512,10 @@ public class PackageParser {
this.extractNativeLibs = extractNativeLibs;
this.isolatedSplits = isolatedSplits;
}
+
+ public long getLongVersionCode() {
+ return PackageInfo.composeLongVersionCode(versionCodeMajor, versionCode);
+ }
}
/**
@@ -663,6 +666,7 @@ public class PackageParser {
pi.packageName = p.packageName;
pi.splitNames = p.splitNames;
pi.versionCode = p.mVersionCode;
+ pi.versionCodeMajor = p.mVersionCodeMajor;
pi.baseRevisionCode = p.baseRevisionCode;
pi.splitRevisionCodes = p.splitRevisionCodes;
pi.versionName = p.mVersionName;
@@ -680,7 +684,15 @@ public class PackageParser {
pi.requiredAccountType = p.mRequiredAccountType;
pi.overlayTarget = p.mOverlayTarget;
pi.overlayPriority = p.mOverlayPriority;
- pi.isStaticOverlay = p.mIsStaticOverlay;
+
+ if (p.mIsStaticOverlay) {
+ pi.mOverlayFlags |= PackageInfo.FLAG_OVERLAY_STATIC;
+ }
+
+ if (p.mTrustedOverlay) {
+ pi.mOverlayFlags |= PackageInfo.FLAG_OVERLAY_TRUSTED;
+ }
+
pi.compileSdkVersion = p.mCompileSdkVersion;
pi.compileSdkVersionCodename = p.mCompileSdkVersionCodename;
pi.firstInstallTime = firstInstallTime;
@@ -804,23 +816,6 @@ public class PackageParser {
return pi;
}
- private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry)
- throws PackageParserException {
- InputStream is = null;
- try {
- // We must read the stream for the JarEntry to retrieve
- // its certificates.
- is = jarFile.getInputStream(entry);
- readFullyIgnoringContents(is);
- return jarFile.getCertificateChains(entry);
- } catch (IOException | RuntimeException e) {
- throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
- "Failed reading " + entry.getName() + " in " + jarFile, e);
- } finally {
- IoUtils.closeQuietly(is);
- }
- }
-
public static final int PARSE_MUST_BE_APK = 1 << 0;
public static final int PARSE_IGNORE_PROCESSES = 1 << 1;
public static final int PARSE_FORWARD_LOCK = 1 << 2;
@@ -1500,7 +1495,7 @@ public class PackageParser {
pkg.mCertificates = certificates;
try {
- pkg.mSignatures = convertToSignatures(certificates);
+ pkg.mSignatures = ApkSignatureVerifier.convertToSignatures(certificates);
} catch (CertificateEncodingException e) {
// certificates weren't encoded properly; something went wrong
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
@@ -1563,157 +1558,49 @@ public class PackageParser {
throws PackageParserException {
final String apkPath = apkFile.getAbsolutePath();
- // Try to verify the APK using APK Signature Scheme v2.
- boolean verified = false;
- {
- Certificate[][] allSignersCerts = null;
- Signature[] signatures = null;
- try {
- Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV2");
- allSignersCerts = ApkSignatureSchemeV2Verifier.verify(apkPath);
- signatures = convertToSignatures(allSignersCerts);
- // APK verified using APK Signature Scheme v2.
- verified = true;
- } catch (ApkSignatureSchemeV2Verifier.SignatureNotFoundException e) {
- // No APK Signature Scheme v2 signature found
- if ((parseFlags & PARSE_IS_EPHEMERAL) != 0) {
- throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
- "No APK Signature Scheme v2 signature in ephemeral package " + apkPath,
- e);
- }
- // Static shared libraries must use only the V2 signing scheme
- if (pkg.applicationInfo.isStaticSharedLibrary()) {
- throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
- "Static shared libs must use v2 signature scheme " + apkPath);
- }
- } catch (Exception e) {
- // APK Signature Scheme v2 signature was found but did not verify
+ int minSignatureScheme = ApkSignatureVerifier.VERSION_JAR_SIGNATURE_SCHEME;
+ if (pkg.applicationInfo.isStaticSharedLibrary()) {
+ // must use v2 signing scheme
+ minSignatureScheme = ApkSignatureVerifier.VERSION_APK_SIGNATURE_SCHEME_V2;
+ }
+ ApkSignatureVerifier.Result verified;
+ if ((parseFlags & PARSE_IS_SYSTEM_DIR) != 0) {
+ // systemDir APKs are already trusted, save time by not verifying
+ verified = ApkSignatureVerifier.plsCertsNoVerifyOnlyCerts(
+ apkPath, minSignatureScheme);
+ } else {
+ verified = ApkSignatureVerifier.verify(apkPath, minSignatureScheme);
+ }
+ if (verified.signatureSchemeVersion
+ < ApkSignatureVerifier.VERSION_APK_SIGNATURE_SCHEME_V2) {
+ // TODO (b/68860689): move this logic to packagemanagerserivce
+ if ((parseFlags & PARSE_IS_EPHEMERAL) != 0) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
- "Failed to collect certificates from " + apkPath
- + " using APK Signature Scheme v2",
- e);
- } finally {
- Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
- }
-
- if (verified) {
- if (pkg.mCertificates == null) {
- pkg.mCertificates = allSignersCerts;
- pkg.mSignatures = signatures;
- pkg.mSigningKeys = new ArraySet<>(allSignersCerts.length);
- for (int i = 0; i < allSignersCerts.length; i++) {
- Certificate[] signerCerts = allSignersCerts[i];
- Certificate signerCert = signerCerts[0];
- pkg.mSigningKeys.add(signerCert.getPublicKey());
- }
- } else {
- if (!Signature.areExactMatch(pkg.mSignatures, signatures)) {
- throw new PackageParserException(
- INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
- apkPath + " has mismatched certificates");
- }
- }
- // Not yet done, because we need to confirm that AndroidManifest.xml exists and,
- // if requested, that classes.dex exists.
+ "No APK Signature Scheme v2 signature in ephemeral package " + apkPath);
}
}
- StrictJarFile jarFile = null;
- try {
- Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "strictJarFileCtor");
- // Ignore signature stripping protections when verifying APKs from system partition.
- // For those APKs we only care about extracting signer certificates, and don't care
- // about verifying integrity.
- boolean signatureSchemeRollbackProtectionsEnforced =
- (parseFlags & PARSE_IS_SYSTEM_DIR) == 0;
- jarFile = new StrictJarFile(
- apkPath,
- !verified, // whether to verify JAR signature
- signatureSchemeRollbackProtectionsEnforced);
- Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
-
- // Always verify manifest, regardless of source
- final ZipEntry manifestEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME);
- if (manifestEntry == null) {
- throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
- "Package " + apkPath + " has no manifest");
- }
-
- // Optimization: early termination when APK already verified
- if (verified) {
- return;
+ // Verify that entries are signed consistently with the first pkg
+ // we encountered. Note that for splits, certificates may have
+ // already been populated during an earlier parse of a base APK.
+ if (pkg.mCertificates == null) {
+ pkg.mCertificates = verified.certs;
+ pkg.mSignatures = verified.sigs;
+ pkg.mSigningKeys = new ArraySet<>(verified.certs.length);
+ for (int i = 0; i < verified.certs.length; i++) {
+ Certificate[] signerCerts = verified.certs[i];
+ Certificate signerCert = signerCerts[0];
+ pkg.mSigningKeys.add(signerCert.getPublicKey());
}
-
- // APK's integrity needs to be verified using JAR signature scheme.
- Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV1");
- final List<ZipEntry> toVerify = new ArrayList<>();
- toVerify.add(manifestEntry);
-
- // If we're parsing an untrusted package, verify all contents
- if ((parseFlags & PARSE_IS_SYSTEM_DIR) == 0) {
- final Iterator<ZipEntry> i = jarFile.iterator();
- while (i.hasNext()) {
- final ZipEntry entry = i.next();
-
- if (entry.isDirectory()) continue;
-
- final String entryName = entry.getName();
- if (entryName.startsWith("META-INF/")) continue;
- if (entryName.equals(ANDROID_MANIFEST_FILENAME)) continue;
-
- toVerify.add(entry);
- }
- }
-
- // Verify that entries are signed consistently with the first entry
- // we encountered. Note that for splits, certificates may have
- // already been populated during an earlier parse of a base APK.
- for (ZipEntry entry : toVerify) {
- final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
- if (ArrayUtils.isEmpty(entryCerts)) {
- throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
- "Package " + apkPath + " has no certificates at entry "
- + entry.getName());
- }
- final Signature[] entrySignatures = convertToSignatures(entryCerts);
-
- if (pkg.mCertificates == null) {
- pkg.mCertificates = entryCerts;
- pkg.mSignatures = entrySignatures;
- pkg.mSigningKeys = new ArraySet<PublicKey>();
- for (int i=0; i < entryCerts.length; i++) {
- pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey());
- }
- } else {
- if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) {
- throw new PackageParserException(
- INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, "Package " + apkPath
- + " has mismatched certificates at entry "
- + entry.getName());
- }
- }
+ } else {
+ if (!Signature.areExactMatch(pkg.mSignatures, verified.sigs)) {
+ throw new PackageParserException(
+ INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
+ apkPath + " has mismatched certificates");
}
- Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
- } catch (GeneralSecurityException e) {
- throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
- "Failed to collect certificates from " + apkPath, e);
- } catch (IOException | RuntimeException e) {
- throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
- "Failed to collect certificates from " + apkPath, e);
- } finally {
- closeQuietly(jarFile);
}
}
- private static Signature[] convertToSignatures(Certificate[][] certs)
- throws CertificateEncodingException {
- final Signature[] res = new Signature[certs.length];
- for (int i = 0; i < certs.length; i++) {
- res[i] = new Signature(certs[i]);
- }
- return res;
- }
-
private static AssetManager newConfiguredAssetManager() {
AssetManager assetManager = new AssetManager();
assetManager.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
@@ -1880,6 +1767,7 @@ public class PackageParser {
int installLocation = PARSE_DEFAULT_INSTALL_LOCATION;
int versionCode = 0;
+ int versionCodeMajor = 0;
int revisionCode = 0;
boolean coreApp = false;
boolean debuggable = false;
@@ -1898,6 +1786,8 @@ public class PackageParser {
PARSE_DEFAULT_INSTALL_LOCATION);
} else if (attr.equals("versionCode")) {
versionCode = attrs.getAttributeIntValue(i, 0);
+ } else if (attr.equals("versionCodeMajor")) {
+ versionCodeMajor = attrs.getAttributeIntValue(i, 0);
} else if (attr.equals("revisionCode")) {
revisionCode = attrs.getAttributeIntValue(i, 0);
} else if (attr.equals("coreApp")) {
@@ -1963,9 +1853,9 @@ public class PackageParser {
}
return new ApkLite(codePath, packageSplit.first, packageSplit.second, isFeatureSplit,
- configForSplit, usesSplitName, versionCode, revisionCode, installLocation,
- verifiers, signatures, certificates, coreApp, debuggable, multiArch, use32bitAbi,
- extractNativeLibs, isolatedSplits);
+ configForSplit, usesSplitName, versionCode, versionCodeMajor, revisionCode,
+ installLocation, verifiers, signatures, certificates, coreApp, debuggable,
+ multiArch, use32bitAbi, extractNativeLibs, isolatedSplits);
}
/**
@@ -2086,8 +1976,11 @@ public class PackageParser {
TypedArray sa = res.obtainAttributes(parser,
com.android.internal.R.styleable.AndroidManifest);
- pkg.mVersionCode = pkg.applicationInfo.versionCode = sa.getInteger(
+ pkg.mVersionCode = sa.getInteger(
com.android.internal.R.styleable.AndroidManifest_versionCode, 0);
+ pkg.mVersionCodeMajor = sa.getInteger(
+ com.android.internal.R.styleable.AndroidManifest_versionCodeMajor, 0);
+ pkg.applicationInfo.versionCode = pkg.getLongVersionCode();
pkg.baseRevisionCode = sa.getInteger(
com.android.internal.R.styleable.AndroidManifest_revisionCode, 0);
pkg.mVersionName = sa.getNonConfigurationString(
@@ -2912,7 +2805,7 @@ public class PackageParser {
1, additionalCertSha256Digests.length);
pkg.usesStaticLibraries = ArrayUtils.add(pkg.usesStaticLibraries, lname);
- pkg.usesStaticLibrariesVersions = ArrayUtils.appendInt(
+ pkg.usesStaticLibrariesVersions = ArrayUtils.appendLong(
pkg.usesStaticLibrariesVersions, version, true);
pkg.usesStaticLibrariesCertDigests = ArrayUtils.appendElement(String[].class,
pkg.usesStaticLibrariesCertDigests, certSha256Digests, true);
@@ -3733,6 +3626,11 @@ public class PackageParser {
}
ai.taskAffinity = buildTaskAffinityName(ai.packageName, ai.packageName,
str, outError);
+ String factory = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestApplication_appComponentFactory);
+ if (factory != null) {
+ ai.appComponentFactory = buildClassName(ai.packageName, factory, outError);
+ }
if (outError[0] == null) {
CharSequence pname;
@@ -3867,6 +3765,9 @@ public class PackageParser {
com.android.internal.R.styleable.AndroidManifestStaticLibrary_name);
final int version = sa.getInt(
com.android.internal.R.styleable.AndroidManifestStaticLibrary_version, -1);
+ final int versionMajor = sa.getInt(
+ com.android.internal.R.styleable.AndroidManifestStaticLibrary_versionMajor,
+ 0);
sa.recycle();
@@ -3894,7 +3795,12 @@ public class PackageParser {
}
owner.staticSharedLibName = lname.intern();
- owner.staticSharedLibVersion = version;
+ if (version >= 0) {
+ owner.staticSharedLibVersion =
+ PackageInfo.composeLongVersionCode(versionMajor, version);
+ } else {
+ owner.staticSharedLibVersion = version;
+ }
ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_STATIC_SHARED_LIBRARY;
XmlUtils.skipCurrentTag(parser);
@@ -5895,11 +5801,11 @@ public class PackageParser {
public ArrayList<Package> childPackages;
public String staticSharedLibName = null;
- public int staticSharedLibVersion = 0;
+ public long staticSharedLibVersion = 0;
public ArrayList<String> libraryNames = null;
public ArrayList<String> usesLibraries = null;
public ArrayList<String> usesStaticLibraries = null;
- public int[] usesStaticLibrariesVersions = null;
+ public long[] usesStaticLibrariesVersions = null;
public String[][] usesStaticLibrariesCertDigests = null;
public ArrayList<String> usesOptionalLibraries = null;
public String[] usesLibraryFiles = null;
@@ -5916,6 +5822,14 @@ public class PackageParser {
// The version code declared for this package.
public int mVersionCode;
+ // The major version code declared for this package.
+ public int mVersionCodeMajor;
+
+ // Return long containing mVersionCode and mVersionCodeMajor.
+ public long getLongVersionCode() {
+ return PackageInfo.composeLongVersionCode(mVersionCodeMajor, mVersionCode);
+ }
+
// The version name declared for this package.
public String mVersionName;
@@ -6267,6 +6181,11 @@ public class PackageParser {
}
/** @hide */
+ public boolean isVendor() {
+ return applicationInfo.isVendor();
+ }
+
+ /** @hide */
public boolean isPrivileged() {
return applicationInfo.isPrivilegedApp();
}
@@ -6385,7 +6304,7 @@ public class PackageParser {
if (staticSharedLibName != null) {
staticSharedLibName = staticSharedLibName.intern();
}
- staticSharedLibVersion = dest.readInt();
+ staticSharedLibVersion = dest.readLong();
libraryNames = dest.createStringArrayList();
internStringArrayList(libraryNames);
usesLibraries = dest.createStringArrayList();
@@ -6399,8 +6318,8 @@ public class PackageParser {
usesStaticLibraries = new ArrayList<>(libCount);
dest.readStringList(usesStaticLibraries);
internStringArrayList(usesStaticLibraries);
- usesStaticLibrariesVersions = new int[libCount];
- dest.readIntArray(usesStaticLibrariesVersions);
+ usesStaticLibrariesVersions = new long[libCount];
+ dest.readLongArray(usesStaticLibrariesVersions);
usesStaticLibrariesCertDigests = new String[libCount][];
for (int i = 0; i < libCount; i++) {
usesStaticLibrariesCertDigests[i] = dest.createStringArray();
@@ -6418,6 +6337,7 @@ public class PackageParser {
mAdoptPermissions = dest.createStringArrayList();
mAppMetaData = dest.readBundle();
mVersionCode = dest.readInt();
+ mVersionCodeMajor = dest.readInt();
mVersionName = dest.readString();
if (mVersionName != null) {
mVersionName = mVersionName.intern();
@@ -6540,7 +6460,7 @@ public class PackageParser {
dest.writeParcelableList(childPackages, flags);
dest.writeString(staticSharedLibName);
- dest.writeInt(staticSharedLibVersion);
+ dest.writeLong(staticSharedLibVersion);
dest.writeStringList(libraryNames);
dest.writeStringList(usesLibraries);
dest.writeStringList(usesOptionalLibraries);
@@ -6551,7 +6471,7 @@ public class PackageParser {
} else {
dest.writeInt(usesStaticLibraries.size());
dest.writeStringList(usesStaticLibraries);
- dest.writeIntArray(usesStaticLibrariesVersions);
+ dest.writeLongArray(usesStaticLibrariesVersions);
for (String[] usesStaticLibrariesCertDigest : usesStaticLibrariesCertDigests) {
dest.writeStringArray(usesStaticLibrariesCertDigest);
}
@@ -6564,6 +6484,7 @@ public class PackageParser {
dest.writeStringList(mAdoptPermissions);
dest.writeBundle(mAppMetaData);
dest.writeInt(mVersionCode);
+ dest.writeInt(mVersionCodeMajor);
dest.writeString(mVersionName);
dest.writeString(mSharedUserId);
dest.writeInt(mSharedUserLabel);
@@ -7601,33 +7522,6 @@ public class PackageParser {
sCompatibilityModeEnabled = compatibilityModeEnabled;
}
- private static AtomicReference<byte[]> sBuffer = new AtomicReference<byte[]>();
-
- public static long readFullyIgnoringContents(InputStream in) throws IOException {
- byte[] buffer = sBuffer.getAndSet(null);
- if (buffer == null) {
- buffer = new byte[4096];
- }
-
- int n = 0;
- int count = 0;
- while ((n = in.read(buffer, 0, buffer.length)) != -1) {
- count += n;
- }
-
- sBuffer.set(buffer);
- return count;
- }
-
- public static void closeQuietly(StrictJarFile jarFile) {
- if (jarFile != null) {
- try {
- jarFile.close();
- } catch (Exception ignored) {
- }
- }
- }
-
public static class PackageParserException extends Exception {
public final int error;
diff --git a/android/content/pm/PermissionInfo.java b/android/content/pm/PermissionInfo.java
index 75887624..21bd7f0c 100644
--- a/android/content/pm/PermissionInfo.java
+++ b/android/content/pm/PermissionInfo.java
@@ -17,6 +17,7 @@
package android.content.pm;
import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
@@ -144,6 +145,16 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable {
public static final int PROTECTION_FLAG_OEM = 0x4000;
/**
+ * Additional flag for {${link #protectionLevel}, corresponding
+ * to the <code>vendorPrivileged</code> value of
+ * {@link android.R.attr#protectionLevel}.
+ *
+ * @hide
+ */
+ @TestApi
+ public static final int PROTECTION_FLAG_VENDOR_PRIVILEGED = 0x8000;
+
+ /**
* Mask for {@link #protectionLevel}: the basic protection type.
*/
public static final int PROTECTION_MASK_BASE = 0xf;
@@ -231,6 +242,12 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable {
if (level == PROTECTION_SIGNATURE_OR_SYSTEM) {
level = PROTECTION_SIGNATURE | PROTECTION_FLAG_PRIVILEGED;
}
+ if ((level & PROTECTION_FLAG_VENDOR_PRIVILEGED) != 0
+ && (level & PROTECTION_FLAG_PRIVILEGED) == 0) {
+ // 'vendorPrivileged' must be 'privileged'. If not,
+ // drop the vendorPrivileged.
+ level = level & ~PROTECTION_FLAG_VENDOR_PRIVILEGED;
+ }
return level;
}
@@ -284,6 +301,9 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable {
if ((level & PermissionInfo.PROTECTION_FLAG_OEM) != 0) {
protLevel += "|oem";
}
+ if ((level & PermissionInfo.PROTECTION_FLAG_VENDOR_PRIVILEGED) != 0) {
+ protLevel += "|vendorPrivileged";
+ }
return protLevel;
}
diff --git a/android/content/pm/RegisteredServicesCache.java b/android/content/pm/RegisteredServicesCache.java
index aea843ad..56d61efd 100644
--- a/android/content/pm/RegisteredServicesCache.java
+++ b/android/content/pm/RegisteredServicesCache.java
@@ -361,7 +361,7 @@ public abstract class RegisteredServicesCache<V> {
}
IntArray updatedUids = null;
for (ServiceInfo<V> service : allServices) {
- int versionCode = service.componentInfo.applicationInfo.versionCode;
+ long versionCode = service.componentInfo.applicationInfo.versionCode;
String pkg = service.componentInfo.packageName;
ApplicationInfo newAppInfo = null;
try {
diff --git a/android/content/pm/SharedLibraryInfo.java b/android/content/pm/SharedLibraryInfo.java
index 7d301a31..33bc9515 100644
--- a/android/content/pm/SharedLibraryInfo.java
+++ b/android/content/pm/SharedLibraryInfo.java
@@ -36,13 +36,11 @@ import java.util.List;
public final class SharedLibraryInfo implements Parcelable {
/** @hide */
- @IntDef(
- flag = true,
- value = {
- TYPE_BUILTIN,
- TYPE_DYNAMIC,
- TYPE_STATIC,
- })
+ @IntDef(flag = true, prefix = { "TYPE_" }, value = {
+ TYPE_BUILTIN,
+ TYPE_DYNAMIC,
+ TYPE_STATIC,
+ })
@Retention(RetentionPolicy.SOURCE)
@interface Type{}
@@ -73,8 +71,7 @@ public final class SharedLibraryInfo implements Parcelable {
private final String mName;
- // TODO: Make long when we change the paltform to use longs
- private final int mVersion;
+ private final long mVersion;
private final @Type int mType;
private final VersionedPackage mDeclaringPackage;
private final List<VersionedPackage> mDependentPackages;
@@ -90,7 +87,7 @@ public final class SharedLibraryInfo implements Parcelable {
*
* @hide
*/
- public SharedLibraryInfo(String name, int version, int type,
+ public SharedLibraryInfo(String name, long version, int type,
VersionedPackage declaringPackage, List<VersionedPackage> dependentPackages) {
mName = name;
mVersion = version;
@@ -100,7 +97,7 @@ public final class SharedLibraryInfo implements Parcelable {
}
private SharedLibraryInfo(Parcel parcel) {
- this(parcel.readString(), parcel.readInt(), parcel.readInt(),
+ this(parcel.readString(), parcel.readLong(), parcel.readInt(),
parcel.readParcelable(null), parcel.readArrayList(null));
}
@@ -124,6 +121,14 @@ public final class SharedLibraryInfo implements Parcelable {
}
/**
+ * @deprecated Use {@link #getLongVersion()} instead.
+ */
+ @Deprecated
+ public @IntRange(from = -1) int getVersion() {
+ return mVersion < 0 ? (int) mVersion : (int) (mVersion & 0x7fffffff);
+ }
+
+ /**
* Gets the version of the library. For {@link #TYPE_STATIC static} libraries
* this is the declared version and for {@link #TYPE_DYNAMIC dynamic} and
* {@link #TYPE_BUILTIN builtin} it is {@link #VERSION_UNDEFINED} as these
@@ -131,7 +136,7 @@ public final class SharedLibraryInfo implements Parcelable {
*
* @return The version.
*/
- public @IntRange(from = -1) int getVersion() {
+ public @IntRange(from = -1) long getLongVersion() {
return mVersion;
}
@@ -192,7 +197,7 @@ public final class SharedLibraryInfo implements Parcelable {
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(mName);
- parcel.writeInt(mVersion);
+ parcel.writeLong(mVersion);
parcel.writeInt(mType);
parcel.writeParcelable(mDeclaringPackage, flags);
parcel.writeList(mDependentPackages);
diff --git a/android/content/pm/ShortcutInfo.java b/android/content/pm/ShortcutInfo.java
index 9ff07757..8839cf9d 100644
--- a/android/content/pm/ShortcutInfo.java
+++ b/android/content/pm/ShortcutInfo.java
@@ -109,8 +109,7 @@ public final class ShortcutInfo implements Parcelable {
public static final int FLAG_SHADOW = 1 << 12;
/** @hide */
- @IntDef(flag = true,
- value = {
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
FLAG_DYNAMIC,
FLAG_PINNED,
FLAG_HAS_ICON_RES,
@@ -153,15 +152,14 @@ public final class ShortcutInfo implements Parcelable {
| CLONE_REMOVE_RES_NAMES;
/** @hide */
- @IntDef(flag = true,
- value = {
- CLONE_REMOVE_ICON,
- CLONE_REMOVE_INTENT,
- CLONE_REMOVE_NON_KEY_INFO,
- CLONE_REMOVE_RES_NAMES,
- CLONE_REMOVE_FOR_CREATOR,
- CLONE_REMOVE_FOR_LAUNCHER
- })
+ @IntDef(flag = true, prefix = { "CLONE_" }, value = {
+ CLONE_REMOVE_ICON,
+ CLONE_REMOVE_INTENT,
+ CLONE_REMOVE_NON_KEY_INFO,
+ CLONE_REMOVE_RES_NAMES,
+ CLONE_REMOVE_FOR_CREATOR,
+ CLONE_REMOVE_FOR_LAUNCHER
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface CloneFlags {}
@@ -212,7 +210,7 @@ public final class ShortcutInfo implements Parcelable {
public static final int DISABLED_REASON_OTHER_RESTORE_ISSUE = 103;
/** @hide */
- @IntDef(value = {
+ @IntDef(prefix = { "DISABLED_REASON_" }, value = {
DISABLED_REASON_NOT_DISABLED,
DISABLED_REASON_BY_APP,
DISABLED_REASON_APP_CHANGED,
@@ -220,7 +218,7 @@ public final class ShortcutInfo implements Parcelable {
DISABLED_REASON_BACKUP_NOT_SUPPORTED,
DISABLED_REASON_SIGNATURE_MISMATCH,
DISABLED_REASON_OTHER_RESTORE_ISSUE,
- })
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface DisabledReason{}
diff --git a/android/content/pm/ShortcutManager.java b/android/content/pm/ShortcutManager.java
index 61b0eb0b..30222b74 100644
--- a/android/content/pm/ShortcutManager.java
+++ b/android/content/pm/ShortcutManager.java
@@ -36,15 +36,26 @@ import com.android.internal.annotations.VisibleForTesting;
import java.util.List;
/**
- * The ShortcutManager manages an app's <em>shortcuts</em>. Shortcuts provide users with quick
- * access to activities other than an app's main activity in the currently-active launcher, provided
- * that the launcher supports app shortcuts. For example, an email app may publish the "compose new
- * email" action, which will directly open the compose activity. The {@link ShortcutInfo} class
- * contains information about each of the shortcuts themselves.
+ * The ShortcutManager performs operations on an app's set of <em>shortcuts</em>. The
+ * {@link ShortcutInfo} class contains information about each of the shortcuts themselves.
+ *
+ * <p>An app's shortcuts represent specific tasks and actions that users can perform within your
+ * app. When a user selects a shortcut in the currently-active launcher, your app opens an activity
+ * other than the app's starting activity, provided that the currently-active launcher supports app
+ * shortcuts.</p>
+ *
+ * <p>The types of shortcuts that you create for your app depend on the app's key use cases. For
+ * example, an email app may publish the "compose new email" shortcut, which allows the app to
+ * directly open the compose activity.</p>
+ *
+ * <p class="note"><b>Note:</b> Only main activities&mdash;activities that handle the
+ * {@link Intent#ACTION_MAIN} action and the {@link Intent#CATEGORY_LAUNCHER} category&mdash;can
+ * have shortcuts. If an app has multiple main activities, you need to define the set of shortcuts
+ * for <em>each</em> activity.
*
* <p>This page discusses the implementation details of the <code>ShortcutManager</code> class. For
- * guidance on performing operations on app shortcuts within your app, see the
- * <a href="/guide/topics/ui/shortcuts.html">App Shortcuts</a> feature guide.
+ * definitions of key terms and guidance on performing operations on shortcuts within your app, see
+ * the <a href="/guide/topics/ui/shortcuts.html">App Shortcuts</a> feature guide.
*
* <h3>Shortcut characteristics</h3>
*
@@ -69,8 +80,8 @@ import java.util.List;
* <ul>
* <li>The user removes it.
* <li>The publisher app associated with the shortcut is uninstalled.
- * <li>The user performs the clear data action on the publisher app from the device's
- * <b>Settings</b> app.
+ * <li>The user selects <b>Clear data</b> from the publisher app's <i>Storage</i> screen, within
+ * the system's <b>Settings</b> app.
* </ul>
*
* <p>Because the system performs
@@ -83,12 +94,17 @@ import java.util.List;
*
* <p>When the launcher displays an app's shortcuts, they should appear in the following order:
*
- * <ul>
- * <li>Static shortcuts (if {@link ShortcutInfo#isDeclaredInManifest()} is {@code true}),
- * and then show dynamic shortcuts (if {@link ShortcutInfo#isDynamic()} is {@code true}).
- * <li>Within each shortcut type (static and dynamic), sort the shortcuts in order of increasing
- * rank according to {@link ShortcutInfo#getRank()}.
- * </ul>
+ * <ol>
+ * <li><b>Static shortcuts:</b> Shortcuts whose {@link ShortcutInfo#isDeclaredInManifest()} method
+ * returns {@code true}.</li>
+ * <li><b>Dynamic shortcuts:</b> Shortcuts whose {@link ShortcutInfo#isDynamic()} method returns
+ * {@code true}.</li>
+ * </ol>
+ *
+ * <p>Within each shortcut type (static and dynamic), shortcuts are sorted in order of increasing
+ * rank according to {@link ShortcutInfo#getRank()}.</p>
+ *
+ * <h4>Shortcut ranks</h4>
*
* <p>Shortcut ranks are non-negative, sequential integers that determine the order in which
* shortcuts appear, assuming that the shortcuts are all in the same category. You can update ranks
@@ -103,64 +119,99 @@ import java.util.List;
*
* <h3>Options for static shortcuts</h3>
*
- * The following list includes descriptions for the different attributes within a static shortcut:
+ * The following list includes descriptions for the different attributes within a static shortcut.
+ * You must provide a value for {@code android:shortcutId} and {@code android:shortcutShortLabel};
+ * all other values are optional.
+ *
* <dl>
* <dt>{@code android:shortcutId}</dt>
- * <dd>Mandatory shortcut ID.
- * <p>
- * This must be a string literal.
- * A resource string, such as <code>@string/foo</code>, cannot be used.
+ * <dd><p>A string literal, which represents the shortcut when a {@code ShortcutManager} object
+ * performs operations on it.</p>
+ * <p class="note"><b>Note: </b>You cannot set this attribute's value to a resource string, such
+ * as <code>@string/foo</code>.</p>
* </dd>
*
* <dt>{@code android:enabled}</dt>
- * <dd>Default is {@code true}. Can be set to {@code false} in order
- * to disable a static shortcut that was published in a previous version and set a custom
- * disabled message. If a custom disabled message is not needed, then a static shortcut can
- * be simply removed from the XML file rather than keeping it with {@code enabled="false"}.</dd>
+ * <dd><p>Whether the user can interact with the shortcut from a supported launcher.</p>
+ * <p>The default value is {@code true}. If you set it to {@code false}, you should also set
+ * {@code android:shortcutDisabledMessage} to a message that explains why you've disabled the
+ * shortcut. If you don't think you need to provide such a message, it's easiest to just remove
+ * the shortcut from the XML file entirely, rather than changing the values of the shortcut's
+ * {@code android:enabled} and {@code android:shortcutDisabledMessage} attributes.
+ * </dd>
*
* <dt>{@code android:icon}</dt>
- * <dd>Shortcut icon.</dd>
+ * <dd><p>The <a href="/topic/performance/graphics/index.html">bitmap</a> or
+ * <a href="/guide/practices/ui_guidelines/icon_design_adaptive.html">adaptive icon</a> that the
+ * launcher uses when displaying the shortcut to the user. This value can be either the path to an
+ * image or the resource file that contains the image. Use adaptive icons whenever possible to
+ * improve performance and consistency.</p>
+ * <p class="note"><b>Note: </b>Shortcut icons cannot include
+ * <a href="/training/material/drawables.html#DrawableTint">tints</a>.
+ * </dd>
*
* <dt>{@code android:shortcutShortLabel}</dt>
- * <dd>Mandatory shortcut short label.
- * See {@link ShortcutInfo.Builder#setShortLabel(CharSequence)}.
- * <p>
- * This must be a resource string, such as <code>@string/shortcut_label</code>.
+ * <dd><p>A concise phrase that describes the shortcut's purpose. For more information, see
+ * {@link ShortcutInfo.Builder#setShortLabel(CharSequence)}.</p>
+ * <p class="note"><b>Note: </b>This attribute's value must be a resource string, such as
+ * <code>@string/shortcut_short_label</code>.</p>
* </dd>
*
* <dt>{@code android:shortcutLongLabel}</dt>
- * <dd>Shortcut long label.
- * See {@link ShortcutInfo.Builder#setLongLabel(CharSequence)}.
- * <p>
- * This must be a resource string, such as <code>@string/shortcut_long_label</code>.
+ * <dd><p>An extended phrase that describes the shortcut's purpose. If there's enough space, the
+ * launcher displays this value instead of {@code android:shortcutShortLabel}. For more
+ * information, see {@link ShortcutInfo.Builder#setLongLabel(CharSequence)}.</p>
+ * <p class="note"><b>Note: </b>This attribute's value must be a resource string, such as
+ * <code>@string/shortcut_long_label</code>.</p>
* </dd>
*
* <dt>{@code android:shortcutDisabledMessage}</dt>
- * <dd>When {@code android:enabled} is set to
- * {@code false}, this attribute is used to display a custom disabled message.
- * <p>
- * This must be a resource string, such as <code>@string/shortcut_disabled_message</code>.
+ * <dd><p>The message that appears in a supported launcher when the user attempts to launch a
+ * disabled shortcut. The message should explain to the user why the shortcut is now disabled.
+ * This attribute's value has no effect if {@code android:enabled} is {@code true}.</p>
+ * <p class="note"><b>Note: </b>This attribute's value must be a resource string, such as
+ * <code>@string/shortcut_disabled_message</code>.</p>
* </dd>
+ * </dl>
+ *
+ * <h3>Inner elements that define static shortcuts</h3>
+ *
+ * <p>The XML file that lists an app's static shortcuts supports the following elements inside each
+ * {@code <shortcut>} element. You must include an {@code intent} inner element for each
+ * static shortcut that you define.</p>
*
+ * <dl>
* <dt>{@code intent}</dt>
- * <dd>Intent to launch when the user selects the shortcut.
- * {@code android:action} is mandatory.
- * See <a href="{@docRoot}guide/topics/ui/settings.html#Intents">Using intents</a> for the
- * other supported tags.
- * <p>You can provide multiple intents for a single shortcut so that the last defined activity is
- * launched with the other activities in the
+ * <dd><p>The action that the system launches when the user selects the shortcut. This intent must
+ * provide a value for the {@code android:action} attribute.</p>
+ * <p>You can provide multiple intents for a single shortcut. If you do so, the last defined
+ * activity is launched, and the other activities are placed in the
* <a href="/guide/components/tasks-and-back-stack.html">back stack</a>. See
- * {@link android.app.TaskStackBuilder} for details.
- * <p><b>Note:</b> String resources may not be used within an {@code <intent>} element.
+ * <a href="/guide/topics/ui/shortcuts.html#static">Using Static Shortcuts</a> and the
+ * {@link android.app.TaskStackBuilder} class reference for details.</p>
+ * <p class="note"><b>Note:</b> This {@code intent} element cannot include string resources.</p>
+ * <p>To learn more about how to configure intents, see
+ * <a href="{@docRoot}guide/topics/ui/settings.html#Intents">Using intents</a>.</p>
* </dd>
+ *
* <dt>{@code categories}</dt>
- * <dd>Specify shortcut categories. Currently only
- * {@link ShortcutInfo#SHORTCUT_CATEGORY_CONVERSATION} is defined in the framework.
+ * <dd><p>Provides a grouping for the types of actions that your app's shortcuts perform, such as
+ * creating new chat messages.</p>
+ * <p>For a list of supported shortcut categories, see the {@link ShortcutInfo} class reference
+ * for a list of supported shortcut categories.
* </dd>
* </dl>
*
* <h3>Updating shortcuts</h3>
*
+ * <p>Each app's launcher icon can contain at most {@link #getMaxShortcutCountPerActivity()} number
+ * of static and dynamic shortcuts combined. There is no limit to the number of pinned shortcuts
+ * that an app can create, though.
+ *
+ * <p>When a dynamic shortcut is pinned, even when the publisher removes it as a dynamic shortcut,
+ * the pinned shortcut is still visible and launchable. This allows an app to have more than
+ * {@link #getMaxShortcutCountPerActivity()} number of shortcuts.
+ *
* <p>As an example, suppose {@link #getMaxShortcutCountPerActivity()} is 5:
* <ol>
* <li>A chat app publishes 5 dynamic shortcuts for the 5 most recent
@@ -168,18 +219,13 @@ import java.util.List;
*
* <li>The user pins all 5 of the shortcuts.
*
- * <li>Later, the user has started 3 additional conversations (c6, c7, and c8),
- * so the publisher app
- * re-publishes its dynamic shortcuts. The new dynamic shortcut list is:
- * c4, c5, ..., c8.
- * The publisher app has to remove c1, c2, and c3 because it can't have more than
- * 5 dynamic shortcuts.
- *
- * <li>However, even though c1, c2, and c3 are no longer dynamic shortcuts, the pinned
- * shortcuts for these conversations are still available and launchable.
- *
- * <li>At this point, the user can access a total of 8 shortcuts that link to activities in
- * the publisher app, including the 3 pinned shortcuts, even though an app can have at most 5
+ * <li>Later, the user has started 3 additional conversations (c6, c7, and c8), so the publisher
+ * app re-publishes its dynamic shortcuts. The new dynamic shortcut list is: c4, c5, ..., c8.
+ * <p>The publisher app has to remove c1, c2, and c3 because it can't have more than 5 dynamic
+ * shortcuts. However, c1, c2, and c3 are still pinned shortcuts that the user can access and
+ * launch.
+ * <p>At this point, the user can access a total of 8 shortcuts that link to activities in the
+ * publisher app, including the 3 pinned shortcuts, even though an app can have at most 5
* dynamic shortcuts.
*
* <li>The app can use {@link #updateShortcuts(List)} to update <em>any</em> of the existing
@@ -196,44 +242,23 @@ import java.util.List;
* Dynamic shortcuts can be published with any set of {@link Intent#addFlags Intent} flags.
* Typically, {@link Intent#FLAG_ACTIVITY_CLEAR_TASK} is specified, possibly along with other
* flags; otherwise, if the app is already running, the app is simply brought to
- * the foreground, and the target activity may not appear.
+ * the foreground, and the target activity might not appear.
*
* <p>Static shortcuts <b>cannot</b> have custom intent flags.
* The first intent of a static shortcut will always have {@link Intent#FLAG_ACTIVITY_NEW_TASK}
* and {@link Intent#FLAG_ACTIVITY_CLEAR_TASK} set. This means, when the app is already running, all
- * the existing activities in your app will be destroyed when a static shortcut is launched.
+ * the existing activities in your app are destroyed when a static shortcut is launched.
* If this behavior is not desirable, you can use a <em>trampoline activity</em>, or an invisible
* activity that starts another activity in {@link Activity#onCreate}, then calls
* {@link Activity#finish()}:
* <ol>
* <li>In the <code>AndroidManifest.xml</code> file, the trampoline activity should include the
* attribute assignment {@code android:taskAffinity=""}.
- * <li>In the shortcuts resource file, the intent within the static shortcut should point at
+ * <li>In the shortcuts resource file, the intent within the static shortcut should reference
* the trampoline activity.
* </ol>
*
- * <h3>Handling system locale changes</h3>
- *
- * <p>Apps should update dynamic and pinned shortcuts when the system locale changes using the
- * {@link Intent#ACTION_LOCALE_CHANGED} broadcast. When the system locale changes,
- * <a href="/guide/topics/ui/shortcuts.html#rate-limit">rate limiting</a> is reset, so even
- * background apps can add and update dynamic shortcuts until the rate limit is reached again.
- *
- * <h3>Shortcut limits</h3>
- *
- * <p>Only main activities&mdash;activities that handle the {@code MAIN} action and the
- * {@code LAUNCHER} category&mdash;can have shortcuts. If an app has multiple main activities, you
- * need to define the set of shortcuts for <em>each</em> activity.
- *
- * <p>Each launcher icon can have at most {@link #getMaxShortcutCountPerActivity()} number of
- * static and dynamic shortcuts combined. There is no limit to the number of pinned shortcuts that
- * an app can create.
- *
- * <p>When a dynamic shortcut is pinned, even when the publisher removes it as a dynamic shortcut,
- * the pinned shortcut is still visible and launchable. This allows an app to have more than
- * {@link #getMaxShortcutCountPerActivity()} number of shortcuts.
- *
- * <h4>Rate limiting</h4>
+ * <h3>Rate limiting</h3>
*
* <p>When <a href="/guide/topics/ui/shortcuts.html#rate-limit">rate limiting</a> is active,
* {@link #isRateLimitingActive()} returns {@code true}.
@@ -243,8 +268,20 @@ import java.util.List;
* <ul>
* <li>An app comes to the foreground.
* <li>The system locale changes.
- * <li>The user performs the <strong>inline reply</strong> action on a notification.
+ * <li>The user performs the <a href="/guide/topics/ui/notifiers/notifications.html#direct">inline
+ * reply</a> action on a notification.
* </ul>
+ *
+ * <h3>Handling system locale changes</h3>
+ *
+ * <p>Apps should update dynamic and pinned shortcuts when they receive the
+ * {@link Intent#ACTION_LOCALE_CHANGED} broadcast, indicating that the system locale has changed.
+ * <p>When the system locale changes, <a href="/guide/topics/ui/shortcuts.html#rate-limit">rate
+ * limiting</a> is reset, so even background apps can add and update dynamic shortcuts until the
+ * rate limit is reached again.
+ *
+ * <h3>Retrieving class instances</h3>
+ * <!-- Provides a heading for the content filled in by the @SystemService annotation below -->
*/
@SystemService(Context.SHORTCUT_SERVICE)
public class ShortcutManager {
diff --git a/android/content/pm/ShortcutServiceInternal.java b/android/content/pm/ShortcutServiceInternal.java
index dadfaa9f..e6f682d2 100644
--- a/android/content/pm/ShortcutServiceInternal.java
+++ b/android/content/pm/ShortcutServiceInternal.java
@@ -81,4 +81,7 @@ public abstract class ShortcutServiceInternal {
@Nullable IntentSender resultIntent, int userId);
public abstract boolean isRequestPinItemSupported(int callingUserId, int requestType);
+
+ public abstract boolean isForegroundDefaultLauncher(@NonNull String callingPackage,
+ int callingUid);
}
diff --git a/android/content/pm/VersionedPackage.java b/android/content/pm/VersionedPackage.java
index 29c5efe7..39534664 100644
--- a/android/content/pm/VersionedPackage.java
+++ b/android/content/pm/VersionedPackage.java
@@ -28,7 +28,7 @@ import java.lang.annotation.RetentionPolicy;
*/
public final class VersionedPackage implements Parcelable {
private final String mPackageName;
- private final int mVersionCode;
+ private final long mVersionCode;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@@ -47,9 +47,21 @@ public final class VersionedPackage implements Parcelable {
mVersionCode = versionCode;
}
+ /**
+ * Creates a new instance. Use {@link PackageManager#VERSION_CODE_HIGHEST}
+ * to refer to the highest version code of this package.
+ * @param packageName The package name.
+ * @param versionCode The version code.
+ */
+ public VersionedPackage(@NonNull String packageName,
+ @VersionCode long versionCode) {
+ mPackageName = packageName;
+ mVersionCode = versionCode;
+ }
+
private VersionedPackage(Parcel parcel) {
mPackageName = parcel.readString();
- mVersionCode = parcel.readInt();
+ mVersionCode = parcel.readLong();
}
/**
@@ -62,11 +74,19 @@ public final class VersionedPackage implements Parcelable {
}
/**
+ * @deprecated use {@link #getLongVersionCode()} instead.
+ */
+ @Deprecated
+ public @VersionCode int getVersionCode() {
+ return (int) (mVersionCode & 0x7fffffff);
+ }
+
+ /**
* Gets the version code.
*
* @return The version code.
*/
- public @VersionCode int getVersionCode() {
+ public @VersionCode long getLongVersionCode() {
return mVersionCode;
}
@@ -83,7 +103,7 @@ public final class VersionedPackage implements Parcelable {
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(mPackageName);
- parcel.writeInt(mVersionCode);
+ parcel.writeLong(mVersionCode);
}
public static final Creator<VersionedPackage> CREATOR = new Creator<VersionedPackage>() {
diff --git a/android/content/pm/crossprofile/CrossProfileApps.java b/android/content/pm/crossprofile/CrossProfileApps.java
index c441b5f3..414c1389 100644
--- a/android/content/pm/crossprofile/CrossProfileApps.java
+++ b/android/content/pm/crossprofile/CrossProfileApps.java
@@ -16,15 +16,19 @@
package android.content.pm.crossprofile;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
+import android.content.res.Resources;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
+import com.android.internal.R;
+import com.android.internal.util.UserIcons;
+
import java.util.List;
/**
@@ -35,11 +39,15 @@ import java.util.List;
public class CrossProfileApps {
private final Context mContext;
private final ICrossProfileApps mService;
+ private final UserManager mUserManager;
+ private final Resources mResources;
/** @hide */
public CrossProfileApps(Context context, ICrossProfileApps service) {
mContext = context;
mService = service;
+ mUserManager = context.getSystemService(UserManager.class);
+ mResources = context.getResources();
}
/**
@@ -52,15 +60,10 @@ public class CrossProfileApps {
* @param user The UserHandle of the profile, must be one of the users returned by
* {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will
* be thrown.
- * @param sourceBounds The Rect containing the source bounds of the clicked icon, see
- * {@link android.content.Intent#setSourceBounds(Rect)}.
- * @param startActivityOptions Options to pass to startActivity
*/
- public void startMainActivity(@NonNull ComponentName component, @NonNull UserHandle user,
- @Nullable Rect sourceBounds, @Nullable Bundle startActivityOptions) {
+ public void startMainActivity(@NonNull ComponentName component, @NonNull UserHandle user) {
try {
- mService.startActivityAsUser(mContext.getPackageName(),
- component, sourceBounds, startActivityOptions, user);
+ mService.startActivityAsUser(mContext.getPackageName(), component, user);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
@@ -87,4 +90,58 @@ public class CrossProfileApps {
throw ex.rethrowFromSystemServer();
}
}
+
+ /**
+ * Return a label that calling app can show to user for the semantic of profile switching --
+ * launching its own activity in specified user profile. For example, it may return
+ * "Switch to work" if the given user handle is the managed profile one.
+ *
+ * @param userHandle The UserHandle of the target profile, must be one of the users returned by
+ * {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will
+ * be thrown.
+ * @return a label that calling app can show user for the semantic of launching its own
+ * activity in the specified user profile.
+ *
+ * @see #startMainActivity(ComponentName, UserHandle, Rect, Bundle)
+ */
+ public @NonNull CharSequence getProfileSwitchingLabel(@NonNull UserHandle userHandle) {
+ verifyCanAccessUser(userHandle);
+
+ final int stringRes = mUserManager.isManagedProfile(userHandle.getIdentifier())
+ ? R.string.managed_profile_label
+ : R.string.user_owner_label;
+ return mResources.getString(stringRes);
+ }
+
+ /**
+ * Return an icon that calling app can show to user for the semantic of profile switching --
+ * launching its own activity in specified user profile. For example, it may return a briefcase
+ * icon if the given user handle is the managed profile one.
+ *
+ * @param userHandle The UserHandle of the target profile, must be one of the users returned by
+ * {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will
+ * be thrown.
+ * @return an icon that calling app can show user for the semantic of launching its own
+ * activity in specified user profile.
+ *
+ * @see #startMainActivity(ComponentName, UserHandle, Rect, Bundle)
+ */
+ public @NonNull Drawable getProfileSwitchingIcon(@NonNull UserHandle userHandle) {
+ verifyCanAccessUser(userHandle);
+
+ final boolean isManagedProfile =
+ mUserManager.isManagedProfile(userHandle.getIdentifier());
+ if (isManagedProfile) {
+ return mResources.getDrawable(R.drawable.ic_corp_badge, null);
+ } else {
+ return UserIcons.getDefaultUserIcon(
+ mResources, UserHandle.USER_SYSTEM, true /* light */);
+ }
+ }
+
+ private void verifyCanAccessUser(UserHandle userHandle) {
+ if (!getTargetUserProfiles().contains(userHandle)) {
+ throw new SecurityException("Not allowed to access " + userHandle);
+ }
+ }
}
diff --git a/android/content/pm/dex/ArtManager.java b/android/content/pm/dex/ArtManager.java
new file mode 100644
index 00000000..201cd8d3
--- /dev/null
+++ b/android/content/pm/dex/ArtManager.java
@@ -0,0 +1,156 @@
+/**
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.dex;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Slog;
+
+/**
+ * Class for retrieving various kinds of information related to the runtime artifacts of
+ * packages that are currently installed on the device.
+ *
+ * @hide
+ */
+@SystemApi
+public class ArtManager {
+ private static final String TAG = "ArtManager";
+
+ /** The snapshot failed because the package was not found. */
+ public static final int SNAPSHOT_FAILED_PACKAGE_NOT_FOUND = 0;
+ /** The snapshot failed because the package code path does not exist. */
+ public static final int SNAPSHOT_FAILED_CODE_PATH_NOT_FOUND = 1;
+ /** The snapshot failed because of an internal error (e.g. error during opening profiles). */
+ public static final int SNAPSHOT_FAILED_INTERNAL_ERROR = 2;
+
+ private IArtManager mArtManager;
+
+ /**
+ * @hide
+ */
+ public ArtManager(@NonNull IArtManager manager) {
+ mArtManager = manager;
+ }
+
+ /**
+ * Snapshots the runtime profile for an apk belonging to the package {@code packageName}.
+ * The apk is identified by {@code codePath}. The calling process must have
+ * {@code android.permission.READ_RUNTIME_PROFILE} permission.
+ *
+ * The result will be posted on {@code handler} using the given {@code callback}.
+ * The profile being available as a read-only {@link android.os.ParcelFileDescriptor}.
+ *
+ * @param packageName the target package name
+ * @param codePath the code path for which the profile should be retrieved
+ * @param callback the callback which should be used for the result
+ * @param handler the handler which should be used to post the result
+ */
+ @RequiresPermission(android.Manifest.permission.READ_RUNTIME_PROFILES)
+ public void snapshotRuntimeProfile(@NonNull String packageName, @NonNull String codePath,
+ @NonNull SnapshotRuntimeProfileCallback callback, @NonNull Handler handler) {
+ Slog.d(TAG, "Requesting profile snapshot for " + packageName + ":" + codePath);
+
+ SnapshotRuntimeProfileCallbackDelegate delegate =
+ new SnapshotRuntimeProfileCallbackDelegate(callback, handler.getLooper());
+ try {
+ mArtManager.snapshotRuntimeProfile(packageName, codePath, delegate);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ /**
+ * Returns true if runtime profiles are enabled, false otherwise.
+ *
+ * The calling process must have {@code android.permission.READ_RUNTIME_PROFILE} permission.
+ */
+ @RequiresPermission(android.Manifest.permission.READ_RUNTIME_PROFILES)
+ public boolean isRuntimeProfilingEnabled() {
+ try {
+ return mArtManager.isRuntimeProfilingEnabled();
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ return false;
+ }
+
+ /**
+ * Callback used for retrieving runtime profiles.
+ */
+ public abstract static class SnapshotRuntimeProfileCallback {
+ /**
+ * Called when the profile snapshot finished with success.
+ *
+ * @param profileReadFd the file descriptor that can be used to read the profile. Note that
+ * the file might be empty (which is valid profile).
+ */
+ public abstract void onSuccess(ParcelFileDescriptor profileReadFd);
+
+ /**
+ * Called when the profile snapshot finished with an error.
+ *
+ * @param errCode the error code {@see SNAPSHOT_FAILED_PACKAGE_NOT_FOUND,
+ * SNAPSHOT_FAILED_CODE_PATH_NOT_FOUND, SNAPSHOT_FAILED_INTERNAL_ERROR}.
+ */
+ public abstract void onError(int errCode);
+ }
+
+ private static class SnapshotRuntimeProfileCallbackDelegate
+ extends android.content.pm.dex.ISnapshotRuntimeProfileCallback.Stub
+ implements Handler.Callback {
+ private static final int MSG_SNAPSHOT_OK = 1;
+ private static final int MSG_ERROR = 2;
+ private final ArtManager.SnapshotRuntimeProfileCallback mCallback;
+ private final Handler mHandler;
+
+ private SnapshotRuntimeProfileCallbackDelegate(
+ ArtManager.SnapshotRuntimeProfileCallback callback, Looper looper) {
+ mCallback = callback;
+ mHandler = new Handler(looper, this);
+ }
+
+ @Override
+ public void onSuccess(ParcelFileDescriptor profileReadFd) {
+ mHandler.obtainMessage(MSG_SNAPSHOT_OK, profileReadFd).sendToTarget();
+ }
+
+ @Override
+ public void onError(int errCode) {
+ mHandler.obtainMessage(MSG_ERROR, errCode, 0).sendToTarget();
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_SNAPSHOT_OK:
+ mCallback.onSuccess((ParcelFileDescriptor) msg.obj);
+ break;
+ case MSG_ERROR:
+ mCallback.onError(msg.arg1);
+ break;
+ default: return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/android/content/res/AssetFileDescriptor.java b/android/content/res/AssetFileDescriptor.java
index 28edde02..be410362 100644
--- a/android/content/res/AssetFileDescriptor.java
+++ b/android/content/res/AssetFileDescriptor.java
@@ -195,7 +195,7 @@ public class AssetFileDescriptor implements Parcelable, Closeable {
/**
* An InputStream you can create on a ParcelFileDescriptor, which will
* take care of calling {@link ParcelFileDescriptor#close
- * ParcelFileDescritor.close()} for you when the stream is closed.
+ * ParcelFileDescriptor.close()} for you when the stream is closed.
*/
public static class AutoCloseInputStream
extends ParcelFileDescriptor.AutoCloseInputStream {
@@ -282,7 +282,7 @@ public class AssetFileDescriptor implements Parcelable, Closeable {
/**
* An OutputStream you can create on a ParcelFileDescriptor, which will
* take care of calling {@link ParcelFileDescriptor#close
- * ParcelFileDescritor.close()} for you when the stream is closed.
+ * ParcelFileDescriptor.close()} for you when the stream is closed.
*/
public static class AutoCloseOutputStream
extends ParcelFileDescriptor.AutoCloseOutputStream {
diff --git a/android/content/res/Configuration.java b/android/content/res/Configuration.java
index 26efda10..eb309799 100644
--- a/android/content/res/Configuration.java
+++ b/android/content/res/Configuration.java
@@ -781,25 +781,24 @@ public final class Configuration implements Parcelable, Comparable<Configuration
public int seq;
/** @hide */
- @IntDef(flag = true,
- value = {
- NATIVE_CONFIG_MCC,
- NATIVE_CONFIG_MNC,
- NATIVE_CONFIG_LOCALE,
- NATIVE_CONFIG_TOUCHSCREEN,
- NATIVE_CONFIG_KEYBOARD,
- NATIVE_CONFIG_KEYBOARD_HIDDEN,
- NATIVE_CONFIG_NAVIGATION,
- NATIVE_CONFIG_ORIENTATION,
- NATIVE_CONFIG_DENSITY,
- NATIVE_CONFIG_SCREEN_SIZE,
- NATIVE_CONFIG_VERSION,
- NATIVE_CONFIG_SCREEN_LAYOUT,
- NATIVE_CONFIG_UI_MODE,
- NATIVE_CONFIG_SMALLEST_SCREEN_SIZE,
- NATIVE_CONFIG_LAYOUTDIR,
- NATIVE_CONFIG_COLOR_MODE,
- })
+ @IntDef(flag = true, prefix = { "NATIVE_CONFIG_" }, value = {
+ NATIVE_CONFIG_MCC,
+ NATIVE_CONFIG_MNC,
+ NATIVE_CONFIG_LOCALE,
+ NATIVE_CONFIG_TOUCHSCREEN,
+ NATIVE_CONFIG_KEYBOARD,
+ NATIVE_CONFIG_KEYBOARD_HIDDEN,
+ NATIVE_CONFIG_NAVIGATION,
+ NATIVE_CONFIG_ORIENTATION,
+ NATIVE_CONFIG_DENSITY,
+ NATIVE_CONFIG_SCREEN_SIZE,
+ NATIVE_CONFIG_VERSION,
+ NATIVE_CONFIG_SCREEN_LAYOUT,
+ NATIVE_CONFIG_UI_MODE,
+ NATIVE_CONFIG_SMALLEST_SCREEN_SIZE,
+ NATIVE_CONFIG_LAYOUTDIR,
+ NATIVE_CONFIG_COLOR_MODE,
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface NativeConfig {}
diff --git a/android/content/res/GradientColor.java b/android/content/res/GradientColor.java
index e4659613..35ad5033 100644
--- a/android/content/res/GradientColor.java
+++ b/android/content/res/GradientColor.java
@@ -75,9 +75,14 @@ public class GradientColor extends ComplexColor {
private static final boolean DBG_GRADIENT = false;
- @IntDef({TILE_MODE_CLAMP, TILE_MODE_REPEAT, TILE_MODE_MIRROR})
+ @IntDef(prefix = { "TILE_MODE_" }, value = {
+ TILE_MODE_CLAMP,
+ TILE_MODE_REPEAT,
+ TILE_MODE_MIRROR
+ })
@Retention(RetentionPolicy.SOURCE)
private @interface GradientTileMode {}
+
private static final int TILE_MODE_CLAMP = 0;
private static final int TILE_MODE_REPEAT = 1;
private static final int TILE_MODE_MIRROR = 2;
diff --git a/android/content/res/XmlResourceParser.java b/android/content/res/XmlResourceParser.java
index 5af49d4d..6be9b9eb 100644
--- a/android/content/res/XmlResourceParser.java
+++ b/android/content/res/XmlResourceParser.java
@@ -16,20 +16,19 @@
package android.content.res;
-import org.xmlpull.v1.XmlPullParser;
-
import android.util.AttributeSet;
+import org.xmlpull.v1.XmlPullParser;
+
/**
* The XML parsing interface returned for an XML resource. This is a standard
- * XmlPullParser interface, as well as an extended AttributeSet interface and
- * an additional close() method on this interface for the client to indicate
- * when it is done reading the resource.
+ * {@link XmlPullParser} interface but also extends {@link AttributeSet} and
+ * adds an additional {@link #close()} method for the client to indicate when
+ * it is done reading the resource.
*/
public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable {
/**
- * Close this interface to the resource. Calls on the interface are no
- * longer value after this call.
+ * Close this parser. Calls on the interface are no longer valid after this call.
*/
public void close();
}
diff --git a/android/database/sqlite/SQLiteCompatibilityWalFlags.java b/android/database/sqlite/SQLiteCompatibilityWalFlags.java
new file mode 100644
index 00000000..5bf3a7c4
--- /dev/null
+++ b/android/database/sqlite/SQLiteCompatibilityWalFlags.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+import android.app.ActivityThread;
+import android.app.Application;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.KeyValueListParser;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Helper class for accessing
+ * {@link Settings.Global#SQLITE_COMPATIBILITY_WAL_FLAGS global compatibility WAL settings}.
+ *
+ * <p>The value of {@link Settings.Global#SQLITE_COMPATIBILITY_WAL_FLAGS} is cached on first access
+ * for consistent behavior across all connections opened in the process.
+ * @hide
+ */
+public class SQLiteCompatibilityWalFlags {
+
+ private static final String TAG = "SQLiteCompatibilityWalFlags";
+
+ private static volatile boolean sInitialized;
+ private static volatile boolean sFlagsSet;
+ private static volatile boolean sCompatibilityWalSupported;
+ private static volatile String sWALSyncMode;
+ // This flag is used to avoid recursive initialization due to circular dependency on Settings
+ private static volatile boolean sCallingGlobalSettings;
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public static boolean areFlagsSet() {
+ initIfNeeded();
+ return sFlagsSet;
+ }
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public static boolean isCompatibilityWalSupported() {
+ initIfNeeded();
+ return sCompatibilityWalSupported;
+ }
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public static String getWALSyncMode() {
+ initIfNeeded();
+ return sWALSyncMode;
+ }
+
+ private static void initIfNeeded() {
+ if (sInitialized || sCallingGlobalSettings) {
+ return;
+ }
+ ActivityThread activityThread = ActivityThread.currentActivityThread();
+ Application app = activityThread == null ? null : activityThread.getApplication();
+ String flags = null;
+ if (app == null) {
+ Log.w(TAG, "Cannot read global setting "
+ + Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS + " - "
+ + "Application state not available");
+ } else {
+ try {
+ sCallingGlobalSettings = true;
+ flags = Settings.Global.getString(app.getContentResolver(),
+ Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS);
+ } finally {
+ sCallingGlobalSettings = false;
+ }
+ }
+
+ init(flags);
+ }
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public static void init(String flags) {
+ if (TextUtils.isEmpty(flags)) {
+ sInitialized = true;
+ return;
+ }
+ KeyValueListParser parser = new KeyValueListParser(',');
+ try {
+ parser.setString(flags);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Setting has invalid format: " + flags, e);
+ sInitialized = true;
+ return;
+ }
+ sCompatibilityWalSupported = parser.getBoolean("compatibility_wal_supported",
+ SQLiteGlobal.isCompatibilityWalSupported());
+ sWALSyncMode = parser.getString("wal_syncmode", SQLiteGlobal.getWALSyncMode());
+ Log.i(TAG, "Read compatibility WAL flags: compatibility_wal_supported="
+ + sCompatibilityWalSupported + ", wal_syncmode=" + sWALSyncMode);
+ sFlagsSet = true;
+ sInitialized = true;
+ }
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public static void reset() {
+ sInitialized = false;
+ sFlagsSet = false;
+ sCompatibilityWalSupported = false;
+ sWALSyncMode = null;
+ }
+}
diff --git a/android/database/sqlite/SQLiteConnection.java b/android/database/sqlite/SQLiteConnection.java
index 2c93a7fe..7717b8d3 100644
--- a/android/database/sqlite/SQLiteConnection.java
+++ b/android/database/sqlite/SQLiteConnection.java
@@ -296,7 +296,11 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen
&& mConfiguration.syncMode == null && mConfiguration.useCompatibilityWal;
if (walEnabled || useCompatibilityWal) {
setJournalMode("WAL");
- setSyncMode(SQLiteGlobal.getWALSyncMode());
+ if (useCompatibilityWal && SQLiteCompatibilityWalFlags.areFlagsSet()) {
+ setSyncMode(SQLiteCompatibilityWalFlags.getWALSyncMode());
+ } else {
+ setSyncMode(SQLiteGlobal.getWALSyncMode());
+ }
} else {
setJournalMode(mConfiguration.journalMode == null
? SQLiteGlobal.getDefaultJournalMode() : mConfiguration.journalMode);
diff --git a/android/database/sqlite/SQLiteConnectionPool.java b/android/database/sqlite/SQLiteConnectionPool.java
index 5adb1196..b2117003 100644
--- a/android/database/sqlite/SQLiteConnectionPool.java
+++ b/android/database/sqlite/SQLiteConnectionPool.java
@@ -1094,6 +1094,12 @@ public final class SQLiteConnectionPool implements Closeable {
printer.println(" Open: " + mIsOpen);
printer.println(" Max connections: " + mMaxConnectionPoolSize);
printer.println(" Total execution time: " + mTotalExecutionTimeCounter);
+ if (SQLiteCompatibilityWalFlags.areFlagsSet()) {
+ printer.println(" Compatibility WAL settings: compatibility_wal_supported="
+ + SQLiteCompatibilityWalFlags
+ .isCompatibilityWalSupported() + ", wal_syncmode="
+ + SQLiteCompatibilityWalFlags.getWALSyncMode());
+ }
if (mConfiguration.isLookasideConfigSet()) {
printer.println(" Lookaside config: sz=" + mConfiguration.lookasideSlotSize
+ " cnt=" + mConfiguration.lookasideSlotCount);
diff --git a/android/database/sqlite/SQLiteDatabase.java b/android/database/sqlite/SQLiteDatabase.java
index 09bb9c69..c1c0812e 100644
--- a/android/database/sqlite/SQLiteDatabase.java
+++ b/android/database/sqlite/SQLiteDatabase.java
@@ -289,6 +289,10 @@ public final class SQLiteDatabase extends SQLiteClosable {
mConfigurationLocked.journalMode = journalMode;
mConfigurationLocked.syncMode = syncMode;
mConfigurationLocked.useCompatibilityWal = SQLiteGlobal.isCompatibilityWalSupported();
+ if (!mConfigurationLocked.isInMemoryDb() && SQLiteCompatibilityWalFlags.areFlagsSet()) {
+ mConfigurationLocked.useCompatibilityWal = SQLiteCompatibilityWalFlags
+ .isCompatibilityWalSupported();
+ }
}
@Override
diff --git a/android/graphics/BitmapFactory_Delegate.java b/android/graphics/BitmapFactory_Delegate.java
index 8bd2a7ac..ee099e1d 100644
--- a/android/graphics/BitmapFactory_Delegate.java
+++ b/android/graphics/BitmapFactory_Delegate.java
@@ -101,20 +101,26 @@ import java.util.Set;
@LayoutlibDelegate
/*package*/ static Bitmap nativeDecodeFileDescriptor(FileDescriptor fd,
Rect padding, Options opts) {
- opts.inBitmap = null;
+ if (opts != null) {
+ opts.inBitmap = null;
+ }
return null;
}
@LayoutlibDelegate
/*package*/ static Bitmap nativeDecodeAsset(long asset, Rect padding, Options opts) {
- opts.inBitmap = null;
+ if (opts != null) {
+ opts.inBitmap = null;
+ }
return null;
}
@LayoutlibDelegate
/*package*/ static Bitmap nativeDecodeByteArray(byte[] data, int offset,
int length, Options opts) {
- opts.inBitmap = null;
+ if (opts != null) {
+ opts.inBitmap = null;
+ }
return null;
}
diff --git a/android/graphics/Bitmap_Delegate.java b/android/graphics/Bitmap_Delegate.java
index 00645379..6c72cb2f 100644
--- a/android/graphics/Bitmap_Delegate.java
+++ b/android/graphics/Bitmap_Delegate.java
@@ -608,7 +608,8 @@ public final class Bitmap_Delegate {
if (delegate == null) {
return 0;
}
- return nativeRowBytes(nativeBitmap) * delegate.mImage.getHeight();
+ int size = nativeRowBytes(nativeBitmap) * delegate.mImage.getHeight();
+ return size < 0 ? Integer.MAX_VALUE : size;
}
diff --git a/android/graphics/ImageDecoder.java b/android/graphics/ImageDecoder.java
new file mode 100644
index 00000000..60416a72
--- /dev/null
+++ b/android/graphics/ImageDecoder.java
@@ -0,0 +1,665 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RawRes;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.NinePatchDrawable;
+
+import java.nio.ByteBuffer;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ArrayIndexOutOfBoundsException;
+import java.lang.NullPointerException;
+import java.lang.RuntimeException;
+import java.lang.annotation.Retention;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+/**
+ * Class for decoding images as {@link Bitmap}s or {@link Drawable}s.
+ * @hide
+ */
+public final class ImageDecoder {
+ /**
+ * Source of the encoded image data.
+ */
+ public static abstract class Source {
+ /* @hide */
+ Resources getResources() { return null; }
+
+ /* @hide */
+ void close() {}
+
+ /* @hide */
+ abstract ImageDecoder createImageDecoder();
+ };
+
+ private static class ByteArraySource extends Source {
+ ByteArraySource(byte[] data, int offset, int length) {
+ mData = data;
+ mOffset = offset;
+ mLength = length;
+ };
+ private final byte[] mData;
+ private final int mOffset;
+ private final int mLength;
+
+ @Override
+ public ImageDecoder createImageDecoder() {
+ return nCreate(mData, mOffset, mLength);
+ }
+ }
+
+ private static class ByteBufferSource extends Source {
+ ByteBufferSource(ByteBuffer buffer) {
+ mBuffer = buffer;
+ }
+ private final ByteBuffer mBuffer;
+
+ @Override
+ public ImageDecoder createImageDecoder() {
+ if (!mBuffer.isDirect() && mBuffer.hasArray()) {
+ int offset = mBuffer.arrayOffset() + mBuffer.position();
+ int length = mBuffer.limit() - mBuffer.position();
+ return nCreate(mBuffer.array(), offset, length);
+ }
+ return nCreate(mBuffer, mBuffer.position(), mBuffer.limit());
+ }
+ }
+
+ private static class ResourceSource extends Source {
+ ResourceSource(Resources res, int resId)
+ throws Resources.NotFoundException {
+ // Test that the resource can be found.
+ InputStream is = null;
+ try {
+ is = res.openRawResource(resId);
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ mResources = res;
+ mResId = resId;
+ }
+
+ final Resources mResources;
+ final int mResId;
+ // This is just stored here in order to keep the underlying Asset
+ // alive. FIXME: Can I access the Asset (and keep it alive) without
+ // this object?
+ InputStream mInputStream;
+
+ @Override
+ public Resources getResources() { return mResources; }
+
+ @Override
+ public ImageDecoder createImageDecoder() {
+ // FIXME: Can I bypass creating the stream?
+ try {
+ mInputStream = mResources.openRawResource(mResId);
+ } catch (Resources.NotFoundException e) {
+ // This should never happen, since we already tested in the
+ // constructor.
+ }
+ if (!(mInputStream instanceof AssetManager.AssetInputStream)) {
+ // This should never happen.
+ throw new RuntimeException("Resource is not an asset?");
+ }
+ long asset = ((AssetManager.AssetInputStream) mInputStream).getNativeAsset();
+ return nCreate(asset);
+ }
+
+ @Override
+ public void close() {
+ try {
+ mInputStream.close();
+ } catch (IOException e) {
+ } finally {
+ mInputStream = null;
+ }
+ }
+ }
+
+ /**
+ * Contains information about the encoded image.
+ */
+ public static class ImageInfo {
+ public final int width;
+ public final int height;
+ // TODO?: Add more info? mimetype, ninepatch etc?
+
+ ImageInfo(int width, int height) {
+ this.width = width;
+ this.height = height;
+ }
+ };
+
+ /**
+ * Used if the provided data is incomplete.
+ *
+ * There may be a partial image to display.
+ */
+ public class IncompleteException extends Exception {};
+
+ /**
+ * Used if the provided data is corrupt.
+ *
+ * There may be a partial image to display.
+ */
+ public class CorruptException extends Exception {};
+
+ /**
+ * Optional listener supplied to {@link #decodeDrawable} or
+ * {@link #decodeBitmap}.
+ */
+ public static interface OnHeaderDecodedListener {
+ /**
+ * Called when the header is decoded and the size is known.
+ *
+ * @param info Information about the encoded image.
+ * @param decoder allows changing the default settings of the decode.
+ */
+ public void onHeaderDecoded(ImageInfo info, ImageDecoder decoder);
+
+ };
+
+ /**
+ * Optional listener supplied to the ImageDecoder.
+ */
+ public static interface OnExceptionListener {
+ /**
+ * Called when there is a problem in the stream or in the data.
+ * FIXME: Or do not allow streams?
+ * FIXME: Report how much of the image has been decoded?
+ *
+ * @param e Exception containing information about the error.
+ * @return True to create and return a {@link Drawable}/
+ * {@link Bitmap} with partial data. False to return
+ * {@code null}. True is the default.
+ */
+ public boolean onException(Exception e);
+ };
+
+ // Fields
+ private long mNativePtr;
+ private final int mWidth;
+ private final int mHeight;
+
+ private int mDesiredWidth;
+ private int mDesiredHeight;
+ private int mAllocator = DEFAULT_ALLOCATOR;
+ private boolean mRequireUnpremultiplied = false;
+ private boolean mMutable = false;
+ private boolean mPreferRamOverQuality = false;
+ private boolean mAsAlphaMask = false;
+ private Rect mCropRect;
+
+ private PostProcess mPostProcess;
+ private OnExceptionListener mOnExceptionListener;
+
+
+ /**
+ * Private constructor called by JNI. {@link #recycle} must be
+ * called after decoding to delete native resources.
+ */
+ @SuppressWarnings("unused")
+ private ImageDecoder(long nativePtr, int width, int height) {
+ mNativePtr = nativePtr;
+ mWidth = width;
+ mHeight = height;
+ mDesiredWidth = width;
+ mDesiredHeight = height;
+ }
+
+ /**
+ * Create a new {@link Source} from an asset.
+ *
+ * @param res the {@link Resources} object containing the image data.
+ * @param resId resource ID of the image data.
+ * // FIXME: Can be an @DrawableRes?
+ * @return a new Source object, which can be passed to
+ * {@link #decodeDrawable} or {@link #decodeBitmap}.
+ * @throws Resources.NotFoundException if the asset does not exist.
+ */
+ public static Source createSource(@NonNull Resources res, @RawRes int resId)
+ throws Resources.NotFoundException {
+ return new ResourceSource(res, resId);
+ }
+
+ /**
+ * Create a new {@link Source} from a byte array.
+ * @param data byte array of compressed image data.
+ * @param offset offset into data for where the decoder should begin
+ * parsing.
+ * @param length number of bytes, beginning at offset, to parse.
+ * @throws NullPointerException if data is null.
+ * @throws ArrayIndexOutOfBoundsException if offset and length are
+ * not within data.
+ */
+ // TODO: Overloads that don't use offset, length
+ public static Source createSource(@NonNull byte[] data, int offset,
+ int length) throws ArrayIndexOutOfBoundsException {
+ if (data == null) {
+ throw new NullPointerException("null byte[] in createSource!");
+ }
+ if (offset < 0 || length < 0 || offset >= data.length ||
+ offset + length > data.length) {
+ throw new ArrayIndexOutOfBoundsException(
+ "invalid offset/length!");
+ }
+ return new ByteArraySource(data, offset, length);
+ }
+
+ /**
+ * Create a new {@link Source} from a {@link java.nio.ByteBuffer}.
+ *
+ * The returned {@link Source} effectively takes ownership of the
+ * {@link java.nio.ByteBuffer}; i.e. no other code should modify it after
+ * this call.
+ *
+ * Decoding will start from {@link java.nio.ByteBuffer#position()}.
+ */
+ public static Source createSource(ByteBuffer buffer) {
+ return new ByteBufferSource(buffer);
+ }
+
+ /**
+ * Return the width and height of a given sample size.
+ *
+ * This takes an input that functions like
+ * {@link BitmapFactory.Options#inSampleSize}. It returns a width and
+ * height that can be acheived by sampling the encoded image. Other widths
+ * and heights may be supported, but will require an additional (internal)
+ * scaling step. Such internal scaling is *not* supported with
+ * {@link #requireUnpremultiplied}.
+ *
+ * @param sampleSize Sampling rate of the encoded image.
+ * @return Point {@link Point#x} and {@link Point#y} correspond to the
+ * width and height after sampling.
+ */
+ public Point getSampledSize(int sampleSize) {
+ if (sampleSize <= 0) {
+ throw new IllegalArgumentException("sampleSize must be positive! "
+ + "provided " + sampleSize);
+ }
+ if (mNativePtr == 0) {
+ throw new IllegalStateException("ImageDecoder is recycled!");
+ }
+
+ return nGetSampledSize(mNativePtr, sampleSize);
+ }
+
+ // Modifiers
+ /**
+ * Resize the output to have the following size.
+ *
+ * @param width must be greater than 0.
+ * @param height must be greater than 0.
+ */
+ public void resize(int width, int height) {
+ if (width <= 0 || height <= 0) {
+ throw new IllegalArgumentException("Dimensions must be positive! "
+ + "provided (" + width + ", " + height + ")");
+ }
+
+ mDesiredWidth = width;
+ mDesiredHeight = height;
+ }
+
+ /**
+ * Resize based on a sample size.
+ *
+ * This has the same effect as passing the result of
+ * {@link #getSampledSize} to {@link #resize(int, int)}.
+ *
+ * @param sampleSize Sampling rate of the encoded image.
+ */
+ public void resize(int sampleSize) {
+ Point dimensions = this.getSampledSize(sampleSize);
+ this.resize(dimensions.x, dimensions.y);
+ }
+
+ // These need to stay in sync with ImageDecoder.cpp's Allocator enum.
+ /**
+ * Use the default allocation for the pixel memory.
+ *
+ * Will typically result in a {@link Bitmap.Config#HARDWARE}
+ * allocation, but may be software for small images. In addition, this will
+ * switch to software when HARDWARE is incompatible, e.g.
+ * {@link #setMutable}, {@link #setAsAlphaMask}.
+ */
+ public static final int DEFAULT_ALLOCATOR = 0;
+
+ /**
+ * Use a software allocation for the pixel memory.
+ *
+ * Useful for drawing to a software {@link Canvas} or for
+ * accessing the pixels on the final output.
+ */
+ public static final int SOFTWARE_ALLOCATOR = 1;
+
+ /**
+ * Use shared memory for the pixel memory.
+ *
+ * Useful for sharing across processes.
+ */
+ public static final int SHARED_MEMORY_ALLOCATOR = 2;
+
+ /**
+ * Require a {@link Bitmap.Config#HARDWARE} {@link Bitmap}.
+ *
+ * This will throw an {@link java.lang.IllegalStateException} when combined
+ * with incompatible options, like {@link #setMutable} or
+ * {@link #setAsAlphaMask}.
+ */
+ public static final int HARDWARE_ALLOCATOR = 3;
+
+ /** @hide **/
+ @Retention(SOURCE)
+ @IntDef({ DEFAULT_ALLOCATOR, SOFTWARE_ALLOCATOR, SHARED_MEMORY_ALLOCATOR,
+ HARDWARE_ALLOCATOR })
+ public @interface Allocator {};
+
+ /**
+ * Choose the backing for the pixel memory.
+ *
+ * This is ignored for animated drawables.
+ *
+ * TODO: Allow accessing the backing from the Bitmap.
+ *
+ * @param allocator Type of allocator to use.
+ */
+ public void setAllocator(@Allocator int allocator) {
+ if (allocator < DEFAULT_ALLOCATOR || allocator > HARDWARE_ALLOCATOR) {
+ throw new IllegalArgumentException("invalid allocator " + allocator);
+ }
+ mAllocator = allocator;
+ }
+
+ /**
+ * Create a {@link Bitmap} with unpremultiplied pixels.
+ *
+ * By default, ImageDecoder will create a {@link Bitmap} with
+ * premultiplied pixels, which is required for drawing with the
+ * {@link android.view.View} system (i.e. to a {@link Canvas}). Calling
+ * this method will result in {@link #decodeBitmap} returning a
+ * {@link Bitmap} with unpremultiplied pixels. See
+ * {@link Bitmap#isPremultiplied}. Incompatible with
+ * {@link #decodeDrawable}; attempting to decode an unpremultiplied
+ * {@link Drawable} will throw an {@link java.lang.IllegalStateException}.
+ */
+ public void requireUnpremultiplied() {
+ mRequireUnpremultiplied = true;
+ }
+
+ /**
+ * Modify the image after decoding and scaling.
+ *
+ * This allows adding effects prior to returning a {@link Drawable} or
+ * {@link Bitmap}. For a {@code Drawable} or an immutable {@code Bitmap},
+ * this is the only way to process the image after decoding.
+ *
+ * If set on a nine-patch image, the nine-patch data is ignored.
+ *
+ * For an animated image, the drawing commands drawn on the {@link Canvas}
+ * will be recorded immediately and then applied to each frame.
+ */
+ public void setPostProcess(PostProcess p) {
+ mPostProcess = p;
+ }
+
+ /**
+ * Set (replace) the {@link OnExceptionListener} on this object.
+ *
+ * Will be called if there is an error in the input. Without one, a
+ * partial {@link Bitmap} will be created.
+ */
+ public void setOnExceptionListener(OnExceptionListener l) {
+ mOnExceptionListener = l;
+ }
+
+ /**
+ * Crop the output to {@code subset} of the (possibly) scaled image.
+ *
+ * {@code subset} must be contained within the size set by {@link #resize}
+ * or the bounds of the image if resize was not called. Otherwise an
+ * {@link IllegalStateException} will be thrown.
+ *
+ * NOT intended as a replacement for
+ * {@link BitmapRegionDecoder#decodeRegion}. This supports all formats,
+ * but merely crops the output.
+ */
+ public void crop(Rect subset) {
+ mCropRect = subset;
+ }
+
+ /**
+ * Create a mutable {@link Bitmap}.
+ *
+ * By default, a {@link Bitmap} created will be immutable, but that can be
+ * changed with this call.
+ *
+ * Incompatible with {@link #HARDWARE_ALLOCATOR}, because
+ * {@link Bitmap.Config#HARDWARE} Bitmaps cannot be mutable. Attempting to
+ * combine them will throw an {@link java.lang.IllegalStateException}.
+ *
+ * Incompatible with {@link #decodeDrawable}, which would require
+ * retrieving the Bitmap from the returned Drawable in order to modify.
+ * Attempting to decode a mutable {@link Drawable} will throw an
+ * {@link java.lang.IllegalStateException}
+ */
+ public void setMutable() {
+ mMutable = true;
+ }
+
+ /**
+ * Potentially save RAM at the expense of quality.
+ *
+ * This may result in a {@link Bitmap} with a denser {@link Bitmap.Config},
+ * depending on the image. For example, for an opaque {@link Bitmap}, this
+ * may result in a {@link Bitmap.Config} with no alpha information.
+ */
+ public void setPreferRamOverQuality() {
+ mPreferRamOverQuality = true;
+ }
+
+ /**
+ * Potentially treat the output as an alpha mask.
+ *
+ * If the image is encoded in a format with only one channel, treat that
+ * channel as alpha. Otherwise this call has no effect.
+ *
+ * Incompatible with {@link #HARDWARE_ALLOCATOR}. Trying to combine them
+ * will throw an {@link java.lang.IllegalStateException}.
+ */
+ public void setAsAlphaMask() {
+ mAsAlphaMask = true;
+ }
+
+ /**
+ * Clean up resources.
+ *
+ * ImageDecoder has a private constructor, and will always be recycled
+ * by decodeDrawable or decodeBitmap which creates it, so there is no
+ * need for a finalizer.
+ */
+ private void recycle() {
+ if (mNativePtr == 0) {
+ return;
+ }
+ nRecycle(mNativePtr);
+ mNativePtr = 0;
+ }
+
+ private void checkState() {
+ if (mNativePtr == 0) {
+ throw new IllegalStateException("Cannot reuse ImageDecoder.Source!");
+ }
+
+ checkSubset(mDesiredWidth, mDesiredHeight, mCropRect);
+
+ if (mAllocator == HARDWARE_ALLOCATOR) {
+ if (mMutable) {
+ throw new IllegalStateException("Cannot make mutable HARDWARE Bitmap!");
+ }
+ if (mAsAlphaMask) {
+ throw new IllegalStateException("Cannot make HARDWARE Alpha mask Bitmap!");
+ }
+ }
+
+ if (mPostProcess != null && mRequireUnpremultiplied) {
+ throw new IllegalStateException("Cannot draw to unpremultiplied pixels!");
+ }
+ }
+
+ private static void checkSubset(int width, int height, Rect r) {
+ if (r == null) {
+ return;
+ }
+ if (r.left < 0 || r.top < 0 || r.right > width || r.bottom > height) {
+ throw new IllegalStateException("Subset " + r + " not contained by "
+ + "scaled image bounds: (" + width + " x " + height + ")");
+ }
+ }
+
+ /**
+ * Create a {@link Drawable}.
+ */
+ public static Drawable decodeDrawable(Source src, OnHeaderDecodedListener listener) {
+ ImageDecoder decoder = src.createImageDecoder();
+ if (decoder == null) {
+ return null;
+ }
+
+ if (listener != null) {
+ ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight);
+ listener.onHeaderDecoded(info, decoder);
+ }
+
+ decoder.checkState();
+
+ if (decoder.mRequireUnpremultiplied) {
+ // Though this could be supported (ignored) for opaque images, it
+ // seems better to always report this error.
+ throw new IllegalStateException("Cannot decode a Drawable with" +
+ " unpremultiplied pixels!");
+ }
+
+ if (decoder.mMutable) {
+ throw new IllegalStateException("Cannot decode a mutable Drawable!");
+ }
+
+ try {
+ Bitmap bm = nDecodeBitmap(decoder.mNativePtr,
+ decoder.mOnExceptionListener,
+ decoder.mPostProcess,
+ decoder.mDesiredWidth, decoder.mDesiredHeight,
+ decoder.mCropRect,
+ false, // decoder.mMutable
+ decoder.mAllocator,
+ false, // decoder.mRequireUnpremultiplied
+ decoder.mPreferRamOverQuality,
+ decoder.mAsAlphaMask
+ );
+ if (bm == null) {
+ return null;
+ }
+
+ Resources res = src.getResources();
+ if (res == null) {
+ bm.setDensity(Bitmap.DENSITY_NONE);
+ }
+
+ byte[] np = bm.getNinePatchChunk();
+ if (np != null && NinePatch.isNinePatchChunk(np)) {
+ Rect opticalInsets = new Rect();
+ bm.getOpticalInsets(opticalInsets);
+ Rect padding = new Rect();
+ nGetPadding(decoder.mNativePtr, padding);
+ return new NinePatchDrawable(res, bm, np, padding,
+ opticalInsets, null);
+ }
+
+ // TODO: Handle animation.
+ return new BitmapDrawable(res, bm);
+ } finally {
+ decoder.recycle();
+ src.close();
+ }
+ }
+
+ /**
+ * Create a {@link Bitmap}.
+ */
+ public static Bitmap decodeBitmap(Source src, OnHeaderDecodedListener listener) {
+ ImageDecoder decoder = src.createImageDecoder();
+ if (decoder == null) {
+ return null;
+ }
+
+ if (listener != null) {
+ ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight);
+ listener.onHeaderDecoded(info, decoder);
+ }
+
+ decoder.checkState();
+
+ try {
+ return nDecodeBitmap(decoder.mNativePtr,
+ decoder.mOnExceptionListener,
+ decoder.mPostProcess,
+ decoder.mDesiredWidth, decoder.mDesiredHeight,
+ decoder.mCropRect,
+ decoder.mMutable,
+ decoder.mAllocator,
+ decoder.mRequireUnpremultiplied,
+ decoder.mPreferRamOverQuality,
+ decoder.mAsAlphaMask);
+ } finally {
+ decoder.recycle();
+ src.close();
+ }
+ }
+
+ private static native ImageDecoder nCreate(long asset);
+ private static native ImageDecoder nCreate(ByteBuffer buffer,
+ int position,
+ int limit);
+ private static native ImageDecoder nCreate(byte[] data, int offset,
+ int length);
+ private static native Bitmap nDecodeBitmap(long nativePtr,
+ OnExceptionListener listener,
+ PostProcess postProcess,
+ int width, int height,
+ Rect cropRect, boolean mutable,
+ int allocator, boolean requireUnpremul,
+ boolean preferRamOverQuality, boolean asAlphaMask);
+ private static native Point nGetSampledSize(long nativePtr,
+ int sampleSize);
+ private static native void nGetPadding(long nativePtr, Rect outRect);
+ private static native void nRecycle(long nativePtr);
+}
diff --git a/android/graphics/Point.java b/android/graphics/Point.java
index abcccbdb..c6b6c668 100644
--- a/android/graphics/Point.java
+++ b/android/graphics/Point.java
@@ -18,6 +18,7 @@ package android.graphics;
import android.os.Parcel;
import android.os.Parcelable;
+import android.util.proto.ProtoOutputStream;
import java.io.PrintWriter;
@@ -121,6 +122,21 @@ public class Point implements Parcelable {
out.writeInt(y);
}
+ /**
+ * Write to a protocol buffer output stream.
+ * Protocol buffer message definition at {@link android.graphics.PointProto}
+ *
+ * @param protoOutputStream Stream to write the Rect object to.
+ * @param fieldId Field Id of the Rect as defined in the parent message
+ * @hide
+ */
+ public void writeToProto(ProtoOutputStream protoOutputStream, long fieldId) {
+ final long token = protoOutputStream.start(fieldId);
+ protoOutputStream.write(PointProto.X, x);
+ protoOutputStream.write(PointProto.Y, y);
+ protoOutputStream.end(token);
+ }
+
public static final Parcelable.Creator<Point> CREATOR = new Parcelable.Creator<Point>() {
/**
* Return a new point from the data in the specified parcel.
diff --git a/android/graphics/PostProcess.java b/android/graphics/PostProcess.java
new file mode 100644
index 00000000..c5a31e82
--- /dev/null
+++ b/android/graphics/PostProcess.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.graphics.drawable.Drawable;
+
+
+/**
+ * Helper interface for adding custom processing to an image.
+ *
+ * The image being processed may be a {@link Drawable}, {@link Bitmap} or frame
+ * of an animated image produced by {@link ImageDecoder}. This is called before
+ * the requested object is returned.
+ *
+ * This custom processing also applies to image types that are otherwise
+ * immutable, such as {@link Bitmap.Config#HARDWARE}.
+ *
+ * On an animated image, the callback will only be called once, but the drawing
+ * commands will be applied to each frame, as if the {@code Canvas} had been
+ * returned by {@link Picture#beginRecording}.
+ *
+ * Supplied to ImageDecoder via {@link ImageDecoder#setPostProcess}.
+ * @hide
+ */
+public interface PostProcess {
+ /**
+ * Do any processing after (for example) decoding.
+ *
+ * Drawing to the {@link Canvas} will behave as if the initial processing
+ * (e.g. decoding) already exists in the Canvas. An implementation can draw
+ * effects on top of this, or it can even draw behind it using
+ * {@link PorterDuff.Mode#DST_OVER}. A common effect is to add transparency
+ * to the corners to achieve rounded corners. That can be done with the
+ * following code:
+ *
+ * <code>
+ * Path path = new Path();
+ * path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
+ * path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW);
+ * Paint paint = new Paint();
+ * paint.setAntiAlias(true);
+ * paint.setColor(Color.TRANSPARENT);
+ * paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+ * canvas.drawPath(path, paint);
+ * return PixelFormat.TRANSLUCENT;
+ * </code>
+ *
+ *
+ * @param canvas The {@link Canvas} to draw to.
+ * @param width Width of {@code canvas}. Anything drawn outside of this
+ * will be ignored.
+ * @param height Height of {@code canvas}. Anything drawn outside of this
+ * will be ignored.
+ * @return Opacity of the result after drawing.
+ * {@link PixelFormat#UNKNOWN} means that the implementation did not
+ * change whether the image has alpha. Return this unless you added
+ * transparency (e.g. with the code above, in which case you should
+ * return {@code PixelFormat.TRANSLUCENT}) or you forced the image to
+ * be opaque (e.g. by drawing everywhere with an opaque color and
+ * {@code PorterDuff.Mode.DST_OVER}, in which case you should return
+ * {@code PixelFormat.OPAQUE}).
+ * {@link PixelFormat#TRANSLUCENT} means that the implementation added
+ * transparency. This is safe to return even if the image already had
+ * transparency. This is also safe to return if the result is opaque,
+ * though it may draw more slowly.
+ * {@link PixelFormat#OPAQUE} means that the implementation forced the
+ * image to be opaque. This is safe to return even if the image was
+ * already opaque.
+ * {@link PixelFormat#TRANSPARENT} (or any other integer) is not
+ * allowed, and will result in throwing an
+ * {@link java.lang.IllegalArgumentException}.
+ */
+ @PixelFormat.Opacity
+ public int postProcess(@NonNull Canvas canvas, int width, int height);
+}
diff --git a/android/graphics/drawable/RippleComponent.java b/android/graphics/drawable/RippleComponent.java
index 0e38826e..626bcee9 100644
--- a/android/graphics/drawable/RippleComponent.java
+++ b/android/graphics/drawable/RippleComponent.java
@@ -93,12 +93,8 @@ abstract class RippleComponent {
protected final void onHotspotBoundsChanged() {
if (!mHasMaxRadius) {
- final float halfWidth = mBounds.width() / 2.0f;
- final float halfHeight = mBounds.height() / 2.0f;
- final float targetRadius = (float) Math.sqrt(halfWidth * halfWidth
- + halfHeight * halfHeight);
-
- onTargetRadiusChanged(targetRadius);
+ mTargetRadius = getTargetRadius(mBounds);
+ onTargetRadiusChanged(mTargetRadius);
}
}
diff --git a/android/graphics/drawable/RippleDrawable.java b/android/graphics/drawable/RippleDrawable.java
index 8b185f2b..734cff54 100644
--- a/android/graphics/drawable/RippleDrawable.java
+++ b/android/graphics/drawable/RippleDrawable.java
@@ -299,6 +299,12 @@ public class RippleDrawable extends LayerDrawable {
onHotspotBoundsChanged();
}
+ final int count = mExitingRipplesCount;
+ final RippleForeground[] ripples = mExitingRipples;
+ for (int i = 0; i < count; i++) {
+ ripples[i].onBoundsChange();
+ }
+
if (mBackground != null) {
mBackground.onBoundsChange();
}
@@ -560,8 +566,7 @@ public class RippleDrawable extends LayerDrawable {
y = mHotspotBounds.exactCenterY();
}
- final boolean isBounded = isBounded();
- mRipple = new RippleForeground(this, mHotspotBounds, x, y, isBounded, mForceSoftware);
+ mRipple = new RippleForeground(this, mHotspotBounds, x, y, mForceSoftware);
}
mRipple.setup(mState.mMaxRadius, mDensity);
diff --git a/android/graphics/drawable/RippleForeground.java b/android/graphics/drawable/RippleForeground.java
index 0b5020cb..ecbf5780 100644
--- a/android/graphics/drawable/RippleForeground.java
+++ b/android/graphics/drawable/RippleForeground.java
@@ -30,6 +30,7 @@ import android.view.DisplayListCanvas;
import android.view.RenderNodeAnimator;
import android.view.animation.AnimationUtils;
import android.view.animation.LinearInterpolator;
+import android.view.animation.PathInterpolator;
import java.util.ArrayList;
@@ -38,18 +39,14 @@ import java.util.ArrayList;
*/
class RippleForeground extends RippleComponent {
private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
- private static final TimeInterpolator DECELERATE_INTERPOLATOR = new LogDecelerateInterpolator(
- 400f, 1.4f, 0);
+ // Matches R.interpolator.fast_out_slow_in but as we have no context we can't just import that
+ private static final TimeInterpolator DECELERATE_INTERPOLATOR =
+ new PathInterpolator(0.4f, 0f, 0.2f, 1f);
- // Pixel-based accelerations and velocities.
- private static final float WAVE_TOUCH_DOWN_ACCELERATION = 2048;
- private static final float WAVE_OPACITY_DECAY_VELOCITY = 3;
-
- // Bounded ripple animation properties.
- private static final int BOUNDED_ORIGIN_EXIT_DURATION = 300;
- private static final int BOUNDED_RADIUS_EXIT_DURATION = 800;
- private static final int BOUNDED_OPACITY_EXIT_DURATION = 400;
- private static final float MAX_BOUNDED_RADIUS = 350;
+ // Time it takes for the ripple to expand
+ private static final int RIPPLE_ENTER_DURATION = 225;
+ // Time it takes for the ripple to slide from the touch to the center point
+ private static final int RIPPLE_ORIGIN_DURATION = 225;
private static final int OPACITY_ENTER_DURATION = 75;
private static final int OPACITY_EXIT_DURATION = 150;
@@ -71,9 +68,6 @@ class RippleForeground extends RippleComponent {
private float mTargetX = 0;
private float mTargetY = 0;
- /** Ripple target radius used when bounded. Not used for clamping. */
- private float mBoundedRadius = 0;
-
// Software rendering properties.
private float mOpacity = 0;
@@ -107,19 +101,13 @@ class RippleForeground extends RippleComponent {
private float mStartRadius = 0;
public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY,
- boolean isBounded, boolean forceSoftware) {
+ boolean forceSoftware) {
super(owner, bounds);
mForceSoftware = forceSoftware;
mStartingX = startingX;
mStartingY = startingY;
- if (isBounded) {
- mBoundedRadius = MAX_BOUNDED_RADIUS * 0.9f
- + (float) (MAX_BOUNDED_RADIUS * Math.random() * 0.1);
- } else {
- mBoundedRadius = 0;
- }
// Take 60% of the maximum of the width and height, then divided half to get the radius.
mStartRadius = Math.max(bounds.width(), bounds.height()) * 0.3f;
}
@@ -127,6 +115,7 @@ class RippleForeground extends RippleComponent {
@Override
protected void onTargetRadiusChanged(float targetRadius) {
clampStartingPosition();
+ switchToUiThreadAnimation();
}
private void drawSoftware(Canvas c, Paint p) {
@@ -228,16 +217,14 @@ class RippleForeground extends RippleComponent {
}
mRunningSwAnimators.clear();
- final int duration = getRadiusDuration();
-
final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
- tweenRadius.setDuration(duration);
+ tweenRadius.setDuration(RIPPLE_ENTER_DURATION);
tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR);
tweenRadius.start();
mRunningSwAnimators.add(tweenRadius);
final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
- tweenOrigin.setDuration(duration);
+ tweenOrigin.setDuration(RIPPLE_ORIGIN_DURATION);
tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR);
tweenOrigin.start();
mRunningSwAnimators.add(tweenOrigin);
@@ -267,20 +254,18 @@ class RippleForeground extends RippleComponent {
final Paint paint = mOwner.getRipplePaint();
mPropPaint = CanvasProperty.createPaint(paint);
- final int radiusDuration = getRadiusDuration();
-
final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius);
- radius.setDuration(radiusDuration);
+ radius.setDuration(RIPPLE_ORIGIN_DURATION);
radius.setInterpolator(DECELERATE_INTERPOLATOR);
mPendingHwAnimators.add(radius);
final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX);
- x.setDuration(radiusDuration);
+ x.setDuration(RIPPLE_ORIGIN_DURATION);
x.setInterpolator(DECELERATE_INTERPOLATOR);
mPendingHwAnimators.add(x);
final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY);
- y.setDuration(radiusDuration);
+ y.setDuration(RIPPLE_ORIGIN_DURATION);
y.setInterpolator(DECELERATE_INTERPOLATOR);
mPendingHwAnimators.add(y);
@@ -333,12 +318,6 @@ class RippleForeground extends RippleComponent {
return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY);
}
- private int getRadiusDuration() {
- final float remainingRadius = mTargetRadius - getCurrentRadius();
- return (int) (1000 * Math.sqrt(remainingRadius / WAVE_TOUCH_DOWN_ACCELERATION *
- mDensityScale) + 0.5);
- }
-
private float getCurrentRadius() {
return MathUtils.lerp(mStartRadius, mTargetRadius, mTweenRadius);
}
@@ -402,6 +381,14 @@ class RippleForeground extends RippleComponent {
}
}
+ private void clearHwProps() {
+ mPropPaint = null;
+ mPropRadius = null;
+ mPropX = null;
+ mPropY = null;
+ mUsingProperties = false;
+ }
+
private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
@@ -410,39 +397,20 @@ class RippleForeground extends RippleComponent {
pruneSwFinished();
if (mRunningHwAnimators.isEmpty()) {
- mPropPaint = null;
- mPropRadius = null;
- mPropX = null;
- mPropY = null;
+ clearHwProps();
}
}
};
- /**
- * Interpolator with a smooth log deceleration.
- */
- private static final class LogDecelerateInterpolator implements TimeInterpolator {
- private final float mBase;
- private final float mDrift;
- private final float mTimeScale;
- private final float mOutputScale;
-
- public LogDecelerateInterpolator(float base, float timeScale, float drift) {
- mBase = base;
- mDrift = drift;
- mTimeScale = 1f / timeScale;
-
- mOutputScale = 1f / computeLog(1f);
- }
-
- private float computeLog(float t) {
- return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t);
- }
-
- @Override
- public float getInterpolation(float t) {
- return computeLog(t) * mOutputScale;
+ private void switchToUiThreadAnimation() {
+ for (int i = 0; i < mRunningHwAnimators.size(); i++) {
+ Animator animator = mRunningHwAnimators.get(i);
+ animator.removeListener(mAnimationListener);
+ animator.end();
}
+ mRunningHwAnimators.clear();
+ clearHwProps();
+ invalidateSelf();
}
/**
diff --git a/android/graphics/drawable/VectorDrawable.java b/android/graphics/drawable/VectorDrawable.java
index 7b2e21a4..c71585f3 100644
--- a/android/graphics/drawable/VectorDrawable.java
+++ b/android/graphics/drawable/VectorDrawable.java
@@ -896,6 +896,13 @@ public class VectorDrawable extends Drawable {
return mVectorState.getNativeRenderer();
}
+ /**
+ * @hide
+ */
+ public void setAntiAlias(boolean aa) {
+ nSetAntiAlias(mVectorState.mNativeTree.get(), aa);
+ }
+
static class VectorDrawableState extends ConstantState {
// Variables below need to be copied (deep copy if applicable) for mutation.
int[] mThemeAttrs;
@@ -2269,6 +2276,8 @@ public class VectorDrawable extends Drawable {
@FastNative
private static native float nGetRootAlpha(long rendererPtr);
@FastNative
+ private static native void nSetAntiAlias(long rendererPtr, boolean aa);
+ @FastNative
private static native void nSetAllowCaching(long rendererPtr, boolean allowCaching);
@FastNative
diff --git a/android/graphics/drawable/VectorDrawable_Delegate.java b/android/graphics/drawable/VectorDrawable_Delegate.java
index 00630464..d9f8692e 100644
--- a/android/graphics/drawable/VectorDrawable_Delegate.java
+++ b/android/graphics/drawable/VectorDrawable_Delegate.java
@@ -135,6 +135,12 @@ public class VectorDrawable_Delegate {
}
@LayoutlibDelegate
+ static void nSetAntiAlias(long rendererPtr, boolean aa) {
+ VPathRenderer_Delegate nativePathRenderer = VNativeObject.getDelegate(rendererPtr);
+ nativePathRenderer.setAntiAlias(aa);
+ }
+
+ @LayoutlibDelegate
static void nSetAllowCaching(long rendererPtr, boolean allowCaching) {
// ignored
}
@@ -1060,6 +1066,7 @@ public class VectorDrawable_Delegate {
private Paint mStrokePaint;
private Paint mFillPaint;
private PathMeasure mPathMeasure;
+ private boolean mAntiAlias = true;
private VPathRenderer_Delegate(long rootGroupPtr) {
mRootGroupPtr = rootGroupPtr;
@@ -1169,7 +1176,7 @@ public class VectorDrawable_Delegate {
if (mFillPaint == null) {
mFillPaint = new Paint();
mFillPaint.setStyle(Style.FILL);
- mFillPaint.setAntiAlias(true);
+ mFillPaint.setAntiAlias(mAntiAlias);
}
final Paint fillPaint = mFillPaint;
@@ -1203,7 +1210,7 @@ public class VectorDrawable_Delegate {
if (mStrokePaint == null) {
mStrokePaint = new Paint();
mStrokePaint.setStyle(Style.STROKE);
- mStrokePaint.setAntiAlias(true);
+ mStrokePaint.setAntiAlias(mAntiAlias);
}
final Paint strokePaint = mStrokePaint;
@@ -1261,6 +1268,10 @@ public class VectorDrawable_Delegate {
return matrixScale;
}
+ private void setAntiAlias(boolean aa) {
+ mAntiAlias = aa;
+ }
+
@Override
public void setName(String name) {
}
diff --git a/android/graphics/perftests/PaintMeasureTextTest.java b/android/graphics/perftests/PaintMeasureTextTest.java
index b81908c3..b9ee6133 100644
--- a/android/graphics/perftests/PaintMeasureTextTest.java
+++ b/android/graphics/perftests/PaintMeasureTextTest.java
@@ -80,11 +80,11 @@ public class PaintMeasureTextTest {
}
while (state.keepRunning()) {
- state.pauseTiming();
if (mCacheMode == DONT_USE_CACHE) {
+ state.pauseTiming();
Canvas.freeTextLayoutCaches();
+ state.resumeTiming();
}
- state.resumeTiming();
paint.measureText(mText);
}
diff --git a/android/hardware/HardwareBuffer.java b/android/hardware/HardwareBuffer.java
index 7049628b..7866b52c 100644
--- a/android/hardware/HardwareBuffer.java
+++ b/android/hardware/HardwareBuffer.java
@@ -17,6 +17,7 @@
package android.hardware;
import android.annotation.IntDef;
+import android.annotation.LongDef;
import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
@@ -41,7 +42,15 @@ import libcore.util.NativeAllocationRegistry;
public final class HardwareBuffer implements Parcelable, AutoCloseable {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({RGBA_8888, RGBA_FP16, RGBA_1010102, RGBX_8888, RGB_888, RGB_565, BLOB})
+ @IntDef(prefix = { "RGB", "BLOB" }, value = {
+ RGBA_8888,
+ RGBA_FP16,
+ RGBA_1010102,
+ RGBX_8888,
+ RGB_888,
+ RGB_565,
+ BLOB
+ })
public @interface Format {
}
@@ -70,7 +79,7 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true, value = {USAGE_CPU_READ_RARELY, USAGE_CPU_READ_OFTEN,
+ @LongDef(flag = true, value = {USAGE_CPU_READ_RARELY, USAGE_CPU_READ_OFTEN,
USAGE_CPU_WRITE_RARELY, USAGE_CPU_WRITE_OFTEN, USAGE_GPU_SAMPLED_IMAGE,
USAGE_GPU_COLOR_OUTPUT, USAGE_PROTECTED_CONTENT, USAGE_VIDEO_ENCODE,
USAGE_GPU_DATA_BUFFER, USAGE_SENSOR_DIRECT_DATA})
diff --git a/android/hardware/SensorAdditionalInfo.java b/android/hardware/SensorAdditionalInfo.java
index 7c876cfc..5ff627f4 100644
--- a/android/hardware/SensorAdditionalInfo.java
+++ b/android/hardware/SensorAdditionalInfo.java
@@ -68,8 +68,15 @@ public class SensorAdditionalInfo {
*
* @hide
*/
- @IntDef({TYPE_FRAME_BEGIN, TYPE_FRAME_END, TYPE_UNTRACKED_DELAY, TYPE_INTERNAL_TEMPERATURE,
- TYPE_VEC3_CALIBRATION, TYPE_SENSOR_PLACEMENT, TYPE_SAMPLING})
+ @IntDef(prefix = { "TYPE_" }, value = {
+ TYPE_FRAME_BEGIN,
+ TYPE_FRAME_END,
+ TYPE_UNTRACKED_DELAY,
+ TYPE_INTERNAL_TEMPERATURE,
+ TYPE_VEC3_CALIBRATION,
+ TYPE_SENSOR_PLACEMENT,
+ TYPE_SAMPLING
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface AdditionalInfoType {}
diff --git a/android/hardware/SensorDirectChannel.java b/android/hardware/SensorDirectChannel.java
index 36607c90..214d3ec2 100644
--- a/android/hardware/SensorDirectChannel.java
+++ b/android/hardware/SensorDirectChannel.java
@@ -40,8 +40,12 @@ public final class SensorDirectChannel implements Channel {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true, value = {TYPE_MEMORY_FILE, TYPE_HARDWARE_BUFFER})
- public @interface MemoryType {};
+ @IntDef(flag = true, prefix = { "TYPE_" }, value = {
+ TYPE_MEMORY_FILE,
+ TYPE_HARDWARE_BUFFER
+ })
+ public @interface MemoryType {}
+
/**
* Shared memory type ashmem, wrapped in MemoryFile object.
*
@@ -60,8 +64,13 @@ public final class SensorDirectChannel implements Channel {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true, value = {RATE_STOP, RATE_NORMAL, RATE_FAST, RATE_VERY_FAST})
- public @interface RateLevel {};
+ @IntDef(flag = true, prefix = { "RATE_" }, value = {
+ RATE_STOP,
+ RATE_NORMAL,
+ RATE_FAST,
+ RATE_VERY_FAST
+ })
+ public @interface RateLevel {}
/**
* Sensor stopped (no event output).
diff --git a/android/hardware/camera2/CameraAccessException.java b/android/hardware/camera2/CameraAccessException.java
index f9b659c6..d238797c 100644
--- a/android/hardware/camera2/CameraAccessException.java
+++ b/android/hardware/camera2/CameraAccessException.java
@@ -16,7 +16,6 @@
package android.hardware.camera2;
-import android.annotation.NonNull;
import android.annotation.IntDef;
import android.util.AndroidException;
@@ -81,15 +80,16 @@ public class CameraAccessException extends AndroidException {
*/
public static final int CAMERA_DEPRECATED_HAL = 1000;
- /** @hide */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef(
- {CAMERA_IN_USE,
- MAX_CAMERAS_IN_USE,
- CAMERA_DISABLED,
- CAMERA_DISCONNECTED,
- CAMERA_ERROR})
- public @interface AccessError {};
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "CAMERA_", "MAX_CAMERAS_IN_USE" }, value = {
+ CAMERA_IN_USE,
+ MAX_CAMERAS_IN_USE,
+ CAMERA_DISABLED,
+ CAMERA_DISCONNECTED,
+ CAMERA_ERROR
+ })
+ public @interface AccessError {}
// Make the eclipse warning about serializable exceptions go away
private static final long serialVersionUID = 5630338637471475675L; // randomly generated
diff --git a/android/hardware/camera2/CameraCharacteristics.java b/android/hardware/camera2/CameraCharacteristics.java
index 3a3048ef..57ab18e2 100644
--- a/android/hardware/camera2/CameraCharacteristics.java
+++ b/android/hardware/camera2/CameraCharacteristics.java
@@ -21,6 +21,7 @@ import android.annotation.Nullable;
import android.hardware.camera2.impl.CameraMetadataNative;
import android.hardware.camera2.impl.PublicKey;
import android.hardware.camera2.impl.SyntheticKey;
+import android.hardware.camera2.params.SessionConfiguration;
import android.hardware.camera2.utils.TypeReference;
import android.util.Rational;
@@ -169,6 +170,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
private final CameraMetadataNative mProperties;
private List<CameraCharacteristics.Key<?>> mKeys;
private List<CaptureRequest.Key<?>> mAvailableRequestKeys;
+ private List<CaptureRequest.Key<?>> mAvailableSessionKeys;
private List<CaptureResult.Key<?>> mAvailableResultKeys;
/**
@@ -251,6 +253,67 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
}
/**
+ * <p>Returns a subset of {@link #getAvailableCaptureRequestKeys} keys that the
+ * camera device can pass as part of the capture session initialization.</p>
+ *
+ * <p>This list includes keys that are difficult to apply per-frame and
+ * can result in unexpected delays when modified during the capture session
+ * lifetime. Typical examples include parameters that require a
+ * time-consuming hardware re-configuration or internal camera pipeline
+ * change. For performance reasons we suggest clients to pass their initial
+ * values as part of {@link SessionConfiguration#setSessionParameters}. Once
+ * the camera capture session is enabled it is also recommended to avoid
+ * changing them from their initial values set in
+ * {@link SessionConfiguration#setSessionParameters }.
+ * Control over session parameters can still be exerted in capture requests
+ * but clients should be aware and expect delays during their application.
+ * An example usage scenario could look like this:</p>
+ * <ul>
+ * <li>The camera client starts by quering the session parameter key list via
+ * {@link android.hardware.camera2.CameraCharacteristics#getAvailableSessionKeys }.</li>
+ * <li>Before triggering the capture session create sequence, a capture request
+ * must be built via {@link CameraDevice#createCaptureRequest } using an
+ * appropriate template matching the particular use case.</li>
+ * <li>The client should go over the list of session parameters and check
+ * whether some of the keys listed matches with the parameters that
+ * they intend to modify as part of the first capture request.</li>
+ * <li>If there is no such match, the capture request can be passed
+ * unmodified to {@link SessionConfiguration#setSessionParameters }.</li>
+ * <li>If matches do exist, the client should update the respective values
+ * and pass the request to {@link SessionConfiguration#setSessionParameters }.</li>
+ * <li>After the capture session initialization completes the session parameter
+ * key list can continue to serve as reference when posting or updating
+ * further requests. As mentioned above further changes to session
+ * parameters should ideally be avoided, if updates are necessary
+ * however clients could expect a delay/glitch during the
+ * parameter switch.</li>
+ * </ul>
+ *
+ * <p>The list returned is not modifiable, so any attempts to modify it will throw
+ * a {@code UnsupportedOperationException}.</p>
+ *
+ * <p>Each key is only listed once in the list. The order of the keys is undefined.</p>
+ *
+ * @return List of keys that can be passed during capture session initialization. In case the
+ * camera device doesn't support such keys the list can be null.
+ */
+ @SuppressWarnings({"unchecked"})
+ public List<CaptureRequest.Key<?>> getAvailableSessionKeys() {
+ if (mAvailableSessionKeys == null) {
+ Object crKey = CaptureRequest.Key.class;
+ Class<CaptureRequest.Key<?>> crKeyTyped = (Class<CaptureRequest.Key<?>>)crKey;
+
+ int[] filterTags = get(REQUEST_AVAILABLE_SESSION_KEYS);
+ if (filterTags == null) {
+ return null;
+ }
+ mAvailableSessionKeys =
+ getAvailableKeyList(CaptureRequest.class, crKeyTyped, filterTags);
+ }
+ return mAvailableSessionKeys;
+ }
+
+ /**
* Returns the list of keys supported by this {@link CameraDevice} for querying
* with a {@link CaptureRequest}.
*
@@ -1571,6 +1634,48 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
new Key<int[]>("android.request.availableCharacteristicsKeys", int[].class);
/**
+ * <p>A subset of the available request keys that the camera device
+ * can pass as part of the capture session initialization.</p>
+ * <p>This is a subset of android.request.availableRequestKeys which
+ * contains a list of keys that are difficult to apply per-frame and
+ * can result in unexpected delays when modified during the capture session
+ * lifetime. Typical examples include parameters that require a
+ * time-consuming hardware re-configuration or internal camera pipeline
+ * change. For performance reasons we advise clients to pass their initial
+ * values as part of {@link SessionConfiguration#setSessionParameters }. Once
+ * the camera capture session is enabled it is also recommended to avoid
+ * changing them from their initial values set in
+ * {@link SessionConfiguration#setSessionParameters }.
+ * Control over session parameters can still be exerted in capture requests
+ * but clients should be aware and expect delays during their application.
+ * An example usage scenario could look like this:</p>
+ * <ul>
+ * <li>The camera client starts by quering the session parameter key list via
+ * {@link android.hardware.camera2.CameraCharacteristics#getAvailableSessionKeys }.</li>
+ * <li>Before triggering the capture session create sequence, a capture request
+ * must be built via {@link CameraDevice#createCaptureRequest } using an
+ * appropriate template matching the particular use case.</li>
+ * <li>The client should go over the list of session parameters and check
+ * whether some of the keys listed matches with the parameters that
+ * they intend to modify as part of the first capture request.</li>
+ * <li>If there is no such match, the capture request can be passed
+ * unmodified to {@link SessionConfiguration#setSessionParameters }.</li>
+ * <li>If matches do exist, the client should update the respective values
+ * and pass the request to {@link SessionConfiguration#setSessionParameters }.</li>
+ * <li>After the capture session initialization completes the session parameter
+ * key list can continue to serve as reference when posting or updating
+ * further requests. As mentioned above further changes to session
+ * parameters should ideally be avoided, if updates are necessary
+ * however clients could expect a delay/glitch during the
+ * parameter switch.</li>
+ * </ul>
+ * <p>This key is available on all devices.</p>
+ * @hide
+ */
+ public static final Key<int[]> REQUEST_AVAILABLE_SESSION_KEYS =
+ new Key<int[]>("android.request.availableSessionKeys", int[].class);
+
+ /**
* <p>The list of image formats that are supported by this
* camera device for output streams.</p>
* <p>All camera devices will support JPEG and YUV_420_888 formats.</p>
diff --git a/android/hardware/camera2/CameraDevice.java b/android/hardware/camera2/CameraDevice.java
index 55343a29..87e503de 100644
--- a/android/hardware/camera2/CameraDevice.java
+++ b/android/hardware/camera2/CameraDevice.java
@@ -26,6 +26,7 @@ import static android.hardware.camera2.ICameraDeviceUser.CONSTRAINED_HIGH_SPEED_
import android.hardware.camera2.params.InputConfiguration;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.hardware.camera2.params.OutputConfiguration;
+import android.hardware.camera2.params.SessionConfiguration;
import android.os.Handler;
import android.view.Surface;
@@ -811,6 +812,26 @@ public abstract class CameraDevice implements AutoCloseable {
throws CameraAccessException;
/**
+ * <p>Create a new {@link CameraCaptureSession} using a {@link SessionConfiguration} helper
+ * object that aggregates all supported parameters.</p>
+ *
+ * @param config A session configuration (see {@link SessionConfiguration}).
+ *
+ * @throws IllegalArgumentException In case the session configuration is invalid; or the output
+ * configurations are empty.
+ * @throws CameraAccessException In case the camera device is no longer connected or has
+ * encountered a fatal error.
+ * @see #createCaptureSession(List, CameraCaptureSession.StateCallback, Handler)
+ * @see #createCaptureSessionByOutputConfigurations
+ * @see #createReprocessableCaptureSession
+ * @see #createConstrainedHighSpeedCaptureSession
+ */
+ public void createCaptureSession(
+ SessionConfiguration config) throws CameraAccessException {
+ throw new UnsupportedOperationException("No default implementation");
+ }
+
+ /**
* <p>Create a {@link CaptureRequest.Builder} for new capture requests,
* initialized with template for a target use case. The settings are chosen
* to be the best options for the specific camera device, so it is not
diff --git a/android/hardware/camera2/CameraMetadata.java b/android/hardware/camera2/CameraMetadata.java
index 4b57018b..cb11d0f5 100644
--- a/android/hardware/camera2/CameraMetadata.java
+++ b/android/hardware/camera2/CameraMetadata.java
@@ -2725,6 +2725,22 @@ public abstract class CameraMetadata<TKey> {
public static final int CONTROL_AWB_STATE_LOCKED = 3;
//
+ // Enumeration values for CaptureResult#CONTROL_AF_SCENE_CHANGE
+ //
+
+ /**
+ * <p>Scene change is not detected within the AF region(s).</p>
+ * @see CaptureResult#CONTROL_AF_SCENE_CHANGE
+ */
+ public static final int CONTROL_AF_SCENE_CHANGE_NOT_DETECTED = 0;
+
+ /**
+ * <p>Scene change is detected within the AF region(s).</p>
+ * @see CaptureResult#CONTROL_AF_SCENE_CHANGE
+ */
+ public static final int CONTROL_AF_SCENE_CHANGE_DETECTED = 1;
+
+ //
// Enumeration values for CaptureResult#FLASH_STATE
//
diff --git a/android/hardware/camera2/CaptureRequest.java b/android/hardware/camera2/CaptureRequest.java
index 0262ecb5..77da2a51 100644
--- a/android/hardware/camera2/CaptureRequest.java
+++ b/android/hardware/camera2/CaptureRequest.java
@@ -21,15 +21,18 @@ import android.annotation.Nullable;
import android.hardware.camera2.impl.CameraMetadataNative;
import android.hardware.camera2.impl.PublicKey;
import android.hardware.camera2.impl.SyntheticKey;
+import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.utils.HashCodeHelpers;
import android.hardware.camera2.utils.TypeReference;
import android.os.Parcel;
import android.os.Parcelable;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
import android.view.Surface;
import java.util.Collection;
import java.util.Collections;
-import java.util.HashSet;
import java.util.List;
import java.util.Objects;
@@ -198,7 +201,24 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
}
}
- private final HashSet<Surface> mSurfaceSet;
+ private final String TAG = "CaptureRequest-JV";
+
+ private final ArraySet<Surface> mSurfaceSet = new ArraySet<Surface>();
+
+ // Speed up sending CaptureRequest across IPC:
+ // mSurfaceConverted should only be set to true during capture request
+ // submission by {@link #convertSurfaceToStreamId}. The method will convert
+ // surfaces to stream/surface indexes based on passed in stream configuration at that time.
+ // This will save significant unparcel time for remote camera device.
+ // Once the request is submitted, camera device will call {@link #recoverStreamIdToSurface}
+ // to reset the capture request back to its original state.
+ private final Object mSurfacesLock = new Object();
+ private boolean mSurfaceConverted = false;
+ private int[] mStreamIdxArray;
+ private int[] mSurfaceIdxArray;
+
+ private static final ArraySet<Surface> mEmptySurfaceSet = new ArraySet<Surface>();
+
private final CameraMetadataNative mSettings;
private boolean mIsReprocess;
// If this request is part of constrained high speed request list that was created by
@@ -218,7 +238,6 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
private CaptureRequest() {
mSettings = new CameraMetadataNative();
setNativeInstance(mSettings);
- mSurfaceSet = new HashSet<Surface>();
mIsReprocess = false;
mReprocessableSessionId = CameraCaptureSession.SESSION_ID_NONE;
}
@@ -232,7 +251,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
private CaptureRequest(CaptureRequest source) {
mSettings = new CameraMetadataNative(source.mSettings);
setNativeInstance(mSettings);
- mSurfaceSet = (HashSet<Surface>) source.mSurfaceSet.clone();
+ mSurfaceSet.addAll(source.mSurfaceSet);
mIsReprocess = source.mIsReprocess;
mIsPartOfCHSRequestList = source.mIsPartOfCHSRequestList;
mReprocessableSessionId = source.mReprocessableSessionId;
@@ -263,7 +282,6 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
int reprocessableSessionId) {
mSettings = CameraMetadataNative.move(settings);
setNativeInstance(mSettings);
- mSurfaceSet = new HashSet<Surface>();
mIsReprocess = isReprocess;
if (isReprocess) {
if (reprocessableSessionId == CameraCaptureSession.SESSION_ID_NONE) {
@@ -463,22 +481,25 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
private void readFromParcel(Parcel in) {
mSettings.readFromParcel(in);
setNativeInstance(mSettings);
-
- mSurfaceSet.clear();
-
- Parcelable[] parcelableArray = in.readParcelableArray(Surface.class.getClassLoader());
-
- if (parcelableArray == null) {
- return;
- }
-
- for (Parcelable p : parcelableArray) {
- Surface s = (Surface) p;
- mSurfaceSet.add(s);
- }
-
mIsReprocess = (in.readInt() == 0) ? false : true;
mReprocessableSessionId = CameraCaptureSession.SESSION_ID_NONE;
+
+ synchronized (mSurfacesLock) {
+ mSurfaceSet.clear();
+ Parcelable[] parcelableArray = in.readParcelableArray(Surface.class.getClassLoader());
+ if (parcelableArray != null) {
+ for (Parcelable p : parcelableArray) {
+ Surface s = (Surface) p;
+ mSurfaceSet.add(s);
+ }
+ }
+ // Intentionally disallow java side readFromParcel to receive streamIdx/surfaceIdx
+ // Since there is no good way to convert indexes back to Surface
+ int streamSurfaceSize = in.readInt();
+ if (streamSurfaceSize != 0) {
+ throw new RuntimeException("Reading cached CaptureRequest is not supported");
+ }
+ }
}
@Override
@@ -489,8 +510,21 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
@Override
public void writeToParcel(Parcel dest, int flags) {
mSettings.writeToParcel(dest, flags);
- dest.writeParcelableArray(mSurfaceSet.toArray(new Surface[mSurfaceSet.size()]), flags);
dest.writeInt(mIsReprocess ? 1 : 0);
+
+ synchronized (mSurfacesLock) {
+ final ArraySet<Surface> surfaces = mSurfaceConverted ? mEmptySurfaceSet : mSurfaceSet;
+ dest.writeParcelableArray(surfaces.toArray(new Surface[surfaces.size()]), flags);
+ if (mSurfaceConverted) {
+ dest.writeInt(mStreamIdxArray.length);
+ for (int i = 0; i < mStreamIdxArray.length; i++) {
+ dest.writeInt(mStreamIdxArray[i]);
+ dest.writeInt(mSurfaceIdxArray[i]);
+ }
+ } else {
+ dest.writeInt(0);
+ }
+ }
}
/**
@@ -508,6 +542,67 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
}
/**
+ * @hide
+ */
+ public void convertSurfaceToStreamId(
+ final SparseArray<OutputConfiguration> configuredOutputs) {
+ synchronized (mSurfacesLock) {
+ if (mSurfaceConverted) {
+ Log.v(TAG, "Cannot convert already converted surfaces!");
+ return;
+ }
+
+ mStreamIdxArray = new int[mSurfaceSet.size()];
+ mSurfaceIdxArray = new int[mSurfaceSet.size()];
+ int i = 0;
+ for (Surface s : mSurfaceSet) {
+ boolean streamFound = false;
+ for (int j = 0; j < configuredOutputs.size(); ++j) {
+ int streamId = configuredOutputs.keyAt(j);
+ OutputConfiguration outConfig = configuredOutputs.valueAt(j);
+ int surfaceId = 0;
+ for (Surface outSurface : outConfig.getSurfaces()) {
+ if (s == outSurface) {
+ streamFound = true;
+ mStreamIdxArray[i] = streamId;
+ mSurfaceIdxArray[i] = surfaceId;
+ i++;
+ break;
+ }
+ surfaceId++;
+ }
+ if (streamFound) {
+ break;
+ }
+ }
+ if (!streamFound) {
+ mStreamIdxArray = null;
+ mSurfaceIdxArray = null;
+ throw new IllegalArgumentException(
+ "CaptureRequest contains unconfigured Input/Output Surface!");
+ }
+ }
+ mSurfaceConverted = true;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public void recoverStreamIdToSurface() {
+ synchronized (mSurfacesLock) {
+ if (!mSurfaceConverted) {
+ Log.v(TAG, "Cannot convert already converted surfaces!");
+ return;
+ }
+
+ mStreamIdxArray = null;
+ mSurfaceIdxArray = null;
+ mSurfaceConverted = false;
+ }
+ }
+
+ /**
* A builder for capture requests.
*
* <p>To obtain a builder instance, use the
diff --git a/android/hardware/camera2/CaptureResult.java b/android/hardware/camera2/CaptureResult.java
index cfad098c..6d7b06fc 100644
--- a/android/hardware/camera2/CaptureResult.java
+++ b/android/hardware/camera2/CaptureResult.java
@@ -2185,6 +2185,30 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
new Key<Boolean>("android.control.enableZsl", boolean.class);
/**
+ * <p>Whether a significant scene change is detected within the currently-set AF
+ * region(s).</p>
+ * <p>When the camera focus routine detects a change in the scene it is looking at,
+ * such as a large shift in camera viewpoint, significant motion in the scene, or a
+ * significant illumination change, this value will be set to DETECTED for a single capture
+ * result. Otherwise the value will be NOT_DETECTED. The threshold for detection is similar
+ * to what would trigger a new passive focus scan to begin in CONTINUOUS autofocus modes.</p>
+ * <p>afSceneChange may be DETECTED only if afMode is AF_MODE_CONTINUOUS_VIDEO or
+ * AF_MODE_CONTINUOUS_PICTURE. In other AF modes, afSceneChange must be NOT_DETECTED.</p>
+ * <p>This key will be available if the camera device advertises this key via {@link android.hardware.camera2.CameraCharacteristics#getAvailableCaptureResultKeys }.</p>
+ * <p><b>Possible values:</b>
+ * <ul>
+ * <li>{@link #CONTROL_AF_SCENE_CHANGE_NOT_DETECTED NOT_DETECTED}</li>
+ * <li>{@link #CONTROL_AF_SCENE_CHANGE_DETECTED DETECTED}</li>
+ * </ul></p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ * @see #CONTROL_AF_SCENE_CHANGE_NOT_DETECTED
+ * @see #CONTROL_AF_SCENE_CHANGE_DETECTED
+ */
+ @PublicKey
+ public static final Key<Integer> CONTROL_AF_SCENE_CHANGE =
+ new Key<Integer>("android.control.afSceneChange", int.class);
+
+ /**
* <p>Operation mode for edge
* enhancement.</p>
* <p>Edge enhancement improves sharpness and details in the captured image. OFF means
diff --git a/android/hardware/camera2/impl/CameraCaptureSessionImpl.java b/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
index 374789c6..8b8bbc34 100644
--- a/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
+++ b/android/hardware/camera2/impl/CameraCaptureSessionImpl.java
@@ -800,7 +800,8 @@ public class CameraCaptureSessionImpl extends CameraCaptureSession
try {
// begin transition to unconfigured
mDeviceImpl.configureStreamsChecked(/*inputConfig*/null, /*outputs*/null,
- /*operatingMode*/ ICameraDeviceUser.NORMAL_MODE);
+ /*operatingMode*/ ICameraDeviceUser.NORMAL_MODE,
+ /*sessionParams*/ null);
} catch (CameraAccessException e) {
// OK: do not throw checked exceptions.
Log.e(TAG, mIdString + "Exception while unconfiguring outputs: ", e);
diff --git a/android/hardware/camera2/impl/CameraDeviceImpl.java b/android/hardware/camera2/impl/CameraDeviceImpl.java
index 6787d84b..f1ffb890 100644
--- a/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -32,6 +32,7 @@ import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.InputConfiguration;
import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.params.ReprocessFormatsMap;
+import android.hardware.camera2.params.SessionConfiguration;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.hardware.camera2.utils.SubmitInfo;
import android.hardware.camera2.utils.SurfaceUtils;
@@ -362,7 +363,7 @@ public class CameraDeviceImpl extends CameraDevice
outputConfigs.add(new OutputConfiguration(s));
}
configureStreamsChecked(/*inputConfig*/null, outputConfigs,
- /*operatingMode*/ICameraDeviceUser.NORMAL_MODE);
+ /*operatingMode*/ICameraDeviceUser.NORMAL_MODE, /*sessionParams*/ null);
}
@@ -382,12 +383,13 @@ public class CameraDeviceImpl extends CameraDevice
* @param outputs a list of one or more surfaces, or {@code null} to unconfigure
* @param operatingMode If the stream configuration is for a normal session,
* a constrained high speed session, or something else.
+ * @param sessionParams Session parameters.
* @return whether or not the configuration was successful
*
* @throws CameraAccessException if there were any unexpected problems during configuration
*/
public boolean configureStreamsChecked(InputConfiguration inputConfig,
- List<OutputConfiguration> outputs, int operatingMode)
+ List<OutputConfiguration> outputs, int operatingMode, CaptureRequest sessionParams)
throws CameraAccessException {
// Treat a null input the same an empty list
if (outputs == null) {
@@ -463,7 +465,11 @@ public class CameraDeviceImpl extends CameraDevice
}
}
- mRemoteDevice.endConfigure(operatingMode);
+ if (sessionParams != null) {
+ mRemoteDevice.endConfigure(operatingMode, sessionParams.getNativeCopy());
+ } else {
+ mRemoteDevice.endConfigure(operatingMode, null);
+ }
success = true;
} catch (IllegalArgumentException e) {
@@ -499,7 +505,7 @@ public class CameraDeviceImpl extends CameraDevice
outConfigurations.add(new OutputConfiguration(surface));
}
createCaptureSessionInternal(null, outConfigurations, callback, handler,
- /*operatingMode*/ICameraDeviceUser.NORMAL_MODE);
+ /*operatingMode*/ICameraDeviceUser.NORMAL_MODE, /*sessionParams*/ null);
}
@Override
@@ -515,7 +521,7 @@ public class CameraDeviceImpl extends CameraDevice
List<OutputConfiguration> currentOutputs = new ArrayList<>(outputConfigurations);
createCaptureSessionInternal(null, currentOutputs, callback, handler,
- /*operatingMode*/ICameraDeviceUser.NORMAL_MODE);
+ /*operatingMode*/ICameraDeviceUser.NORMAL_MODE, /*sessionParams*/null);
}
@Override
@@ -535,7 +541,7 @@ public class CameraDeviceImpl extends CameraDevice
outConfigurations.add(new OutputConfiguration(surface));
}
createCaptureSessionInternal(inputConfig, outConfigurations, callback, handler,
- /*operatingMode*/ICameraDeviceUser.NORMAL_MODE);
+ /*operatingMode*/ICameraDeviceUser.NORMAL_MODE, /*sessionParams*/ null);
}
@Override
@@ -563,7 +569,8 @@ public class CameraDeviceImpl extends CameraDevice
currentOutputs.add(new OutputConfiguration(output));
}
createCaptureSessionInternal(inputConfig, currentOutputs,
- callback, handler, /*operatingMode*/ICameraDeviceUser.NORMAL_MODE);
+ callback, handler, /*operatingMode*/ICameraDeviceUser.NORMAL_MODE,
+ /*sessionParams*/ null);
}
@Override
@@ -574,16 +581,13 @@ public class CameraDeviceImpl extends CameraDevice
throw new IllegalArgumentException(
"Output surface list must not be null and the size must be no more than 2");
}
- StreamConfigurationMap config =
- getCharacteristics().get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
- SurfaceUtils.checkConstrainedHighSpeedSurfaces(outputs, /*fpsRange*/null, config);
-
List<OutputConfiguration> outConfigurations = new ArrayList<>(outputs.size());
for (Surface surface : outputs) {
outConfigurations.add(new OutputConfiguration(surface));
}
createCaptureSessionInternal(null, outConfigurations, callback, handler,
- /*operatingMode*/ICameraDeviceUser.CONSTRAINED_HIGH_SPEED_MODE);
+ /*operatingMode*/ICameraDeviceUser.CONSTRAINED_HIGH_SPEED_MODE,
+ /*sessionParams*/ null);
}
@Override
@@ -596,13 +600,30 @@ public class CameraDeviceImpl extends CameraDevice
for (OutputConfiguration output : outputs) {
currentOutputs.add(new OutputConfiguration(output));
}
- createCaptureSessionInternal(inputConfig, currentOutputs, callback, handler, operatingMode);
+ createCaptureSessionInternal(inputConfig, currentOutputs, callback, handler, operatingMode,
+ /*sessionParams*/ null);
+ }
+
+ @Override
+ public void createCaptureSession(SessionConfiguration config)
+ throws CameraAccessException {
+ if (config == null) {
+ throw new IllegalArgumentException("Invalid session configuration");
+ }
+
+ List<OutputConfiguration> outputConfigs = config.getOutputConfigurations();
+ if (outputConfigs == null) {
+ throw new IllegalArgumentException("Invalid output configurations");
+ }
+ createCaptureSessionInternal(config.getInputConfiguration(), outputConfigs,
+ config.getStateCallback(), config.getHandler(), config.getSessionType(),
+ config.getSessionParameters());
}
private void createCaptureSessionInternal(InputConfiguration inputConfig,
List<OutputConfiguration> outputConfigurations,
CameraCaptureSession.StateCallback callback, Handler handler,
- int operatingMode) throws CameraAccessException {
+ int operatingMode, CaptureRequest sessionParams) throws CameraAccessException {
synchronized(mInterfaceLock) {
if (DEBUG) {
Log.d(TAG, "createCaptureSessionInternal");
@@ -630,7 +651,7 @@ public class CameraDeviceImpl extends CameraDevice
try {
// configure streams and then block until IDLE
configureSuccess = configureStreamsChecked(inputConfig, outputConfigurations,
- operatingMode);
+ operatingMode, sessionParams);
if (configureSuccess == true && inputConfig != null) {
input = mRemoteDevice.getInputSurface();
}
@@ -646,6 +667,14 @@ public class CameraDeviceImpl extends CameraDevice
// Fire onConfigured if configureOutputs succeeded, fire onConfigureFailed otherwise.
CameraCaptureSessionCore newSession = null;
if (isConstrainedHighSpeed) {
+ ArrayList<Surface> surfaces = new ArrayList<>(outputConfigurations.size());
+ for (OutputConfiguration outConfig : outputConfigurations) {
+ surfaces.add(outConfig.getSurface());
+ }
+ StreamConfigurationMap config =
+ getCharacteristics().get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+ SurfaceUtils.checkConstrainedHighSpeedSurfaces(surfaces, /*fpsRange*/null, config);
+
newSession = new CameraConstrainedHighSpeedCaptureSessionImpl(mNextSessionId++,
callback, handler, this, mDeviceHandler, configureSuccess,
mCharacteristics);
@@ -779,6 +808,7 @@ public class CameraDeviceImpl extends CameraDevice
}
mRemoteDevice.updateOutputConfiguration(streamId, config);
+ mConfiguredOutputs.put(streamId, config);
}
}
@@ -828,6 +858,7 @@ public class CameraDeviceImpl extends CameraDevice
+ " must have at least 1 surface");
}
mRemoteDevice.finalizeOutputConfigurations(streamId, config);
+ mConfiguredOutputs.put(streamId, config);
}
}
}
@@ -950,11 +981,20 @@ public class CameraDeviceImpl extends CameraDevice
SubmitInfo requestInfo;
CaptureRequest[] requestArray = requestList.toArray(new CaptureRequest[requestList.size()]);
+ // Convert Surface to streamIdx and surfaceIdx
+ for (CaptureRequest request : requestArray) {
+ request.convertSurfaceToStreamId(mConfiguredOutputs);
+ }
+
requestInfo = mRemoteDevice.submitRequestList(requestArray, repeating);
if (DEBUG) {
Log.v(TAG, "last frame number " + requestInfo.getLastFrameNumber());
}
+ for (CaptureRequest request : requestArray) {
+ request.recoverStreamIdToSurface();
+ }
+
if (callback != null) {
mCaptureCallbackMap.put(requestInfo.getRequestId(),
new CaptureCallbackHolder(
diff --git a/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java b/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java
index 0978ff87..1f4ed13e 100644
--- a/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java
+++ b/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java
@@ -106,9 +106,11 @@ public class ICameraDeviceUserWrapper {
}
}
- public void endConfigure(int operatingMode) throws CameraAccessException {
+ public void endConfigure(int operatingMode, CameraMetadataNative sessionParams)
+ throws CameraAccessException {
try {
- mRemoteDevice.endConfigure(operatingMode);
+ mRemoteDevice.endConfigure(operatingMode, (sessionParams == null) ?
+ new CameraMetadataNative() : sessionParams);
} catch (Throwable t) {
CameraManager.throwAsPublicException(t);
throw new UnsupportedOperationException("Unexpected exception", t);
diff --git a/android/hardware/camera2/legacy/CameraDeviceUserShim.java b/android/hardware/camera2/legacy/CameraDeviceUserShim.java
index 119cca8d..eccab750 100644
--- a/android/hardware/camera2/legacy/CameraDeviceUserShim.java
+++ b/android/hardware/camera2/legacy/CameraDeviceUserShim.java
@@ -498,7 +498,7 @@ public class CameraDeviceUserShim implements ICameraDeviceUser {
}
@Override
- public void endConfigure(int operatingMode) {
+ public void endConfigure(int operatingMode, CameraMetadataNative sessionParams) {
if (DEBUG) {
Log.d(TAG, "endConfigure called.");
}
diff --git a/android/hardware/camera2/params/OutputConfiguration.java b/android/hardware/camera2/params/OutputConfiguration.java
index 7409671f..a85b5f71 100644
--- a/android/hardware/camera2/params/OutputConfiguration.java
+++ b/android/hardware/camera2/params/OutputConfiguration.java
@@ -486,6 +486,7 @@ public final class OutputConfiguration implements Parcelable {
this.mConfiguredSize = other.mConfiguredSize;
this.mConfiguredGenerationId = other.mConfiguredGenerationId;
this.mIsDeferredConfig = other.mIsDeferredConfig;
+ this.mIsShared = other.mIsShared;
}
/**
@@ -498,6 +499,7 @@ public final class OutputConfiguration implements Parcelable {
int width = source.readInt();
int height = source.readInt();
boolean isDeferred = source.readInt() == 1;
+ boolean isShared = source.readInt() == 1;
ArrayList<Surface> surfaces = new ArrayList<Surface>();
source.readTypedList(surfaces, Surface.CREATOR);
@@ -508,6 +510,7 @@ public final class OutputConfiguration implements Parcelable {
mSurfaces = surfaces;
mConfiguredSize = new Size(width, height);
mIsDeferredConfig = isDeferred;
+ mIsShared = isShared;
mSurfaces = surfaces;
if (mSurfaces.size() > 0) {
mSurfaceType = SURFACE_TYPE_UNKNOWN;
diff --git a/android/hardware/camera2/params/SessionConfiguration.java b/android/hardware/camera2/params/SessionConfiguration.java
new file mode 100644
index 00000000..a79a6c17
--- /dev/null
+++ b/android/hardware/camera2/params/SessionConfiguration.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.hardware.camera2.params;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.IntDef;
+import android.os.Handler;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.InputConfiguration;
+import android.hardware.camera2.params.OutputConfiguration;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.ArrayList;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import static com.android.internal.util.Preconditions.*;
+
+/**
+ * A helper class that aggregates all supported arguments for capture session initialization.
+ */
+public final class SessionConfiguration {
+ /**
+ * A regular session type containing instances of {@link OutputConfiguration} running
+ * at regular non high speed FPS ranges and optionally {@link InputConfiguration} for
+ * reprocessable sessions.
+ *
+ * @see CameraDevice#createCaptureSession
+ * @see CameraDevice#createReprocessableCaptureSession
+ */
+ public static final int SESSION_REGULAR = CameraDevice.SESSION_OPERATION_MODE_NORMAL;
+
+ /**
+ * A high speed session type that can only contain instances of {@link OutputConfiguration}.
+ * The outputs can run using high speed FPS ranges. Calls to {@link #setInputConfiguration}
+ * are not supported.
+ *
+ * @see CameraDevice#createConstrainedHighSpeedCaptureSession
+ */
+ public static final int SESSION_HIGH_SPEED =
+ CameraDevice.SESSION_OPERATION_MODE_CONSTRAINED_HIGH_SPEED;
+
+ /**
+ * First vendor-specific session mode
+ * @hide
+ */
+ public static final int SESSION_VENDOR_START =
+ CameraDevice.SESSION_OPERATION_MODE_VENDOR_START;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"SESSION_"}, value =
+ {SESSION_REGULAR,
+ SESSION_HIGH_SPEED })
+ public @interface SessionMode {};
+
+ // Camera capture session related parameters.
+ private List<OutputConfiguration> mOutputConfigurations;
+ private CameraCaptureSession.StateCallback mStateCallback;
+ private int mSessionType;
+ private Handler mHandler = null;
+ private InputConfiguration mInputConfig = null;
+ private CaptureRequest mSessionParameters = null;
+
+ /**
+ * Create a new {@link SessionConfiguration}.
+ *
+ * @param sessionType The session type.
+ * @param outputs A list of output configurations for the capture session.
+ * @param cb A state callback interface implementation.
+ * @param handler The handler on which the callback will be invoked. If it is
+ * set to null, the callback will be invoked on the current thread's
+ * {@link android.os.Looper looper}.
+ *
+ * @see #SESSION_REGULAR
+ * @see #SESSION_HIGH_SPEED
+ * @see CameraDevice#createCaptureSession(List, CameraCaptureSession.StateCallback, Handler)
+ * @see CameraDevice#createCaptureSessionByOutputConfigurations
+ * @see CameraDevice#createReprocessableCaptureSession
+ * @see CameraDevice#createConstrainedHighSpeedCaptureSession
+ */
+ public SessionConfiguration(@SessionMode int sessionType,
+ @NonNull List<OutputConfiguration> outputs,
+ @NonNull CameraCaptureSession.StateCallback cb, @Nullable Handler handler) {
+ mSessionType = sessionType;
+ mOutputConfigurations = Collections.unmodifiableList(new ArrayList<>(outputs));
+ mStateCallback = cb;
+ mHandler = handler;
+ }
+
+ /**
+ * Retrieve the type of the capture session.
+ *
+ * @return The capture session type.
+ */
+ public @SessionMode int getSessionType() {
+ return mSessionType;
+ }
+
+ /**
+ * Retrieve the {@link OutputConfiguration} list for the capture session.
+ *
+ * @return A list of output configurations for the capture session.
+ */
+ public List<OutputConfiguration> getOutputConfigurations() {
+ return mOutputConfigurations;
+ }
+
+ /**
+ * Retrieve the {@link CameraCaptureSession.StateCallback} for the capture session.
+ *
+ * @return A state callback interface implementation.
+ */
+ public CameraCaptureSession.StateCallback getStateCallback() {
+ return mStateCallback;
+ }
+
+ /**
+ * Retrieve the {@link CameraCaptureSession.StateCallback} for the capture session.
+ *
+ * @return The handler on which the callback will be invoked. If it is
+ * set to null, the callback will be invoked on the current thread's
+ * {@link android.os.Looper looper}.
+ */
+ public Handler getHandler() {
+ return mHandler;
+ }
+
+ /**
+ * Sets the {@link InputConfiguration} for a reprocessable session. Input configuration are not
+ * supported for {@link #SESSION_HIGH_SPEED}.
+ *
+ * @param input Input configuration.
+ * @throws UnsupportedOperationException In case it is called for {@link #SESSION_HIGH_SPEED}
+ * type session configuration.
+ */
+ public void setInputConfiguration(@NonNull InputConfiguration input) {
+ if (mSessionType != SESSION_HIGH_SPEED) {
+ mInputConfig = input;
+ } else {
+ throw new UnsupportedOperationException("Method not supported for high speed session" +
+ " types");
+ }
+ }
+
+ /**
+ * Retrieve the {@link InputConfiguration}.
+ *
+ * @return The capture session input configuration.
+ */
+ public InputConfiguration getInputConfiguration() {
+ return mInputConfig;
+ }
+
+ /**
+ * Sets the session wide camera parameters (see {@link CaptureRequest}). This argument can
+ * be set for every supported session type and will be passed to the camera device as part
+ * of the capture session initialization. Session parameters are a subset of the available
+ * capture request parameters (see {@link CameraCharacteristics#getAvailableSessionKeys})
+ * and their application can introduce internal camera delays. To improve camera performance
+ * it is suggested to change them sparingly within the lifetime of the capture session and
+ * to pass their initial values as part of this method.
+ *
+ * @param params A capture request that includes the initial values for any available
+ * session wide capture keys.
+ */
+ public void setSessionParameters(CaptureRequest params) {
+ mSessionParameters = params;
+ }
+
+ /**
+ * Retrieve the session wide camera parameters (see {@link CaptureRequest}).
+ *
+ * @return A capture request that includes the initial values for any available
+ * session wide capture keys.
+ */
+ public CaptureRequest getSessionParameters() {
+ return mSessionParameters;
+ }
+}
diff --git a/android/hardware/display/BrightnessChangeEvent.java b/android/hardware/display/BrightnessChangeEvent.java
index fe24e32e..3003607e 100644
--- a/android/hardware/display/BrightnessChangeEvent.java
+++ b/android/hardware/display/BrightnessChangeEvent.java
@@ -21,6 +21,8 @@ import android.os.Parcelable;
/**
* Data about a brightness settings change.
+ *
+ * {@see DisplayManager.getBrightnessEvents()}
* TODO make this SystemAPI
* @hide
*/
@@ -31,7 +33,9 @@ public final class BrightnessChangeEvent implements Parcelable {
/** Timestamp of the change {@see System.currentTimeMillis()} */
public long timeStamp;
- /** Package name of focused activity when brightness was changed. */
+ /** Package name of focused activity when brightness was changed.
+ * This will be null if the caller of {@see DisplayManager.getBrightnessEvents()}
+ * does not have access to usage stats {@see UsageStatsManager} */
public String packageName;
/** User id of of the user running when brightness was changed.
@@ -59,6 +63,20 @@ public final class BrightnessChangeEvent implements Parcelable {
public BrightnessChangeEvent() {
}
+ /** @hide */
+ public BrightnessChangeEvent(BrightnessChangeEvent other) {
+ this.brightness = other.brightness;
+ this.timeStamp = other.timeStamp;
+ this.packageName = other.packageName;
+ this.userId = other.userId;
+ this.luxValues = other.luxValues;
+ this.luxTimestamps = other.luxTimestamps;
+ this.batteryLevel = other.batteryLevel;
+ this.nightMode = other.nightMode;
+ this.colorTemperature = other.colorTemperature;
+ this.lastBrightness = other.lastBrightness;
+ }
+
private BrightnessChangeEvent(Parcel source) {
brightness = source.readInt();
timeStamp = source.readLong();
diff --git a/android/hardware/display/BrightnessConfiguration.java b/android/hardware/display/BrightnessConfiguration.java
new file mode 100644
index 00000000..6c3be816
--- /dev/null
+++ b/android/hardware/display/BrightnessConfiguration.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.display;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Arrays;
+
+/** @hide */
+public final class BrightnessConfiguration implements Parcelable {
+ private final float[] mLux;
+ private final float[] mNits;
+
+ private BrightnessConfiguration(float[] lux, float[] nits) {
+ mLux = lux;
+ mNits = nits;
+ }
+
+ /**
+ * Gets the base brightness as curve.
+ *
+ * The curve is returned as a pair of float arrays, the first representing all of the lux
+ * points of the brightness curve and the second representing all of the nits values of the
+ * brightness curve.
+ *
+ * @return the control points for the brightness curve.
+ */
+ public Pair<float[], float[]> getCurve() {
+ return Pair.create(Arrays.copyOf(mLux, mLux.length), Arrays.copyOf(mNits, mNits.length));
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeFloatArray(mLux);
+ dest.writeFloatArray(mNits);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("BrightnessConfiguration{[");
+ final int size = mLux.length;
+ for (int i = 0; i < size; i++) {
+ if (i != 0) {
+ sb.append(", ");
+ }
+ sb.append("(").append(mLux[i]).append(", ").append(mNits[i]).append(")");
+ }
+ sb.append("]}");
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = result * 31 + Arrays.hashCode(mLux);
+ result = result * 31 + Arrays.hashCode(mNits);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof BrightnessConfiguration)) {
+ return false;
+ }
+ final BrightnessConfiguration other = (BrightnessConfiguration) o;
+ return Arrays.equals(mLux, other.mLux) && Arrays.equals(mNits, other.mNits);
+ }
+
+ public static final Creator<BrightnessConfiguration> CREATOR =
+ new Creator<BrightnessConfiguration>() {
+ public BrightnessConfiguration createFromParcel(Parcel in) {
+ Builder builder = new Builder();
+ float[] lux = in.createFloatArray();
+ float[] nits = in.createFloatArray();
+ builder.setCurve(lux, nits);
+ return builder.build();
+ }
+
+ public BrightnessConfiguration[] newArray(int size) {
+ return new BrightnessConfiguration[size];
+ }
+ };
+
+ /**
+ * A builder class for {@link BrightnessConfiguration}s.
+ */
+ public static class Builder {
+ private float[] mCurveLux;
+ private float[] mCurveNits;
+
+ /**
+ * Sets the control points for the brightness curve.
+ *
+ * Brightness curves must have strictly increasing ambient brightness values in lux and
+ * monotonically increasing display brightness values in nits. In addition, the initial
+ * control point must be 0 lux.
+ *
+ * @throws IllegalArgumentException if the initial control point is not at 0 lux.
+ * @throws IllegalArgumentException if the lux levels are not strictly increasing.
+ * @throws IllegalArgumentException if the nit levels are not monotonically increasing.
+ */
+ public Builder setCurve(float[] lux, float[] nits) {
+ Preconditions.checkNotNull(lux);
+ Preconditions.checkNotNull(nits);
+ if (lux.length == 0 || nits.length == 0) {
+ throw new IllegalArgumentException("Lux and nits arrays must not be empty");
+ }
+ if (lux.length != nits.length) {
+ throw new IllegalArgumentException("Lux and nits arrays must be the same length");
+ }
+ if (lux[0] != 0) {
+ throw new IllegalArgumentException("Initial control point must be for 0 lux");
+ }
+ Preconditions.checkArrayElementsInRange(lux, 0, Float.MAX_VALUE, "lux");
+ Preconditions.checkArrayElementsInRange(nits, 0, Float.MAX_VALUE, "nits");
+ checkMonotonic(lux, true/*strictly increasing*/, "lux");
+ checkMonotonic(nits, false /*strictly increasing*/, "nits");
+ mCurveLux = lux;
+ mCurveNits = nits;
+ return this;
+ }
+
+ /**
+ * Builds the {@link BrightnessConfiguration}.
+ *
+ * A brightness curve <b>must</b> be set before calling this.
+ */
+ public BrightnessConfiguration build() {
+ if (mCurveLux == null || mCurveNits == null) {
+ throw new IllegalStateException("A curve must be set!");
+ }
+ return new BrightnessConfiguration(mCurveLux, mCurveNits);
+ }
+
+ private static void checkMonotonic(float[] vals, boolean strictlyIncreasing, String name) {
+ if (vals.length <= 1) {
+ return;
+ }
+ float prev = vals[0];
+ for (int i = 1; i < vals.length; i++) {
+ if (prev > vals[i] || prev == vals[i] && strictlyIncreasing) {
+ String condition = strictlyIncreasing ? "strictly increasing" : "monotonic";
+ throw new IllegalArgumentException(name + " values must be " + condition);
+ }
+ prev = vals[i];
+ }
+ }
+ }
+}
diff --git a/android/hardware/display/DisplayManager.java b/android/hardware/display/DisplayManager.java
index 89357456..7de667dc 100644
--- a/android/hardware/display/DisplayManager.java
+++ b/android/hardware/display/DisplayManager.java
@@ -16,8 +16,10 @@
package android.hardware.display;
+import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.app.KeyguardManager;
@@ -25,6 +27,7 @@ import android.content.Context;
import android.graphics.Point;
import android.media.projection.MediaProjection;
import android.os.Handler;
+import android.os.UserHandle;
import android.util.SparseArray;
import android.view.Display;
import android.view.Surface;
@@ -619,8 +622,9 @@ public final class DisplayManager {
* Fetch {@link BrightnessChangeEvent}s.
* @hide until we make it a system api.
*/
+ @RequiresPermission(Manifest.permission.BRIGHTNESS_SLIDER_USAGE)
public List<BrightnessChangeEvent> getBrightnessEvents() {
- return mGlobal.getBrightnessEvents();
+ return mGlobal.getBrightnessEvents(mContext.getOpPackageName());
}
/**
@@ -631,6 +635,27 @@ public final class DisplayManager {
}
/**
+ * Sets the global display brightness configuration.
+ *
+ * @hide
+ */
+ public void setBrightnessConfiguration(BrightnessConfiguration c) {
+ setBrightnessConfigurationForUser(c, UserHandle.myUserId());
+ }
+
+ /**
+ * Sets the global display brightness configuration for a specific user.
+ *
+ * Note this requires the INTERACT_ACROSS_USERS permission if setting the configuration for a
+ * user other than the one you're currently running as.
+ *
+ * @hide
+ */
+ public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId) {
+ mGlobal.setBrightnessConfigurationForUser(c, userId);
+ }
+
+ /**
* Listens for changes in available display devices.
*/
public interface DisplayListener {
diff --git a/android/hardware/display/DisplayManagerGlobal.java b/android/hardware/display/DisplayManagerGlobal.java
index d93d0e4e..bf4cc1d8 100644
--- a/android/hardware/display/DisplayManagerGlobal.java
+++ b/android/hardware/display/DisplayManagerGlobal.java
@@ -462,9 +462,10 @@ public final class DisplayManagerGlobal {
/**
* Retrieves brightness change events.
*/
- public List<BrightnessChangeEvent> getBrightnessEvents() {
+ public List<BrightnessChangeEvent> getBrightnessEvents(String callingPackage) {
try {
- ParceledListSlice<BrightnessChangeEvent> events = mDm.getBrightnessEvents();
+ ParceledListSlice<BrightnessChangeEvent> events =
+ mDm.getBrightnessEvents(callingPackage);
if (events == null) {
return Collections.emptyList();
}
@@ -486,6 +487,19 @@ public final class DisplayManagerGlobal {
}
}
+ /**
+ * Sets the global brightness configuration for a given user.
+ *
+ * @hide
+ */
+ public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId) {
+ try {
+ mDm.setBrightnessConfigurationForUser(c, userId);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
private final class DisplayManagerCallback extends IDisplayManagerCallback.Stub {
@Override
public void onDisplayEvent(int displayId, int event) {
diff --git a/android/hardware/input/InputManager.java b/android/hardware/input/InputManager.java
index c531a899..1de8882e 100644
--- a/android/hardware/input/InputManager.java
+++ b/android/hardware/input/InputManager.java
@@ -188,7 +188,11 @@ public final class InputManager {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({SWITCH_STATE_UNKNOWN, SWITCH_STATE_OFF, SWITCH_STATE_ON})
+ @IntDef(prefix = { "SWITCH_STATE_" }, value = {
+ SWITCH_STATE_UNKNOWN,
+ SWITCH_STATE_OFF,
+ SWITCH_STATE_ON
+ })
public @interface SwitchState {}
/**
diff --git a/android/hardware/location/ContextHubInfo.java b/android/hardware/location/ContextHubInfo.java
index e1137aa5..c2b28001 100644
--- a/android/hardware/location/ContextHubInfo.java
+++ b/android/hardware/location/ContextHubInfo.java
@@ -26,7 +26,7 @@ import java.util.Arrays;
* @hide
*/
@SystemApi
-public class ContextHubInfo {
+public class ContextHubInfo implements Parcelable {
private int mId;
private String mName;
private String mVendor;
@@ -262,7 +262,7 @@ public class ContextHubInfo {
@Override
public String toString() {
String retVal = "";
- retVal += "Id : " + mId;
+ retVal += "ID/handle : " + mId;
retVal += ", Name : " + mName;
retVal += "\n\tVendor : " + mVendor;
retVal += ", Toolchain : " + mToolchain;
@@ -275,8 +275,6 @@ public class ContextHubInfo {
retVal += ", StoppedPowerDraw : " + mStoppedPowerDrawMw + " mW";
retVal += ", PeakPowerDraw : " + mPeakPowerDrawMw + " mW";
retVal += ", MaxPacketLength : " + mMaxPacketLengthBytes + " Bytes";
- retVal += "\n\tSupported sensors : " + Arrays.toString(mSupportedSensors);
- retVal += "\n\tMemory Regions : " + Arrays.toString(mMemoryRegions);
return retVal;
}
diff --git a/android/hardware/location/ContextHubManager.java b/android/hardware/location/ContextHubManager.java
index b31c7bcd..4cea0acd 100644
--- a/android/hardware/location/ContextHubManager.java
+++ b/android/hardware/location/ContextHubManager.java
@@ -15,13 +15,15 @@
*/
package android.hardware.location;
-import android.annotation.Nullable;
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.content.Context;
import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -29,6 +31,7 @@ import android.os.ServiceManager.ServiceNotFoundException;
import android.util.Log;
import java.util.List;
+import java.util.concurrent.Executor;
/**
* A class that exposes the Context hubs on a device to applications.
@@ -258,9 +261,9 @@ public final class ContextHubManager {
}
/**
- * Returns the list of context hubs in the system.
+ * Returns the list of ContextHubInfo objects describing the available Context Hubs.
*
- * @return the list of context hub informations
+ * @return the list of ContextHubInfo objects
*
* @see ContextHubInfo
*
@@ -268,10 +271,14 @@ public final class ContextHubManager {
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
public List<ContextHubInfo> getContextHubs() {
- throw new UnsupportedOperationException("TODO: Implement this");
+ try {
+ return mService.getContextHubs();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
- /*
+ /**
* Helper function to generate a stub for a non-query transaction callback.
*
* @param transaction the transaction to unblock when complete
@@ -287,7 +294,7 @@ public final class ContextHubManager {
public void onQueryResponse(int result, List<NanoAppState> nanoappList) {
Log.e(TAG, "Received a query callback on a non-query request");
transaction.setResponse(new ContextHubTransaction.Response<Void>(
- ContextHubTransaction.TRANSACTION_FAILED_SERVICE_INTERNAL_FAILURE, null));
+ ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE, null));
}
@Override
@@ -297,7 +304,7 @@ public final class ContextHubManager {
};
}
- /*
+ /**
* Helper function to generate a stub for a query transaction callback.
*
* @param transaction the transaction to unblock when complete
@@ -319,7 +326,7 @@ public final class ContextHubManager {
public void onTransactionComplete(int result) {
Log.e(TAG, "Received a non-query callback on a query request");
transaction.setResponse(new ContextHubTransaction.Response<List<NanoAppState>>(
- ContextHubTransaction.TRANSACTION_FAILED_SERVICE_INTERNAL_FAILURE, null));
+ ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE, null));
}
};
}
@@ -342,7 +349,17 @@ public final class ContextHubManager {
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
public ContextHubTransaction<Void> loadNanoApp(
ContextHubInfo hubInfo, NanoAppBinary appBinary) {
- throw new UnsupportedOperationException("TODO: Implement this");
+ ContextHubTransaction<Void> transaction =
+ new ContextHubTransaction<>(ContextHubTransaction.TYPE_LOAD_NANOAPP);
+ IContextHubTransactionCallback callback = createTransactionCallback(transaction);
+
+ try {
+ mService.loadNanoAppOnHub(hubInfo.getId(), callback, appBinary);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ return transaction;
}
/**
@@ -357,7 +374,17 @@ public final class ContextHubManager {
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
public ContextHubTransaction<Void> unloadNanoApp(ContextHubInfo hubInfo, long nanoAppId) {
- throw new UnsupportedOperationException("TODO: Implement this");
+ ContextHubTransaction<Void> transaction =
+ new ContextHubTransaction<>(ContextHubTransaction.TYPE_UNLOAD_NANOAPP);
+ IContextHubTransactionCallback callback = createTransactionCallback(transaction);
+
+ try {
+ mService.unloadNanoAppFromHub(hubInfo.getId(), callback, nanoAppId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ return transaction;
}
/**
@@ -372,7 +399,17 @@ public final class ContextHubManager {
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
public ContextHubTransaction<Void> enableNanoApp(ContextHubInfo hubInfo, long nanoAppId) {
- throw new UnsupportedOperationException("TODO: Implement this");
+ ContextHubTransaction<Void> transaction =
+ new ContextHubTransaction<>(ContextHubTransaction.TYPE_ENABLE_NANOAPP);
+ IContextHubTransactionCallback callback = createTransactionCallback(transaction);
+
+ try {
+ mService.enableNanoApp(hubInfo.getId(), callback, nanoAppId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ return transaction;
}
/**
@@ -387,7 +424,17 @@ public final class ContextHubManager {
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
public ContextHubTransaction<Void> disableNanoApp(ContextHubInfo hubInfo, long nanoAppId) {
- throw new UnsupportedOperationException("TODO: Implement this");
+ ContextHubTransaction<Void> transaction =
+ new ContextHubTransaction<>(ContextHubTransaction.TYPE_DISABLE_NANOAPP);
+ IContextHubTransactionCallback callback = createTransactionCallback(transaction);
+
+ try {
+ mService.disableNanoApp(hubInfo.getId(), callback, nanoAppId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ return transaction;
}
/**
@@ -401,7 +448,17 @@ public final class ContextHubManager {
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
public ContextHubTransaction<List<NanoAppState>> queryNanoApps(ContextHubInfo hubInfo) {
- throw new UnsupportedOperationException("TODO: Implement this");
+ ContextHubTransaction<List<NanoAppState>> transaction =
+ new ContextHubTransaction<>(ContextHubTransaction.TYPE_QUERY_NANOAPPS);
+ IContextHubTransactionCallback callback = createQueryCallback(transaction);
+
+ try {
+ mService.queryNanoApps(hubInfo.getId(), callback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ return transaction;
}
/**
@@ -459,46 +516,46 @@ public final class ContextHubManager {
* Creates an interface to the ContextHubClient to send down to the service.
*
* @param callback the callback to invoke at the client process
- * @param handler the handler to post callbacks for this client
+ * @param executor the executor to invoke callbacks for this client
*
* @return the callback interface
*/
private IContextHubClientCallback createClientCallback(
- ContextHubClientCallback callback, Handler handler) {
+ ContextHubClientCallback callback, Executor executor) {
return new IContextHubClientCallback.Stub() {
@Override
public void onMessageFromNanoApp(NanoAppMessage message) {
- handler.post(() -> callback.onMessageFromNanoApp(message));
+ executor.execute(() -> callback.onMessageFromNanoApp(message));
}
@Override
public void onHubReset() {
- handler.post(() -> callback.onHubReset());
+ executor.execute(() -> callback.onHubReset());
}
@Override
public void onNanoAppAborted(long nanoAppId, int abortCode) {
- handler.post(() -> callback.onNanoAppAborted(nanoAppId, abortCode));
+ executor.execute(() -> callback.onNanoAppAborted(nanoAppId, abortCode));
}
@Override
public void onNanoAppLoaded(long nanoAppId) {
- handler.post(() -> callback.onNanoAppLoaded(nanoAppId));
+ executor.execute(() -> callback.onNanoAppLoaded(nanoAppId));
}
@Override
public void onNanoAppUnloaded(long nanoAppId) {
- handler.post(() -> callback.onNanoAppUnloaded(nanoAppId));
+ executor.execute(() -> callback.onNanoAppUnloaded(nanoAppId));
}
@Override
public void onNanoAppEnabled(long nanoAppId) {
- handler.post(() -> callback.onNanoAppEnabled(nanoAppId));
+ executor.execute(() -> callback.onNanoAppEnabled(nanoAppId));
}
@Override
public void onNanoAppDisabled(long nanoAppId) {
- handler.post(() -> callback.onNanoAppDisabled(nanoAppId));
+ executor.execute(() -> callback.onNanoAppDisabled(nanoAppId));
}
};
}
@@ -510,9 +567,9 @@ public final class ContextHubManager {
* registration succeeds, the client can send messages to nanoapps through the returned
* {@link ContextHubClient} object, and receive notifications through the provided callback.
*
- * @param callback the notification callback to register
* @param hubInfo the hub to attach this client to
- * @param handler the handler to invoke the callback, if null uses the main thread's Looper
+ * @param callback the notification callback to register
+ * @param executor the executor to invoke the callback
* @return the registered client object
*
* @throws IllegalArgumentException if hubInfo does not represent a valid hub
@@ -522,8 +579,9 @@ public final class ContextHubManager {
* @hide
* @see ContextHubClientCallback
*/
- public ContextHubClient createClient(
- ContextHubClientCallback callback, ContextHubInfo hubInfo, @Nullable Handler handler) {
+ @NonNull public ContextHubClient createClient(
+ @NonNull ContextHubInfo hubInfo, @NonNull ContextHubClientCallback callback,
+ @NonNull @CallbackExecutor Executor executor) {
if (callback == null) {
throw new NullPointerException("Callback cannot be null");
}
@@ -531,8 +589,7 @@ public final class ContextHubManager {
throw new NullPointerException("Hub info cannot be null");
}
- Handler realHandler = (handler == null) ? new Handler(mMainLooper) : handler;
- IContextHubClientCallback clientInterface = createClientCallback(callback, realHandler);
+ IContextHubClientCallback clientInterface = createClientCallback(callback, executor);
IContextHubClient client;
try {
@@ -545,6 +602,25 @@ public final class ContextHubManager {
}
/**
+ * Equivalent to {@link #createClient(ContextHubInfo, ContextHubClientCallback, Executor)}
+ * with the executor using the main thread's Looper.
+ *
+ * @param hubInfo the hub to attach this client to
+ * @param callback the notification callback to register
+ * @return the registered client object
+ *
+ * @throws IllegalArgumentException if hubInfo does not represent a valid hub
+ * @throws IllegalStateException if there were too many registered clients at the service
+ * @throws NullPointerException if callback or hubInfo is null
+ * @hide
+ * @see ContextHubClientCallback
+ */
+ @NonNull public ContextHubClient createClient(
+ @NonNull ContextHubInfo hubInfo, @NonNull ContextHubClientCallback callback) {
+ return createClient(hubInfo, callback, new HandlerExecutor(Handler.getMain()));
+ }
+
+ /**
* Unregister a callback for receive messages from the context hub.
*
* @see Callback
diff --git a/android/hardware/location/ContextHubTransaction.java b/android/hardware/location/ContextHubTransaction.java
index a8569ef4..a1b743da 100644
--- a/android/hardware/location/ContextHubTransaction.java
+++ b/android/hardware/location/ContextHubTransaction.java
@@ -15,15 +15,16 @@
*/
package android.hardware.location;
+import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
+import android.os.HandlerExecutor;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -33,8 +34,8 @@ import java.util.concurrent.TimeoutException;
* This object is generated as a result of an asynchronous request sent to the Context Hub
* through the ContextHubManager APIs. The caller can either retrieve the result
* synchronously through a blocking call ({@link #waitForResponse(long, TimeUnit)}) or
- * asynchronously through a user-defined callback
- * ({@link #setCallbackOnComplete(ContextHubTransaction.Callback, Handler)}).
+ * asynchronously through a user-defined listener
+ * ({@link #setOnCompleteListener(Listener, Executor)} )}).
*
* @param <T> the type of the contents in the transaction response
*
@@ -47,13 +48,15 @@ public class ContextHubTransaction<T> {
* Constants describing the type of a transaction through the Context Hub Service.
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({
+ @IntDef(prefix = { "TYPE_" }, value = {
TYPE_LOAD_NANOAPP,
TYPE_UNLOAD_NANOAPP,
TYPE_ENABLE_NANOAPP,
TYPE_DISABLE_NANOAPP,
- TYPE_QUERY_NANOAPPS})
- public @interface Type {}
+ TYPE_QUERY_NANOAPPS
+ })
+ public @interface Type { }
+
public static final int TYPE_LOAD_NANOAPP = 0;
public static final int TYPE_UNLOAD_NANOAPP = 1;
public static final int TYPE_ENABLE_NANOAPP = 2;
@@ -64,45 +67,51 @@ public class ContextHubTransaction<T> {
* Constants describing the result of a transaction or request through the Context Hub Service.
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({
- TRANSACTION_SUCCESS,
- TRANSACTION_FAILED_UNKNOWN,
- TRANSACTION_FAILED_BAD_PARAMS,
- TRANSACTION_FAILED_UNINITIALIZED,
- TRANSACTION_FAILED_PENDING,
- TRANSACTION_FAILED_AT_HUB,
- TRANSACTION_FAILED_TIMEOUT,
- TRANSACTION_FAILED_SERVICE_INTERNAL_FAILURE})
+ @IntDef(prefix = { "RESULT_" }, value = {
+ RESULT_SUCCESS,
+ RESULT_FAILED_UNKNOWN,
+ RESULT_FAILED_BAD_PARAMS,
+ RESULT_FAILED_UNINITIALIZED,
+ RESULT_FAILED_PENDING,
+ RESULT_FAILED_AT_HUB,
+ RESULT_FAILED_TIMEOUT,
+ RESULT_FAILED_SERVICE_INTERNAL_FAILURE,
+ RESULT_FAILED_HAL_UNAVAILABLE
+ })
public @interface Result {}
- public static final int TRANSACTION_SUCCESS = 0;
+ public static final int RESULT_SUCCESS = 0;
/**
* Generic failure mode.
*/
- public static final int TRANSACTION_FAILED_UNKNOWN = 1;
+ public static final int RESULT_FAILED_UNKNOWN = 1;
/**
* Failure mode when the request parameters were not valid.
*/
- public static final int TRANSACTION_FAILED_BAD_PARAMS = 2;
+ public static final int RESULT_FAILED_BAD_PARAMS = 2;
/**
* Failure mode when the Context Hub is not initialized.
*/
- public static final int TRANSACTION_FAILED_UNINITIALIZED = 3;
+ public static final int RESULT_FAILED_UNINITIALIZED = 3;
/**
* Failure mode when there are too many transactions pending.
*/
- public static final int TRANSACTION_FAILED_PENDING = 4;
+ public static final int RESULT_FAILED_PENDING = 4;
/**
* Failure mode when the request went through, but failed asynchronously at the hub.
*/
- public static final int TRANSACTION_FAILED_AT_HUB = 5;
+ public static final int RESULT_FAILED_AT_HUB = 5;
/**
* Failure mode when the transaction has timed out.
*/
- public static final int TRANSACTION_FAILED_TIMEOUT = 6;
+ public static final int RESULT_FAILED_TIMEOUT = 6;
/**
* Failure mode when the transaction has failed internally at the service.
*/
- public static final int TRANSACTION_FAILED_SERVICE_INTERNAL_FAILURE = 7;
+ public static final int RESULT_FAILED_SERVICE_INTERNAL_FAILURE = 7;
+ /**
+ * Failure mode when the Context Hub HAL was not available.
+ */
+ public static final int RESULT_FAILED_HAL_UNAVAILABLE = 8;
/**
* A class describing the response for a ContextHubTransaction.
@@ -137,20 +146,20 @@ public class ContextHubTransaction<T> {
}
/**
- * An interface describing the callback to be invoked when a transaction completes.
+ * An interface describing the listener for a transaction completion.
*
- * @param <C> the type of the contents in the transaction response
+ * @param <L> the type of the contents in the transaction response
*/
@FunctionalInterface
- public interface Callback<C> {
+ public interface Listener<L> {
/**
- * The callback to invoke when the transaction completes.
+ * The listener function to invoke when the transaction completes.
*
* @param transaction the transaction that this callback was attached to.
* @param response the response of the transaction.
*/
void onComplete(
- ContextHubTransaction<C> transaction, ContextHubTransaction.Response<C> response);
+ ContextHubTransaction<L> transaction, ContextHubTransaction.Response<L> response);
}
/*
@@ -165,14 +174,14 @@ public class ContextHubTransaction<T> {
private ContextHubTransaction.Response<T> mResponse;
/*
- * The handler to invoke the aynsc response supplied by onComplete.
+ * The executor to invoke the onComplete async callback.
*/
- private Handler mHandler = null;
+ private Executor mExecutor = null;
/*
- * The callback to invoke when the transaction completes.
+ * The listener to be invoked when the transaction completes.
*/
- private ContextHubTransaction.Callback<T> mCallback = null;
+ private ContextHubTransaction.Listener<T> mListener = null;
/*
* Synchronization latch used to block on response.
@@ -189,6 +198,30 @@ public class ContextHubTransaction<T> {
}
/**
+ * Converts a transaction type to a human-readable string
+ *
+ * @param type the type of a transaction
+ * @param upperCase {@code true} if upper case the first letter, {@code false} otherwise
+ * @return a string describing the transaction
+ */
+ public static String typeToString(@Type int type, boolean upperCase) {
+ switch (type) {
+ case ContextHubTransaction.TYPE_LOAD_NANOAPP:
+ return upperCase ? "Load" : "load";
+ case ContextHubTransaction.TYPE_UNLOAD_NANOAPP:
+ return upperCase ? "Unload" : "unload";
+ case ContextHubTransaction.TYPE_ENABLE_NANOAPP:
+ return upperCase ? "Enable" : "enable";
+ case ContextHubTransaction.TYPE_DISABLE_NANOAPP:
+ return upperCase ? "Disable" : "disable";
+ case ContextHubTransaction.TYPE_QUERY_NANOAPPS:
+ return upperCase ? "Query" : "query";
+ default:
+ return upperCase ? "Unknown" : "unknown";
+ }
+ }
+
+ /**
* @return the type of the transaction
*/
@Type
@@ -226,73 +259,68 @@ public class ContextHubTransaction<T> {
}
/**
- * Sets a callback to be invoked when the transaction completes.
+ * Sets the listener to be invoked invoked when the transaction completes.
*
* This function provides an asynchronous approach to retrieve the result of the
* transaction. When the transaction response has been provided by the Context Hub,
- * the given callback will be posted by the provided handler.
+ * the given listener will be invoked.
*
- * If the transaction has already completed at the time of invocation, the callback
- * will be immediately posted by the handler. If the transaction has been invalidated,
- * the callback will never be invoked.
+ * If the transaction has already completed at the time of invocation, the listener
+ * will be immediately invoked. If the transaction has been invalidated,
+ * the listener will never be invoked.
*
* A transaction can be invalidated if the process owning the transaction is no longer active
* and the reference to this object is lost.
*
- * This method or {@link #setCallbackOnCompletecan(ContextHubTransaction.Callback)} can only be
+ * This method or {@link #setOnCompleteListener(ContextHubTransaction.Listener)} can only be
* invoked once, or an IllegalStateException will be thrown.
*
- * @param callback the callback to be invoked upon completion
- * @param handler the handler to post the callback
+ * @param listener the listener to be invoked upon completion
+ * @param executor the executor to invoke the callback
*
* @throws IllegalStateException if this method is called multiple times
* @throws NullPointerException if the callback or handler is null
*/
- public void setCallbackOnComplete(
- @NonNull ContextHubTransaction.Callback<T> callback, @NonNull Handler handler) {
+ public void setOnCompleteListener(
+ @NonNull ContextHubTransaction.Listener<T> listener,
+ @NonNull @CallbackExecutor Executor executor) {
synchronized (this) {
- if (callback == null) {
- throw new NullPointerException("Callback cannot be null");
+ if (listener == null) {
+ throw new NullPointerException("Listener cannot be null");
}
- if (handler == null) {
- throw new NullPointerException("Handler cannot be null");
+ if (executor == null) {
+ throw new NullPointerException("Executor cannot be null");
}
- if (mCallback != null) {
+ if (mListener != null) {
throw new IllegalStateException(
- "Cannot set ContextHubTransaction callback multiple times");
+ "Cannot set ContextHubTransaction listener multiple times");
}
- mCallback = callback;
- mHandler = handler;
+ mListener = listener;
+ mExecutor = executor;
if (mDoneSignal.getCount() == 0) {
- boolean callbackPosted = mHandler.post(() -> {
- mCallback.onComplete(this, mResponse);
- });
-
- if (!callbackPosted) {
- Log.e(TAG, "Failed to post callback to Handler");
- }
+ mExecutor.execute(() -> mListener.onComplete(this, mResponse));
}
}
}
/**
- * Sets a callback to be invoked when the transaction completes.
+ * Sets the listener to be invoked invoked when the transaction completes.
*
- * Equivalent to {@link #setCallbackOnComplete(ContextHubTransaction.Callback, Handler)}
- * with the handler being that of the main thread's Looper.
+ * Equivalent to {@link #setOnCompleteListener(ContextHubTransaction.Listener, Executor)}
+ * with the executor using the main thread's Looper.
*
- * This method or {@link #setCallbackOnComplete(ContextHubTransaction.Callback, Handler)}
+ * This method or {@link #setOnCompleteListener(ContextHubTransaction.Listener, Executor)}
* can only be invoked once, or an IllegalStateException will be thrown.
*
- * @param callback the callback to be invoked upon completion
+ * @param listener the listener to be invoked upon completion
*
* @throws IllegalStateException if this method is called multiple times
* @throws NullPointerException if the callback is null
*/
- public void setCallbackOnComplete(@NonNull ContextHubTransaction.Callback<T> callback) {
- setCallbackOnComplete(callback, new Handler(Looper.getMainLooper()));
+ public void setOnCompleteListener(@NonNull ContextHubTransaction.Listener<T> listener) {
+ setOnCompleteListener(listener, new HandlerExecutor(Handler.getMain()));
}
/**
@@ -307,7 +335,7 @@ public class ContextHubTransaction<T> {
* @throws IllegalStateException if this method is invoked multiple times
* @throws NullPointerException if the response is null
*/
- void setResponse(ContextHubTransaction.Response<T> response) {
+ /* package */ void setResponse(ContextHubTransaction.Response<T> response) {
synchronized (this) {
if (response == null) {
throw new NullPointerException("Response cannot be null");
@@ -321,14 +349,8 @@ public class ContextHubTransaction<T> {
mIsResponseSet = true;
mDoneSignal.countDown();
- if (mCallback != null) {
- boolean callbackPosted = mHandler.post(() -> {
- mCallback.onComplete(this, mResponse);
- });
-
- if (!callbackPosted) {
- Log.e(TAG, "Failed to post callback to Handler");
- }
+ if (mListener != null) {
+ mExecutor.execute(() -> mListener.onComplete(this, mResponse));
}
}
}
diff --git a/android/hardware/location/NanoAppFilter.java b/android/hardware/location/NanoAppFilter.java
index bf35a3d6..5ccf546a 100644
--- a/android/hardware/location/NanoAppFilter.java
+++ b/android/hardware/location/NanoAppFilter.java
@@ -20,7 +20,6 @@ package android.hardware.location;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
-import android.util.Log;
/**
* @hide
@@ -130,6 +129,14 @@ public class NanoAppFilter {
(versionsMatch(mVersionRestrictionMask, mAppVersion, info.getAppVersion()));
}
+ @Override
+ public String toString() {
+ return "nanoAppId: 0x" + Long.toHexString(mAppId)
+ + ", nanoAppVersion: 0x" + Integer.toHexString(mAppVersion)
+ + ", versionMask: " + mVersionRestrictionMask
+ + ", vendorMask: " + mAppIdVendorMask;
+ }
+
public static final Parcelable.Creator<NanoAppFilter> CREATOR
= new Parcelable.Creator<NanoAppFilter>() {
public NanoAppFilter createFromParcel(Parcel in) {
diff --git a/android/hardware/location/NanoAppInstanceInfo.java b/android/hardware/location/NanoAppInstanceInfo.java
index 26238304..f73fd87b 100644
--- a/android/hardware/location/NanoAppInstanceInfo.java
+++ b/android/hardware/location/NanoAppInstanceInfo.java
@@ -16,9 +16,7 @@
package android.hardware.location;
-
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -26,50 +24,49 @@ import android.os.Parcelable;
import libcore.util.EmptyArray;
/**
+ * Describes an instance of a nanoapp, used by the internal state manged by ContextHubService.
+ *
+ * TODO(b/69270990) Remove this class once the old API is deprecated.
+ *
* @hide
*/
@SystemApi
public class NanoAppInstanceInfo {
- private String mPublisher;
- private String mName;
+ private String mPublisher = "Unknown";
+ private String mName = "Unknown";
+ private int mHandle;
private long mAppId;
private int mAppVersion;
+ private int mContexthubId;
- private int mNeededReadMemBytes;
- private int mNeededWriteMemBytes;
- private int mNeededExecMemBytes;
+ private int mNeededReadMemBytes = 0;
+ private int mNeededWriteMemBytes = 0;
+ private int mNeededExecMemBytes = 0;
- private int[] mNeededSensors;
- private int[] mOutputEvents;
-
- private int mContexthubId;
- private int mHandle;
+ private int[] mNeededSensors = EmptyArray.INT;
+ private int[] mOutputEvents = EmptyArray.INT;
public NanoAppInstanceInfo() {
- mNeededSensors = EmptyArray.INT;
- mOutputEvents = EmptyArray.INT;
}
/**
- * get the publisher of this app
- *
- * @return String - name of the publisher
+ * @hide
*/
- public String getPublisher() {
- return mPublisher;
+ public NanoAppInstanceInfo(int handle, long appId, int appVersion, int contextHubId) {
+ mHandle = handle;
+ mAppId = appId;
+ mAppVersion = appVersion;
+ mContexthubId = contextHubId;
}
-
/**
- * set the publisher name for the app
- *
- * @param publisher - name of the publisher
+ * get the publisher of this app
*
- * @hide
+ * @return String - name of the publisher
*/
- public void setPublisher(String publisher) {
- mPublisher = publisher;
+ public String getPublisher() {
+ return mPublisher;
}
/**
@@ -82,17 +79,6 @@ public class NanoAppInstanceInfo {
}
/**
- * set the name of the app
- *
- * @param name - name of the app
- *
- * @hide
- */
- public void setName(String name) {
- mName = name;
- }
-
- /**
* Get the application identifier
*
* @return int - application identifier
@@ -102,17 +88,6 @@ public class NanoAppInstanceInfo {
}
/**
- * Set the application identifier
- *
- * @param appId - application identifier
- *
- * @hide
- */
- public void setAppId(long appId) {
- mAppId = appId;
- }
-
- /**
* Get the application version
*
* NOTE: There is a race condition where shortly after loading, this
@@ -127,17 +102,6 @@ public class NanoAppInstanceInfo {
}
/**
- * Set the application version
- *
- * @param appVersion - version of the app
- *
- * @hide
- */
- public void setAppVersion(int appVersion) {
- mAppVersion = appVersion;
- }
-
- /**
* Get the read memory needed by the app
*
* @return int - readable memory needed in bytes
@@ -147,17 +111,6 @@ public class NanoAppInstanceInfo {
}
/**
- * Set the read memory needed by the app
- *
- * @param neededReadMemBytes - readable Memory needed in bytes
- *
- * @hide
- */
- public void setNeededReadMemBytes(int neededReadMemBytes) {
- mNeededReadMemBytes = neededReadMemBytes;
- }
-
- /**
* get writable memory needed by the app
*
* @return int - writable memory needed by the app
@@ -167,18 +120,6 @@ public class NanoAppInstanceInfo {
}
/**
- * set writable memory needed by the app
- *
- * @param neededWriteMemBytes - writable memory needed by the
- * app
- *
- * @hide
- */
- public void setNeededWriteMemBytes(int neededWriteMemBytes) {
- mNeededWriteMemBytes = neededWriteMemBytes;
- }
-
- /**
* get executable memory needed by the app
*
* @return int - executable memory needed by the app
@@ -188,18 +129,6 @@ public class NanoAppInstanceInfo {
}
/**
- * set executable memory needed by the app
- *
- * @param neededExecMemBytes - executable memory needed by the
- * app
- *
- * @hide
- */
- public void setNeededExecMemBytes(int neededExecMemBytes) {
- mNeededExecMemBytes = neededExecMemBytes;
- }
-
- /**
* Get the sensors needed by this app
*
* @return int[] all the required sensors needed by this app
@@ -210,17 +139,6 @@ public class NanoAppInstanceInfo {
}
/**
- * set the sensors needed by this app
- *
- * @param neededSensors - all the sensors needed by this app
- *
- * @hide
- */
- public void setNeededSensors(@Nullable int[] neededSensors) {
- mNeededSensors = neededSensors != null ? neededSensors : EmptyArray.INT;
- }
-
- /**
* get the events generated by this app
*
* @return all the events that can be generated by this app
@@ -231,18 +149,6 @@ public class NanoAppInstanceInfo {
}
/**
- * set the output events that can be generated by this app
- *
- * @param outputEvents - the events that may be generated by
- * this app
- *
- * @hide
- */
- public void setOutputEvents(@Nullable int[] outputEvents) {
- mOutputEvents = outputEvents != null ? outputEvents : EmptyArray.INT;
- }
-
- /**
* get the context hub identifier
*
* @return int - system unique hub identifier
@@ -252,17 +158,6 @@ public class NanoAppInstanceInfo {
}
/**
- * set the context hub identifier
- *
- * @param contexthubId - system wide unique identifier
- *
- * @hide
- */
- public void setContexthubId(int contexthubId) {
- mContexthubId = contexthubId;
- }
-
- /**
* get a handle to the nano app instance
*
* @return int - handle to this instance
@@ -271,18 +166,6 @@ public class NanoAppInstanceInfo {
return mHandle;
}
- /**
- * set the handle for an app instance
- *
- * @param handle - handle to this instance
- *
- * @hide
- */
- public void setHandle(int handle) {
- mHandle = handle;
- }
-
-
private NanoAppInstanceInfo(Parcel in) {
mPublisher = in.readString();
mName = in.readString();
@@ -342,9 +225,7 @@ public class NanoAppInstanceInfo {
public String toString() {
String retVal = "handle : " + mHandle;
retVal += ", Id : 0x" + Long.toHexString(mAppId);
- retVal += ", Version : " + mAppVersion;
- retVal += ", Name : " + mName;
- retVal += ", Publisher : " + mPublisher;
+ retVal += ", Version : 0x" + Integer.toHexString(mAppVersion);
return retVal;
}
diff --git a/android/hardware/radio/RadioManager.java b/android/hardware/radio/RadioManager.java
index 4f4361f6..4d54e31b 100644
--- a/android/hardware/radio/RadioManager.java
+++ b/android/hardware/radio/RadioManager.java
@@ -161,7 +161,8 @@ public class RadioManager {
private final Set<Integer> mSupportedIdentifierTypes;
@NonNull private final Map<String, String> mVendorInfo;
- ModuleProperties(int id, String serviceName, int classId, String implementor,
+ /** @hide */
+ public ModuleProperties(int id, String serviceName, int classId, String implementor,
String product, String version, String serial, int numTuners, int numAudioSources,
boolean isCaptureSupported, BandDescriptor[] bands, boolean isBgScanSupported,
@ProgramSelector.ProgramType int[] supportedProgramTypes,
diff --git a/android/inputmethodservice/InputMethodService.java b/android/inputmethodservice/InputMethodService.java
index 223ed73b..02b1c658 100644
--- a/android/inputmethodservice/InputMethodService.java
+++ b/android/inputmethodservice/InputMethodService.java
@@ -392,7 +392,7 @@ public class InputMethodService extends AbstractInputMethodService {
mWindow.setToken(token);
}
}
-
+
/**
* {@inheritDoc}
*
@@ -1064,7 +1064,89 @@ public class InputMethodService extends AbstractInputMethodService {
}
return mInputConnection;
}
-
+
+ /**
+ * Force switch to a new input method component. This can only be called
+ * from an application or a service which has a token of the currently active input method.
+ * @param id The unique identifier for the new input method to be switched to.
+ */
+ public void setInputMethod(String id) {
+ mImm.setInputMethodInternal(mToken, id);
+ }
+
+ /**
+ * Force switch to a new input method and subtype. This can only be called
+ * from an application or a service which has a token of the currently active input method.
+ * @param id The unique identifier for the new input method to be switched to.
+ * @param subtype The new subtype of the new input method to be switched to.
+ */
+ public void setInputMethodAndSubtype(String id, InputMethodSubtype subtype) {
+ mImm.setInputMethodAndSubtypeInternal(mToken, id, subtype);
+ }
+
+ /**
+ * Close/hide the input method's soft input area, so the user no longer
+ * sees it or can interact with it. This can only be called
+ * from the currently active input method, as validated by the given token.
+ *
+ * @param flags Provides additional operating flags. Currently may be
+ * 0 or have the {@link InputMethodManager#HIDE_IMPLICIT_ONLY},
+ * {@link InputMethodManager#HIDE_NOT_ALWAYS} bit set.
+ */
+ public void hideSoftInputFromInputMethod(int flags) {
+ mImm.hideSoftInputFromInputMethodInternal(mToken, flags);
+ }
+
+ /**
+ * Show the input method's soft input area, so the user
+ * sees the input method window and can interact with it.
+ * This can only be called from the currently active input method,
+ * as validated by the given token.
+ *
+ * @param flags Provides additional operating flags. Currently may be
+ * 0 or have the {@link InputMethodManager#SHOW_IMPLICIT} or
+ * {@link InputMethodManager#SHOW_FORCED} bit set.
+ */
+ public void showSoftInputFromInputMethod(int flags) {
+ mImm.showSoftInputFromInputMethodInternal(mToken, flags);
+ }
+
+ /**
+ * Force switch to the last used input method and subtype. If the last input method didn't have
+ * any subtypes, the framework will simply switch to the last input method with no subtype
+ * specified.
+ * @return true if the current input method and subtype was successfully switched to the last
+ * used input method and subtype.
+ */
+ public boolean switchToLastInputMethod() {
+ return mImm.switchToLastInputMethodInternal(mToken);
+ }
+
+ /**
+ * Force switch to the next input method and subtype. If there is no IME enabled except
+ * current IME and subtype, do nothing.
+ * @param onlyCurrentIme if true, the framework will find the next subtype which
+ * belongs to the current IME
+ * @return true if the current input method and subtype was successfully switched to the next
+ * input method and subtype.
+ */
+ public boolean switchToNextInputMethod(boolean onlyCurrentIme) {
+ return mImm.switchToNextInputMethodInternal(mToken, onlyCurrentIme);
+ }
+
+ /**
+ * Returns true if the current IME needs to offer the users ways to switch to a next input
+ * method (e.g. a globe key.).
+ * When an IME sets supportsSwitchingToNextInputMethod and this method returns true,
+ * the IME has to offer ways to to invoke {@link #switchToNextInputMethod} accordingly.
+ * <p> Note that the system determines the most appropriate next input method
+ * and subtype in order to provide the consistent user experience in switching
+ * between IMEs and subtypes.
+ */
+ public boolean shouldOfferSwitchingToNextInputMethod() {
+ return mImm.shouldOfferSwitchingToNextInputMethodInternal(mToken);
+ }
+
public boolean getCurrentInputStarted() {
return mInputStarted;
}
diff --git a/android/inputmethodservice/KeyboardView.java b/android/inputmethodservice/KeyboardView.java
index 7836cd09..13b9206b 100644
--- a/android/inputmethodservice/KeyboardView.java
+++ b/android/inputmethodservice/KeyboardView.java
@@ -21,16 +21,18 @@ import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
-import android.graphics.Paint.Align;
import android.graphics.PorterDuff;
import android.graphics.Rect;
-import android.graphics.Region.Op;
import android.graphics.Typeface;
+import android.graphics.Paint.Align;
+import android.graphics.Region.Op;
import android.graphics.drawable.Drawable;
import android.inputmethodservice.Keyboard.Key;
import android.media.AudioManager;
import android.os.Handler;
import android.os.Message;
+import android.os.UserHandle;
+import android.provider.Settings;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.GestureDetector;
@@ -984,9 +986,6 @@ public class KeyboardView extends View implements View.OnClickListener {
private void sendAccessibilityEventForUnicodeCharacter(int eventType, int code) {
if (mAccessibilityManager.isEnabled()) {
- if (!mAccessibilityManager.isObservedEventType(eventType)) {
- return;
- }
AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
onInitializeAccessibilityEvent(event);
final String text;
diff --git a/android/location/GnssMeasurementsEvent.java b/android/location/GnssMeasurementsEvent.java
index d66fd9c4..072a7fef 100644
--- a/android/location/GnssMeasurementsEvent.java
+++ b/android/location/GnssMeasurementsEvent.java
@@ -49,7 +49,7 @@ public final class GnssMeasurementsEvent implements Parcelable {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({STATUS_NOT_SUPPORTED, STATUS_READY, STATUS_LOCATION_DISABLED})
+ @IntDef({STATUS_NOT_SUPPORTED, STATUS_READY, STATUS_LOCATION_DISABLED, STATUS_NOT_ALLOWED})
public @interface GnssMeasurementsStatus {}
/**
@@ -72,6 +72,12 @@ public final class GnssMeasurementsEvent implements Parcelable {
public static final int STATUS_LOCATION_DISABLED = 2;
/**
+ * The client is not allowed to register for GNSS Measurements in general or in the
+ * requested mode.
+ */
+ public static final int STATUS_NOT_ALLOWED = 3;
+
+ /**
* Reports the latest collected GNSS Measurements.
*/
public void onGnssMeasurementsReceived(GnssMeasurementsEvent eventArgs) {}
diff --git a/android/location/LocalListenerHelper.java b/android/location/LocalListenerHelper.java
index d7d2c513..592d01d2 100644
--- a/android/location/LocalListenerHelper.java
+++ b/android/location/LocalListenerHelper.java
@@ -16,14 +16,14 @@
package android.location;
-import com.android.internal.util.Preconditions;
-
import android.annotation.NonNull;
import android.content.Context;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.util.Preconditions;
+
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -46,6 +46,11 @@ abstract class LocalListenerHelper<TListener> {
mTag = name;
}
+ /**
+ * Adds a {@param listener} to the list of listeners on which callbacks will be executed. The
+ * execution will happen on the {@param handler} thread or alternatively in the callback thread
+ * if a {@code null} handler value is passed.
+ */
public boolean add(@NonNull TListener listener, Handler handler) {
Preconditions.checkNotNull(listener);
synchronized (mListeners) {
diff --git a/android/location/LocationManager.java b/android/location/LocationManager.java
index 968f596e..4802b235 100644
--- a/android/location/LocationManager.java
+++ b/android/location/LocationManager.java
@@ -19,6 +19,7 @@ package android.location;
import com.android.internal.location.ProviderProperties;
import android.Manifest;
+import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
@@ -184,6 +185,17 @@ public class LocationManager {
public static final String MODE_CHANGED_ACTION = "android.location.MODE_CHANGED";
/**
+ * Broadcast intent action when {@link android.provider.Settings.Secure#LOCATION_MODE} is
+ * about to be changed through Settings app or Quick Settings.
+ * For use with the {@link android.provider.Settings.Secure#LOCATION_MODE} API.
+ * If you're interacting with {@link #isProviderEnabled(String)}, use
+ * {@link #PROVIDERS_CHANGED_ACTION} instead.
+ *
+ * @hide
+ */
+ public static final String MODE_CHANGING_ACTION = "com.android.settings.location.MODE_CHANGING";
+
+ /**
* Broadcast intent action indicating that the GPS has either started or
* stopped receiving GPS fixes. An intent extra provides this state as a
* boolean, where {@code true} means that the GPS is actively receiving fixes.
@@ -214,6 +226,12 @@ public class LocationManager {
public static final String HIGH_POWER_REQUEST_CHANGE_ACTION =
"android.location.HIGH_POWER_REQUEST_CHANGE";
+ /**
+ * The value returned by {@link LocationManager#getGnssHardwareModelName()} when the hardware
+ * does not support providing the actual value.
+ */
+ public static final String GNSS_HARDWARE_MODEL_NAME_UNKNOWN = "Model Name Unknown";
+
// Map from LocationListeners to their associated ListenerTransport objects
private HashMap<LocationListener,ListenerTransport> mListeners =
new HashMap<LocationListener,ListenerTransport>();
@@ -1958,11 +1976,10 @@ public class LocationManager {
}
/**
- * Returns the system information of the GPS hardware.
- * May return 0 if GPS hardware is earlier than 2016.
- * @hide
+ * Returns the model year of the GNSS hardware and software build.
+ *
+ * May return 0 if the model year is less than 2016.
*/
- @TestApi
public int getGnssYearOfHardware() {
try {
return mService.getGnssYearOfHardware();
@@ -1972,6 +1989,22 @@ public class LocationManager {
}
/**
+ * Returns the Model Name (including Vendor and Hardware/Software Version) of the GNSS hardware
+ * driver.
+ *
+ * Will return {@link LocationManager#GNSS_HARDWARE_MODEL_NAME_UNKNOWN} when the GNSS hardware
+ * abstraction layer does not support providing this value.
+ */
+ @NonNull
+ public String getGnssHardwareModelName() {
+ try {
+ return mService.getGnssHardwareModelName();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Returns the batch size (in number of Location objects) that are supported by the batching
* interface.
*
diff --git a/android/location/LocationRequest.java b/android/location/LocationRequest.java
index 65e7cedf..6abba954 100644
--- a/android/location/LocationRequest.java
+++ b/android/location/LocationRequest.java
@@ -143,7 +143,7 @@ public final class LocationRequest implements Parcelable {
private int mQuality = POWER_LOW;
private long mInterval = 60 * 60 * 1000; // 60 minutes
- private long mFastestInterval = (long)(mInterval / FASTEST_INTERVAL_FACTOR); // 10 minutes
+ private long mFastestInterval = (long) (mInterval / FASTEST_INTERVAL_FACTOR); // 10 minutes
private boolean mExplicitFastestInterval = false;
private long mExpireAt = Long.MAX_VALUE; // no expiry
private int mNumUpdates = Integer.MAX_VALUE; // no expiry
@@ -151,7 +151,11 @@ public final class LocationRequest implements Parcelable {
private WorkSource mWorkSource = null;
private boolean mHideFromAppOps = false; // True if this request shouldn't be counted by AppOps
- private String mProvider = LocationManager.FUSED_PROVIDER; // for deprecated APIs that explicitly request a provider
+ private String mProvider = LocationManager.FUSED_PROVIDER;
+ // for deprecated APIs that explicitly request a provider
+
+ /** If true, GNSS chipset will make strong tradeoffs to substantially restrict power use */
+ private boolean mLowPowerMode = false;
/**
* Create a location request with default parameters.
@@ -184,11 +188,11 @@ public final class LocationRequest implements Parcelable {
}
LocationRequest request = new LocationRequest()
- .setProvider(provider)
- .setQuality(quality)
- .setInterval(minTime)
- .setFastestInterval(minTime)
- .setSmallestDisplacement(minDistance);
+ .setProvider(provider)
+ .setQuality(quality)
+ .setInterval(minTime)
+ .setFastestInterval(minTime)
+ .setSmallestDisplacement(minDistance);
if (singleShot) request.setNumUpdates(1);
return request;
}
@@ -220,16 +224,17 @@ public final class LocationRequest implements Parcelable {
}
LocationRequest request = new LocationRequest()
- .setQuality(quality)
- .setInterval(minTime)
- .setFastestInterval(minTime)
- .setSmallestDisplacement(minDistance);
+ .setQuality(quality)
+ .setInterval(minTime)
+ .setFastestInterval(minTime)
+ .setSmallestDisplacement(minDistance);
if (singleShot) request.setNumUpdates(1);
return request;
}
/** @hide */
- public LocationRequest() { }
+ public LocationRequest() {
+ }
/** @hide */
public LocationRequest(LocationRequest src) {
@@ -243,6 +248,7 @@ public final class LocationRequest implements Parcelable {
mProvider = src.mProvider;
mWorkSource = src.mWorkSource;
mHideFromAppOps = src.mHideFromAppOps;
+ mLowPowerMode = src.mLowPowerMode;
}
/**
@@ -263,8 +269,8 @@ public final class LocationRequest implements Parcelable {
* on a location request.
*
* @param quality an accuracy or power constant
- * @throws InvalidArgumentException if the quality constant is not valid
* @return the same object, so that setters can be chained
+ * @throws InvalidArgumentException if the quality constant is not valid
*/
public LocationRequest setQuality(int quality) {
checkQuality(quality);
@@ -306,14 +312,14 @@ public final class LocationRequest implements Parcelable {
* on a location request.
*
* @param millis desired interval in millisecond, inexact
- * @throws InvalidArgumentException if the interval is less than zero
* @return the same object, so that setters can be chained
+ * @throws InvalidArgumentException if the interval is less than zero
*/
public LocationRequest setInterval(long millis) {
checkInterval(millis);
mInterval = millis;
if (!mExplicitFastestInterval) {
- mFastestInterval = (long)(mInterval / FASTEST_INTERVAL_FACTOR);
+ mFastestInterval = (long) (mInterval / FASTEST_INTERVAL_FACTOR);
}
return this;
}
@@ -327,6 +333,34 @@ public final class LocationRequest implements Parcelable {
return mInterval;
}
+
+ /**
+ * Requests the GNSS chipset to run in a low power mode and make strong tradeoffs to
+ * substantially restrict power.
+ *
+ * <p>In this mode, the GNSS chipset will not, on average, run power hungry operations like RF &
+ * signal searches for more than one second per interval {@link #mInterval}
+ *
+ * @param enabled Enable or disable low power mode
+ * @return the same object, so that setters can be chained
+ * @hide
+ */
+ @SystemApi
+ public LocationRequest setLowPowerMode(boolean enabled) {
+ mLowPowerMode = enabled;
+ return this;
+ }
+
+ /**
+ * Returns true if low power mode is enabled.
+ *
+ * @hide
+ */
+ @SystemApi
+ public boolean isLowPowerMode() {
+ return mLowPowerMode;
+ }
+
/**
* Explicitly set the fastest interval for location updates, in
* milliseconds.
@@ -353,8 +387,8 @@ public final class LocationRequest implements Parcelable {
* then your effective fastest interval is {@link #setInterval}.
*
* @param millis fastest interval for updates in milliseconds, exact
- * @throws InvalidArgumentException if the interval is less than zero
* @return the same object, so that setters can be chained
+ * @throws InvalidArgumentException if the interval is less than zero
*/
public LocationRequest setFastestInterval(long millis) {
checkInterval(millis);
@@ -397,9 +431,9 @@ public final class LocationRequest implements Parcelable {
// Check for > Long.MAX_VALUE overflow (elapsedRealtime > 0):
if (millis > Long.MAX_VALUE - elapsedRealtime) {
- mExpireAt = Long.MAX_VALUE;
+ mExpireAt = Long.MAX_VALUE;
} else {
- mExpireAt = millis + elapsedRealtime;
+ mExpireAt = millis + elapsedRealtime;
}
if (mExpireAt < 0) mExpireAt = 0;
@@ -448,11 +482,14 @@ public final class LocationRequest implements Parcelable {
* to the location manager.
*
* @param numUpdates the number of location updates requested
- * @throws InvalidArgumentException if numUpdates is 0 or less
* @return the same object, so that setters can be chained
+ * @throws InvalidArgumentException if numUpdates is 0 or less
*/
public LocationRequest setNumUpdates(int numUpdates) {
- if (numUpdates <= 0) throw new IllegalArgumentException("invalid numUpdates: " + numUpdates);
+ if (numUpdates <= 0) {
+ throw new IllegalArgumentException(
+ "invalid numUpdates: " + numUpdates);
+ }
mNumUpdates = numUpdates;
return this;
}
@@ -462,6 +499,7 @@ public final class LocationRequest implements Parcelable {
*
* <p>By default this is {@link Integer#MAX_VALUE}, which indicates that
* locations are updated until the request is explicitly removed.
+ *
* @return number of updates
*/
public int getNumUpdates() {
@@ -539,8 +577,8 @@ public final class LocationRequest implements Parcelable {
* doesn't have the {@link android.Manifest.permission#UPDATE_APP_OPS_STATS} permission.
*
* @param hideFromAppOps If true AppOps won't keep track of this location request.
- * @see android.app.AppOpsManager
* @hide
+ * @see android.app.AppOpsManager
*/
@SystemApi
public void setHideFromAppOps(boolean hideFromAppOps) {
@@ -587,27 +625,29 @@ public final class LocationRequest implements Parcelable {
public static final Parcelable.Creator<LocationRequest> CREATOR =
new Parcelable.Creator<LocationRequest>() {
- @Override
- public LocationRequest createFromParcel(Parcel in) {
- LocationRequest request = new LocationRequest();
- request.setQuality(in.readInt());
- request.setFastestInterval(in.readLong());
- request.setInterval(in.readLong());
- request.setExpireAt(in.readLong());
- request.setNumUpdates(in.readInt());
- request.setSmallestDisplacement(in.readFloat());
- request.setHideFromAppOps(in.readInt() != 0);
- String provider = in.readString();
- if (provider != null) request.setProvider(provider);
- WorkSource workSource = in.readParcelable(null);
- if (workSource != null) request.setWorkSource(workSource);
- return request;
- }
- @Override
- public LocationRequest[] newArray(int size) {
- return new LocationRequest[size];
- }
- };
+ @Override
+ public LocationRequest createFromParcel(Parcel in) {
+ LocationRequest request = new LocationRequest();
+ request.setQuality(in.readInt());
+ request.setFastestInterval(in.readLong());
+ request.setInterval(in.readLong());
+ request.setExpireAt(in.readLong());
+ request.setNumUpdates(in.readInt());
+ request.setSmallestDisplacement(in.readFloat());
+ request.setHideFromAppOps(in.readInt() != 0);
+ request.setLowPowerMode(in.readInt() != 0);
+ String provider = in.readString();
+ if (provider != null) request.setProvider(provider);
+ WorkSource workSource = in.readParcelable(null);
+ if (workSource != null) request.setWorkSource(workSource);
+ return request;
+ }
+
+ @Override
+ public LocationRequest[] newArray(int size) {
+ return new LocationRequest[size];
+ }
+ };
@Override
public int describeContents() {
@@ -623,6 +663,7 @@ public final class LocationRequest implements Parcelable {
parcel.writeInt(mNumUpdates);
parcel.writeFloat(mSmallestDisplacement);
parcel.writeInt(mHideFromAppOps ? 1 : 0);
+ parcel.writeInt(mLowPowerMode ? 1 : 0);
parcel.writeString(mProvider);
parcel.writeParcelable(mWorkSource, 0);
}
@@ -663,9 +704,10 @@ public final class LocationRequest implements Parcelable {
s.append(" expireIn=");
TimeUtils.formatDuration(expireIn, s);
}
- if (mNumUpdates != Integer.MAX_VALUE){
+ if (mNumUpdates != Integer.MAX_VALUE) {
s.append(" num=").append(mNumUpdates);
}
+ s.append(" lowPowerMode=").append(mLowPowerMode);
s.append(']');
return s.toString();
}
diff --git a/android/media/AudioAttributes.java b/android/media/AudioAttributes.java
index 7afe267f..e0289f0b 100644
--- a/android/media/AudioAttributes.java
+++ b/android/media/AudioAttributes.java
@@ -741,7 +741,7 @@ public final class AudioAttributes implements Parcelable {
/**
* @hide
* Same as {@link #setCapturePreset(int)} but authorizes the use of HOTWORD,
- * REMOTE_SUBMIX and RADIO_TUNER.
+ * REMOTE_SUBMIX, RADIO_TUNER, VOICE_DOWNLINK, VOICE_UPLINK and VOICE_CALL.
* @param preset
* @return the same Builder instance.
*/
@@ -749,7 +749,10 @@ public final class AudioAttributes implements Parcelable {
public Builder setInternalCapturePreset(int preset) {
if ((preset == MediaRecorder.AudioSource.HOTWORD)
|| (preset == MediaRecorder.AudioSource.REMOTE_SUBMIX)
- || (preset == MediaRecorder.AudioSource.RADIO_TUNER)) {
+ || (preset == MediaRecorder.AudioSource.RADIO_TUNER)
+ || (preset == MediaRecorder.AudioSource.VOICE_DOWNLINK)
+ || (preset == MediaRecorder.AudioSource.VOICE_UPLINK)
+ || (preset == MediaRecorder.AudioSource.VOICE_CALL)) {
mSource = preset;
} else {
setCapturePreset(preset);
diff --git a/android/media/AudioDeviceInfo.java b/android/media/AudioDeviceInfo.java
index 1b89c966..1a97b6ba 100644
--- a/android/media/AudioDeviceInfo.java
+++ b/android/media/AudioDeviceInfo.java
@@ -16,9 +16,12 @@
package android.media;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.util.SparseIntArray;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.TreeSet;
/**
@@ -120,6 +123,57 @@ public final class AudioDeviceInfo {
*/
public static final int TYPE_USB_HEADSET = 22;
+ /** @hide */
+ @IntDef(flag = false, prefix = "TYPE", value = {
+ TYPE_BUILTIN_EARPIECE,
+ TYPE_BUILTIN_SPEAKER,
+ TYPE_WIRED_HEADSET,
+ TYPE_WIRED_HEADPHONES,
+ TYPE_BLUETOOTH_SCO,
+ TYPE_BLUETOOTH_A2DP,
+ TYPE_HDMI,
+ TYPE_DOCK,
+ TYPE_USB_ACCESSORY,
+ TYPE_USB_DEVICE,
+ TYPE_USB_HEADSET,
+ TYPE_TELEPHONY,
+ TYPE_LINE_ANALOG,
+ TYPE_HDMI_ARC,
+ TYPE_LINE_DIGITAL,
+ TYPE_FM,
+ TYPE_AUX_LINE,
+ TYPE_IP }
+ )
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AudioDeviceTypeOut {}
+
+ /** @hide */
+ /*package*/ static boolean isValidAudioDeviceTypeOut(int type) {
+ switch (type) {
+ case TYPE_BUILTIN_EARPIECE:
+ case TYPE_BUILTIN_SPEAKER:
+ case TYPE_WIRED_HEADSET:
+ case TYPE_WIRED_HEADPHONES:
+ case TYPE_BLUETOOTH_SCO:
+ case TYPE_BLUETOOTH_A2DP:
+ case TYPE_HDMI:
+ case TYPE_DOCK:
+ case TYPE_USB_ACCESSORY:
+ case TYPE_USB_DEVICE:
+ case TYPE_USB_HEADSET:
+ case TYPE_TELEPHONY:
+ case TYPE_LINE_ANALOG:
+ case TYPE_HDMI_ARC:
+ case TYPE_LINE_DIGITAL:
+ case TYPE_FM:
+ case TYPE_AUX_LINE:
+ case TYPE_IP:
+ return true;
+ default:
+ return false;
+ }
+ }
+
private final AudioDevicePort mPort;
AudioDeviceInfo(AudioDevicePort port) {
@@ -127,6 +181,14 @@ public final class AudioDeviceInfo {
}
/**
+ * @hide
+ * @return The underlying {@link AudioDevicePort} instance.
+ */
+ public AudioDevicePort getPort() {
+ return mPort;
+ }
+
+ /**
* @return The internal device ID.
*/
public int getId() {
diff --git a/android/media/AudioManager.java b/android/media/AudioManager.java
index 58976ca0..913b5e84 100644
--- a/android/media/AudioManager.java
+++ b/android/media/AudioManager.java
@@ -16,6 +16,7 @@
package android.media;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -52,6 +53,8 @@ import android.util.Log;
import android.util.Slog;
import android.view.KeyEvent;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@@ -911,13 +914,28 @@ public class AudioManager {
/**
* Returns the minimum volume index for a particular stream.
- *
- * @param streamType The stream type whose minimum volume index is returned.
+ * @param streamType The stream type whose minimum volume index is returned. Must be one of
+ * {@link #STREAM_VOICE_CALL}, {@link #STREAM_SYSTEM},
+ * {@link #STREAM_RING}, {@link #STREAM_MUSIC}, {@link #STREAM_ALARM},
+ * {@link #STREAM_NOTIFICATION}, {@link #STREAM_DTMF} or {@link #STREAM_ACCESSIBILITY}.
* @return The minimum valid volume index for the stream.
* @see #getStreamVolume(int)
- * @hide
*/
public int getStreamMinVolume(int streamType) {
+ if (!isPublicStreamType(streamType)) {
+ throw new IllegalArgumentException("Invalid stream type " + streamType);
+ }
+ return getStreamMinVolumeInt(streamType);
+ }
+
+ /**
+ * @hide
+ * Same as {@link #getStreamMinVolume(int)} but without the check on the public stream type.
+ * @param streamType The stream type whose minimum volume index is returned.
+ * @return The minimum valid volume index for the stream.
+ * @see #getStreamVolume(int)
+ */
+ public int getStreamMinVolumeInt(int streamType) {
final IAudioService service = getService();
try {
return service.getStreamMinVolume(streamType);
@@ -943,6 +961,72 @@ public class AudioManager {
}
}
+ // keep in sync with frameworks/av/services/audiopolicy/common/include/Volume.h
+ private static final float VOLUME_MIN_DB = -758.0f;
+
+ /** @hide */
+ @IntDef(flag = false, prefix = "STREAM", value = {
+ STREAM_VOICE_CALL,
+ STREAM_SYSTEM,
+ STREAM_RING,
+ STREAM_MUSIC,
+ STREAM_ALARM,
+ STREAM_NOTIFICATION,
+ STREAM_DTMF,
+ STREAM_ACCESSIBILITY }
+ )
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PublicStreamTypes {}
+
+ /**
+ * Returns the volume in dB (decibel) for the given stream type at the given volume index, on
+ * the given type of audio output device.
+ * @param streamType stream type for which the volume is queried.
+ * @param index the volume index for which the volume is queried. The index value must be
+ * between the minimum and maximum index values for the given stream type (see
+ * {@link #getStreamMinVolume(int)} and {@link #getStreamMaxVolume(int)}).
+ * @param deviceType the type of audio output device for which volume is queried.
+ * @return a volume expressed in dB.
+ * A negative value indicates the audio signal is attenuated. A typical maximum value
+ * at the maximum volume index is 0 dB (no attenuation nor amplification). Muting is
+ * reflected by a value of {@link Float#NEGATIVE_INFINITY}.
+ */
+ public float getStreamVolumeDb(@PublicStreamTypes int streamType, int index,
+ @AudioDeviceInfo.AudioDeviceTypeOut int deviceType) {
+ if (!isPublicStreamType(streamType)) {
+ throw new IllegalArgumentException("Invalid stream type " + streamType);
+ }
+ if (index > getStreamMaxVolume(streamType) || index < getStreamMinVolume(streamType)) {
+ throw new IllegalArgumentException("Invalid stream volume index " + index);
+ }
+ if (!AudioDeviceInfo.isValidAudioDeviceTypeOut(deviceType)) {
+ throw new IllegalArgumentException("Invalid audio output device type " + deviceType);
+ }
+ final float gain = AudioSystem.getStreamVolumeDB(streamType, index,
+ AudioDeviceInfo.convertDeviceTypeToInternalDevice(deviceType));
+ if (gain <= VOLUME_MIN_DB) {
+ return Float.NEGATIVE_INFINITY;
+ } else {
+ return gain;
+ }
+ }
+
+ private static boolean isPublicStreamType(int streamType) {
+ switch (streamType) {
+ case STREAM_VOICE_CALL:
+ case STREAM_SYSTEM:
+ case STREAM_RING:
+ case STREAM_MUSIC:
+ case STREAM_ALARM:
+ case STREAM_NOTIFICATION:
+ case STREAM_DTMF:
+ case STREAM_ACCESSIBILITY:
+ return true;
+ default:
+ return false;
+ }
+ }
+
/**
* Get last audible volume before stream was muted.
*
@@ -1551,6 +1635,21 @@ public class AudioManager {
}
/**
+ * Broadcast Action: microphone muting state changed.
+ *
+ * You <em>cannot</em> receive this through components declared
+ * in manifests, only by explicitly registering for it with
+ * {@link Context#registerReceiver(BroadcastReceiver, IntentFilter)
+ * Context.registerReceiver()}.
+ *
+ * <p>The intent has no extra values, use {@link #isMicrophoneMute} to check whether the
+ * microphone is muted.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MICROPHONE_MUTE_CHANGED =
+ "android.media.action.MICROPHONE_MUTE_CHANGED";
+
+ /**
* Sets the audio mode.
* <p>
* The audio mode encompasses audio routing AND the behavior of
diff --git a/android/media/AudioRecord.java b/android/media/AudioRecord.java
index 0906ba50..27784e96 100644
--- a/android/media/AudioRecord.java
+++ b/android/media/AudioRecord.java
@@ -1516,66 +1516,13 @@ public class AudioRecord implements AudioRouting
}
/**
- * Helper class to handle the forwarding of native events to the appropriate listener
- * (potentially) handled in a different thread
- */
- private class NativeRoutingEventHandlerDelegate {
- private final Handler mHandler;
-
- NativeRoutingEventHandlerDelegate(final AudioRecord record,
- final AudioRouting.OnRoutingChangedListener listener,
- Handler handler) {
- // find the looper for our new event handler
- Looper looper;
- if (handler != null) {
- looper = handler.getLooper();
- } else {
- // no given handler, use the looper the AudioRecord was created in
- looper = mInitializationLooper;
- }
-
- // 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) {
- if (record == null) {
- return;
- }
- switch(msg.what) {
- case AudioSystem.NATIVE_EVENT_ROUTING_CHANGE:
- if (listener != null) {
- listener.onRoutingChanged(record);
- }
- break;
- default:
- loge("Unknown native event type: " + msg.what);
- break;
- }
- }
- };
- } else {
- mHandler = null;
- }
- }
-
- Handler getHandler() {
- return mHandler;
- }
- }
-
- /**
* Sends device list change notification to all listeners.
*/
private void broadcastRoutingChange() {
AudioManager.resetAudioPortGeneration();
synchronized (mRoutingChangeListeners) {
for (NativeRoutingEventHandlerDelegate delegate : mRoutingChangeListeners.values()) {
- Handler handler = delegate.getHandler();
- if (handler != null) {
- handler.sendEmptyMessage(AudioSystem.NATIVE_EVENT_ROUTING_CHANGE);
- }
+ delegate.notifyClient();
}
}
}
diff --git a/android/media/AudioTrack.java b/android/media/AudioTrack.java
index 50145f8a..e535fdf5 100644
--- a/android/media/AudioTrack.java
+++ b/android/media/AudioTrack.java
@@ -2856,10 +2856,7 @@ public class AudioTrack extends PlayerBase
AudioManager.resetAudioPortGeneration();
synchronized (mRoutingChangeListeners) {
for (NativeRoutingEventHandlerDelegate delegate : mRoutingChangeListeners.values()) {
- Handler handler = delegate.getHandler();
- if (handler != null) {
- handler.sendEmptyMessage(AudioSystem.NATIVE_EVENT_ROUTING_CHANGE);
- }
+ delegate.notifyClient();
}
}
}
@@ -2943,56 +2940,6 @@ public class AudioTrack extends PlayerBase
}
}
- /**
- * Helper class to handle the forwarding of native events to the appropriate listener
- * (potentially) handled in a different thread
- */
- private class NativeRoutingEventHandlerDelegate {
- private final Handler mHandler;
-
- NativeRoutingEventHandlerDelegate(final AudioTrack track,
- final AudioRouting.OnRoutingChangedListener listener,
- Handler handler) {
- // find the looper for our new event handler
- Looper looper;
- if (handler != null) {
- looper = handler.getLooper();
- } else {
- // no given handler, use the looper the AudioTrack was created in
- looper = mInitializationLooper;
- }
-
- // 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) {
- if (track == null) {
- return;
- }
- switch(msg.what) {
- case AudioSystem.NATIVE_EVENT_ROUTING_CHANGE:
- if (listener != null) {
- listener.onRoutingChanged(track);
- }
- break;
- default:
- loge("Unknown native event type: " + msg.what);
- break;
- }
- }
- };
- } else {
- mHandler = null;
- }
- }
-
- Handler getHandler() {
- return mHandler;
- }
- }
-
//---------------------------------------------------------
// Methods for IPlayer interface
//--------------------
diff --git a/android/media/MediaDrm.java b/android/media/MediaDrm.java
index 12e5744d..e2f9b47e 100644
--- a/android/media/MediaDrm.java
+++ b/android/media/MediaDrm.java
@@ -977,7 +977,7 @@ public final class MediaDrm {
public static final String PROPERTY_ALGORITHMS = "algorithms";
/** @hide */
- @StringDef({
+ @StringDef(prefix = { "PROPERTY_" }, value = {
PROPERTY_VENDOR,
PROPERTY_VERSION,
PROPERTY_DESCRIPTION,
@@ -1010,7 +1010,7 @@ public final class MediaDrm {
public static final String PROPERTY_DEVICE_UNIQUE_ID = "deviceUniqueId";
/** @hide */
- @StringDef({
+ @StringDef(prefix = { "PROPERTY_" }, value = {
PROPERTY_DEVICE_UNIQUE_ID,
})
@Retention(RetentionPolicy.SOURCE)
diff --git a/android/media/MediaMetadata.java b/android/media/MediaMetadata.java
index 31eb948d..94d4d556 100644
--- a/android/media/MediaMetadata.java
+++ b/android/media/MediaMetadata.java
@@ -45,34 +45,61 @@ public final class MediaMetadata implements Parcelable {
/**
* @hide
*/
- @StringDef({METADATA_KEY_TITLE, METADATA_KEY_ARTIST, METADATA_KEY_ALBUM, METADATA_KEY_AUTHOR,
- METADATA_KEY_WRITER, METADATA_KEY_COMPOSER, METADATA_KEY_COMPILATION,
- METADATA_KEY_DATE, METADATA_KEY_GENRE, METADATA_KEY_ALBUM_ARTIST, METADATA_KEY_ART_URI,
- METADATA_KEY_ALBUM_ART_URI, METADATA_KEY_DISPLAY_TITLE, METADATA_KEY_DISPLAY_SUBTITLE,
- METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_KEY_DISPLAY_ICON_URI,
- METADATA_KEY_MEDIA_ID, METADATA_KEY_MEDIA_URI})
+ @StringDef(prefix = { "METADATA_KEY_" }, value = {
+ METADATA_KEY_TITLE,
+ METADATA_KEY_ARTIST,
+ METADATA_KEY_ALBUM,
+ METADATA_KEY_AUTHOR,
+ METADATA_KEY_WRITER,
+ METADATA_KEY_COMPOSER,
+ METADATA_KEY_COMPILATION,
+ METADATA_KEY_DATE,
+ METADATA_KEY_GENRE,
+ METADATA_KEY_ALBUM_ARTIST,
+ METADATA_KEY_ART_URI,
+ METADATA_KEY_ALBUM_ART_URI,
+ METADATA_KEY_DISPLAY_TITLE,
+ METADATA_KEY_DISPLAY_SUBTITLE,
+ METADATA_KEY_DISPLAY_DESCRIPTION,
+ METADATA_KEY_DISPLAY_ICON_URI,
+ METADATA_KEY_MEDIA_ID,
+ METADATA_KEY_MEDIA_URI,
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface TextKey {}
/**
* @hide
*/
- @StringDef({METADATA_KEY_DURATION, METADATA_KEY_YEAR, METADATA_KEY_TRACK_NUMBER,
- METADATA_KEY_NUM_TRACKS, METADATA_KEY_DISC_NUMBER, METADATA_KEY_BT_FOLDER_TYPE})
+ @StringDef(prefix = { "METADATA_KEY_" }, value = {
+ METADATA_KEY_DURATION,
+ METADATA_KEY_YEAR,
+ METADATA_KEY_TRACK_NUMBER,
+ METADATA_KEY_NUM_TRACKS,
+ METADATA_KEY_DISC_NUMBER,
+ METADATA_KEY_BT_FOLDER_TYPE,
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface LongKey {}
/**
* @hide
*/
- @StringDef({METADATA_KEY_ART, METADATA_KEY_ALBUM_ART, METADATA_KEY_DISPLAY_ICON})
+ @StringDef(prefix = { "METADATA_KEY_" }, value = {
+ METADATA_KEY_ART,
+ METADATA_KEY_ALBUM_ART,
+ METADATA_KEY_DISPLAY_ICON,
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface BitmapKey {}
/**
* @hide
*/
- @StringDef({METADATA_KEY_USER_RATING, METADATA_KEY_RATING})
+ @StringDef(prefix = { "METADATA_KEY_" }, value = {
+ METADATA_KEY_USER_RATING,
+ METADATA_KEY_RATING,
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface RatingKey {}
diff --git a/android/media/MediaMetadataRetriever.java b/android/media/MediaMetadataRetriever.java
index 0b864018..745eb74d 100644
--- a/android/media/MediaMetadataRetriever.java
+++ b/android/media/MediaMetadataRetriever.java
@@ -226,9 +226,12 @@ public class MediaMetadataRetriever
/**
* Call this method after setDataSource(). This method finds a
* representative frame close to the given time position by considering
- * the given option if possible, and returns it as a bitmap. This is
- * useful for generating a thumbnail for an input data source or just
- * obtain and display a frame at the given time position.
+ * the given option if possible, and returns it as a bitmap.
+ *
+ * <p>If you don't need a full-resolution
+ * frame (for example, because you need a thumbnail image), use
+ * {@link #getScaledFrameAtTime getScaledFrameAtTime()} instead of this
+ * method.</p>
*
* @param timeUs The time position where the frame will be retrieved.
* When retrieving the frame at the given time position, there is no
@@ -315,11 +318,15 @@ public class MediaMetadataRetriever
/**
* Call this method after setDataSource(). This method finds a
* representative frame close to the given time position if possible,
- * and returns it as a bitmap. This is useful for generating a thumbnail
- * for an input data source. Call this method if one does not care
+ * and returns it as a bitmap. Call this method if one does not care
* how the frame is found as long as it is close to the given time;
* otherwise, please call {@link #getFrameAtTime(long, int)}.
*
+ * <p>If you don't need a full-resolution
+ * frame (for example, because you need a thumbnail image), use
+ * {@link #getScaledFrameAtTime getScaledFrameAtTime()} instead of this
+ * method.</p>
+ *
* @param timeUs The time position where the frame will be retrieved.
* When retrieving the frame at the given time position, there is no
* guarentee that the data source has a frame located at the position.
@@ -339,11 +346,15 @@ public class MediaMetadataRetriever
/**
* Call this method after setDataSource(). This method finds a
* representative frame at any time position if possible,
- * and returns it as a bitmap. This is useful for generating a thumbnail
- * for an input data source. Call this method if one does not
+ * and returns it as a bitmap. Call this method if one does not
* care about where the frame is located; otherwise, please call
* {@link #getFrameAtTime(long)} or {@link #getFrameAtTime(long, int)}
*
+ * <p>If you don't need a full-resolution
+ * frame (for example, because you need a thumbnail image), use
+ * {@link #getScaledFrameAtTime getScaledFrameAtTime()} instead of this
+ * method.</p>
+ *
* @return A Bitmap containing a representative video frame, which
* can be null, if such a frame cannot be retrieved.
*
diff --git a/android/media/MediaPlayer.java b/android/media/MediaPlayer.java
index 649c091b..1bc3dfa4 100644
--- a/android/media/MediaPlayer.java
+++ b/android/media/MediaPlayer.java
@@ -1514,7 +1514,8 @@ public class MediaPlayer extends PlayerBase
if (listener != null && !mRoutingChangeListeners.containsKey(listener)) {
enableNativeRoutingCallbacksLocked(true);
mRoutingChangeListeners.put(
- listener, new NativeRoutingEventHandlerDelegate(this, listener, handler));
+ listener, new NativeRoutingEventHandlerDelegate(this, listener,
+ handler != null ? handler : mEventHandler));
}
}
}
@@ -1535,36 +1536,6 @@ public class MediaPlayer extends PlayerBase
}
}
- /**
- * Helper class to handle the forwarding of native events to the appropriate listener
- * (potentially) handled in a different thread
- */
- private class NativeRoutingEventHandlerDelegate {
- private MediaPlayer mMediaPlayer;
- private AudioRouting.OnRoutingChangedListener mOnRoutingChangedListener;
- private Handler mHandler;
-
- NativeRoutingEventHandlerDelegate(final MediaPlayer mediaPlayer,
- final AudioRouting.OnRoutingChangedListener listener, Handler handler) {
- mMediaPlayer = mediaPlayer;
- mOnRoutingChangedListener = listener;
- mHandler = handler != null ? handler : mEventHandler;
- }
-
- void notifyClient() {
- if (mHandler != null) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- if (mOnRoutingChangedListener != null) {
- mOnRoutingChangedListener.onRoutingChanged(mMediaPlayer);
- }
- }
- });
- }
- }
- }
-
private native final boolean native_setOutputDevice(int deviceId);
private native final int native_getRoutedDeviceId();
private native final void native_enableDeviceCallback(boolean enabled);
diff --git a/android/media/NativeRoutingEventHandlerDelegate.java b/android/media/NativeRoutingEventHandlerDelegate.java
new file mode 100644
index 00000000..9a6baf17
--- /dev/null
+++ b/android/media/NativeRoutingEventHandlerDelegate.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.os.Handler;
+
+/**
+ * Helper class {@link AudioTrack}, {@link AudioRecord}, {@link MediaPlayer} and {@link MediaRecorder}
+ * to handle the forwarding of native events to the appropriate listener
+ * (potentially) handled in a different thread.
+ * @hide
+ */
+class NativeRoutingEventHandlerDelegate {
+ private AudioRouting mAudioRouting;
+ private AudioRouting.OnRoutingChangedListener mOnRoutingChangedListener;
+ private Handler mHandler;
+
+ NativeRoutingEventHandlerDelegate(final AudioRouting audioRouting,
+ final AudioRouting.OnRoutingChangedListener listener, Handler handler) {
+ mAudioRouting = audioRouting;
+ mOnRoutingChangedListener = listener;
+ mHandler = handler;
+ }
+
+ void notifyClient() {
+ if (mHandler != null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mOnRoutingChangedListener != null) {
+ mOnRoutingChangedListener.onRoutingChanged(mAudioRouting);
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/android/media/session/PlaybackState.java b/android/media/session/PlaybackState.java
index 8283c8b9..17d16b89 100644
--- a/android/media/session/PlaybackState.java
+++ b/android/media/session/PlaybackState.java
@@ -17,6 +17,7 @@ package android.media.session;
import android.annotation.DrawableRes;
import android.annotation.IntDef;
+import android.annotation.LongDef;
import android.annotation.Nullable;
import android.media.RemoteControlClient;
import android.os.Bundle;
@@ -41,7 +42,7 @@ public final class PlaybackState implements Parcelable {
/**
* @hide
*/
- @IntDef(flag=true, value={ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND,
+ @LongDef(flag=true, value={ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND,
ACTION_SKIP_TO_PREVIOUS, ACTION_SKIP_TO_NEXT, ACTION_FAST_FORWARD, ACTION_SET_RATING,
ACTION_SEEK_TO, ACTION_PLAY_PAUSE, ACTION_PLAY_FROM_MEDIA_ID, ACTION_PLAY_FROM_SEARCH,
ACTION_SKIP_TO_QUEUE_ITEM, ACTION_PLAY_FROM_URI, ACTION_PREPARE,
diff --git a/android/media/tv/TvContract.java b/android/media/tv/TvContract.java
index 0f460960..3bbc2c4e 100644
--- a/android/media/tv/TvContract.java
+++ b/android/media/tv/TvContract.java
@@ -1650,7 +1650,7 @@ public final class TvContract {
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/channel";
/** @hide */
- @StringDef({
+ @StringDef(prefix = { "TYPE_" }, value = {
TYPE_OTHER,
TYPE_NTSC,
TYPE_PAL,
@@ -1863,7 +1863,7 @@ public final class TvContract {
public static final String TYPE_PREVIEW = "TYPE_PREVIEW";
/** @hide */
- @StringDef({
+ @StringDef(prefix = { "SERVICE_TYPE_" }, value = {
SERVICE_TYPE_OTHER,
SERVICE_TYPE_AUDIO_VIDEO,
SERVICE_TYPE_AUDIO,
@@ -1881,7 +1881,7 @@ public final class TvContract {
public static final String SERVICE_TYPE_AUDIO = "SERVICE_TYPE_AUDIO";
/** @hide */
- @StringDef({
+ @StringDef(prefix = { "VIDEO_FORMAT_" }, value = {
VIDEO_FORMAT_240P,
VIDEO_FORMAT_360P,
VIDEO_FORMAT_480I,
@@ -1930,7 +1930,7 @@ public final class TvContract {
public static final String VIDEO_FORMAT_4320P = "VIDEO_FORMAT_4320P";
/** @hide */
- @StringDef({
+ @StringDef(prefix = { "VIDEO_RESOLUTION_" }, value = {
VIDEO_RESOLUTION_SD,
VIDEO_RESOLUTION_ED,
VIDEO_RESOLUTION_HD,
diff --git a/android/mtp/MtpDatabase.java b/android/mtp/MtpDatabase.java
index aaf18e7f..a647dcc2 100644
--- a/android/mtp/MtpDatabase.java
+++ b/android/mtp/MtpDatabase.java
@@ -30,6 +30,7 @@ import android.net.Uri;
import android.os.BatteryManager;
import android.os.RemoteException;
import android.os.SystemProperties;
+import android.os.storage.StorageVolume;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio;
import android.provider.MediaStore.Files;
@@ -40,21 +41,31 @@ import android.view.WindowManager;
import dalvik.system.CloseGuard;
+import com.google.android.collect.Sets;
+
import java.io.File;
-import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
/**
+ * MtpDatabase provides an interface for MTP operations that MtpServer can use. To do this, it uses
+ * MtpStorageManager for filesystem operations and MediaProvider to get media metadata. File
+ * operations are also reflected in MediaProvider if possible.
+ * operations
* {@hide}
*/
public class MtpDatabase implements AutoCloseable {
- private static final String TAG = "MtpDatabase";
+ private static final String TAG = MtpDatabase.class.getSimpleName();
- private final Context mUserContext;
private final Context mContext;
- private final String mPackageName;
private final ContentProviderClient mMediaProvider;
private final String mVolumeName;
private final Uri mObjectsUri;
@@ -63,86 +74,158 @@ public class MtpDatabase implements AutoCloseable {
private final AtomicBoolean mClosed = new AtomicBoolean();
private final CloseGuard mCloseGuard = CloseGuard.get();
- // path to primary storage
- private final String mMediaStoragePath;
- // if not null, restrict all queries to these subdirectories
- private final String[] mSubDirectories;
- // where clause for restricting queries to files in mSubDirectories
- private String mSubDirectoriesWhere;
- // where arguments for restricting queries to files in mSubDirectories
- private String[] mSubDirectoriesWhereArgs;
-
- private final HashMap<String, MtpStorage> mStorageMap = new HashMap<String, MtpStorage>();
+ private final HashMap<String, MtpStorage> mStorageMap = new HashMap<>();
// cached property groups for single properties
- private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByProperty
- = new HashMap<Integer, MtpPropertyGroup>();
+ private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByProperty = new HashMap<>();
// cached property groups for all properties for a given format
- private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByFormat
- = new HashMap<Integer, MtpPropertyGroup>();
-
- // true if the database has been modified in the current MTP session
- private boolean mDatabaseModified;
+ private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByFormat = new HashMap<>();
// SharedPreferences for writable MTP device properties
private SharedPreferences mDeviceProperties;
- private static final int DEVICE_PROPERTIES_DATABASE_VERSION = 1;
- private static final String[] ID_PROJECTION = new String[] {
- Files.FileColumns._ID, // 0
+ // Cached device properties
+ private int mBatteryLevel;
+ private int mBatteryScale;
+ private int mDeviceType;
+
+ private MtpServer mServer;
+ private MtpStorageManager mManager;
+
+ private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
+ private static final String[] ID_PROJECTION = new String[] {Files.FileColumns._ID};
+ private static final String[] PATH_PROJECTION = new String[] {Files.FileColumns.DATA};
+ private static final String NO_MEDIA = ".nomedia";
+
+ static {
+ System.loadLibrary("media_jni");
+ }
+
+ private static final int[] PLAYBACK_FORMATS = {
+ // allow transferring arbitrary files
+ MtpConstants.FORMAT_UNDEFINED,
+
+ MtpConstants.FORMAT_ASSOCIATION,
+ MtpConstants.FORMAT_TEXT,
+ MtpConstants.FORMAT_HTML,
+ MtpConstants.FORMAT_WAV,
+ MtpConstants.FORMAT_MP3,
+ MtpConstants.FORMAT_MPEG,
+ MtpConstants.FORMAT_EXIF_JPEG,
+ MtpConstants.FORMAT_TIFF_EP,
+ MtpConstants.FORMAT_BMP,
+ MtpConstants.FORMAT_GIF,
+ MtpConstants.FORMAT_JFIF,
+ MtpConstants.FORMAT_PNG,
+ MtpConstants.FORMAT_TIFF,
+ MtpConstants.FORMAT_WMA,
+ MtpConstants.FORMAT_OGG,
+ MtpConstants.FORMAT_AAC,
+ MtpConstants.FORMAT_MP4_CONTAINER,
+ MtpConstants.FORMAT_MP2,
+ MtpConstants.FORMAT_3GP_CONTAINER,
+ MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST,
+ MtpConstants.FORMAT_WPL_PLAYLIST,
+ MtpConstants.FORMAT_M3U_PLAYLIST,
+ MtpConstants.FORMAT_PLS_PLAYLIST,
+ MtpConstants.FORMAT_XML_DOCUMENT,
+ MtpConstants.FORMAT_FLAC,
+ MtpConstants.FORMAT_DNG,
+ MtpConstants.FORMAT_HEIF,
};
- private static final String[] PATH_PROJECTION = new String[] {
- Files.FileColumns._ID, // 0
- Files.FileColumns.DATA, // 1
+
+ private static final int[] FILE_PROPERTIES = {
+ MtpConstants.PROPERTY_STORAGE_ID,
+ MtpConstants.PROPERTY_OBJECT_FORMAT,
+ MtpConstants.PROPERTY_PROTECTION_STATUS,
+ MtpConstants.PROPERTY_OBJECT_SIZE,
+ MtpConstants.PROPERTY_OBJECT_FILE_NAME,
+ MtpConstants.PROPERTY_DATE_MODIFIED,
+ MtpConstants.PROPERTY_PERSISTENT_UID,
+ MtpConstants.PROPERTY_PARENT_OBJECT,
+ MtpConstants.PROPERTY_NAME,
+ MtpConstants.PROPERTY_DISPLAY_NAME,
+ MtpConstants.PROPERTY_DATE_ADDED,
};
- private static final String[] FORMAT_PROJECTION = new String[] {
- Files.FileColumns._ID, // 0
- Files.FileColumns.FORMAT, // 1
+
+ private static final int[] AUDIO_PROPERTIES = {
+ MtpConstants.PROPERTY_ARTIST,
+ MtpConstants.PROPERTY_ALBUM_NAME,
+ MtpConstants.PROPERTY_ALBUM_ARTIST,
+ MtpConstants.PROPERTY_TRACK,
+ MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
+ MtpConstants.PROPERTY_DURATION,
+ MtpConstants.PROPERTY_GENRE,
+ MtpConstants.PROPERTY_COMPOSER,
+ MtpConstants.PROPERTY_AUDIO_WAVE_CODEC,
+ MtpConstants.PROPERTY_BITRATE_TYPE,
+ MtpConstants.PROPERTY_AUDIO_BITRATE,
+ MtpConstants.PROPERTY_NUMBER_OF_CHANNELS,
+ MtpConstants.PROPERTY_SAMPLE_RATE,
};
- private static final String[] PATH_FORMAT_PROJECTION = new String[] {
- Files.FileColumns._ID, // 0
- Files.FileColumns.DATA, // 1
- Files.FileColumns.FORMAT, // 2
+
+ private static final int[] VIDEO_PROPERTIES = {
+ MtpConstants.PROPERTY_ARTIST,
+ MtpConstants.PROPERTY_ALBUM_NAME,
+ MtpConstants.PROPERTY_DURATION,
+ MtpConstants.PROPERTY_DESCRIPTION,
};
- private static final String[] OBJECT_INFO_PROJECTION = new String[] {
- Files.FileColumns._ID, // 0
- Files.FileColumns.STORAGE_ID, // 1
- Files.FileColumns.FORMAT, // 2
- Files.FileColumns.PARENT, // 3
- Files.FileColumns.DATA, // 4
- Files.FileColumns.DATE_ADDED, // 5
- Files.FileColumns.DATE_MODIFIED, // 6
+
+ private static final int[] IMAGE_PROPERTIES = {
+ MtpConstants.PROPERTY_DESCRIPTION,
};
- private static final String ID_WHERE = Files.FileColumns._ID + "=?";
- private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
- private static final String STORAGE_WHERE = Files.FileColumns.STORAGE_ID + "=?";
- private static final String FORMAT_WHERE = Files.FileColumns.FORMAT + "=?";
- private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?";
- private static final String STORAGE_FORMAT_WHERE = STORAGE_WHERE + " AND "
- + Files.FileColumns.FORMAT + "=?";
- private static final String STORAGE_PARENT_WHERE = STORAGE_WHERE + " AND "
- + Files.FileColumns.PARENT + "=?";
- private static final String FORMAT_PARENT_WHERE = FORMAT_WHERE + " AND "
- + Files.FileColumns.PARENT + "=?";
- private static final String STORAGE_FORMAT_PARENT_WHERE = STORAGE_FORMAT_WHERE + " AND "
- + Files.FileColumns.PARENT + "=?";
+ private static final int[] DEVICE_PROPERTIES = {
+ MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
+ MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
+ MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
+ MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL,
+ MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE,
+ };
- private MtpServer mServer;
+ private int[] getSupportedObjectProperties(int format) {
+ switch (format) {
+ case MtpConstants.FORMAT_MP3:
+ case MtpConstants.FORMAT_WAV:
+ case MtpConstants.FORMAT_WMA:
+ case MtpConstants.FORMAT_OGG:
+ case MtpConstants.FORMAT_AAC:
+ return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
+ Arrays.stream(AUDIO_PROPERTIES)).toArray();
+ case MtpConstants.FORMAT_MPEG:
+ case MtpConstants.FORMAT_3GP_CONTAINER:
+ case MtpConstants.FORMAT_WMV:
+ return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
+ Arrays.stream(VIDEO_PROPERTIES)).toArray();
+ case MtpConstants.FORMAT_EXIF_JPEG:
+ case MtpConstants.FORMAT_GIF:
+ case MtpConstants.FORMAT_PNG:
+ case MtpConstants.FORMAT_BMP:
+ case MtpConstants.FORMAT_DNG:
+ case MtpConstants.FORMAT_HEIF:
+ return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
+ Arrays.stream(IMAGE_PROPERTIES)).toArray();
+ default:
+ return FILE_PROPERTIES;
+ }
+ }
- // read from native code
- private int mBatteryLevel;
- private int mBatteryScale;
+ private int[] getSupportedDeviceProperties() {
+ return DEVICE_PROPERTIES;
+ }
- private int mDeviceType;
+ private int[] getSupportedPlaybackFormats() {
+ return PLAYBACK_FORMATS;
+ }
- static {
- System.loadLibrary("media_jni");
+ private int[] getSupportedCaptureFormats() {
+ // no capture formats yet
+ return null;
}
private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() {
- @Override
+ @Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
@@ -160,61 +243,42 @@ public class MtpDatabase implements AutoCloseable {
}
};
- public MtpDatabase(Context context, Context userContext, String volumeName, String storagePath,
+ public MtpDatabase(Context context, Context userContext, String volumeName,
String[] subDirectories) {
native_setup();
-
mContext = context;
- mUserContext = userContext;
- mPackageName = context.getPackageName();
mMediaProvider = userContext.getContentResolver()
.acquireContentProviderClient(MediaStore.AUTHORITY);
mVolumeName = volumeName;
- mMediaStoragePath = storagePath;
mObjectsUri = Files.getMtpObjectsUri(volumeName);
mMediaScanner = new MediaScanner(context, mVolumeName);
-
- mSubDirectories = subDirectories;
- if (subDirectories != null) {
- // Compute "where" string for restricting queries to subdirectories
- StringBuilder builder = new StringBuilder();
- builder.append("(");
- int count = subDirectories.length;
- for (int i = 0; i < count; i++) {
- builder.append(Files.FileColumns.DATA + "=? OR "
- + Files.FileColumns.DATA + " LIKE ?");
- if (i != count - 1) {
- builder.append(" OR ");
- }
+ mManager = new MtpStorageManager(new MtpStorageManager.MtpNotifier() {
+ @Override
+ public void sendObjectAdded(int id) {
+ if (MtpDatabase.this.mServer != null)
+ MtpDatabase.this.mServer.sendObjectAdded(id);
}
- builder.append(")");
- mSubDirectoriesWhere = builder.toString();
-
- // Compute "where" arguments for restricting queries to subdirectories
- mSubDirectoriesWhereArgs = new String[count * 2];
- for (int i = 0, j = 0; i < count; i++) {
- String path = subDirectories[i];
- mSubDirectoriesWhereArgs[j++] = path;
- mSubDirectoriesWhereArgs[j++] = path + "/%";
+
+ @Override
+ public void sendObjectRemoved(int id) {
+ if (MtpDatabase.this.mServer != null)
+ MtpDatabase.this.mServer.sendObjectRemoved(id);
}
- }
+ }, subDirectories == null ? null : Sets.newHashSet(subDirectories));
initDeviceProperties(context);
mDeviceType = SystemProperties.getInt("sys.usb.mtp.device_type", 0);
-
mCloseGuard.open("close");
}
public void setServer(MtpServer server) {
mServer = server;
-
// always unregister before registering
try {
mContext.unregisterReceiver(mBatteryReceiver);
} catch (IllegalArgumentException e) {
// wasn't previously registered, ignore
}
-
// register for battery notifications when we are connected
if (server != null) {
mContext.registerReceiver(mBatteryReceiver,
@@ -224,6 +288,7 @@ public class MtpDatabase implements AutoCloseable {
@Override
public void close() {
+ mManager.close();
mCloseGuard.close();
if (mClosed.compareAndSet(false, true)) {
mMediaScanner.close();
@@ -238,24 +303,32 @@ public class MtpDatabase implements AutoCloseable {
if (mCloseGuard != null) {
mCloseGuard.warnIfOpen();
}
-
close();
} finally {
super.finalize();
}
}
- public void addStorage(MtpStorage storage) {
- mStorageMap.put(storage.getPath(), storage);
+ public void addStorage(StorageVolume storage) {
+ MtpStorage mtpStorage = mManager.addMtpStorage(storage);
+ mStorageMap.put(storage.getPath(), mtpStorage);
+ mServer.addStorage(mtpStorage);
}
- public void removeStorage(MtpStorage storage) {
+ public void removeStorage(StorageVolume storage) {
+ MtpStorage mtpStorage = mStorageMap.get(storage.getPath());
+ if (mtpStorage == null) {
+ return;
+ }
+ mServer.removeStorage(mtpStorage);
+ mManager.removeMtpStorage(mtpStorage);
mStorageMap.remove(storage.getPath());
}
private void initDeviceProperties(Context context) {
final String devicePropertiesName = "device-properties";
- mDeviceProperties = context.getSharedPreferences(devicePropertiesName, Context.MODE_PRIVATE);
+ mDeviceProperties = context.getSharedPreferences(devicePropertiesName,
+ Context.MODE_PRIVATE);
File databaseFile = context.getDatabasePath(devicePropertiesName);
if (databaseFile.exists()) {
@@ -266,7 +339,7 @@ public class MtpDatabase implements AutoCloseable {
try {
db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null);
if (db != null) {
- c = db.query("properties", new String[] { "_id", "code", "value" },
+ c = db.query("properties", new String[]{"_id", "code", "value"},
null, null, null, null, null);
if (c != null) {
SharedPreferences.Editor e = mDeviceProperties.edit();
@@ -288,602 +361,371 @@ public class MtpDatabase implements AutoCloseable {
}
}
- // check to see if the path is contained in one of our storage subdirectories
- // returns true if we have no special subdirectories
- private boolean inStorageSubDirectory(String path) {
- if (mSubDirectories == null) return true;
- if (path == null) return false;
-
- boolean allowed = false;
- int pathLength = path.length();
- for (int i = 0; i < mSubDirectories.length && !allowed; i++) {
- String subdir = mSubDirectories[i];
- int subdirLength = subdir.length();
- if (subdirLength < pathLength &&
- path.charAt(subdirLength) == '/' &&
- path.startsWith(subdir)) {
- allowed = true;
- }
+ private int beginSendObject(String path, int format, int parent, int storageId) {
+ MtpStorageManager.MtpObject parentObj =
+ parent == 0 ? mManager.getStorageRoot(storageId) : mManager.getObject(parent);
+ if (parentObj == null) {
+ return -1;
}
- return allowed;
+
+ Path objPath = Paths.get(path);
+ return mManager.beginSendObject(parentObj, objPath.getFileName().toString(), format);
}
- // check to see if the path matches one of our storage subdirectories
- // returns true if we have no special subdirectories
- private boolean isStorageSubDirectory(String path) {
- if (mSubDirectories == null) return false;
- for (int i = 0; i < mSubDirectories.length; i++) {
- if (path.equals(mSubDirectories[i])) {
- return true;
- }
+ private void endSendObject(int handle, boolean succeeded) {
+ MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+ if (obj == null || !mManager.endSendObject(obj, succeeded)) {
+ Log.e(TAG, "Failed to successfully end send object");
+ return;
}
- return false;
- }
+ // Add the new file to MediaProvider
+ if (succeeded) {
+ String path = obj.getPath().toString();
+ int format = obj.getFormat();
+ // Get parent info from MediaProvider, since the id is different from MTP's
+ ContentValues values = new ContentValues();
+ values.put(Files.FileColumns.DATA, path);
+ values.put(Files.FileColumns.FORMAT, format);
+ values.put(Files.FileColumns.SIZE, obj.getSize());
+ values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
+ try {
+ if (obj.getParent().isRoot()) {
+ values.put(Files.FileColumns.PARENT, 0);
+ } else {
+ int parentId = findInMedia(obj.getParent().getPath());
+ if (parentId != -1) {
+ values.put(Files.FileColumns.PARENT, parentId);
+ } else {
+ // The parent isn't in MediaProvider. Don't add the new file.
+ return;
+ }
+ }
- // returns true if the path is in the storage root
- private boolean inStorageRoot(String path) {
- try {
- File f = new File(path);
- String canonical = f.getCanonicalPath();
- for (String root: mStorageMap.keySet()) {
- if (canonical.startsWith(root)) {
- return true;
+ Uri uri = mMediaProvider.insert(mObjectsUri, values);
+ if (uri != null) {
+ rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format);
}
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in beginSendObject", e);
}
- } catch (IOException e) {
- // ignore
}
- return false;
}
- private int beginSendObject(String path, int format, int parent,
- int storageId, long size, long modified) {
- // if the path is outside of the storage root, do not allow access
- if (!inStorageRoot(path)) {
- Log.e(TAG, "attempt to put file outside of storage area: " + path);
- return -1;
- }
- // if mSubDirectories is not null, do not allow copying files to any other locations
- if (!inStorageSubDirectory(path)) return -1;
+ private void rescanFile(String path, int handle, int format) {
+ // handle abstract playlists separately
+ // they do not exist in the file system so don't use the media scanner here
+ if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) {
+ // extract name from path
+ String name = path;
+ int lastSlash = name.lastIndexOf('/');
+ if (lastSlash >= 0) {
+ name = name.substring(lastSlash + 1);
+ }
+ // strip trailing ".pla" from the name
+ if (name.endsWith(".pla")) {
+ name = name.substring(0, name.length() - 4);
+ }
- // make sure the object does not exist
- if (path != null) {
- Cursor c = null;
+ ContentValues values = new ContentValues(1);
+ values.put(Audio.Playlists.DATA, path);
+ values.put(Audio.Playlists.NAME, name);
+ values.put(Files.FileColumns.FORMAT, format);
+ values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000);
+ values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle);
try {
- c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE,
- new String[] { path }, null, null);
- if (c != null && c.getCount() > 0) {
- Log.w(TAG, "file already exists in beginSendObject: " + path);
- return -1;
- }
+ mMediaProvider.insert(
+ Audio.Playlists.EXTERNAL_CONTENT_URI, values);
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException in beginSendObject", e);
- } finally {
- if (c != null) {
- c.close();
- }
+ Log.e(TAG, "RemoteException in endSendObject", e);
}
+ } else {
+ mMediaScanner.scanMtpFile(path, handle, format);
}
+ }
- mDatabaseModified = true;
- ContentValues values = new ContentValues();
- values.put(Files.FileColumns.DATA, path);
- values.put(Files.FileColumns.FORMAT, format);
- values.put(Files.FileColumns.PARENT, parent);
- values.put(Files.FileColumns.STORAGE_ID, storageId);
- values.put(Files.FileColumns.SIZE, size);
- values.put(Files.FileColumns.DATE_MODIFIED, modified);
-
- try {
- Uri uri = mMediaProvider.insert(mObjectsUri, values);
- if (uri != null) {
- return Integer.parseInt(uri.getPathSegments().get(2));
- } else {
- return -1;
- }
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in beginSendObject", e);
- return -1;
+ private int[] getObjectList(int storageID, int format, int parent) {
+ Stream<MtpStorageManager.MtpObject> objectStream = mManager.getObjects(parent,
+ format, storageID);
+ if (objectStream == null) {
+ return null;
}
+ return objectStream.mapToInt(MtpStorageManager.MtpObject::getId).toArray();
}
- private void endSendObject(String path, int handle, int format, boolean succeeded) {
- if (succeeded) {
- // handle abstract playlists separately
- // they do not exist in the file system so don't use the media scanner here
- if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) {
- // extract name from path
- String name = path;
- int lastSlash = name.lastIndexOf('/');
- if (lastSlash >= 0) {
- name = name.substring(lastSlash + 1);
- }
- // strip trailing ".pla" from the name
- if (name.endsWith(".pla")) {
- name = name.substring(0, name.length() - 4);
- }
-
- ContentValues values = new ContentValues(1);
- values.put(Audio.Playlists.DATA, path);
- values.put(Audio.Playlists.NAME, name);
- values.put(Files.FileColumns.FORMAT, format);
- values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000);
- values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle);
- try {
- Uri uri = mMediaProvider.insert(
- Audio.Playlists.EXTERNAL_CONTENT_URI, values);
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in endSendObject", e);
- }
- } else {
- mMediaScanner.scanMtpFile(path, handle, format);
- }
- } else {
- deleteFile(handle);
+ private int getNumObjects(int storageID, int format, int parent) {
+ Stream<MtpStorageManager.MtpObject> objectStream = mManager.getObjects(parent,
+ format, storageID);
+ if (objectStream == null) {
+ return -1;
}
+ return (int) objectStream.count();
}
- private Cursor createObjectQuery(int storageID, int format, int parent) throws RemoteException {
- String where;
- String[] whereArgs;
-
- if (storageID == 0xFFFFFFFF) {
- // query all stores
- if (format == 0) {
- // query all formats
- if (parent == 0) {
- // query all objects
- where = null;
- whereArgs = null;
- } else {
- if (parent == 0xFFFFFFFF) {
- // all objects in root of store
- parent = 0;
- }
- where = PARENT_WHERE;
- whereArgs = new String[] { Integer.toString(parent) };
- }
- } else {
- // query specific format
- if (parent == 0) {
- // query all objects
- where = FORMAT_WHERE;
- whereArgs = new String[] { Integer.toString(format) };
- } else {
- if (parent == 0xFFFFFFFF) {
- // all objects in root of store
- parent = 0;
- }
- where = FORMAT_PARENT_WHERE;
- whereArgs = new String[] { Integer.toString(format),
- Integer.toString(parent) };
- }
- }
- } else {
- // query specific store
- if (format == 0) {
- // query all formats
- if (parent == 0) {
- // query all objects
- where = STORAGE_WHERE;
- whereArgs = new String[] { Integer.toString(storageID) };
- } else {
- if (parent == 0xFFFFFFFF) {
- // all objects in root of store
- parent = 0;
- where = STORAGE_PARENT_WHERE;
- whereArgs = new String[]{Integer.toString(storageID),
- Integer.toString(parent)};
- } else {
- // If a parent is specified, the storage is redundant
- where = PARENT_WHERE;
- whereArgs = new String[]{Integer.toString(parent)};
- }
- }
- } else {
- // query specific format
- if (parent == 0) {
- // query all objects
- where = STORAGE_FORMAT_WHERE;
- whereArgs = new String[] { Integer.toString(storageID),
- Integer.toString(format) };
- } else {
- if (parent == 0xFFFFFFFF) {
- // all objects in root of store
- parent = 0;
- where = STORAGE_FORMAT_PARENT_WHERE;
- whereArgs = new String[]{Integer.toString(storageID),
- Integer.toString(format),
- Integer.toString(parent)};
- } else {
- // If a parent is specified, the storage is redundant
- where = FORMAT_PARENT_WHERE;
- whereArgs = new String[]{Integer.toString(format),
- Integer.toString(parent)};
- }
- }
+ private MtpPropertyList getObjectPropertyList(int handle, int format, int property,
+ int groupCode, int depth) {
+ // FIXME - implement group support
+ if (property == 0) {
+ if (groupCode == 0) {
+ return new MtpPropertyList(MtpConstants.RESPONSE_PARAMETER_NOT_SUPPORTED);
}
+ return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
}
-
- // if we are restricting queries to mSubDirectories, we need to add the restriction
- // onto our "where" arguments
- if (mSubDirectoriesWhere != null) {
- if (where == null) {
- where = mSubDirectoriesWhere;
- whereArgs = mSubDirectoriesWhereArgs;
- } else {
- where = where + " AND " + mSubDirectoriesWhere;
-
- // create new array to hold whereArgs and mSubDirectoriesWhereArgs
- String[] newWhereArgs =
- new String[whereArgs.length + mSubDirectoriesWhereArgs.length];
- int i, j;
- for (i = 0; i < whereArgs.length; i++) {
- newWhereArgs[i] = whereArgs[i];
- }
- for (j = 0; j < mSubDirectoriesWhereArgs.length; i++, j++) {
- newWhereArgs[i] = mSubDirectoriesWhereArgs[j];
- }
- whereArgs = newWhereArgs;
- }
+ if (depth == 0xFFFFFFFF && (handle == 0 || handle == 0xFFFFFFFF)) {
+ // request all objects starting at root
+ handle = 0xFFFFFFFF;
+ depth = 0;
}
-
- return mMediaProvider.query(mObjectsUri, ID_PROJECTION, where,
- whereArgs, null, null);
- }
-
- private int[] getObjectList(int storageID, int format, int parent) {
- Cursor c = null;
- try {
- c = createObjectQuery(storageID, format, parent);
- if (c == null) {
- return null;
+ if (!(depth == 0 || depth == 1)) {
+ // we only support depth 0 and 1
+ // depth 0: single object, depth 1: immediate children
+ return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED);
+ }
+ Stream<MtpStorageManager.MtpObject> objectStream = Stream.of();
+ if (handle == 0xFFFFFFFF) {
+ // All objects are requested
+ objectStream = mManager.getObjects(0, format, 0xFFFFFFFF);
+ if (objectStream == null) {
+ return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
}
- int count = c.getCount();
- if (count > 0) {
- int[] result = new int[count];
- for (int i = 0; i < count; i++) {
- c.moveToNext();
- result[i] = c.getInt(0);
- }
- return result;
+ } else if (handle != 0) {
+ // Add the requested object if format matches
+ MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+ if (obj == null) {
+ return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
}
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in getObjectList", e);
- } finally {
- if (c != null) {
- c.close();
+ if (obj.getFormat() == format || format == 0) {
+ objectStream = Stream.of(obj);
}
}
- return null;
- }
-
- private int getNumObjects(int storageID, int format, int parent) {
- Cursor c = null;
- try {
- c = createObjectQuery(storageID, format, parent);
- if (c != null) {
- return c.getCount();
+ if (handle == 0 || depth == 1) {
+ if (handle == 0) {
+ handle = 0xFFFFFFFF;
}
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in getNumObjects", e);
- } finally {
- if (c != null) {
- c.close();
+ // Get the direct children of root or this object.
+ Stream<MtpStorageManager.MtpObject> childStream = mManager.getObjects(handle, format,
+ 0xFFFFFFFF);
+ if (childStream == null) {
+ return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
}
- }
- return -1;
- }
-
- private int[] getSupportedPlaybackFormats() {
- return new int[] {
- // allow transfering arbitrary files
- MtpConstants.FORMAT_UNDEFINED,
-
- MtpConstants.FORMAT_ASSOCIATION,
- MtpConstants.FORMAT_TEXT,
- MtpConstants.FORMAT_HTML,
- MtpConstants.FORMAT_WAV,
- MtpConstants.FORMAT_MP3,
- MtpConstants.FORMAT_MPEG,
- MtpConstants.FORMAT_EXIF_JPEG,
- MtpConstants.FORMAT_TIFF_EP,
- MtpConstants.FORMAT_BMP,
- MtpConstants.FORMAT_GIF,
- MtpConstants.FORMAT_JFIF,
- MtpConstants.FORMAT_PNG,
- MtpConstants.FORMAT_TIFF,
- MtpConstants.FORMAT_WMA,
- MtpConstants.FORMAT_OGG,
- MtpConstants.FORMAT_AAC,
- MtpConstants.FORMAT_MP4_CONTAINER,
- MtpConstants.FORMAT_MP2,
- MtpConstants.FORMAT_3GP_CONTAINER,
- MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST,
- MtpConstants.FORMAT_WPL_PLAYLIST,
- MtpConstants.FORMAT_M3U_PLAYLIST,
- MtpConstants.FORMAT_PLS_PLAYLIST,
- MtpConstants.FORMAT_XML_DOCUMENT,
- MtpConstants.FORMAT_FLAC,
- MtpConstants.FORMAT_DNG,
- MtpConstants.FORMAT_HEIF,
- };
- }
-
- private int[] getSupportedCaptureFormats() {
- // no capture formats yet
- return null;
- }
-
- static final int[] FILE_PROPERTIES = {
- // NOTE must match beginning of AUDIO_PROPERTIES, VIDEO_PROPERTIES
- // and IMAGE_PROPERTIES below
- MtpConstants.PROPERTY_STORAGE_ID,
- MtpConstants.PROPERTY_OBJECT_FORMAT,
- MtpConstants.PROPERTY_PROTECTION_STATUS,
- MtpConstants.PROPERTY_OBJECT_SIZE,
- MtpConstants.PROPERTY_OBJECT_FILE_NAME,
- MtpConstants.PROPERTY_DATE_MODIFIED,
- MtpConstants.PROPERTY_PARENT_OBJECT,
- MtpConstants.PROPERTY_PERSISTENT_UID,
- MtpConstants.PROPERTY_NAME,
- MtpConstants.PROPERTY_DISPLAY_NAME,
- MtpConstants.PROPERTY_DATE_ADDED,
- };
-
- static final int[] AUDIO_PROPERTIES = {
- // NOTE must match FILE_PROPERTIES above
- MtpConstants.PROPERTY_STORAGE_ID,
- MtpConstants.PROPERTY_OBJECT_FORMAT,
- MtpConstants.PROPERTY_PROTECTION_STATUS,
- MtpConstants.PROPERTY_OBJECT_SIZE,
- MtpConstants.PROPERTY_OBJECT_FILE_NAME,
- MtpConstants.PROPERTY_DATE_MODIFIED,
- MtpConstants.PROPERTY_PARENT_OBJECT,
- MtpConstants.PROPERTY_PERSISTENT_UID,
- MtpConstants.PROPERTY_NAME,
- MtpConstants.PROPERTY_DISPLAY_NAME,
- MtpConstants.PROPERTY_DATE_ADDED,
-
- // audio specific properties
- MtpConstants.PROPERTY_ARTIST,
- MtpConstants.PROPERTY_ALBUM_NAME,
- MtpConstants.PROPERTY_ALBUM_ARTIST,
- MtpConstants.PROPERTY_TRACK,
- MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
- MtpConstants.PROPERTY_DURATION,
- MtpConstants.PROPERTY_GENRE,
- MtpConstants.PROPERTY_COMPOSER,
- MtpConstants.PROPERTY_AUDIO_WAVE_CODEC,
- MtpConstants.PROPERTY_BITRATE_TYPE,
- MtpConstants.PROPERTY_AUDIO_BITRATE,
- MtpConstants.PROPERTY_NUMBER_OF_CHANNELS,
- MtpConstants.PROPERTY_SAMPLE_RATE,
- };
-
- static final int[] VIDEO_PROPERTIES = {
- // NOTE must match FILE_PROPERTIES above
- MtpConstants.PROPERTY_STORAGE_ID,
- MtpConstants.PROPERTY_OBJECT_FORMAT,
- MtpConstants.PROPERTY_PROTECTION_STATUS,
- MtpConstants.PROPERTY_OBJECT_SIZE,
- MtpConstants.PROPERTY_OBJECT_FILE_NAME,
- MtpConstants.PROPERTY_DATE_MODIFIED,
- MtpConstants.PROPERTY_PARENT_OBJECT,
- MtpConstants.PROPERTY_PERSISTENT_UID,
- MtpConstants.PROPERTY_NAME,
- MtpConstants.PROPERTY_DISPLAY_NAME,
- MtpConstants.PROPERTY_DATE_ADDED,
-
- // video specific properties
- MtpConstants.PROPERTY_ARTIST,
- MtpConstants.PROPERTY_ALBUM_NAME,
- MtpConstants.PROPERTY_DURATION,
- MtpConstants.PROPERTY_DESCRIPTION,
- };
-
- static final int[] IMAGE_PROPERTIES = {
- // NOTE must match FILE_PROPERTIES above
- MtpConstants.PROPERTY_STORAGE_ID,
- MtpConstants.PROPERTY_OBJECT_FORMAT,
- MtpConstants.PROPERTY_PROTECTION_STATUS,
- MtpConstants.PROPERTY_OBJECT_SIZE,
- MtpConstants.PROPERTY_OBJECT_FILE_NAME,
- MtpConstants.PROPERTY_DATE_MODIFIED,
- MtpConstants.PROPERTY_PARENT_OBJECT,
- MtpConstants.PROPERTY_PERSISTENT_UID,
- MtpConstants.PROPERTY_NAME,
- MtpConstants.PROPERTY_DISPLAY_NAME,
- MtpConstants.PROPERTY_DATE_ADDED,
-
- // image specific properties
- MtpConstants.PROPERTY_DESCRIPTION,
- };
-
- private int[] getSupportedObjectProperties(int format) {
- switch (format) {
- case MtpConstants.FORMAT_MP3:
- case MtpConstants.FORMAT_WAV:
- case MtpConstants.FORMAT_WMA:
- case MtpConstants.FORMAT_OGG:
- case MtpConstants.FORMAT_AAC:
- return AUDIO_PROPERTIES;
- case MtpConstants.FORMAT_MPEG:
- case MtpConstants.FORMAT_3GP_CONTAINER:
- case MtpConstants.FORMAT_WMV:
- return VIDEO_PROPERTIES;
- case MtpConstants.FORMAT_EXIF_JPEG:
- case MtpConstants.FORMAT_GIF:
- case MtpConstants.FORMAT_PNG:
- case MtpConstants.FORMAT_BMP:
- case MtpConstants.FORMAT_DNG:
- case MtpConstants.FORMAT_HEIF:
- return IMAGE_PROPERTIES;
- default:
- return FILE_PROPERTIES;
- }
- }
-
- private int[] getSupportedDeviceProperties() {
- return new int[] {
- MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
- MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
- MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
- MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL,
- MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE,
- };
- }
-
- private MtpPropertyList getObjectPropertyList(int handle, int format, int property,
- int groupCode, int depth) {
- // FIXME - implement group support
- if (groupCode != 0) {
- return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
+ objectStream = Stream.concat(objectStream, childStream);
}
+ MtpPropertyList ret = new MtpPropertyList(MtpConstants.RESPONSE_OK);
MtpPropertyGroup propertyGroup;
- if (property == 0xffffffff) {
- if (format == 0 && handle != 0 && handle != 0xffffffff) {
- // return properties based on the object's format
- format = getObjectFormat(handle);
- }
- propertyGroup = mPropertyGroupsByFormat.get(format);
- if (propertyGroup == null) {
- int[] propertyList = getSupportedObjectProperties(format);
- propertyGroup = new MtpPropertyGroup(this, mMediaProvider,
- mVolumeName, propertyList);
- mPropertyGroupsByFormat.put(format, propertyGroup);
+ Iterator<MtpStorageManager.MtpObject> iter = objectStream.iterator();
+ while (iter.hasNext()) {
+ MtpStorageManager.MtpObject obj = iter.next();
+ if (property == 0xffffffff) {
+ // Get all properties supported by this object
+ propertyGroup = mPropertyGroupsByFormat.get(obj.getFormat());
+ if (propertyGroup == null) {
+ int[] propertyList = getSupportedObjectProperties(format);
+ propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName,
+ propertyList);
+ mPropertyGroupsByFormat.put(format, propertyGroup);
+ }
+ } else {
+ // Get this property value
+ final int[] propertyList = new int[]{property};
+ propertyGroup = mPropertyGroupsByProperty.get(property);
+ if (propertyGroup == null) {
+ propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName,
+ propertyList);
+ mPropertyGroupsByProperty.put(property, propertyGroup);
+ }
}
- } else {
- propertyGroup = mPropertyGroupsByProperty.get(property);
- if (propertyGroup == null) {
- final int[] propertyList = new int[] { property };
- propertyGroup = new MtpPropertyGroup(
- this, mMediaProvider, mVolumeName, propertyList);
- mPropertyGroupsByProperty.put(property, propertyGroup);
+ int err = propertyGroup.getPropertyList(obj, ret);
+ if (err != MtpConstants.RESPONSE_OK) {
+ return new MtpPropertyList(err);
}
}
-
- return propertyGroup.getPropertyList(handle, format, depth);
+ return ret;
}
private int renameFile(int handle, String newName) {
- Cursor c = null;
-
- // first compute current path
- String path = null;
- String[] whereArgs = new String[] { Integer.toString(handle) };
- try {
- c = mMediaProvider.query(mObjectsUri, PATH_PROJECTION, ID_WHERE,
- whereArgs, null, null);
- if (c != null && c.moveToNext()) {
- path = c.getString(1);
- }
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in getObjectFilePath", e);
- return MtpConstants.RESPONSE_GENERAL_ERROR;
- } finally {
- if (c != null) {
- c.close();
- }
- }
- if (path == null) {
+ MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+ if (obj == null) {
return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
}
-
- // do not allow renaming any of the special subdirectories
- if (isStorageSubDirectory(path)) {
- return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED;
- }
+ Path oldPath = obj.getPath();
// now rename the file. make sure this succeeds before updating database
- File oldFile = new File(path);
- int lastSlash = path.lastIndexOf('/');
- if (lastSlash <= 1) {
+ if (!mManager.beginRenameObject(obj, newName))
return MtpConstants.RESPONSE_GENERAL_ERROR;
+ Path newPath = obj.getPath();
+ boolean success = oldPath.toFile().renameTo(newPath.toFile());
+ if (!mManager.endRenameObject(obj, oldPath.getFileName().toString(), success)) {
+ Log.e(TAG, "Failed to end rename object");
}
- String newPath = path.substring(0, lastSlash + 1) + newName;
- File newFile = new File(newPath);
- boolean success = oldFile.renameTo(newFile);
if (!success) {
- Log.w(TAG, "renaming "+ path + " to " + newPath + " failed");
return MtpConstants.RESPONSE_GENERAL_ERROR;
}
- // finally update database
+ // finally update MediaProvider
ContentValues values = new ContentValues();
- values.put(Files.FileColumns.DATA, newPath);
- int updated = 0;
+ values.put(Files.FileColumns.DATA, newPath.toString());
+ String[] whereArgs = new String[]{oldPath.toString()};
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.
- updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs);
+ mMediaProvider.update(mObjectsUri, values, PATH_WHERE, whereArgs);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in mMediaProvider.update", e);
}
- if (updated == 0) {
- Log.e(TAG, "Unable to update path for " + path + " to " + newPath);
- // this shouldn't happen, but if it does we need to rename the file to its original name
- newFile.renameTo(oldFile);
- return MtpConstants.RESPONSE_GENERAL_ERROR;
- }
// check if nomedia status changed
- if (newFile.isDirectory()) {
+ if (obj.isDir()) {
// for directories, check if renamed from something hidden to something non-hidden
- if (oldFile.getName().startsWith(".") && !newPath.startsWith(".")) {
+ if (oldPath.getFileName().startsWith(".") && !newPath.startsWith(".")) {
// directory was unhidden
try {
- mMediaProvider.call(MediaStore.UNHIDE_CALL, newPath, null);
+ mMediaProvider.call(MediaStore.UNHIDE_CALL, newPath.toString(), null);
} catch (RemoteException e) {
Log.e(TAG, "failed to unhide/rescan for " + newPath);
}
}
} else {
// for files, check if renamed from .nomedia to something else
- if (oldFile.getName().toLowerCase(Locale.US).equals(".nomedia")
- && !newPath.toLowerCase(Locale.US).equals(".nomedia")) {
+ if (oldPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)
+ && !newPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)) {
try {
- mMediaProvider.call(MediaStore.UNHIDE_CALL, oldFile.getParent(), null);
+ mMediaProvider.call(MediaStore.UNHIDE_CALL,
+ oldPath.getParent().toString(), null);
} catch (RemoteException e) {
Log.e(TAG, "failed to unhide/rescan for " + newPath);
}
}
}
-
return MtpConstants.RESPONSE_OK;
}
- private int moveObject(int handle, int newParent, int newStorage, String newPath) {
- String[] whereArgs = new String[] { Integer.toString(handle) };
+ private int beginMoveObject(int handle, int newParent, int newStorage) {
+ MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+ MtpStorageManager.MtpObject parent = newParent == 0 ?
+ mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
+ if (obj == null || parent == null)
+ return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
+
+ boolean allowed = mManager.beginMoveObject(obj, parent);
+ return allowed ? MtpConstants.RESPONSE_OK : MtpConstants.RESPONSE_GENERAL_ERROR;
+ }
- // do not allow renaming any of the special subdirectories
- if (isStorageSubDirectory(newPath)) {
- return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED;
+ private void endMoveObject(int oldParent, int newParent, int oldStorage, int newStorage,
+ int objId, boolean success) {
+ MtpStorageManager.MtpObject oldParentObj = oldParent == 0 ?
+ mManager.getStorageRoot(oldStorage) : mManager.getObject(oldParent);
+ MtpStorageManager.MtpObject newParentObj = newParent == 0 ?
+ mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
+ MtpStorageManager.MtpObject obj = mManager.getObject(objId);
+ String name = obj.getName();
+ if (newParentObj == null || oldParentObj == null
+ ||!mManager.endMoveObject(oldParentObj, newParentObj, name, success)) {
+ Log.e(TAG, "Failed to end move object");
+ return;
}
- // update database
+ obj = mManager.getObject(objId);
+ if (!success || obj == null)
+ return;
+ // Get parent info from MediaProvider, since the id is different from MTP's
ContentValues values = new ContentValues();
- values.put(Files.FileColumns.DATA, newPath);
- values.put(Files.FileColumns.PARENT, newParent);
- values.put(Files.FileColumns.STORAGE_ID, newStorage);
- int updated = 0;
+ Path path = newParentObj.getPath().resolve(name);
+ Path oldPath = oldParentObj.getPath().resolve(name);
+ values.put(Files.FileColumns.DATA, path.toString());
+ if (obj.getParent().isRoot()) {
+ values.put(Files.FileColumns.PARENT, 0);
+ } else {
+ int parentId = findInMedia(path.getParent());
+ if (parentId != -1) {
+ values.put(Files.FileColumns.PARENT, parentId);
+ } else {
+ // The new parent isn't in MediaProvider, so delete the object instead
+ deleteFromMedia(oldPath, obj.isDir());
+ return;
+ }
+ }
+ // update MediaProvider
+ Cursor c = null;
+ String[] whereArgs = new String[]{oldPath.toString()};
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.
- updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs);
+ int parentId = -1;
+ if (!oldParentObj.isRoot()) {
+ parentId = findInMedia(oldPath.getParent());
+ }
+ if (oldParentObj.isRoot() || parentId != -1) {
+ // 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.
+ mMediaProvider.update(mObjectsUri, values, PATH_WHERE, whereArgs);
+ } else {
+ // Old parent doesn't exist - add the object
+ values.put(Files.FileColumns.FORMAT, obj.getFormat());
+ values.put(Files.FileColumns.SIZE, obj.getSize());
+ values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
+ Uri uri = mMediaProvider.insert(mObjectsUri, values);
+ if (uri != null) {
+ rescanFile(path.toString(),
+ Integer.parseInt(uri.getPathSegments().get(2)), obj.getFormat());
+ }
+ }
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in mMediaProvider.update", e);
}
- if (updated == 0) {
- Log.e(TAG, "Unable to update path for " + handle + " to " + newPath);
- return MtpConstants.RESPONSE_GENERAL_ERROR;
+ }
+
+ private int beginCopyObject(int handle, int newParent, int newStorage) {
+ MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+ MtpStorageManager.MtpObject parent = newParent == 0 ?
+ mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
+ if (obj == null || parent == null)
+ return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
+ return mManager.beginCopyObject(obj, parent);
+ }
+
+ private void endCopyObject(int handle, boolean success) {
+ MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+ if (obj == null || !mManager.endCopyObject(obj, success)) {
+ Log.e(TAG, "Failed to end copy object");
+ return;
+ }
+ if (!success) {
+ return;
+ }
+ String path = obj.getPath().toString();
+ int format = obj.getFormat();
+ // Get parent info from MediaProvider, since the id is different from MTP's
+ ContentValues values = new ContentValues();
+ values.put(Files.FileColumns.DATA, path);
+ values.put(Files.FileColumns.FORMAT, format);
+ values.put(Files.FileColumns.SIZE, obj.getSize());
+ values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
+ try {
+ if (obj.getParent().isRoot()) {
+ values.put(Files.FileColumns.PARENT, 0);
+ } else {
+ int parentId = findInMedia(obj.getParent().getPath());
+ if (parentId != -1) {
+ values.put(Files.FileColumns.PARENT, parentId);
+ } else {
+ // The parent isn't in MediaProvider. Don't add the new file.
+ return;
+ }
+ }
+ if (obj.isDir()) {
+ mMediaScanner.scanDirectories(new String[]{path});
+ } else {
+ Uri uri = mMediaProvider.insert(mObjectsUri, values);
+ if (uri != null) {
+ rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format);
+ }
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in beginSendObject", e);
}
- return MtpConstants.RESPONSE_OK;
}
private int setObjectProperty(int handle, int property,
- long intValue, String stringValue) {
+ long intValue, String stringValue) {
switch (property) {
case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
return renameFile(handle, stringValue);
@@ -906,24 +748,23 @@ public class MtpDatabase implements AutoCloseable {
value.getChars(0, length, outStringValue, 0);
outStringValue[length] = 0;
return MtpConstants.RESPONSE_OK;
-
case MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE:
// use screen size as max image size
- Display display = ((WindowManager)mContext.getSystemService(
+ Display display = ((WindowManager) mContext.getSystemService(
Context.WINDOW_SERVICE)).getDefaultDisplay();
int width = display.getMaximumSizeDimension();
int height = display.getMaximumSizeDimension();
- String imageSize = Integer.toString(width) + "x" + Integer.toString(height);
+ String imageSize = Integer.toString(width) + "x" + Integer.toString(height);
imageSize.getChars(0, imageSize.length(), outStringValue, 0);
outStringValue[imageSize.length()] = 0;
return MtpConstants.RESPONSE_OK;
-
case MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE:
outIntValue[0] = mDeviceType;
return MtpConstants.RESPONSE_OK;
-
- // DEVICE_PROPERTY_BATTERY_LEVEL is implemented in the JNI code
-
+ case MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL:
+ outIntValue[0] = mBatteryLevel;
+ outIntValue[1] = mBatteryScale;
+ return MtpConstants.RESPONSE_OK;
default:
return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
}
@@ -944,179 +785,144 @@ public class MtpDatabase implements AutoCloseable {
}
private boolean getObjectInfo(int handle, int[] outStorageFormatParent,
- char[] outName, long[] outCreatedModified) {
- Cursor c = null;
- try {
- c = mMediaProvider.query(mObjectsUri, OBJECT_INFO_PROJECTION,
- ID_WHERE, new String[] { Integer.toString(handle) }, null, null);
- if (c != null && c.moveToNext()) {
- outStorageFormatParent[0] = c.getInt(1);
- outStorageFormatParent[1] = c.getInt(2);
- outStorageFormatParent[2] = c.getInt(3);
-
- // extract name from path
- String path = c.getString(4);
- int lastSlash = path.lastIndexOf('/');
- int start = (lastSlash >= 0 ? lastSlash + 1 : 0);
- int end = path.length();
- if (end - start > 255) {
- end = start + 255;
- }
- path.getChars(start, end, outName, 0);
- outName[end - start] = 0;
-
- outCreatedModified[0] = c.getLong(5);
- outCreatedModified[1] = c.getLong(6);
- // use modification date as creation date if date added is not set
- if (outCreatedModified[0] == 0) {
- outCreatedModified[0] = outCreatedModified[1];
- }
- return true;
- }
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in getObjectInfo", e);
- } finally {
- if (c != null) {
- c.close();
- }
+ char[] outName, long[] outCreatedModified) {
+ MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+ if (obj == null) {
+ return false;
}
- return false;
+ outStorageFormatParent[0] = obj.getStorageId();
+ outStorageFormatParent[1] = obj.getFormat();
+ outStorageFormatParent[2] = obj.getParent().isRoot() ? 0 : obj.getParent().getId();
+
+ int nameLen = Integer.min(obj.getName().length(), 255);
+ obj.getName().getChars(0, nameLen, outName, 0);
+ outName[nameLen] = 0;
+
+ outCreatedModified[0] = obj.getModifiedTime();
+ outCreatedModified[1] = obj.getModifiedTime();
+ return true;
}
private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) {
- if (handle == 0) {
- // special case root directory
- mMediaStoragePath.getChars(0, mMediaStoragePath.length(), outFilePath, 0);
- outFilePath[mMediaStoragePath.length()] = 0;
- outFileLengthFormat[0] = 0;
- outFileLengthFormat[1] = MtpConstants.FORMAT_ASSOCIATION;
- return MtpConstants.RESPONSE_OK;
+ MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+ if (obj == null) {
+ return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
}
- Cursor c = null;
- try {
- c = mMediaProvider.query(mObjectsUri, PATH_FORMAT_PROJECTION,
- ID_WHERE, new String[] { Integer.toString(handle) }, null, null);
- if (c != null && c.moveToNext()) {
- String path = c.getString(1);
- path.getChars(0, path.length(), outFilePath, 0);
- outFilePath[path.length()] = 0;
- // File transfers from device to host will likely fail if the size is incorrect.
- // So to be safe, use the actual file size here.
- outFileLengthFormat[0] = new File(path).length();
- outFileLengthFormat[1] = c.getLong(2);
- return MtpConstants.RESPONSE_OK;
- } else {
- return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
- }
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in getObjectFilePath", e);
+
+ String path = obj.getPath().toString();
+ int pathLen = Integer.min(path.length(), 4096);
+ path.getChars(0, pathLen, outFilePath, 0);
+ outFilePath[pathLen] = 0;
+
+ outFileLengthFormat[0] = obj.getSize();
+ outFileLengthFormat[1] = obj.getFormat();
+ return MtpConstants.RESPONSE_OK;
+ }
+
+ private int getObjectFormat(int handle) {
+ MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+ if (obj == null) {
+ return -1;
+ }
+ return obj.getFormat();
+ }
+
+ private int beginDeleteObject(int handle) {
+ MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+ if (obj == null) {
+ return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
+ }
+ if (!mManager.beginRemoveObject(obj)) {
return MtpConstants.RESPONSE_GENERAL_ERROR;
- } finally {
- if (c != null) {
- c.close();
- }
}
+ return MtpConstants.RESPONSE_OK;
}
- private int getObjectFormat(int handle) {
+ private void endDeleteObject(int handle, boolean success) {
+ MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+ if (obj == null) {
+ return;
+ }
+ if (!mManager.endRemoveObject(obj, success))
+ Log.e(TAG, "Failed to end remove object");
+ if (success)
+ deleteFromMedia(obj.getPath(), obj.isDir());
+ }
+
+ private int findInMedia(Path path) {
+ int ret = -1;
Cursor c = null;
try {
- c = mMediaProvider.query(mObjectsUri, FORMAT_PROJECTION,
- ID_WHERE, new String[] { Integer.toString(handle) }, null, null);
+ c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE,
+ new String[]{path.toString()}, null, null);
if (c != null && c.moveToNext()) {
- return c.getInt(1);
- } else {
- return -1;
+ ret = c.getInt(0);
}
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException in getObjectFilePath", e);
- return -1;
+ Log.e(TAG, "Error finding " + path + " in MediaProvider");
} finally {
- if (c != null) {
+ if (c != null)
c.close();
- }
}
+ return ret;
}
- private int deleteFile(int handle) {
- mDatabaseModified = true;
- String path = null;
- int format = 0;
-
- Cursor c = null;
+ private void deleteFromMedia(Path path, boolean isDir) {
try {
- c = mMediaProvider.query(mObjectsUri, PATH_FORMAT_PROJECTION,
- ID_WHERE, new String[] { Integer.toString(handle) }, null, null);
- if (c != null && c.moveToNext()) {
- // don't convert to media path here, since we will be matching
- // against paths in the database matching /data/media
- path = c.getString(1);
- format = c.getInt(2);
- } else {
- return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
- }
-
- if (path == null || format == 0) {
- return MtpConstants.RESPONSE_GENERAL_ERROR;
- }
-
- // do not allow deleting any of the special subdirectories
- if (isStorageSubDirectory(path)) {
- return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED;
- }
-
- if (format == MtpConstants.FORMAT_ASSOCIATION) {
+ // Delete the object(s) from MediaProvider, but ignore errors.
+ if (isDir) {
// recursive case - delete all children first
- Uri uri = Files.getMtpObjectsUri(mVolumeName);
- int count = mMediaProvider.delete(uri,
- // the 'like' makes it use the index, the 'lower()' makes it correct
- // when the path contains sqlite wildcard characters
- "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
- new String[] { path + "/%",Integer.toString(path.length() + 1), path + "/"});
+ mMediaProvider.delete(mObjectsUri,
+ // the 'like' makes it use the index, the 'lower()' makes it correct
+ // when the path contains sqlite wildcard characters
+ "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
+ new String[]{path + "/%", Integer.toString(path.toString().length() + 1),
+ path.toString() + "/"});
}
- Uri uri = Files.getMtpObjectsUri(mVolumeName, handle);
- if (mMediaProvider.delete(uri, null, null) > 0) {
- if (format != MtpConstants.FORMAT_ASSOCIATION
- && path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
+ String[] whereArgs = new String[]{path.toString()};
+ if (mMediaProvider.delete(mObjectsUri, PATH_WHERE, whereArgs) > 0) {
+ if (!isDir && path.toString().toLowerCase(Locale.US).endsWith(NO_MEDIA)) {
try {
- String parentPath = path.substring(0, path.lastIndexOf("/"));
+ String parentPath = path.getParent().toString();
mMediaProvider.call(MediaStore.UNHIDE_CALL, parentPath, null);
} catch (RemoteException e) {
Log.e(TAG, "failed to unhide/rescan for " + path);
}
}
- return MtpConstants.RESPONSE_OK;
} else {
- return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
- }
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in deleteFile", e);
- return MtpConstants.RESPONSE_GENERAL_ERROR;
- } finally {
- if (c != null) {
- c.close();
+ Log.i(TAG, "Mediaprovider didn't delete " + path);
}
+ } catch (Exception e) {
+ Log.d(TAG, "Failed to delete " + path + " from MediaProvider");
}
}
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.getPath());
+ if (handle == -1)
+ return null;
Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
Cursor c = null;
try {
- c = mMediaProvider.query(uri, ID_PROJECTION, null, null, null, null);
+ c = mMediaProvider.query(uri, PATH_PROJECTION, null, null, null, null);
if (c == null) {
return null;
}
- int count = c.getCount();
- if (count > 0) {
- int[] result = new int[count];
- for (int i = 0; i < count; i++) {
- c.moveToNext();
- result[i] = c.getInt(0);
+ 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;
- }
+ return result.stream().mapToInt(Integer::intValue).toArray();
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in getObjectList", e);
} finally {
@@ -1128,17 +934,29 @@ public class MtpDatabase implements AutoCloseable {
}
private int setObjectReferences(int handle, int[] references) {
- mDatabaseModified = true;
+ 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.getPath());
+ if (handle == -1)
+ return MtpConstants.RESPONSE_GENERAL_ERROR;
Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
- int count = references.length;
- ContentValues[] valuesList = new ContentValues[count];
- for (int i = 0; i < count; i++) {
+ 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.getPath());
+ if (refHandle == -1)
+ continue;
ContentValues values = new ContentValues();
- values.put(Files.FileColumns._ID, references[i]);
- valuesList[i] = values;
+ values.put(Files.FileColumns._ID, refHandle);
+ valuesList.add(values);
}
try {
- if (mMediaProvider.bulkInsert(uri, valuesList) > 0) {
+ if (mMediaProvider.bulkInsert(uri, valuesList.toArray(new ContentValues[0])) > 0) {
return MtpConstants.RESPONSE_OK;
}
} catch (RemoteException e) {
@@ -1147,17 +965,6 @@ public class MtpDatabase implements AutoCloseable {
return MtpConstants.RESPONSE_GENERAL_ERROR;
}
- private void sessionStarted() {
- mDatabaseModified = false;
- }
-
- private void sessionEnded() {
- if (mDatabaseModified) {
- mUserContext.sendBroadcast(new Intent(MediaStore.ACTION_MTP_SESSION_END));
- mDatabaseModified = false;
- }
- }
-
// used by the JNI code
private long mNativeContext;
diff --git a/android/mtp/MtpPropertyGroup.java b/android/mtp/MtpPropertyGroup.java
index dea30083..77d0f34f 100644
--- a/android/mtp/MtpPropertyGroup.java
+++ b/android/mtp/MtpPropertyGroup.java
@@ -23,22 +23,21 @@ import android.os.RemoteException;
import android.provider.MediaStore.Audio;
import android.provider.MediaStore.Files;
import android.provider.MediaStore.Images;
-import android.provider.MediaStore.MediaColumns;
import android.util.Log;
import java.util.ArrayList;
+/**
+ * MtpPropertyGroup represents a list of MTP properties.
+ * {@hide}
+ */
class MtpPropertyGroup {
-
- private static final String TAG = "MtpPropertyGroup";
+ private static final String TAG = MtpPropertyGroup.class.getSimpleName();
private class Property {
- // MTP property code
- int code;
- // MTP data type
- int type;
- // column index for our query
- int column;
+ int code;
+ int type;
+ int column;
Property(int code, int type, int column) {
this.code = code;
@@ -47,32 +46,26 @@ class MtpPropertyGroup {
}
}
- private final MtpDatabase mDatabase;
private final ContentProviderClient mProvider;
private final String mVolumeName;
private final Uri mUri;
// list of all properties in this group
- private final Property[] mProperties;
+ private final Property[] mProperties;
// list of columns for database query
- private String[] mColumns;
+ private String[] mColumns;
+
+ private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
- private static final String ID_WHERE = Files.FileColumns._ID + "=?";
- private static final String FORMAT_WHERE = Files.FileColumns.FORMAT + "=?";
- private static final String ID_FORMAT_WHERE = ID_WHERE + " AND " + FORMAT_WHERE;
- private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?";
- private static final String PARENT_FORMAT_WHERE = PARENT_WHERE + " AND " + FORMAT_WHERE;
// constructs a property group for a list of properties
- public MtpPropertyGroup(MtpDatabase database, ContentProviderClient provider, String volumeName,
- int[] properties) {
- mDatabase = database;
+ public MtpPropertyGroup(ContentProviderClient provider, String volumeName, int[] properties) {
mProvider = provider;
mVolumeName = volumeName;
mUri = Files.getMtpObjectsUri(volumeName);
int count = properties.length;
- ArrayList<String> columns = new ArrayList<String>(count);
+ ArrayList<String> columns = new ArrayList<>(count);
columns.add(Files.FileColumns._ID);
mProperties = new Property[count];
@@ -90,37 +83,29 @@ class MtpPropertyGroup {
String column = null;
int type;
- switch (code) {
+ switch (code) {
case MtpConstants.PROPERTY_STORAGE_ID:
- column = Files.FileColumns.STORAGE_ID;
type = MtpConstants.TYPE_UINT32;
break;
- case MtpConstants.PROPERTY_OBJECT_FORMAT:
- column = Files.FileColumns.FORMAT;
+ case MtpConstants.PROPERTY_OBJECT_FORMAT:
type = MtpConstants.TYPE_UINT16;
break;
case MtpConstants.PROPERTY_PROTECTION_STATUS:
- // protection status is always 0
type = MtpConstants.TYPE_UINT16;
break;
case MtpConstants.PROPERTY_OBJECT_SIZE:
- column = Files.FileColumns.SIZE;
type = MtpConstants.TYPE_UINT64;
break;
case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
- column = Files.FileColumns.DATA;
type = MtpConstants.TYPE_STR;
break;
case MtpConstants.PROPERTY_NAME:
- column = MediaColumns.TITLE;
type = MtpConstants.TYPE_STR;
break;
case MtpConstants.PROPERTY_DATE_MODIFIED:
- column = Files.FileColumns.DATE_MODIFIED;
type = MtpConstants.TYPE_STR;
break;
case MtpConstants.PROPERTY_DATE_ADDED:
- column = Files.FileColumns.DATE_ADDED;
type = MtpConstants.TYPE_STR;
break;
case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
@@ -128,12 +113,9 @@ class MtpPropertyGroup {
type = MtpConstants.TYPE_STR;
break;
case MtpConstants.PROPERTY_PARENT_OBJECT:
- column = Files.FileColumns.PARENT;
type = MtpConstants.TYPE_UINT32;
break;
case MtpConstants.PROPERTY_PERSISTENT_UID:
- // PUID is concatenation of storageID and object handle
- column = Files.FileColumns.STORAGE_ID;
type = MtpConstants.TYPE_UINT128;
break;
case MtpConstants.PROPERTY_DURATION:
@@ -145,7 +127,6 @@ class MtpPropertyGroup {
type = MtpConstants.TYPE_UINT16;
break;
case MtpConstants.PROPERTY_DISPLAY_NAME:
- column = MediaColumns.DISPLAY_NAME;
type = MtpConstants.TYPE_STR;
break;
case MtpConstants.PROPERTY_ARTIST:
@@ -195,40 +176,19 @@ class MtpPropertyGroup {
}
}
- private String queryString(int id, String column) {
- Cursor c = null;
- try {
- // for now we are only reading properties from the "objects" table
- c = mProvider.query(mUri,
- new String [] { Files.FileColumns._ID, column },
- ID_WHERE, new String[] { Integer.toString(id) }, null, null);
- if (c != null && c.moveToNext()) {
- return c.getString(1);
- } else {
- return "";
- }
- } catch (Exception e) {
- return null;
- } finally {
- if (c != null) {
- c.close();
- }
- }
- }
-
- private String queryAudio(int id, String column) {
+ private String queryAudio(String path, String column) {
Cursor c = null;
try {
c = mProvider.query(Audio.Media.getContentUri(mVolumeName),
- new String [] { Files.FileColumns._ID, column },
- ID_WHERE, new String[] { Integer.toString(id) }, null, null);
+ new String [] { column },
+ PATH_WHERE, new String[] {path}, null, null);
if (c != null && c.moveToNext()) {
- return c.getString(1);
+ return c.getString(0);
} else {
return "";
}
} catch (Exception e) {
- return null;
+ return "";
} finally {
if (c != null) {
c.close();
@@ -236,21 +196,19 @@ class MtpPropertyGroup {
}
}
- private String queryGenre(int id) {
+ private String queryGenre(String path) {
Cursor c = null;
try {
- Uri uri = Audio.Genres.getContentUriForAudioId(mVolumeName, id);
- c = mProvider.query(uri,
- new String [] { Files.FileColumns._ID, Audio.GenresColumns.NAME },
- null, null, null, null);
+ c = mProvider.query(Audio.Genres.getContentUri(mVolumeName),
+ new String [] { Audio.GenresColumns.NAME },
+ PATH_WHERE, new String[] {path}, null, null);
if (c != null && c.moveToNext()) {
- return c.getString(1);
+ return c.getString(0);
} else {
return "";
}
} catch (Exception e) {
- Log.e(TAG, "queryGenre exception", e);
- return null;
+ return "";
} finally {
if (c != null) {
c.close();
@@ -258,211 +216,127 @@ class MtpPropertyGroup {
}
}
- private Long queryLong(int id, String column) {
- Cursor c = null;
- try {
- // for now we are only reading properties from the "objects" table
- c = mProvider.query(mUri,
- new String [] { Files.FileColumns._ID, column },
- ID_WHERE, new String[] { Integer.toString(id) }, null, null);
- if (c != null && c.moveToNext()) {
- return new Long(c.getLong(1));
- }
- } catch (Exception e) {
- } finally {
- if (c != null) {
- c.close();
- }
- }
- return null;
- }
-
- private static String nameFromPath(String path) {
- // extract name from full path
- int start = 0;
- int lastSlash = path.lastIndexOf('/');
- if (lastSlash >= 0) {
- start = lastSlash + 1;
- }
- int end = path.length();
- if (end - start > 255) {
- end = start + 255;
- }
- return path.substring(start, end);
- }
-
- MtpPropertyList getPropertyList(int handle, int format, int depth) {
- //Log.d(TAG, "getPropertyList handle: " + handle + " format: " + format + " depth: " + depth);
- if (depth > 1) {
- // we only support depth 0 and 1
- // depth 0: single object, depth 1: immediate children
- return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED);
- }
-
- String where;
- String[] whereArgs;
- if (format == 0) {
- if (handle == 0xFFFFFFFF) {
- // select all objects
- where = null;
- whereArgs = null;
- } else {
- whereArgs = new String[] { Integer.toString(handle) };
- if (depth == 1) {
- where = PARENT_WHERE;
- } else {
- where = ID_WHERE;
- }
- }
- } else {
- if (handle == 0xFFFFFFFF) {
- // select all objects with given format
- where = FORMAT_WHERE;
- whereArgs = new String[] { Integer.toString(format) };
- } else {
- whereArgs = new String[] { Integer.toString(handle), Integer.toString(format) };
- if (depth == 1) {
- where = PARENT_FORMAT_WHERE;
- } else {
- where = ID_FORMAT_WHERE;
- }
- }
- }
-
+ /**
+ * Gets the values of the properties represented by this property group for the given
+ * object and adds them to the given property list.
+ * @return Response_OK if the operation succeeded.
+ */
+ public int getPropertyList(MtpStorageManager.MtpObject object, MtpPropertyList list) {
Cursor c = null;
- try {
- // don't query if not necessary
- if (depth > 0 || handle == 0xFFFFFFFF || mColumns.length > 1) {
- c = mProvider.query(mUri, mColumns, where, whereArgs, null, null);
- if (c == null) {
- return new MtpPropertyList(0, MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
+ int id = object.getId();
+ String path = object.getPath().toString();
+ for (Property property : mProperties) {
+ if (property.column != -1 && c == null) {
+ try {
+ // Look up the entry in MediaProvider only if one of those properties is needed.
+ c = mProvider.query(mUri, mColumns,
+ PATH_WHERE, new String[] {path}, null, null);
+ if (c != null && !c.moveToNext()) {
+ c.close();
+ c = null;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Mediaprovider lookup failed");
}
}
-
- int count = (c == null ? 1 : c.getCount());
- MtpPropertyList result = new MtpPropertyList(count * mProperties.length,
- MtpConstants.RESPONSE_OK);
-
- // iterate over all objects in the query
- for (int objectIndex = 0; objectIndex < count; objectIndex++) {
- if (c != null) {
- c.moveToNext();
- handle = (int)c.getLong(0);
- }
-
- // iterate over all properties in the query for the given object
- for (int propertyIndex = 0; propertyIndex < mProperties.length; propertyIndex++) {
- Property property = mProperties[propertyIndex];
- int propertyCode = property.code;
- int column = property.column;
-
- // handle some special cases
- switch (propertyCode) {
- case MtpConstants.PROPERTY_PROTECTION_STATUS:
- // protection status is always 0
- result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 0);
- break;
- case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
- // special case - need to extract file name from full path
- String value = c.getString(column);
- if (value != null) {
- result.append(handle, propertyCode, nameFromPath(value));
- } else {
- result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
- }
- break;
- case MtpConstants.PROPERTY_NAME:
- // first try title
- String name = c.getString(column);
- // then try name
- if (name == null) {
- name = queryString(handle, Audio.PlaylistsColumns.NAME);
- }
- // if title and name fail, extract name from full path
- if (name == null) {
- name = queryString(handle, Files.FileColumns.DATA);
- if (name != null) {
- name = nameFromPath(name);
- }
- }
- if (name != null) {
- result.append(handle, propertyCode, name);
- } else {
- result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
- }
- break;
- case MtpConstants.PROPERTY_DATE_MODIFIED:
- case MtpConstants.PROPERTY_DATE_ADDED:
- // convert from seconds to DateTime
- result.append(handle, propertyCode, format_date_time(c.getInt(column)));
- break;
- case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
- // release date is stored internally as just the year
- int year = c.getInt(column);
- String dateTime = Integer.toString(year) + "0101T000000";
- result.append(handle, propertyCode, dateTime);
- break;
- case MtpConstants.PROPERTY_PERSISTENT_UID:
- // PUID is concatenation of storageID and object handle
- long puid = c.getLong(column);
- puid <<= 32;
- puid += handle;
- result.append(handle, propertyCode, MtpConstants.TYPE_UINT128, puid);
- break;
- case MtpConstants.PROPERTY_TRACK:
- result.append(handle, propertyCode, MtpConstants.TYPE_UINT16,
- c.getInt(column) % 1000);
- break;
- case MtpConstants.PROPERTY_ARTIST:
- result.append(handle, propertyCode,
- queryAudio(handle, Audio.AudioColumns.ARTIST));
- break;
- case MtpConstants.PROPERTY_ALBUM_NAME:
- result.append(handle, propertyCode,
- queryAudio(handle, Audio.AudioColumns.ALBUM));
- break;
- case MtpConstants.PROPERTY_GENRE:
- String genre = queryGenre(handle);
- if (genre != null) {
- result.append(handle, propertyCode, genre);
- } else {
- result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
- }
- break;
- case MtpConstants.PROPERTY_AUDIO_WAVE_CODEC:
- case MtpConstants.PROPERTY_AUDIO_BITRATE:
- case MtpConstants.PROPERTY_SAMPLE_RATE:
- // we don't have these in our database, so return 0
- result.append(handle, propertyCode, MtpConstants.TYPE_UINT32, 0);
+ switch (property.code) {
+ case MtpConstants.PROPERTY_PROTECTION_STATUS:
+ // protection status is always 0
+ list.append(id, property.code, property.type, 0);
+ break;
+ case MtpConstants.PROPERTY_NAME:
+ case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
+ case MtpConstants.PROPERTY_DISPLAY_NAME:
+ list.append(id, property.code, object.getName());
+ break;
+ case MtpConstants.PROPERTY_DATE_MODIFIED:
+ case MtpConstants.PROPERTY_DATE_ADDED:
+ // convert from seconds to DateTime
+ list.append(id, property.code,
+ format_date_time(object.getModifiedTime()));
+ break;
+ case MtpConstants.PROPERTY_STORAGE_ID:
+ list.append(id, property.code, property.type, object.getStorageId());
+ break;
+ case MtpConstants.PROPERTY_OBJECT_FORMAT:
+ list.append(id, property.code, property.type, object.getFormat());
+ break;
+ case MtpConstants.PROPERTY_OBJECT_SIZE:
+ list.append(id, property.code, property.type, object.getSize());
+ break;
+ case MtpConstants.PROPERTY_PARENT_OBJECT:
+ list.append(id, property.code, property.type,
+ object.getParent().isRoot() ? 0 : object.getParent().getId());
+ break;
+ case MtpConstants.PROPERTY_PERSISTENT_UID:
+ // The persistent uid must be unique and never reused among all objects,
+ // and remain the same between sessions.
+ long puid = (object.getPath().toString().hashCode() << 32)
+ + object.getModifiedTime();
+ list.append(id, property.code, property.type, puid);
+ break;
+ case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
+ // release date is stored internally as just the year
+ int year = 0;
+ if (c != null)
+ year = c.getInt(property.column);
+ String dateTime = Integer.toString(year) + "0101T000000";
+ list.append(id, property.code, dateTime);
+ break;
+ case MtpConstants.PROPERTY_TRACK:
+ int track = 0;
+ if (c != null)
+ track = c.getInt(property.column);
+ list.append(id, property.code, MtpConstants.TYPE_UINT16,
+ track % 1000);
+ break;
+ case MtpConstants.PROPERTY_ARTIST:
+ list.append(id, property.code,
+ queryAudio(path, Audio.AudioColumns.ARTIST));
+ break;
+ case MtpConstants.PROPERTY_ALBUM_NAME:
+ list.append(id, property.code,
+ queryAudio(path, Audio.AudioColumns.ALBUM));
+ break;
+ case MtpConstants.PROPERTY_GENRE:
+ String genre = queryGenre(path);
+ if (genre != null) {
+ list.append(id, property.code, genre);
+ }
+ break;
+ case MtpConstants.PROPERTY_AUDIO_WAVE_CODEC:
+ case MtpConstants.PROPERTY_AUDIO_BITRATE:
+ case MtpConstants.PROPERTY_SAMPLE_RATE:
+ // we don't have these in our database, so return 0
+ list.append(id, property.code, MtpConstants.TYPE_UINT32, 0);
+ break;
+ case MtpConstants.PROPERTY_BITRATE_TYPE:
+ case MtpConstants.PROPERTY_NUMBER_OF_CHANNELS:
+ // we don't have these in our database, so return 0
+ list.append(id, property.code, MtpConstants.TYPE_UINT16, 0);
+ break;
+ default:
+ switch(property.type) {
+ case MtpConstants.TYPE_UNDEFINED:
+ list.append(id, property.code, property.type, 0);
break;
- case MtpConstants.PROPERTY_BITRATE_TYPE:
- case MtpConstants.PROPERTY_NUMBER_OF_CHANNELS:
- // we don't have these in our database, so return 0
- result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 0);
+ case MtpConstants.TYPE_STR:
+ String value = "";
+ if (c != null)
+ value = c.getString(property.column);
+ list.append(id, property.code, value);
break;
default:
- if (property.type == MtpConstants.TYPE_STR) {
- result.append(handle, propertyCode, c.getString(column));
- } else if (property.type == MtpConstants.TYPE_UNDEFINED) {
- result.append(handle, propertyCode, property.type, 0);
- } else {
- result.append(handle, propertyCode, property.type,
- c.getLong(column));
- }
- break;
+ long longValue = 0L;
+ if (c != null)
+ longValue = c.getLong(property.column);
+ list.append(id, property.code, property.type, longValue);
}
- }
- }
-
- return result;
- } catch (RemoteException e) {
- return new MtpPropertyList(0, MtpConstants.RESPONSE_GENERAL_ERROR);
- } finally {
- if (c != null) {
- c.close();
}
}
- // impossible to get here, so no return statement
+ if (c != null)
+ c.close();
+ return MtpConstants.RESPONSE_OK;
}
private native String format_date_time(long seconds);
diff --git a/android/mtp/MtpPropertyList.java b/android/mtp/MtpPropertyList.java
index f9bc603e..ede90dac 100644
--- a/android/mtp/MtpPropertyList.java
+++ b/android/mtp/MtpPropertyList.java
@@ -16,6 +16,9 @@
package android.mtp;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Encapsulates the ObjectPropList dataset used by the GetObjectPropList command.
* The fields of this class are read by JNI code in android_media_MtpDatabase.cpp
@@ -23,56 +26,70 @@ package android.mtp;
class MtpPropertyList {
- // number of results returned
- private int mCount;
- // maximum number of results
- private final int mMaxCount;
- // result code for GetObjectPropList
- public int mResult;
// list of object handles (first field in quadruplet)
- public final int[] mObjectHandles;
- // list of object propery codes (second field in quadruplet)
- public final int[] mPropertyCodes;
+ private List<Integer> mObjectHandles;
+ // list of object property codes (second field in quadruplet)
+ private List<Integer> mPropertyCodes;
// list of data type codes (third field in quadruplet)
- public final int[] mDataTypes;
+ private List<Integer> mDataTypes;
// list of long int property values (fourth field in quadruplet, when value is integer type)
- public long[] mLongValues;
+ private List<Long> mLongValues;
// list of long int property values (fourth field in quadruplet, when value is string type)
- public String[] mStringValues;
-
- // constructor only called from MtpDatabase
- public MtpPropertyList(int maxCount, int result) {
- mMaxCount = maxCount;
- mResult = result;
- mObjectHandles = new int[maxCount];
- mPropertyCodes = new int[maxCount];
- mDataTypes = new int[maxCount];
- // mLongValues and mStringValues are created lazily since both might not be necessary
+ private List<String> mStringValues;
+
+ // Return value of this operation
+ private int mCode;
+
+ public MtpPropertyList(int code) {
+ mCode = code;
+ mObjectHandles = new ArrayList<>();
+ mPropertyCodes = new ArrayList<>();
+ mDataTypes = new ArrayList<>();
+ mLongValues = new ArrayList<>();
+ mStringValues = new ArrayList<>();
}
public void append(int handle, int property, int type, long value) {
- int index = mCount++;
- if (mLongValues == null) {
- mLongValues = new long[mMaxCount];
- }
- mObjectHandles[index] = handle;
- mPropertyCodes[index] = property;
- mDataTypes[index] = type;
- mLongValues[index] = value;
+ mObjectHandles.add(handle);
+ mPropertyCodes.add(property);
+ mDataTypes.add(type);
+ mLongValues.add(value);
+ mStringValues.add(null);
}
public void append(int handle, int property, String value) {
- int index = mCount++;
- if (mStringValues == null) {
- mStringValues = new String[mMaxCount];
- }
- mObjectHandles[index] = handle;
- mPropertyCodes[index] = property;
- mDataTypes[index] = MtpConstants.TYPE_STR;
- mStringValues[index] = value;
+ mObjectHandles.add(handle);
+ mPropertyCodes.add(property);
+ mDataTypes.add(MtpConstants.TYPE_STR);
+ mStringValues.add(value);
+ mLongValues.add(0L);
+ }
+
+ public int getCode() {
+ return mCode;
+ }
+
+ public int getCount() {
+ return mObjectHandles.size();
+ }
+
+ public int[] getObjectHandles() {
+ return mObjectHandles.stream().mapToInt(Integer::intValue).toArray();
+ }
+
+ public int[] getPropertyCodes() {
+ return mPropertyCodes.stream().mapToInt(Integer::intValue).toArray();
+ }
+
+ public int[] getDataTypes() {
+ return mDataTypes.stream().mapToInt(Integer::intValue).toArray();
+ }
+
+ public long[] getLongValues() {
+ return mLongValues.stream().mapToLong(Long::longValue).toArray();
}
- public void setResult(int result) {
- mResult = result;
+ public String[] getStringValues() {
+ return mStringValues.toArray(new String[0]);
}
}
diff --git a/android/mtp/MtpStorage.java b/android/mtp/MtpStorage.java
index 6ca442c7..c72b827d 100644
--- a/android/mtp/MtpStorage.java
+++ b/android/mtp/MtpStorage.java
@@ -31,15 +31,13 @@ public class MtpStorage {
private final int mStorageId;
private final String mPath;
private final String mDescription;
- private final long mReserveSpace;
private final boolean mRemovable;
private final long mMaxFileSize;
- public MtpStorage(StorageVolume volume, Context context) {
- mStorageId = volume.getStorageId();
+ public MtpStorage(StorageVolume volume, int storageId) {
+ mStorageId = storageId;
mPath = volume.getPath();
- mDescription = volume.getDescription(context);
- mReserveSpace = volume.getMtpReserveSpace() * 1024L * 1024L;
+ mDescription = volume.getDescription(null);
mRemovable = volume.isRemovable();
mMaxFileSize = volume.getMaxFileSize();
}
@@ -72,16 +70,6 @@ public class MtpStorage {
}
/**
- * Returns the amount of space to reserve on the storage file system.
- * This can be set to a non-zero value to prevent MTP from filling up the entire storage.
- *
- * @return reserved space in bytes.
- */
- public final long getReserveSpace() {
- return mReserveSpace;
- }
-
- /**
* Returns true if the storage is removable.
*
* @return is removable
diff --git a/android/mtp/MtpStorageManager.java b/android/mtp/MtpStorageManager.java
new file mode 100644
index 00000000..bdc87413
--- /dev/null
+++ b/android/mtp/MtpStorageManager.java
@@ -0,0 +1,1210 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.mtp;
+
+import android.media.MediaFile;
+import android.os.FileObserver;
+import android.os.storage.StorageVolume;
+import android.util.Log;
+
+import java.io.IOException;
+import java.nio.file.DirectoryIteratorException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Stream;
+
+/**
+ * MtpStorageManager provides functionality for listing, tracking, and notifying MtpServer of
+ * filesystem changes. As directories are listed, this class will cache the results,
+ * and send events when objects are added/removed from cached directories.
+ * {@hide}
+ */
+public class MtpStorageManager {
+ private static final String TAG = MtpStorageManager.class.getSimpleName();
+ public static boolean sDebug = false;
+
+ // Inotify flags not provided by FileObserver
+ private static final int IN_ONLYDIR = 0x01000000;
+ private static final int IN_Q_OVERFLOW = 0x00004000;
+ private static final int IN_IGNORED = 0x00008000;
+ private static final int IN_ISDIR = 0x40000000;
+
+ private class MtpObjectObserver extends FileObserver {
+ MtpObject mObject;
+
+ MtpObjectObserver(MtpObject object) {
+ super(object.getPath().toString(),
+ MOVED_FROM | MOVED_TO | DELETE | CREATE | IN_ONLYDIR);
+ mObject = object;
+ }
+
+ @Override
+ public void onEvent(int event, String path) {
+ synchronized (MtpStorageManager.this) {
+ if ((event & IN_Q_OVERFLOW) != 0) {
+ // We are out of space in the inotify queue.
+ Log.e(TAG, "Received Inotify overflow event!");
+ }
+ MtpObject obj = mObject.getChild(path);
+ if ((event & MOVED_TO) != 0 || (event & CREATE) != 0) {
+ if (sDebug)
+ Log.i(TAG, "Got inotify added event for " + path + " " + event);
+ handleAddedObject(mObject, path, (event & IN_ISDIR) != 0);
+ } else if ((event & MOVED_FROM) != 0 || (event & DELETE) != 0) {
+ if (obj == null) {
+ Log.w(TAG, "Object was null in event " + path);
+ return;
+ }
+ if (sDebug)
+ Log.i(TAG, "Got inotify removed event for " + path + " " + event);
+ handleRemovedObject(obj);
+ } else if ((event & IN_IGNORED) != 0) {
+ if (sDebug)
+ Log.i(TAG, "inotify for " + mObject.getPath() + " deleted");
+ if (mObject.mObserver != null)
+ mObject.mObserver.stopWatching();
+ mObject.mObserver = null;
+ } else {
+ Log.w(TAG, "Got unrecognized event " + path + " " + event);
+ }
+ }
+ }
+
+ @Override
+ public void finalize() {
+ // If the server shuts down and starts up again, the new server's observers can be
+ // invalidated by the finalize() calls of the previous server's observers.
+ // Hence, disable the automatic stopWatching() call in FileObserver#finalize, and
+ // always call stopWatching() manually whenever an observer should be shut down.
+ }
+ }
+
+ /**
+ * Describes how the object is being acted on, to determine how events are handled.
+ */
+ private enum MtpObjectState {
+ NORMAL,
+ FROZEN, // Object is going to be modified in this session.
+ FROZEN_ADDED, // Object was frozen, and has been added.
+ FROZEN_REMOVED, // Object was frozen, and has been removed.
+ FROZEN_ONESHOT_ADD, // Object is waiting for single add event before being unfrozen.
+ FROZEN_ONESHOT_DEL, // Object is waiting for single remove event and will then be removed.
+ }
+
+ /**
+ * Describes the current operation being done on an object. Determines whether observers are
+ * created on new folders.
+ */
+ private enum MtpOperation {
+ NONE, // Any new folders not added as part of the session are immediately observed.
+ ADD, // New folders added as part of the session are immediately observed.
+ RENAME, // Renamed or moved folders are not immediately observed.
+ COPY, // Copied folders are immediately observed iff the original was.
+ DELETE, // Exists for debugging purposes only.
+ }
+
+ /** MtpObject represents either a file or directory in an associated storage. **/
+ public static class MtpObject {
+ // null for root objects
+ private MtpObject mParent;
+
+ private String mName;
+ private int mId;
+ private MtpObjectState mState;
+ private MtpOperation mOp;
+
+ private boolean mVisited;
+ private boolean mIsDir;
+
+ // null if not a directory
+ private HashMap<String, MtpObject> mChildren;
+ // null if not both a directory and visited
+ private FileObserver mObserver;
+
+ MtpObject(String name, int id, MtpObject parent, boolean isDir) {
+ mId = id;
+ mName = name;
+ mParent = parent;
+ mObserver = null;
+ mVisited = false;
+ mState = MtpObjectState.NORMAL;
+ mIsDir = isDir;
+ mOp = MtpOperation.NONE;
+
+ mChildren = mIsDir ? new HashMap<>() : null;
+ }
+
+ /** Public methods for getting object info **/
+
+ public String getName() {
+ return mName;
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public boolean isDir() {
+ return mIsDir;
+ }
+
+ public int getFormat() {
+ return mIsDir ? MtpConstants.FORMAT_ASSOCIATION : MediaFile.getFormatCode(mName, null);
+ }
+
+ public int getStorageId() {
+ return getRoot().getId();
+ }
+
+ public long getModifiedTime() {
+ return getPath().toFile().lastModified() / 1000;
+ }
+
+ public MtpObject getParent() {
+ return mParent;
+ }
+
+ public MtpObject getRoot() {
+ return isRoot() ? this : mParent.getRoot();
+ }
+
+ public long getSize() {
+ return mIsDir ? 0 : getPath().toFile().length();
+ }
+
+ public Path getPath() {
+ return isRoot() ? Paths.get(mName) : mParent.getPath().resolve(mName);
+ }
+
+ public boolean isRoot() {
+ return mParent == null;
+ }
+
+ /** For MtpStorageManager only **/
+
+ private void setName(String name) {
+ mName = name;
+ }
+
+ private void setId(int id) {
+ mId = id;
+ }
+
+ private boolean isVisited() {
+ return mVisited;
+ }
+
+ private void setParent(MtpObject parent) {
+ mParent = parent;
+ }
+
+ private void setDir(boolean dir) {
+ if (dir != mIsDir) {
+ mIsDir = dir;
+ mChildren = mIsDir ? new HashMap<>() : null;
+ }
+ }
+
+ private void setVisited(boolean visited) {
+ mVisited = visited;
+ }
+
+ private MtpObjectState getState() {
+ return mState;
+ }
+
+ private void setState(MtpObjectState state) {
+ mState = state;
+ if (mState == MtpObjectState.NORMAL)
+ mOp = MtpOperation.NONE;
+ }
+
+ private MtpOperation getOperation() {
+ return mOp;
+ }
+
+ private void setOperation(MtpOperation op) {
+ mOp = op;
+ }
+
+ private FileObserver getObserver() {
+ return mObserver;
+ }
+
+ private void setObserver(FileObserver observer) {
+ mObserver = observer;
+ }
+
+ private void addChild(MtpObject child) {
+ mChildren.put(child.getName(), child);
+ }
+
+ private MtpObject getChild(String name) {
+ return mChildren.get(name);
+ }
+
+ private Collection<MtpObject> getChildren() {
+ return mChildren.values();
+ }
+
+ private boolean exists() {
+ return getPath().toFile().exists();
+ }
+
+ private MtpObject copy(boolean recursive) {
+ MtpObject copy = new MtpObject(mName, mId, mParent, mIsDir);
+ copy.mIsDir = mIsDir;
+ copy.mVisited = mVisited;
+ copy.mState = mState;
+ copy.mChildren = mIsDir ? new HashMap<>() : null;
+ if (recursive && mIsDir) {
+ for (MtpObject child : mChildren.values()) {
+ MtpObject childCopy = child.copy(true);
+ childCopy.setParent(copy);
+ copy.addChild(childCopy);
+ }
+ }
+ return copy;
+ }
+ }
+
+ /**
+ * A class that processes generated filesystem events.
+ */
+ public static abstract class MtpNotifier {
+ /**
+ * Called when an object is added.
+ */
+ public abstract void sendObjectAdded(int id);
+
+ /**
+ * Called when an object is deleted.
+ */
+ public abstract void sendObjectRemoved(int id);
+ }
+
+ private MtpNotifier mMtpNotifier;
+
+ // A cache of MtpObjects. The objects in the cache are keyed by object id.
+ // The root object of each storage isn't in this map since they all have ObjectId 0.
+ // Instead, they can be found in mRoots keyed by storageId.
+ private HashMap<Integer, MtpObject> mObjects;
+
+ // A cache of the root MtpObject for each storage, keyed by storage id.
+ private HashMap<Integer, MtpObject> mRoots;
+
+ // Object and Storage ids are allocated incrementally and not to be reused.
+ private int mNextObjectId;
+ private int mNextStorageId;
+
+ // Special subdirectories. When set, only return objects rooted in these directories, and do
+ // not allow them to be modified.
+ private Set<String> mSubdirectories;
+
+ private volatile boolean mCheckConsistency;
+ private Thread mConsistencyThread;
+
+ public MtpStorageManager(MtpNotifier notifier, Set<String> subdirectories) {
+ mMtpNotifier = notifier;
+ mSubdirectories = subdirectories;
+ mObjects = new HashMap<>();
+ mRoots = new HashMap<>();
+ mNextObjectId = 1;
+ mNextStorageId = 1;
+
+ mCheckConsistency = false; // Set to true to turn on automatic consistency checking
+ mConsistencyThread = new Thread(() -> {
+ while (mCheckConsistency) {
+ try {
+ Thread.sleep(15 * 1000);
+ } catch (InterruptedException e) {
+ return;
+ }
+ if (MtpStorageManager.this.checkConsistency()) {
+ Log.v(TAG, "Cache is consistent");
+ } else {
+ Log.w(TAG, "Cache is not consistent");
+ }
+ }
+ });
+ if (mCheckConsistency)
+ mConsistencyThread.start();
+ }
+
+ /**
+ * Clean up resources used by the storage manager.
+ */
+ public synchronized void close() {
+ Stream<MtpObject> objs = Stream.concat(mRoots.values().stream(),
+ mObjects.values().stream());
+
+ Iterator<MtpObject> iter = objs.iterator();
+ while (iter.hasNext()) {
+ // Close all FileObservers.
+ MtpObject obj = iter.next();
+ if (obj.getObserver() != null) {
+ obj.getObserver().stopWatching();
+ obj.setObserver(null);
+ }
+ }
+
+ // Shut down the consistency checking thread
+ if (mCheckConsistency) {
+ mCheckConsistency = false;
+ mConsistencyThread.interrupt();
+ try {
+ mConsistencyThread.join();
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+ }
+
+ /**
+ * Sets the special subdirectories, which are the subdirectories of root storage that queries
+ * are restricted to. Must be done before any root storages are accessed.
+ * @param subDirs Subdirectories to set, or null to reset.
+ */
+ public synchronized void setSubdirectories(Set<String> subDirs) {
+ mSubdirectories = subDirs;
+ }
+
+ /**
+ * Allocates an MTP storage id for the given volume and add it to current roots.
+ * @param volume Storage to add.
+ * @return the associated MtpStorage
+ */
+ public synchronized MtpStorage addMtpStorage(StorageVolume volume) {
+ int storageId = ((getNextStorageId() & 0x0000FFFF) << 16) + 1;
+ MtpObject root = new MtpObject(volume.getPath(), storageId, null, true);
+ MtpStorage storage = new MtpStorage(volume, storageId);
+ mRoots.put(storageId, root);
+ return storage;
+ }
+
+ /**
+ * Removes the given storage and all associated items from the cache.
+ * @param storage Storage to remove.
+ */
+ public synchronized void removeMtpStorage(MtpStorage storage) {
+ removeObjectFromCache(getStorageRoot(storage.getStorageId()), true, true);
+ }
+
+ /**
+ * Checks if the given object can be renamed, moved, or deleted.
+ * If there are special subdirectories, they cannot be modified.
+ * @param obj Object to check.
+ * @return Whether object can be modified.
+ */
+ private synchronized boolean isSpecialSubDir(MtpObject obj) {
+ return obj.getParent().isRoot() && mSubdirectories != null
+ && !mSubdirectories.contains(obj.getName());
+ }
+
+ /**
+ * Get the object with the specified path. Visit any necessary directories on the way.
+ * @param path Full path of the object to find.
+ * @return The desired object, or null if it cannot be found.
+ */
+ public synchronized MtpObject getByPath(String path) {
+ MtpObject obj = null;
+ for (MtpObject root : mRoots.values()) {
+ if (path.startsWith(root.getName())) {
+ obj = root;
+ path = path.substring(root.getName().length());
+ }
+ }
+ for (String name : path.split("/")) {
+ if (obj == null || !obj.isDir())
+ return null;
+ if ("".equals(name))
+ continue;
+ if (!obj.isVisited())
+ getChildren(obj);
+ obj = obj.getChild(name);
+ }
+ return obj;
+ }
+
+ /**
+ * Get the object with specified id.
+ * @param id Id of object. must not be 0 or 0xFFFFFFFF
+ * @return Object, or null if error.
+ */
+ public synchronized MtpObject getObject(int id) {
+ if (id == 0 || id == 0xFFFFFFFF) {
+ Log.w(TAG, "Can't get root storages with getObject()");
+ return null;
+ }
+ if (!mObjects.containsKey(id)) {
+ Log.w(TAG, "Id " + id + " doesn't exist");
+ return null;
+ }
+ return mObjects.get(id);
+ }
+
+ /**
+ * Get the storage with specified id.
+ * @param id Storage id.
+ * @return Object that is the root of the storage, or null if error.
+ */
+ public MtpObject getStorageRoot(int id) {
+ if (!mRoots.containsKey(id)) {
+ Log.w(TAG, "StorageId " + id + " doesn't exist");
+ return null;
+ }
+ return mRoots.get(id);
+ }
+
+ private int getNextObjectId() {
+ int ret = mNextObjectId;
+ // Treat the id as unsigned int
+ mNextObjectId = (int) ((long) mNextObjectId + 1);
+ return ret;
+ }
+
+ private int getNextStorageId() {
+ return mNextStorageId++;
+ }
+
+ /**
+ * Get all objects matching the given parent, format, and storage
+ * @param parent object id of the parent. 0 for all objects, 0xFFFFFFFF for all object in root
+ * @param format format of returned objects. 0 for any format
+ * @param storageId storage id to look in. 0xFFFFFFFF for all storages
+ * @return A stream of matched objects, or null if error
+ */
+ public synchronized Stream<MtpObject> getObjects(int parent, int format, int storageId) {
+ boolean recursive = parent == 0;
+ if (parent == 0xFFFFFFFF)
+ parent = 0;
+ if (storageId == 0xFFFFFFFF) {
+ // query all stores
+ if (parent == 0) {
+ // Get the objects of this format and parent in each store.
+ ArrayList<Stream<MtpObject>> streamList = new ArrayList<>();
+ for (MtpObject root : mRoots.values()) {
+ streamList.add(getObjects(root, format, recursive));
+ }
+ return Stream.of(streamList).flatMap(Collection::stream).reduce(Stream::concat)
+ .orElseGet(Stream::empty);
+ }
+ }
+ MtpObject obj = parent == 0 ? getStorageRoot(storageId) : getObject(parent);
+ if (obj == null)
+ return null;
+ return getObjects(obj, format, recursive);
+ }
+
+ private synchronized Stream<MtpObject> getObjects(MtpObject parent, int format, boolean rec) {
+ Collection<MtpObject> children = getChildren(parent);
+ if (children == null)
+ return null;
+ Stream<MtpObject> ret = Stream.of(children).flatMap(Collection::stream);
+
+ if (format != 0) {
+ ret = ret.filter(o -> o.getFormat() == format);
+ }
+ if (rec) {
+ // Get all objects recursively.
+ ArrayList<Stream<MtpObject>> streamList = new ArrayList<>();
+ streamList.add(ret);
+ for (MtpObject o : children) {
+ if (o.isDir())
+ streamList.add(getObjects(o, format, true));
+ }
+ ret = Stream.of(streamList).filter(Objects::nonNull).flatMap(Collection::stream)
+ .reduce(Stream::concat).orElseGet(Stream::empty);
+ }
+ return ret;
+ }
+
+ /**
+ * Return the children of the given object. If the object hasn't been visited yet, add
+ * its children to the cache and start observing it.
+ * @param object the parent object
+ * @return The collection of child objects or null if error
+ */
+ private synchronized Collection<MtpObject> getChildren(MtpObject object) {
+ if (object == null || !object.isDir()) {
+ Log.w(TAG, "Can't find children of " + (object == null ? "null" : object.getId()));
+ return null;
+ }
+ if (!object.isVisited()) {
+ Path dir = object.getPath();
+ /*
+ * If a file is added after the observer starts watching the directory, but before
+ * the contents are listed, it will generate an event that will get processed
+ * after this synchronized function returns. We handle this by ignoring object
+ * added events if an object at that path already exists.
+ */
+ if (object.getObserver() != null)
+ Log.e(TAG, "Observer is not null!");
+ object.setObserver(new MtpObjectObserver(object));
+ object.getObserver().startWatching();
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
+ for (Path file : stream) {
+ addObjectToCache(object, file.getFileName().toString(),
+ file.toFile().isDirectory());
+ }
+ } catch (IOException | DirectoryIteratorException e) {
+ Log.e(TAG, e.toString());
+ object.getObserver().stopWatching();
+ object.setObserver(null);
+ return null;
+ }
+ object.setVisited(true);
+ }
+ return object.getChildren();
+ }
+
+ /**
+ * Create a new object from the given path and add it to the cache.
+ * @param parent The parent object
+ * @param newName Path of the new object
+ * @return the new object if success, else null
+ */
+ private synchronized MtpObject addObjectToCache(MtpObject parent, String newName,
+ boolean isDir) {
+ if (!parent.isRoot() && getObject(parent.getId()) != parent)
+ // parent object has been removed
+ return null;
+ if (parent.getChild(newName) != null) {
+ // Object already exists
+ return null;
+ }
+ if (mSubdirectories != null && parent.isRoot() && !mSubdirectories.contains(newName)) {
+ // Not one of the restricted subdirectories.
+ return null;
+ }
+
+ MtpObject obj = new MtpObject(newName, getNextObjectId(), parent, isDir);
+ mObjects.put(obj.getId(), obj);
+ parent.addChild(obj);
+ return obj;
+ }
+
+ /**
+ * Remove the given path from the cache.
+ * @param removed The removed object
+ * @param removeGlobal Whether to remove the object from the global id map
+ * @param recursive Whether to also remove its children recursively.
+ * @return true if successfully removed
+ */
+ private synchronized boolean removeObjectFromCache(MtpObject removed, boolean removeGlobal,
+ boolean recursive) {
+ boolean ret = removed.isRoot()
+ || removed.getParent().mChildren.remove(removed.getName(), removed);
+ if (!ret && sDebug)
+ Log.w(TAG, "Failed to remove from parent " + removed.getPath());
+ if (removed.isRoot()) {
+ ret = mRoots.remove(removed.getId(), removed) && ret;
+ } else if (removeGlobal) {
+ ret = mObjects.remove(removed.getId(), removed) && ret;
+ }
+ if (!ret && sDebug)
+ Log.w(TAG, "Failed to remove from global cache " + removed.getPath());
+ if (removed.getObserver() != null) {
+ removed.getObserver().stopWatching();
+ removed.setObserver(null);
+ }
+ if (removed.isDir() && recursive) {
+ // Remove all descendants from cache recursively
+ Collection<MtpObject> children = new ArrayList<>(removed.getChildren());
+ for (MtpObject child : children) {
+ ret = removeObjectFromCache(child, removeGlobal, true) && ret;
+ }
+ }
+ return ret;
+ }
+
+ private synchronized void handleAddedObject(MtpObject parent, String path, boolean isDir) {
+ MtpOperation op = MtpOperation.NONE;
+ MtpObject obj = parent.getChild(path);
+ if (obj != null) {
+ MtpObjectState state = obj.getState();
+ op = obj.getOperation();
+ if (obj.isDir() != isDir && state != MtpObjectState.FROZEN_REMOVED)
+ Log.d(TAG, "Inconsistent directory info! " + obj.getPath());
+ obj.setDir(isDir);
+ switch (state) {
+ case FROZEN:
+ case FROZEN_REMOVED:
+ obj.setState(MtpObjectState.FROZEN_ADDED);
+ break;
+ case FROZEN_ONESHOT_ADD:
+ obj.setState(MtpObjectState.NORMAL);
+ break;
+ case NORMAL:
+ case FROZEN_ADDED:
+ // This can happen when handling listed object in a new directory.
+ return;
+ default:
+ Log.w(TAG, "Unexpected state in add " + path + " " + state);
+ }
+ if (sDebug)
+ Log.i(TAG, state + " transitioned to " + obj.getState() + " in op " + op);
+ } else {
+ obj = MtpStorageManager.this.addObjectToCache(parent, path, isDir);
+ if (obj != null) {
+ MtpStorageManager.this.mMtpNotifier.sendObjectAdded(obj.getId());
+ } else {
+ if (sDebug)
+ Log.w(TAG, "object " + path + " already exists");
+ return;
+ }
+ }
+ if (isDir) {
+ // If this was added as part of a rename do not visit or send events.
+ if (op == MtpOperation.RENAME)
+ return;
+
+ // If it was part of a copy operation, then only add observer if it was visited before.
+ if (op == MtpOperation.COPY && !obj.isVisited())
+ return;
+
+ if (obj.getObserver() != null) {
+ Log.e(TAG, "Observer is not null!");
+ return;
+ }
+ obj.setObserver(new MtpObjectObserver(obj));
+ obj.getObserver().startWatching();
+ obj.setVisited(true);
+
+ // It's possible that objects were added to a watched directory before the watch can be
+ // created, so manually handle those.
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(obj.getPath())) {
+ for (Path file : stream) {
+ if (sDebug)
+ Log.i(TAG, "Manually handling event for " + file.getFileName().toString());
+ handleAddedObject(obj, file.getFileName().toString(),
+ file.toFile().isDirectory());
+ }
+ } catch (IOException | DirectoryIteratorException e) {
+ Log.e(TAG, e.toString());
+ obj.getObserver().stopWatching();
+ obj.setObserver(null);
+ }
+ }
+ }
+
+ private synchronized void handleRemovedObject(MtpObject obj) {
+ MtpObjectState state = obj.getState();
+ MtpOperation op = obj.getOperation();
+ switch (state) {
+ case FROZEN_ADDED:
+ obj.setState(MtpObjectState.FROZEN_REMOVED);
+ break;
+ case FROZEN_ONESHOT_DEL:
+ removeObjectFromCache(obj, op != MtpOperation.RENAME, false);
+ break;
+ case FROZEN:
+ obj.setState(MtpObjectState.FROZEN_REMOVED);
+ break;
+ case NORMAL:
+ if (MtpStorageManager.this.removeObjectFromCache(obj, true, true))
+ MtpStorageManager.this.mMtpNotifier.sendObjectRemoved(obj.getId());
+ break;
+ default:
+ // This shouldn't happen; states correspond to objects that don't exist
+ Log.e(TAG, "Got unexpected object remove for " + obj.getName());
+ }
+ if (sDebug)
+ Log.i(TAG, state + " transitioned to " + obj.getState() + " in op " + op);
+ }
+
+ /**
+ * Block the caller until all events currently in the event queue have been
+ * read and processed. Used for testing purposes.
+ */
+ public void flushEvents() {
+ try {
+ // TODO make this smarter
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+
+ }
+ }
+
+ /**
+ * Dumps a representation of the cache to log.
+ */
+ public synchronized void dump() {
+ for (int key : mObjects.keySet()) {
+ MtpObject obj = mObjects.get(key);
+ Log.i(TAG, key + " | " + (obj.getParent() == null ? obj.getParent().getId() : "null")
+ + " | " + obj.getName() + " | " + (obj.isDir() ? "dir" : "obj")
+ + " | " + (obj.isVisited() ? "v" : "nv") + " | " + obj.getState());
+ }
+ }
+
+ /**
+ * Checks consistency of the cache. This checks whether all objects have correct links
+ * to their parent, and whether directories are missing or have extraneous objects.
+ * @return true iff cache is consistent
+ */
+ public synchronized boolean checkConsistency() {
+ Stream<MtpObject> objs = Stream.concat(mRoots.values().stream(),
+ mObjects.values().stream());
+ Iterator<MtpObject> iter = objs.iterator();
+ boolean ret = true;
+ while (iter.hasNext()) {
+ MtpObject obj = iter.next();
+ if (!obj.exists()) {
+ Log.w(TAG, "Object doesn't exist " + obj.getPath() + " " + obj.getId());
+ ret = false;
+ }
+ if (obj.getState() != MtpObjectState.NORMAL) {
+ Log.w(TAG, "Object " + obj.getPath() + " in state " + obj.getState());
+ ret = false;
+ }
+ if (obj.getOperation() != MtpOperation.NONE) {
+ Log.w(TAG, "Object " + obj.getPath() + " in operation " + obj.getOperation());
+ ret = false;
+ }
+ if (!obj.isRoot() && mObjects.get(obj.getId()) != obj) {
+ Log.w(TAG, "Object " + obj.getPath() + " is not in map correctly");
+ ret = false;
+ }
+ if (obj.getParent() != null) {
+ if (obj.getParent().isRoot() && obj.getParent()
+ != mRoots.get(obj.getParent().getId())) {
+ Log.w(TAG, "Root parent is not in root mapping " + obj.getPath());
+ ret = false;
+ }
+ if (!obj.getParent().isRoot() && obj.getParent()
+ != mObjects.get(obj.getParent().getId())) {
+ Log.w(TAG, "Parent is not in object mapping " + obj.getPath());
+ ret = false;
+ }
+ if (obj.getParent().getChild(obj.getName()) != obj) {
+ Log.w(TAG, "Child does not exist in parent " + obj.getPath());
+ ret = false;
+ }
+ }
+ if (obj.isDir()) {
+ if (obj.isVisited() == (obj.getObserver() == null)) {
+ Log.w(TAG, obj.getPath() + " is " + (obj.isVisited() ? "" : "not ")
+ + " visited but observer is " + obj.getObserver());
+ ret = false;
+ }
+ if (!obj.isVisited() && obj.getChildren().size() > 0) {
+ Log.w(TAG, obj.getPath() + " is not visited but has children");
+ ret = false;
+ }
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(obj.getPath())) {
+ Set<String> files = new HashSet<>();
+ for (Path file : stream) {
+ if (obj.isVisited() &&
+ obj.getChild(file.getFileName().toString()) == null &&
+ (mSubdirectories == null || !obj.isRoot() ||
+ mSubdirectories.contains(file.getFileName().toString()))) {
+ Log.w(TAG, "File exists in fs but not in children " + file);
+ ret = false;
+ }
+ files.add(file.toString());
+ }
+ for (MtpObject child : obj.getChildren()) {
+ if (!files.contains(child.getPath().toString())) {
+ Log.w(TAG, "File in children doesn't exist in fs " + child.getPath());
+ ret = false;
+ }
+ if (child != mObjects.get(child.getId())) {
+ Log.w(TAG, "Child is not in object map " + child.getPath());
+ ret = false;
+ }
+ }
+ } catch (IOException | DirectoryIteratorException e) {
+ Log.w(TAG, e.toString());
+ ret = false;
+ }
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Informs MtpStorageManager that an object with the given path is about to be added.
+ * @param parent The parent object of the object to be added.
+ * @param name Filename of object to add.
+ * @return Object id of the added object, or -1 if it cannot be added.
+ */
+ public synchronized int beginSendObject(MtpObject parent, String name, int format) {
+ if (sDebug)
+ Log.v(TAG, "beginSendObject " + name);
+ if (!parent.isDir())
+ return -1;
+ if (parent.isRoot() && mSubdirectories != null && !mSubdirectories.contains(name))
+ return -1;
+ getChildren(parent); // Ensure parent is visited
+ MtpObject obj = addObjectToCache(parent, name, format == MtpConstants.FORMAT_ASSOCIATION);
+ if (obj == null)
+ return -1;
+ obj.setState(MtpObjectState.FROZEN);
+ obj.setOperation(MtpOperation.ADD);
+ return obj.getId();
+ }
+
+ /**
+ * Clean up the object state after a sendObject operation.
+ * @param obj The object, returned from beginAddObject().
+ * @param succeeded Whether the file was successfully created.
+ * @return Whether cache state was successfully cleaned up.
+ */
+ public synchronized boolean endSendObject(MtpObject obj, boolean succeeded) {
+ if (sDebug)
+ Log.v(TAG, "endSendObject " + succeeded);
+ return generalEndAddObject(obj, succeeded, true);
+ }
+
+ /**
+ * Informs MtpStorageManager that the given object is about to be renamed.
+ * If this returns true, it must be followed with an endRenameObject()
+ * @param obj Object to be renamed.
+ * @param newName New name of the object.
+ * @return Whether renaming is allowed.
+ */
+ public synchronized boolean beginRenameObject(MtpObject obj, String newName) {
+ if (sDebug)
+ Log.v(TAG, "beginRenameObject " + obj.getName() + " " + newName);
+ if (obj.isRoot())
+ return false;
+ if (isSpecialSubDir(obj))
+ return false;
+ if (obj.getParent().getChild(newName) != null)
+ // Object already exists in parent with that name.
+ return false;
+
+ MtpObject oldObj = obj.copy(false);
+ obj.setName(newName);
+ obj.getParent().addChild(obj);
+ oldObj.getParent().addChild(oldObj);
+ return generalBeginRenameObject(oldObj, obj);
+ }
+
+ /**
+ * Cleans up cache state after a rename operation and sends any events that were missed.
+ * @param obj The object being renamed, the same one that was passed in beginRenameObject().
+ * @param oldName The previous name of the object.
+ * @param success Whether the rename operation succeeded.
+ * @return Whether state was successfully cleaned up.
+ */
+ public synchronized boolean endRenameObject(MtpObject obj, String oldName, boolean success) {
+ if (sDebug)
+ Log.v(TAG, "endRenameObject " + success);
+ MtpObject parent = obj.getParent();
+ MtpObject oldObj = parent.getChild(oldName);
+ if (!success) {
+ // If the rename failed, we want oldObj to be the original and obj to be the dummy.
+ // Switch the objects, except for their name and state.
+ MtpObject temp = oldObj;
+ MtpObjectState oldState = oldObj.getState();
+ temp.setName(obj.getName());
+ temp.setState(obj.getState());
+ oldObj = obj;
+ oldObj.setName(oldName);
+ oldObj.setState(oldState);
+ obj = temp;
+ parent.addChild(obj);
+ parent.addChild(oldObj);
+ }
+ return generalEndRenameObject(oldObj, obj, success);
+ }
+
+ /**
+ * Informs MtpStorageManager that the given object is about to be deleted by the initiator,
+ * so don't send an event.
+ * @param obj Object to be deleted.
+ * @return Whether cache deletion is allowed.
+ */
+ public synchronized boolean beginRemoveObject(MtpObject obj) {
+ if (sDebug)
+ Log.v(TAG, "beginRemoveObject " + obj.getName());
+ return !obj.isRoot() && !isSpecialSubDir(obj)
+ && generalBeginRemoveObject(obj, MtpOperation.DELETE);
+ }
+
+ /**
+ * Clean up cache state after a delete operation and send any events that were missed.
+ * @param obj Object to be deleted, same one passed in beginRemoveObject().
+ * @param success Whether operation was completed successfully.
+ * @return Whether cache state is correct.
+ */
+ public synchronized boolean endRemoveObject(MtpObject obj, boolean success) {
+ if (sDebug)
+ Log.v(TAG, "endRemoveObject " + success);
+ boolean ret = true;
+ if (obj.isDir()) {
+ for (MtpObject child : new ArrayList<>(obj.getChildren()))
+ if (child.getOperation() == MtpOperation.DELETE)
+ ret = endRemoveObject(child, success) && ret;
+ }
+ return generalEndRemoveObject(obj, success, true) && ret;
+ }
+
+ /**
+ * Informs MtpStorageManager that the given object is about to be moved to a new parent.
+ * @param obj Object to be moved.
+ * @param newParent The new parent object.
+ * @return Whether the move is allowed.
+ */
+ public synchronized boolean beginMoveObject(MtpObject obj, MtpObject newParent) {
+ if (sDebug)
+ Log.v(TAG, "beginMoveObject " + newParent.getPath());
+ if (obj.isRoot())
+ return false;
+ if (isSpecialSubDir(obj))
+ return false;
+ getChildren(newParent); // Ensure parent is visited
+ if (newParent.getChild(obj.getName()) != null)
+ // Object already exists in parent with that name.
+ return false;
+ if (obj.getStorageId() != newParent.getStorageId()) {
+ /*
+ * The move is occurring across storages. The observers will not remain functional
+ * after the move, and the move will not be atomic. We have to copy the file tree
+ * to the destination and recreate the observers once copy is complete.
+ */
+ MtpObject newObj = obj.copy(true);
+ newObj.setParent(newParent);
+ newParent.addChild(newObj);
+ return generalBeginRemoveObject(obj, MtpOperation.RENAME)
+ && generalBeginCopyObject(newObj, false);
+ }
+ // Move obj to new parent, create a dummy object in the old parent.
+ MtpObject oldObj = obj.copy(false);
+ obj.setParent(newParent);
+ oldObj.getParent().addChild(oldObj);
+ obj.getParent().addChild(obj);
+ return generalBeginRenameObject(oldObj, obj);
+ }
+
+ /**
+ * Clean up cache state after a move operation and send any events that were missed.
+ * @param oldParent The old parent object.
+ * @param newParent The new parent object.
+ * @param name The name of the object being moved.
+ * @param success Whether operation was completed successfully.
+ * @return Whether cache state is correct.
+ */
+ public synchronized boolean endMoveObject(MtpObject oldParent, MtpObject newParent, String name,
+ boolean success) {
+ if (sDebug)
+ Log.v(TAG, "endMoveObject " + success);
+ MtpObject oldObj = oldParent.getChild(name);
+ MtpObject newObj = newParent.getChild(name);
+ if (oldObj == null || newObj == null)
+ return false;
+ if (oldParent.getStorageId() != newObj.getStorageId()) {
+ boolean ret = endRemoveObject(oldObj, success);
+ return generalEndCopyObject(newObj, success, true) && ret;
+ }
+ if (!success) {
+ // If the rename failed, we want oldObj to be the original and obj to be the dummy.
+ // Switch the objects, except for their parent and state.
+ MtpObject temp = oldObj;
+ MtpObjectState oldState = oldObj.getState();
+ temp.setParent(newObj.getParent());
+ temp.setState(newObj.getState());
+ oldObj = newObj;
+ oldObj.setParent(oldParent);
+ oldObj.setState(oldState);
+ newObj = temp;
+ newObj.getParent().addChild(newObj);
+ oldParent.addChild(oldObj);
+ }
+ return generalEndRenameObject(oldObj, newObj, success);
+ }
+
+ /**
+ * Informs MtpStorageManager that the given object is about to be copied recursively.
+ * @param object Object to be copied
+ * @param newParent New parent for the object.
+ * @return The object id for the new copy, or -1 if error.
+ */
+ public synchronized int beginCopyObject(MtpObject object, MtpObject newParent) {
+ if (sDebug)
+ Log.v(TAG, "beginCopyObject " + object.getName() + " to " + newParent.getPath());
+ String name = object.getName();
+ if (!newParent.isDir())
+ return -1;
+ if (newParent.isRoot() && mSubdirectories != null && !mSubdirectories.contains(name))
+ return -1;
+ getChildren(newParent); // Ensure parent is visited
+ if (newParent.getChild(name) != null)
+ return -1;
+ MtpObject newObj = object.copy(object.isDir());
+ newParent.addChild(newObj);
+ newObj.setParent(newParent);
+ if (!generalBeginCopyObject(newObj, true))
+ return -1;
+ return newObj.getId();
+ }
+
+ /**
+ * Cleans up cache state after a copy operation.
+ * @param object Object that was copied.
+ * @param success Whether the operation was successful.
+ * @return Whether cache state is consistent.
+ */
+ public synchronized boolean endCopyObject(MtpObject object, boolean success) {
+ if (sDebug)
+ Log.v(TAG, "endCopyObject " + object.getName() + " " + success);
+ return generalEndCopyObject(object, success, false);
+ }
+
+ private synchronized boolean generalEndAddObject(MtpObject obj, boolean succeeded,
+ boolean removeGlobal) {
+ switch (obj.getState()) {
+ case FROZEN:
+ // Object was never created.
+ if (succeeded) {
+ // The operation was successful so the event must still be in the queue.
+ obj.setState(MtpObjectState.FROZEN_ONESHOT_ADD);
+ } else {
+ // The operation failed and never created the file.
+ if (!removeObjectFromCache(obj, removeGlobal, false)) {
+ return false;
+ }
+ }
+ break;
+ case FROZEN_ADDED:
+ obj.setState(MtpObjectState.NORMAL);
+ if (!succeeded) {
+ MtpObject parent = obj.getParent();
+ // The operation failed but some other process created the file. Send an event.
+ if (!removeObjectFromCache(obj, removeGlobal, false))
+ return false;
+ handleAddedObject(parent, obj.getName(), obj.isDir());
+ }
+ // else: The operation successfully created the object.
+ break;
+ case FROZEN_REMOVED:
+ if (!removeObjectFromCache(obj, removeGlobal, false))
+ return false;
+ if (succeeded) {
+ // Some other process deleted the object. Send an event.
+ mMtpNotifier.sendObjectRemoved(obj.getId());
+ }
+ // else: Mtp deleted the object as part of cleanup. Don't send an event.
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ private synchronized boolean generalEndRemoveObject(MtpObject obj, boolean success,
+ boolean removeGlobal) {
+ switch (obj.getState()) {
+ case FROZEN:
+ if (success) {
+ // Object was deleted successfully, and event is still in the queue.
+ obj.setState(MtpObjectState.FROZEN_ONESHOT_DEL);
+ } else {
+ // Object was not deleted.
+ obj.setState(MtpObjectState.NORMAL);
+ }
+ break;
+ case FROZEN_ADDED:
+ // Object was deleted, and then readded.
+ obj.setState(MtpObjectState.NORMAL);
+ if (success) {
+ // Some other process readded the object.
+ MtpObject parent = obj.getParent();
+ if (!removeObjectFromCache(obj, removeGlobal, false))
+ return false;
+ handleAddedObject(parent, obj.getName(), obj.isDir());
+ }
+ // else : Object still exists after failure.
+ break;
+ case FROZEN_REMOVED:
+ if (!removeObjectFromCache(obj, removeGlobal, false))
+ return false;
+ if (!success) {
+ // Some other process deleted the object.
+ mMtpNotifier.sendObjectRemoved(obj.getId());
+ }
+ // else : This process deleted the object as part of the operation.
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ private synchronized boolean generalBeginRenameObject(MtpObject fromObj, MtpObject toObj) {
+ fromObj.setState(MtpObjectState.FROZEN);
+ toObj.setState(MtpObjectState.FROZEN);
+ fromObj.setOperation(MtpOperation.RENAME);
+ toObj.setOperation(MtpOperation.RENAME);
+ return true;
+ }
+
+ private synchronized boolean generalEndRenameObject(MtpObject fromObj, MtpObject toObj,
+ boolean success) {
+ boolean ret = generalEndRemoveObject(fromObj, success, !success);
+ return generalEndAddObject(toObj, success, success) && ret;
+ }
+
+ private synchronized boolean generalBeginRemoveObject(MtpObject obj, MtpOperation op) {
+ obj.setState(MtpObjectState.FROZEN);
+ obj.setOperation(op);
+ if (obj.isDir()) {
+ for (MtpObject child : obj.getChildren())
+ generalBeginRemoveObject(child, op);
+ }
+ return true;
+ }
+
+ private synchronized boolean generalBeginCopyObject(MtpObject obj, boolean newId) {
+ obj.setState(MtpObjectState.FROZEN);
+ obj.setOperation(MtpOperation.COPY);
+ if (newId) {
+ obj.setId(getNextObjectId());
+ mObjects.put(obj.getId(), obj);
+ }
+ if (obj.isDir())
+ for (MtpObject child : obj.getChildren())
+ if (!generalBeginCopyObject(child, newId))
+ return false;
+ return true;
+ }
+
+ private synchronized boolean generalEndCopyObject(MtpObject obj, boolean success, boolean addGlobal) {
+ if (success && addGlobal)
+ mObjects.put(obj.getId(), obj);
+ boolean ret = true;
+ if (obj.isDir()) {
+ for (MtpObject child : new ArrayList<>(obj.getChildren())) {
+ if (child.getOperation() == MtpOperation.COPY)
+ ret = generalEndCopyObject(child, success, addGlobal) && ret;
+ }
+ }
+ ret = generalEndAddObject(obj, success, success || !addGlobal) && ret;
+ return ret;
+ }
+}
diff --git a/android/net/ConnectivityManager.java b/android/net/ConnectivityManager.java
index 8071e8b8..11d338d0 100644
--- a/android/net/ConnectivityManager.java
+++ b/android/net/ConnectivityManager.java
@@ -1794,7 +1794,7 @@ public class ConnectivityManager {
ITelephony it = ITelephony.Stub.asInterface(b);
int subId = SubscriptionManager.getDefaultDataSubscriptionId();
Log.d("ConnectivityManager", "getMobileDataEnabled()+ subId=" + subId);
- boolean retVal = it.getDataEnabled(subId);
+ boolean retVal = it.isUserDataEnabled(subId);
Log.d("ConnectivityManager", "getMobileDataEnabled()- subId=" + subId
+ " retVal=" + retVal);
return retVal;
diff --git a/android/net/IpSecAlgorithm.java b/android/net/IpSecAlgorithm.java
index d6e62cf1..f82627b9 100644
--- a/android/net/IpSecAlgorithm.java
+++ b/android/net/IpSecAlgorithm.java
@@ -21,6 +21,7 @@ import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.HexDump;
import java.lang.annotation.Retention;
@@ -34,6 +35,8 @@ import java.util.Arrays;
* Internet Protocol</a>
*/
public final class IpSecAlgorithm implements Parcelable {
+ private static final String TAG = "IpSecAlgorithm";
+
/**
* AES-CBC Encryption/Ciphering Algorithm.
*
@@ -45,6 +48,7 @@ public final class IpSecAlgorithm implements Parcelable {
* MD5 HMAC Authentication/Integrity Algorithm. <b>This algorithm is not recommended for use in
* new applications and is provided for legacy compatibility with 3gpp infrastructure.</b>
*
+ * <p>Keys for this algorithm must be 128 bits in length.
* <p>Valid truncation lengths are multiples of 8 bits from 96 to (default) 128.
*/
public static final String AUTH_HMAC_MD5 = "hmac(md5)";
@@ -53,6 +57,7 @@ public final class IpSecAlgorithm implements Parcelable {
* SHA1 HMAC Authentication/Integrity Algorithm. <b>This algorithm is not recommended for use in
* new applications and is provided for legacy compatibility with 3gpp infrastructure.</b>
*
+ * <p>Keys for this algorithm must be 160 bits in length.
* <p>Valid truncation lengths are multiples of 8 bits from 96 to (default) 160.
*/
public static final String AUTH_HMAC_SHA1 = "hmac(sha1)";
@@ -60,6 +65,7 @@ public final class IpSecAlgorithm implements Parcelable {
/**
* SHA256 HMAC Authentication/Integrity Algorithm.
*
+ * <p>Keys for this algorithm must be 256 bits in length.
* <p>Valid truncation lengths are multiples of 8 bits from 96 to (default) 256.
*/
public static final String AUTH_HMAC_SHA256 = "hmac(sha256)";
@@ -67,6 +73,7 @@ public final class IpSecAlgorithm implements Parcelable {
/**
* SHA384 HMAC Authentication/Integrity Algorithm.
*
+ * <p>Keys for this algorithm must be 384 bits in length.
* <p>Valid truncation lengths are multiples of 8 bits from 192 to (default) 384.
*/
public static final String AUTH_HMAC_SHA384 = "hmac(sha384)";
@@ -74,6 +81,7 @@ public final class IpSecAlgorithm implements Parcelable {
/**
* SHA512 HMAC Authentication/Integrity Algorithm.
*
+ * <p>Keys for this algorithm must be 512 bits in length.
* <p>Valid truncation lengths are multiples of 8 bits from 256 to (default) 512.
*/
public static final String AUTH_HMAC_SHA512 = "hmac(sha512)";
@@ -130,12 +138,10 @@ public final class IpSecAlgorithm implements Parcelable {
* @param truncLenBits number of bits of output hash to use.
*/
public IpSecAlgorithm(@AlgorithmName String algorithm, @NonNull byte[] key, int truncLenBits) {
- if (!isTruncationLengthValid(algorithm, truncLenBits)) {
- throw new IllegalArgumentException("Unknown algorithm or invalid length");
- }
mName = algorithm;
mKey = key.clone();
- mTruncLenBits = Math.min(truncLenBits, key.length * 8);
+ mTruncLenBits = truncLenBits;
+ checkValidOrThrow(mName, mKey.length * 8, mTruncLenBits);
}
/** Get the algorithm name */
@@ -169,7 +175,11 @@ public final class IpSecAlgorithm implements Parcelable {
public static final Parcelable.Creator<IpSecAlgorithm> CREATOR =
new Parcelable.Creator<IpSecAlgorithm>() {
public IpSecAlgorithm createFromParcel(Parcel in) {
- return new IpSecAlgorithm(in);
+ final String name = in.readString();
+ final byte[] key = in.createByteArray();
+ final int truncLenBits = in.readInt();
+
+ return new IpSecAlgorithm(name, key, truncLenBits);
}
public IpSecAlgorithm[] newArray(int size) {
@@ -177,30 +187,47 @@ public final class IpSecAlgorithm implements Parcelable {
}
};
- private IpSecAlgorithm(Parcel in) {
- mName = in.readString();
- mKey = in.createByteArray();
- mTruncLenBits = in.readInt();
- }
+ private static void checkValidOrThrow(String name, int keyLen, int truncLen) {
+ boolean isValidLen = true;
+ boolean isValidTruncLen = true;
- private static boolean isTruncationLengthValid(String algo, int truncLenBits) {
- switch (algo) {
+ switch(name) {
case CRYPT_AES_CBC:
- return (truncLenBits == 128 || truncLenBits == 192 || truncLenBits == 256);
+ isValidLen = keyLen == 128 || keyLen == 192 || keyLen == 256;
+ break;
case AUTH_HMAC_MD5:
- return (truncLenBits >= 96 && truncLenBits <= 128);
+ isValidLen = keyLen == 128;
+ isValidTruncLen = truncLen >= 96 && truncLen <= 128;
+ break;
case AUTH_HMAC_SHA1:
- return (truncLenBits >= 96 && truncLenBits <= 160);
+ isValidLen = keyLen == 160;
+ isValidTruncLen = truncLen >= 96 && truncLen <= 160;
+ break;
case AUTH_HMAC_SHA256:
- return (truncLenBits >= 96 && truncLenBits <= 256);
+ isValidLen = keyLen == 256;
+ isValidTruncLen = truncLen >= 96 && truncLen <= 256;
+ break;
case AUTH_HMAC_SHA384:
- return (truncLenBits >= 192 && truncLenBits <= 384);
+ isValidLen = keyLen == 384;
+ isValidTruncLen = truncLen >= 192 && truncLen <= 384;
+ break;
case AUTH_HMAC_SHA512:
- return (truncLenBits >= 256 && truncLenBits <= 512);
+ isValidLen = keyLen == 512;
+ isValidTruncLen = truncLen >= 256 && truncLen <= 512;
+ break;
case AUTH_CRYPT_AES_GCM:
- return (truncLenBits == 64 || truncLenBits == 96 || truncLenBits == 128);
+ // The keying material for GCM is a key plus a 32-bit salt
+ isValidLen = keyLen == 128 + 32 || keyLen == 192 + 32 || keyLen == 256 + 32;
+ break;
default:
- return false;
+ throw new IllegalArgumentException("Couldn't find an algorithm: " + name);
+ }
+
+ if (!isValidLen) {
+ throw new IllegalArgumentException("Invalid key material keyLength: " + keyLen);
+ }
+ if (!isValidTruncLen) {
+ throw new IllegalArgumentException("Invalid truncation keyLength: " + truncLen);
}
}
@@ -217,8 +244,9 @@ public final class IpSecAlgorithm implements Parcelable {
.toString();
}
- /** package */
- static boolean equals(IpSecAlgorithm lhs, IpSecAlgorithm rhs) {
+ /** @hide */
+ @VisibleForTesting
+ public static boolean equals(IpSecAlgorithm lhs, IpSecAlgorithm rhs) {
if (lhs == null || rhs == null) return (lhs == rhs);
return (lhs.mName.equals(rhs.mName)
&& Arrays.equals(lhs.mKey, rhs.mKey)
diff --git a/android/net/IpSecManager.java b/android/net/IpSecManager.java
index a9e60ec8..6a4b8914 100644
--- a/android/net/IpSecManager.java
+++ b/android/net/IpSecManager.java
@@ -46,7 +46,7 @@ import java.net.Socket;
* to create a VPN should use {@link VpnService}.
*
* @see <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the
- * Internet Protocol</a>
+ * Internet Protocol</a>
*/
@SystemService(Context.IPSEC_SERVICE)
public final class IpSecManager {
@@ -59,8 +59,7 @@ public final class IpSecManager {
*
* @hide
*/
- @TestApi
- public static final int INVALID_SECURITY_PARAMETER_INDEX = 0;
+ @TestApi public static final int INVALID_SECURITY_PARAMETER_INDEX = 0;
/** @hide */
public interface Status {
@@ -78,7 +77,7 @@ public final class IpSecManager {
* <p>The combination of remote {@code InetAddress} and SPI must be unique across all apps on
* one device. If this error is encountered, a new SPI is required before a transform may be
* created. This error can be avoided by calling {@link
- * IpSecManager#reserveSecurityParameterIndex}.
+ * IpSecManager#allocateSecurityParameterIndex}.
*/
public static final class SpiUnavailableException extends AndroidException {
private final int mSpi;
@@ -121,7 +120,7 @@ public final class IpSecManager {
* This class represents a reserved SPI.
*
* <p>Objects of this type are used to track reserved security parameter indices. They can be
- * obtained by calling {@link IpSecManager#reserveSecurityParameterIndex} and must be released
+ * obtained by calling {@link IpSecManager#allocateSecurityParameterIndex} and must be released
* by calling {@link #close()} when they are no longer needed.
*/
public static final class SecurityParameterIndex implements AutoCloseable {
@@ -170,7 +169,7 @@ public final class IpSecManager {
mRemoteAddress = remoteAddress;
try {
IpSecSpiResponse result =
- mService.reserveSecurityParameterIndex(
+ mService.allocateSecurityParameterIndex(
direction, remoteAddress.getHostAddress(), spi, new Binder());
if (result == null) {
@@ -228,7 +227,7 @@ public final class IpSecManager {
* for this user
* @throws SpiUnavailableException indicating that a particular SPI cannot be reserved
*/
- public SecurityParameterIndex reserveSecurityParameterIndex(
+ public SecurityParameterIndex allocateSecurityParameterIndex(
int direction, InetAddress remoteAddress) throws ResourceUnavailableException {
try {
return new SecurityParameterIndex(
@@ -255,7 +254,7 @@ public final class IpSecManager {
* for this user
* @throws SpiUnavailableException indicating that the requested SPI could not be reserved
*/
- public SecurityParameterIndex reserveSecurityParameterIndex(
+ public SecurityParameterIndex allocateSecurityParameterIndex(
int direction, InetAddress remoteAddress, int requestedSpi)
throws SpiUnavailableException, ResourceUnavailableException {
if (requestedSpi == IpSecManager.INVALID_SECURITY_PARAMETER_INDEX) {
@@ -273,16 +272,18 @@ public final class IpSecManager {
* unprotected traffic can resume on that socket.
*
* <p>For security reasons, the destination address of any traffic on the socket must match the
- * remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any
+ * remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any
* other IP address will result in an IOException. In addition, reads and writes on the socket
* will throw IOException if the user deactivates the transform (by calling {@link
* IpSecTransform#close()}) without calling {@link #removeTransportModeTransform}.
*
- * <h4>Rekey Procedure</h4> <p>When applying a new tranform to a socket, the previous transform
- * will be removed. However, inbound traffic on the old transform will continue to be decrypted
- * until that transform is deallocated by calling {@link IpSecTransform#close()}. This overlap
- * allows rekey procedures where both transforms are valid until both endpoints are using the
- * new transform and all in-flight packets have been received.
+ * <h4>Rekey Procedure</h4>
+ *
+ * <p>When applying a new tranform to a socket, the previous transform will be removed. However,
+ * inbound traffic on the old transform will continue to be decrypted until that transform is
+ * deallocated by calling {@link IpSecTransform#close()}. This overlap allows rekey procedures
+ * where both transforms are valid until both endpoints are using the new transform and all
+ * in-flight packets have been received.
*
* @param socket a stream socket
* @param transform a transport mode {@code IpSecTransform}
@@ -310,11 +311,13 @@ public final class IpSecManager {
* will throw IOException if the user deactivates the transform (by calling {@link
* IpSecTransform#close()}) without calling {@link #removeTransportModeTransform}.
*
- * <h4>Rekey Procedure</h4> <p>When applying a new tranform to a socket, the previous transform
- * will be removed. However, inbound traffic on the old transform will continue to be decrypted
- * until that transform is deallocated by calling {@link IpSecTransform#close()}. This overlap
- * allows rekey procedures where both transforms are valid until both endpoints are using the
- * new transform and all in-flight packets have been received.
+ * <h4>Rekey Procedure</h4>
+ *
+ * <p>When applying a new tranform to a socket, the previous transform will be removed. However,
+ * inbound traffic on the old transform will continue to be decrypted until that transform is
+ * deallocated by calling {@link IpSecTransform#close()}. This overlap allows rekey procedures
+ * where both transforms are valid until both endpoints are using the new transform and all
+ * in-flight packets have been received.
*
* @param socket a datagram socket
* @param transform a transport mode {@code IpSecTransform}
@@ -342,11 +345,13 @@ public final class IpSecManager {
* will throw IOException if the user deactivates the transform (by calling {@link
* IpSecTransform#close()}) without calling {@link #removeTransportModeTransform}.
*
- * <h4>Rekey Procedure</h4> <p>When applying a new tranform to a socket, the previous transform
- * will be removed. However, inbound traffic on the old transform will continue to be decrypted
- * until that transform is deallocated by calling {@link IpSecTransform#close()}. This overlap
- * allows rekey procedures where both transforms are valid until both endpoints are using the
- * new transform and all in-flight packets have been received.
+ * <h4>Rekey Procedure</h4>
+ *
+ * <p>When applying a new tranform to a socket, the previous transform will be removed. However,
+ * inbound traffic on the old transform will continue to be decrypted until that transform is
+ * deallocated by calling {@link IpSecTransform#close()}. This overlap allows rekey procedures
+ * where both transforms are valid until both endpoints are using the new transform and all
+ * in-flight packets have been received.
*
* @param socket a socket file descriptor
* @param transform a transport mode {@code IpSecTransform}
@@ -379,7 +384,8 @@ public final class IpSecManager {
* Applications should probably not use this API directly. Instead, they should use {@link
* VpnService} to provide VPN capability in a more generic fashion.
*
- * TODO: Update javadoc for tunnel mode APIs at the same time the APIs are re-worked.
+ * <p>TODO: Update javadoc for tunnel mode APIs at the same time the APIs are re-worked.
+ *
* @param net a {@link Network} that will be tunneled via IP Sec.
* @param transform an {@link IpSecTransform}, which must be an active Tunnel Mode transform.
* @hide
@@ -469,7 +475,8 @@ public final class IpSecManager {
* all traffic that cannot be routed to the Tunnel's outbound interface. If that interface is
* lost, all traffic will drop.
*
- * TODO: Update javadoc for tunnel mode APIs at the same time the APIs are re-worked.
+ * <p>TODO: Update javadoc for tunnel mode APIs at the same time the APIs are re-worked.
+ *
* @param net a network that currently has transform applied to it.
* @param transform a Tunnel Mode IPsec Transform that has been previously applied to the given
* network
diff --git a/android/net/IpSecTransform.java b/android/net/IpSecTransform.java
index cda4ec76..7cd742b4 100644
--- a/android/net/IpSecTransform.java
+++ b/android/net/IpSecTransform.java
@@ -47,7 +47,7 @@ import java.net.InetAddress;
* system resources.
*
* @see <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the
- * Internet Protocol</a>
+ * Internet Protocol</a>
*/
public final class IpSecTransform implements AutoCloseable {
private static final String TAG = "IpSecTransform";
@@ -116,8 +116,7 @@ public final class IpSecTransform implements AutoCloseable {
}
/**
- * Checks the result status and throws an appropriate exception if
- * the status is not Status.OK.
+ * Checks the result status and throws an appropriate exception if the status is not Status.OK.
*/
private void checkResultStatus(int status)
throws IOException, IpSecManager.ResourceUnavailableException,
@@ -267,9 +266,7 @@ public final class IpSecTransform implements AutoCloseable {
return;
}
- /**
- * This class is used to build {@link IpSecTransform} objects.
- */
+ /** This class is used to build {@link IpSecTransform} objects. */
public static class Builder {
private Context mContext;
private IpSecConfig mConfig;
@@ -339,7 +336,7 @@ public final class IpSecTransform implements AutoCloseable {
*
* <p>Because IPsec operates at the IP layer, this 32-bit identifier uniquely identifies
* packets to a given destination address. To prevent SPI collisions, values should be
- * reserved by calling {@link IpSecManager#reserveSecurityParameterIndex}.
+ * reserved by calling {@link IpSecManager#allocateSecurityParameterIndex}.
*
* <p>If the SPI and algorithms are omitted for one direction, traffic in that direction
* will not be encrypted or authenticated.
@@ -374,10 +371,9 @@ public final class IpSecTransform implements AutoCloseable {
* <p>This allows IPsec traffic to pass through a NAT.
*
* @see <a href="https://tools.ietf.org/html/rfc3948">RFC 3948, UDP Encapsulation of IPsec
- * ESP Packets</a>
+ * ESP Packets</a>
* @see <a href="https://tools.ietf.org/html/rfc7296#section-2.23">RFC 7296 section 2.23,
- * NAT Traversal of IKEv2</a>
- *
+ * NAT Traversal of IKEv2</a>
* @param localSocket a socket for sending and receiving encapsulated traffic
* @param remotePort the UDP port number of the remote host that will send and receive
* encapsulated traffic. In the case of IKEv2, this should be port 4500.
@@ -402,7 +398,6 @@ public final class IpSecTransform implements AutoCloseable {
*
* @param intervalSeconds the maximum number of seconds between keepalive packets. Must be
* between 20s and 3600s.
- *
* @hide
*/
@SystemApi
@@ -418,7 +413,6 @@ public final class IpSecTransform implements AutoCloseable {
* will not affect any network traffic until it has been applied to one or more sockets.
*
* @see IpSecManager#applyTransportModeTransform
- *
* @param remoteAddress the remote {@code InetAddress} of traffic on sockets that will use
* this transform
* @throws IllegalArgumentException indicating that a particular combination of transform
@@ -453,8 +447,8 @@ public final class IpSecTransform implements AutoCloseable {
*/
public IpSecTransform buildTunnelModeTransform(
InetAddress localAddress, InetAddress remoteAddress) {
- //FIXME: argument validation here
- //throw new IllegalArgumentException("Natt Keepalive requires UDP Encapsulation");
+ // FIXME: argument validation here
+ // throw new IllegalArgumentException("Natt Keepalive requires UDP Encapsulation");
mConfig.setLocalAddress(localAddress.getHostAddress());
mConfig.setRemoteAddress(remoteAddress.getHostAddress());
mConfig.setMode(MODE_TUNNEL);
diff --git a/android/net/MacAddress.java b/android/net/MacAddress.java
index f6a69bac..d6992aae 100644
--- a/android/net/MacAddress.java
+++ b/android/net/MacAddress.java
@@ -16,95 +16,126 @@
package android.net;
+import android.annotation.IntDef;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.internal.util.BitUtils;
+import com.android.internal.util.Preconditions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Random;
-import java.util.StringJoiner;
/**
- * Represents a mac address.
+ * Representation of a MAC address.
*
- * @hide
+ * This class only supports 48 bits long addresses and does not support 64 bits long addresses.
+ * Instances of this class are immutable.
*/
public final class MacAddress implements Parcelable {
private static final int ETHER_ADDR_LEN = 6;
private static final byte[] ETHER_ADDR_BROADCAST = addr(0xff, 0xff, 0xff, 0xff, 0xff, 0xff);
- /** The broadcast mac address. */
- public static final MacAddress BROADCAST_ADDRESS = new MacAddress(ETHER_ADDR_BROADCAST);
+ /**
+ * The MacAddress representing the unique broadcast MAC address.
+ */
+ public static final MacAddress BROADCAST_ADDRESS = MacAddress.fromBytes(ETHER_ADDR_BROADCAST);
- /** The zero mac address. */
+ /**
+ * The MacAddress zero MAC address.
+ * @hide
+ */
public static final MacAddress ALL_ZEROS_ADDRESS = new MacAddress(0);
- /** Represents categories of mac addresses. */
- public enum MacAddressType {
- UNICAST,
- MULTICAST,
- BROADCAST;
- }
-
- private static final long VALID_LONG_MASK = BROADCAST_ADDRESS.mAddr;
- private static final long LOCALLY_ASSIGNED_MASK = new MacAddress("2:0:0:0:0:0").mAddr;
- private static final long MULTICAST_MASK = new MacAddress("1:0:0:0:0:0").mAddr;
- private static final long OUI_MASK = new MacAddress("ff:ff:ff:0:0:0").mAddr;
- private static final long NIC_MASK = new MacAddress("0:0:0:ff:ff:ff").mAddr;
- private static final MacAddress BASE_ANDROID_MAC = new MacAddress("da:a1:19:0:0:0");
-
- // Internal representation of the mac address as a single 8 byte long.
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "TYPE_" }, value = {
+ TYPE_UNKNOWN,
+ TYPE_UNICAST,
+ TYPE_MULTICAST,
+ TYPE_BROADCAST,
+ })
+ public @interface MacAddressType { }
+
+ /** Indicates a MAC address of unknown type. */
+ public static final int TYPE_UNKNOWN = 0;
+ /** Indicates a MAC address is a unicast address. */
+ public static final int TYPE_UNICAST = 1;
+ /** Indicates a MAC address is a multicast address. */
+ public static final int TYPE_MULTICAST = 2;
+ /** Indicates a MAC address is the broadcast address. */
+ public static final int TYPE_BROADCAST = 3;
+
+ private static final long VALID_LONG_MASK = (1L << 48) - 1;
+ private static final long LOCALLY_ASSIGNED_MASK = MacAddress.fromString("2:0:0:0:0:0").mAddr;
+ private static final long MULTICAST_MASK = MacAddress.fromString("1:0:0:0:0:0").mAddr;
+ private static final long OUI_MASK = MacAddress.fromString("ff:ff:ff:0:0:0").mAddr;
+ private static final long NIC_MASK = MacAddress.fromString("0:0:0:ff:ff:ff").mAddr;
+ private static final MacAddress BASE_GOOGLE_MAC = MacAddress.fromString("da:a1:19:0:0:0");
+
+ // Internal representation of the MAC address as a single 8 byte long.
// The encoding scheme sets the two most significant bytes to 0. The 6 bytes of the
- // mac address are encoded in the 6 least significant bytes of the long, where the first
+ // MAC address are encoded in the 6 least significant bytes of the long, where the first
// byte of the array is mapped to the 3rd highest logical byte of the long, the second
// byte of the array is mapped to the 4th highest logical byte of the long, and so on.
private final long mAddr;
private MacAddress(long addr) {
- mAddr = addr;
- }
-
- /** Creates a MacAddress for the given byte representation. */
- public MacAddress(byte[] addr) {
- this(longAddrFromByteAddr(addr));
+ mAddr = (VALID_LONG_MASK & addr);
}
- /** Creates a MacAddress for the given string representation. */
- public MacAddress(String addr) {
- this(longAddrFromByteAddr(byteAddrFromStringAddr(addr)));
- }
-
- /** Returns the MacAddressType of this MacAddress. */
- public MacAddressType addressType() {
+ /**
+ * Returns the type of this address.
+ *
+ * @return the int constant representing the MAC address type of this MacAddress.
+ */
+ public @MacAddressType int addressType() {
if (equals(BROADCAST_ADDRESS)) {
- return MacAddressType.BROADCAST;
+ return TYPE_BROADCAST;
}
if (isMulticastAddress()) {
- return MacAddressType.MULTICAST;
+ return TYPE_MULTICAST;
}
- return MacAddressType.UNICAST;
+ return TYPE_UNICAST;
}
- /** Returns true if this MacAddress corresponds to a multicast address. */
+ /**
+ * @return true if this MacAddress is a multicast address.
+ * @hide
+ */
public boolean isMulticastAddress() {
return (mAddr & MULTICAST_MASK) != 0;
}
- /** Returns true if this MacAddress corresponds to a locally assigned address. */
+ /**
+ * @return true if this MacAddress is a locally assigned address.
+ */
public boolean isLocallyAssigned() {
return (mAddr & LOCALLY_ASSIGNED_MASK) != 0;
}
- /** Returns a byte array representation of this MacAddress. */
+ /**
+ * @return a byte array representation of this MacAddress.
+ */
public byte[] toByteArray() {
return byteAddrFromLongAddr(mAddr);
}
@Override
public String toString() {
- return stringAddrFromByteAddr(byteAddrFromLongAddr(mAddr));
+ return stringAddrFromLongAddr(mAddr);
+ }
+
+ /**
+ * @return a String representation of the OUI part of this MacAddress made of 3 hexadecimal
+ * numbers in [0,ff] joined by ':' characters.
+ */
+ public String toOuiString() {
+ return String.format(
+ "%02x:%02x:%02x", (mAddr >> 40) & 0xff, (mAddr >> 32) & 0xff, (mAddr >> 24) & 0xff);
}
@Override
@@ -138,27 +169,50 @@ public final class MacAddress implements Parcelable {
}
};
- /** Return true if the given byte array is not null and has the length of a mac address. */
+ /**
+ * Returns true if the given byte array is an valid MAC address.
+ * A valid byte array representation for a MacAddress is a non-null array of length 6.
+ *
+ * @param addr a byte array.
+ * @return true if the given byte array is not null and has the length of a MAC address.
+ *
+ * @hide
+ */
public static boolean isMacAddress(byte[] addr) {
return addr != null && addr.length == ETHER_ADDR_LEN;
}
/**
- * Return the MacAddressType of the mac address represented by the given byte array,
- * or null if the given byte array does not represent an mac address.
+ * Returns the MAC address type of the MAC address represented by the given byte array,
+ * or null if the given byte array does not represent a MAC address.
+ * A valid byte array representation for a MacAddress is a non-null array of length 6.
+ *
+ * @param addr a byte array representing a MAC address.
+ * @return the int constant representing the MAC address type of the MAC address represented
+ * by the given byte array, or type UNKNOWN if the byte array is not a valid MAC address.
+ *
+ * @hide
*/
- public static MacAddressType macAddressType(byte[] addr) {
+ public static int macAddressType(byte[] addr) {
if (!isMacAddress(addr)) {
- return null;
+ return TYPE_UNKNOWN;
}
- return new MacAddress(addr).addressType();
+ return MacAddress.fromBytes(addr).addressType();
}
- /** DOCME */
+ /**
+ * Converts a String representation of a MAC address to a byte array representation.
+ * A valid String representation for a MacAddress is a series of 6 values in the
+ * range [0,ff] printed in hexadecimal and joined by ':' characters.
+ *
+ * @param addr a String representation of a MAC address.
+ * @return the byte representation of the MAC address.
+ * @throws IllegalArgumentException if the given String is not a valid representation.
+ *
+ * @hide
+ */
public static byte[] byteAddrFromStringAddr(String addr) {
- if (addr == null) {
- throw new IllegalArgumentException("cannot convert the null String");
- }
+ Preconditions.checkNotNull(addr);
String[] parts = addr.split(":");
if (parts.length != ETHER_ADDR_LEN) {
throw new IllegalArgumentException(addr + " was not a valid MAC address");
@@ -174,20 +228,26 @@ public final class MacAddress implements Parcelable {
return bytes;
}
- /** DOCME */
+ /**
+ * Converts a byte array representation of a MAC address to a String representation made
+ * of 6 hexadecimal numbers in [0,ff] joined by ':' characters.
+ * A valid byte array representation for a MacAddress is a non-null array of length 6.
+ *
+ * @param addr a byte array representation of a MAC address.
+ * @return the String representation of the MAC address.
+ * @throws IllegalArgumentException if the given byte array is not a valid representation.
+ *
+ * @hide
+ */
public static String stringAddrFromByteAddr(byte[] addr) {
if (!isMacAddress(addr)) {
return null;
}
- StringJoiner j = new StringJoiner(":");
- for (byte b : addr) {
- j.add(Integer.toHexString(BitUtils.uint8(b)));
- }
- return j.toString();
+ return String.format("%02x:%02x:%02x:%02x:%02x:%02x",
+ addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
}
- /** @hide */
- public static byte[] byteAddrFromLongAddr(long addr) {
+ private static byte[] byteAddrFromLongAddr(long addr) {
byte[] bytes = new byte[ETHER_ADDR_LEN];
int index = ETHER_ADDR_LEN;
while (index-- > 0) {
@@ -197,8 +257,8 @@ public final class MacAddress implements Parcelable {
return bytes;
}
- /** @hide */
- public static long longAddrFromByteAddr(byte[] addr) {
+ private static long longAddrFromByteAddr(byte[] addr) {
+ Preconditions.checkNotNull(addr);
if (!isMacAddress(addr)) {
throw new IllegalArgumentException(
Arrays.toString(addr) + " was not a valid MAC address");
@@ -210,19 +270,17 @@ public final class MacAddress implements Parcelable {
return longAddr;
}
- /** @hide */
- public static long longAddrFromStringAddr(String addr) {
- if (addr == null) {
- throw new IllegalArgumentException("cannot convert the null String");
- }
+ // Internal conversion function equivalent to longAddrFromByteAddr(byteAddrFromStringAddr(addr))
+ // that avoids the allocation of an intermediary byte[].
+ private static long longAddrFromStringAddr(String addr) {
+ Preconditions.checkNotNull(addr);
String[] parts = addr.split(":");
if (parts.length != ETHER_ADDR_LEN) {
throw new IllegalArgumentException(addr + " was not a valid MAC address");
}
long longAddr = 0;
- int index = ETHER_ADDR_LEN;
- while (index-- > 0) {
- int x = Integer.valueOf(parts[index], 16);
+ for (int i = 0; i < parts.length; i++) {
+ int x = Integer.valueOf(parts[i], 16);
if (x < 0 || 0xff < x) {
throw new IllegalArgumentException(addr + "was not a valid MAC address");
}
@@ -231,32 +289,74 @@ public final class MacAddress implements Parcelable {
return longAddr;
}
- /** @hide */
- public static String stringAddrFromLongAddr(long addr) {
- addr = Long.reverseBytes(addr) >> 16;
- StringJoiner j = new StringJoiner(":");
- for (int i = 0; i < ETHER_ADDR_LEN; i++) {
- j.add(Integer.toHexString((byte) addr));
- addr = addr >> 8;
- }
- return j.toString();
+ // Internal conversion function equivalent to stringAddrFromByteAddr(byteAddrFromLongAddr(addr))
+ // that avoids the allocation of an intermediary byte[].
+ private static String stringAddrFromLongAddr(long addr) {
+ return String.format("%02x:%02x:%02x:%02x:%02x:%02x",
+ (addr >> 40) & 0xff,
+ (addr >> 32) & 0xff,
+ (addr >> 24) & 0xff,
+ (addr >> 16) & 0xff,
+ (addr >> 8) & 0xff,
+ addr & 0xff);
+ }
+
+ /**
+ * Creates a MacAddress from the given String representation. A valid String representation
+ * for a MacAddress is a series of 6 values in the range [0,ff] printed in hexadecimal
+ * and joined by ':' characters.
+ *
+ * @param addr a String representation of a MAC address.
+ * @return the MacAddress corresponding to the given String representation.
+ * @throws IllegalArgumentException if the given String is not a valid representation.
+ */
+ public static MacAddress fromString(String addr) {
+ return new MacAddress(longAddrFromStringAddr(addr));
+ }
+
+ /**
+ * Creates a MacAddress from the given byte array representation.
+ * A valid byte array representation for a MacAddress is a non-null array of length 6.
+ *
+ * @param addr a byte array representation of a MAC address.
+ * @return the MacAddress corresponding to the given byte array representation.
+ * @throws IllegalArgumentException if the given byte array is not a valid representation.
+ */
+ public static MacAddress fromBytes(byte[] addr) {
+ return new MacAddress(longAddrFromByteAddr(addr));
}
/**
- * Returns a randomely generated mac address with the Android OUI value "DA-A1-19".
- * The locally assigned bit is always set to 1.
+ * Returns a generated MAC address whose 24 least significant bits constituting the
+ * NIC part of the address are randomly selected.
+ *
+ * The locally assigned bit is always set to 1. The multicast bit is always set to 0.
+ *
+ * @return a random locally assigned MacAddress.
+ *
+ * @hide
*/
- public static MacAddress getRandomAddress() {
- return getRandomAddress(BASE_ANDROID_MAC, new Random());
+ public static MacAddress createRandomUnicastAddress() {
+ return createRandomUnicastAddress(BASE_GOOGLE_MAC, new Random());
}
/**
- * Returns a randomely generated mac address using the given Random object and the same
- * OUI values as the given MacAddress. The locally assigned bit is always set to 1.
+ * Returns a randomly generated MAC address using the given Random object and the same
+ * OUI values as the given MacAddress.
+ *
+ * The locally assigned bit is always set to 1. The multicast bit is always set to 0.
+ *
+ * @param base a base MacAddress whose OUI is used for generating the random address.
+ * @param r a standard Java Random object used for generating the random address.
+ * @return a random locally assigned MacAddress.
+ *
+ * @hide
*/
- public static MacAddress getRandomAddress(MacAddress base, Random r) {
- long longAddr = (base.mAddr & OUI_MASK) | (NIC_MASK & r.nextLong()) | LOCALLY_ASSIGNED_MASK;
- return new MacAddress(longAddr);
+ public static MacAddress createRandomUnicastAddress(MacAddress base, Random r) {
+ long addr = (base.mAddr & OUI_MASK) | (NIC_MASK & r.nextLong());
+ addr = addr | LOCALLY_ASSIGNED_MASK;
+ addr = addr & ~MULTICAST_MASK;
+ return new MacAddress(addr);
}
// Convenience function for working around the lack of byte literals.
diff --git a/android/net/TrafficStats.java b/android/net/TrafficStats.java
index c339856f..196a3bc9 100644
--- a/android/net/TrafficStats.java
+++ b/android/net/TrafficStats.java
@@ -17,7 +17,9 @@
package android.net;
import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.app.DownloadManager;
import android.app.backup.BackupManager;
import android.app.usage.NetworkStatsManager;
@@ -30,6 +32,8 @@ import com.android.server.NetworkManagementSocketTagger;
import dalvik.system.SocketTagger;
+import java.io.FileDescriptor;
+import java.io.IOException;
import java.net.DatagramSocket;
import java.net.Socket;
import java.net.SocketException;
@@ -151,6 +155,8 @@ public class TrafficStats {
private static Object sProfilingLock = new Object();
+ private static final String LOOPBACK_IFACE = "lo";
+
/**
* Set active tag to use when accounting {@link Socket} traffic originating
* from the current thread. Only one active tag per thread is supported.
@@ -264,14 +270,25 @@ public class TrafficStats {
}
/**
+ * Set specific UID to use when accounting {@link Socket} traffic
+ * originating from the current thread as the calling UID. Designed for use
+ * when another application is performing operations on your behalf.
+ * <p>
+ * Changes only take effect during subsequent calls to
+ * {@link #tagSocket(Socket)}.
+ */
+ public static void setThreadStatsUidSelf() {
+ setThreadStatsUid(android.os.Process.myUid());
+ }
+
+ /**
* Clear any active UID set to account {@link Socket} traffic originating
* from the current thread.
*
* @see #setThreadStatsUid(int)
- * @hide
*/
@SystemApi
- @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
+ @SuppressLint("Doclava125")
public static void clearThreadStatsUid() {
NetworkManagementSocketTagger.setThreadSocketStatsUid(-1);
}
@@ -316,6 +333,27 @@ public class TrafficStats {
}
/**
+ * Tag the given {@link FileDescriptor} socket with any statistics
+ * parameters active for the current thread. Subsequent calls always replace
+ * any existing parameters. When finished, call
+ * {@link #untagFileDescriptor(FileDescriptor)} to remove statistics
+ * parameters.
+ *
+ * @see #setThreadStatsTag(int)
+ */
+ public static void tagFileDescriptor(FileDescriptor fd) throws IOException {
+ SocketTagger.get().tag(fd);
+ }
+
+ /**
+ * Remove any statistics parameters from the given {@link FileDescriptor}
+ * socket.
+ */
+ public static void untagFileDescriptor(FileDescriptor fd) throws IOException {
+ SocketTagger.get().untag(fd);
+ }
+
+ /**
* Start profiling data usage for current UID. Only one profiling session
* can be active at a time.
*
@@ -467,7 +505,12 @@ public class TrafficStats {
public static long getMobileTcpRxPackets() {
long total = 0;
for (String iface : getMobileIfaces()) {
- final long stat = nativeGetIfaceStat(iface, TYPE_TCP_RX_PACKETS);
+ long stat = UNSUPPORTED;
+ try {
+ stat = getStatsService().getIfaceStats(iface, TYPE_TCP_RX_PACKETS);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
if (stat != UNSUPPORTED) {
total += stat;
}
@@ -479,7 +522,12 @@ public class TrafficStats {
public static long getMobileTcpTxPackets() {
long total = 0;
for (String iface : getMobileIfaces()) {
- final long stat = nativeGetIfaceStat(iface, TYPE_TCP_TX_PACKETS);
+ long stat = UNSUPPORTED;
+ try {
+ stat = getStatsService().getIfaceStats(iface, TYPE_TCP_TX_PACKETS);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
if (stat != UNSUPPORTED) {
total += stat;
}
@@ -489,22 +537,78 @@ public class TrafficStats {
/** {@hide} */
public static long getTxPackets(String iface) {
- return nativeGetIfaceStat(iface, TYPE_TX_PACKETS);
+ try {
+ return getStatsService().getIfaceStats(iface, TYPE_TX_PACKETS);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/** {@hide} */
public static long getRxPackets(String iface) {
- return nativeGetIfaceStat(iface, TYPE_RX_PACKETS);
+ try {
+ return getStatsService().getIfaceStats(iface, TYPE_RX_PACKETS);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/** {@hide} */
public static long getTxBytes(String iface) {
- return nativeGetIfaceStat(iface, TYPE_TX_BYTES);
+ try {
+ return getStatsService().getIfaceStats(iface, TYPE_TX_BYTES);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/** {@hide} */
public static long getRxBytes(String iface) {
- return nativeGetIfaceStat(iface, TYPE_RX_BYTES);
+ try {
+ return getStatsService().getIfaceStats(iface, TYPE_RX_BYTES);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** {@hide} */
+ @TestApi
+ public static long getLoopbackTxPackets() {
+ try {
+ return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_PACKETS);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** {@hide} */
+ @TestApi
+ public static long getLoopbackRxPackets() {
+ try {
+ return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_PACKETS);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** {@hide} */
+ @TestApi
+ public static long getLoopbackTxBytes() {
+ try {
+ return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_BYTES);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** {@hide} */
+ @TestApi
+ public static long getLoopbackRxBytes() {
+ try {
+ return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_BYTES);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
@@ -517,7 +621,11 @@ public class TrafficStats {
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalTxPackets() {
- return nativeGetTotalStat(TYPE_TX_PACKETS);
+ try {
+ return getStatsService().getTotalStats(TYPE_TX_PACKETS);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
@@ -530,7 +638,11 @@ public class TrafficStats {
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalRxPackets() {
- return nativeGetTotalStat(TYPE_RX_PACKETS);
+ try {
+ return getStatsService().getTotalStats(TYPE_RX_PACKETS);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
@@ -543,7 +655,11 @@ public class TrafficStats {
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalTxBytes() {
- return nativeGetTotalStat(TYPE_TX_BYTES);
+ try {
+ return getStatsService().getTotalStats(TYPE_TX_BYTES);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
@@ -556,7 +672,11 @@ public class TrafficStats {
* return {@link #UNSUPPORTED} on devices where statistics aren't available.
*/
public static long getTotalRxBytes() {
- return nativeGetTotalStat(TYPE_RX_BYTES);
+ try {
+ return getStatsService().getTotalStats(TYPE_RX_BYTES);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
@@ -582,7 +702,11 @@ public class TrafficStats {
// unsupported value. The real filtering is done at the kernel level.
final int callingUid = android.os.Process.myUid();
if (callingUid == android.os.Process.SYSTEM_UID || callingUid == uid) {
- return nativeGetUidStat(uid, TYPE_TX_BYTES);
+ try {
+ return getStatsService().getUidStats(uid, TYPE_TX_BYTES);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
} else {
return UNSUPPORTED;
}
@@ -611,7 +735,11 @@ public class TrafficStats {
// unsupported value. The real filtering is done at the kernel level.
final int callingUid = android.os.Process.myUid();
if (callingUid == android.os.Process.SYSTEM_UID || callingUid == uid) {
- return nativeGetUidStat(uid, TYPE_RX_BYTES);
+ try {
+ return getStatsService().getUidStats(uid, TYPE_RX_BYTES);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
} else {
return UNSUPPORTED;
}
@@ -640,7 +768,11 @@ public class TrafficStats {
// unsupported value. The real filtering is done at the kernel level.
final int callingUid = android.os.Process.myUid();
if (callingUid == android.os.Process.SYSTEM_UID || callingUid == uid) {
- return nativeGetUidStat(uid, TYPE_TX_PACKETS);
+ try {
+ return getStatsService().getUidStats(uid, TYPE_TX_PACKETS);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
} else {
return UNSUPPORTED;
}
@@ -669,7 +801,11 @@ public class TrafficStats {
// unsupported value. The real filtering is done at the kernel level.
final int callingUid = android.os.Process.myUid();
if (callingUid == android.os.Process.SYSTEM_UID || callingUid == uid) {
- return nativeGetUidStat(uid, TYPE_RX_PACKETS);
+ try {
+ return getStatsService().getUidStats(uid, TYPE_RX_PACKETS);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
} else {
return UNSUPPORTED;
}
@@ -797,8 +933,4 @@ public class TrafficStats {
private static final int TYPE_TX_PACKETS = 3;
private static final int TYPE_TCP_RX_PACKETS = 4;
private static final int TYPE_TCP_TX_PACKETS = 5;
-
- private static native long nativeGetTotalStat(int type);
- private static native long nativeGetIfaceStat(String iface, int type);
- private static native long nativeGetUidStat(int uid, int type);
}
diff --git a/android/net/ip/ConnectivityPacketTracker.java b/android/net/ip/ConnectivityPacketTracker.java
index 1925c39e..6cf4fa9a 100644
--- a/android/net/ip/ConnectivityPacketTracker.java
+++ b/android/net/ip/ConnectivityPacketTracker.java
@@ -19,7 +19,7 @@ package android.net.ip;
import static android.system.OsConstants.*;
import android.net.NetworkUtils;
-import android.net.util.BlockingSocketReader;
+import android.net.util.PacketReader;
import android.net.util.ConnectivityPacketSummary;
import android.os.Handler;
import android.system.ErrnoException;
@@ -65,7 +65,7 @@ public class ConnectivityPacketTracker {
private final String mTag;
private final LocalLog mLog;
- private final BlockingSocketReader mPacketListener;
+ private final PacketReader mPacketListener;
private boolean mRunning;
private String mDisplayName;
@@ -101,7 +101,7 @@ public class ConnectivityPacketTracker {
mDisplayName = null;
}
- private final class PacketListener extends BlockingSocketReader {
+ private final class PacketListener extends PacketReader {
private final int mIfIndex;
private final byte mHwAddr[];
diff --git a/android/net/ip/IpClient.java b/android/net/ip/IpClient.java
index 70983c86..fdb366c5 100644
--- a/android/net/ip/IpClient.java
+++ b/android/net/ip/IpClient.java
@@ -815,6 +815,15 @@ public class IpClient extends StateMachine {
pw.println(Objects.toString(provisioningConfig, "N/A"));
pw.decreaseIndent();
+ final IpReachabilityMonitor iprm = mIpReachabilityMonitor;
+ if (iprm != null) {
+ pw.println();
+ pw.println(mTag + " current IpReachabilityMonitor state:");
+ pw.increaseIndent();
+ iprm.dump(pw);
+ pw.decreaseIndent();
+ }
+
pw.println();
pw.println(mTag + " StateMachine dump:");
pw.increaseIndent();
@@ -1237,6 +1246,7 @@ public class IpClient extends StateMachine {
mIpReachabilityMonitor = new IpReachabilityMonitor(
mContext,
mInterfaceName,
+ getHandler(),
mLog,
new IpReachabilityMonitor.Callback() {
@Override
diff --git a/android/net/ip/IpNeighborMonitor.java b/android/net/ip/IpNeighborMonitor.java
new file mode 100644
index 00000000..68073347
--- /dev/null
+++ b/android/net/ip/IpNeighborMonitor.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.ip;
+
+import android.net.netlink.NetlinkConstants;
+import android.net.netlink.NetlinkErrorMessage;
+import android.net.netlink.NetlinkMessage;
+import android.net.netlink.NetlinkSocket;
+import android.net.netlink.RtNetlinkNeighborMessage;
+import android.net.netlink.StructNdMsg;
+import android.net.netlink.StructNlMsgHdr;
+import android.net.util.PacketReader;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.NetlinkSocketAddress;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.internal.util.BitUtils;
+
+import libcore.io.IoUtils;
+import libcore.io.Libcore;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.StringJoiner;
+
+
+/**
+ * IpNeighborMonitor.
+ *
+ * Monitors the kernel rtnetlink neighbor notifications and presents to callers
+ * NeighborEvents describing each event. Callers can provide a consumer instance
+ * to both filter (e.g. by interface index and IP address) and handle the
+ * generated NeighborEvents.
+ *
+ * @hide
+ */
+public class IpNeighborMonitor extends PacketReader {
+ private static final String TAG = IpNeighborMonitor.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ /**
+ * Make the kernel perform neighbor reachability detection (IPv4 ARP or IPv6 ND)
+ * for the given IP address on the specified interface index.
+ *
+ * @return 0 if the request was successfully passed to the kernel; otherwise return
+ * a non-zero error code.
+ */
+ public static int startKernelNeighborProbe(int ifIndex, InetAddress ip) {
+ final String msgSnippet = "probing ip=" + ip.getHostAddress() + "%" + ifIndex;
+ if (DBG) { Log.d(TAG, msgSnippet); }
+
+ final byte[] msg = RtNetlinkNeighborMessage.newNewNeighborMessage(
+ 1, ip, StructNdMsg.NUD_PROBE, ifIndex, null);
+
+ try {
+ NetlinkSocket.sendOneShotKernelMessage(OsConstants.NETLINK_ROUTE, msg);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Error " + msgSnippet + ": " + e);
+ return -e.errno;
+ }
+
+ return 0;
+ }
+
+ public static class NeighborEvent {
+ final long elapsedMs;
+ final short msgType;
+ final int ifindex;
+ final InetAddress ip;
+ final short nudState;
+ final byte[] linkLayerAddr;
+
+ public NeighborEvent(long elapsedMs, short msgType, int ifindex, InetAddress ip,
+ short nudState, byte[] linkLayerAddr) {
+ this.elapsedMs = elapsedMs;
+ this.msgType = msgType;
+ this.ifindex = ifindex;
+ this.ip = ip;
+ this.nudState = nudState;
+ this.linkLayerAddr = linkLayerAddr;
+ }
+
+ boolean isConnected() {
+ return (msgType != NetlinkConstants.RTM_DELNEIGH) &&
+ StructNdMsg.isNudStateConnected(nudState);
+ }
+
+ boolean isValid() {
+ return (msgType != NetlinkConstants.RTM_DELNEIGH) &&
+ StructNdMsg.isNudStateValid(nudState);
+ }
+
+ @Override
+ public String toString() {
+ final StringJoiner j = new StringJoiner(",", "NeighborEvent{", "}");
+ return j.add("@" + elapsedMs)
+ .add(NetlinkConstants.stringForNlMsgType(msgType))
+ .add("if=" + ifindex)
+ .add(ip.getHostAddress())
+ .add(StructNdMsg.stringForNudState(nudState))
+ .add("[" + NetlinkConstants.hexify(linkLayerAddr) + "]")
+ .toString();
+ }
+ }
+
+ public interface NeighborEventConsumer {
+ // Every neighbor event received on the netlink socket is passed in
+ // here. Subclasses should filter for events of interest.
+ public void accept(NeighborEvent event);
+ }
+
+ private final SharedLog mLog;
+ private final NeighborEventConsumer mConsumer;
+
+ public IpNeighborMonitor(Handler h, SharedLog log, NeighborEventConsumer cb) {
+ super(h, NetlinkSocket.DEFAULT_RECV_BUFSIZE);
+ mLog = log.forSubComponent(TAG);
+ mConsumer = (cb != null) ? cb : (event) -> { /* discard */ };
+ }
+
+ @Override
+ protected FileDescriptor createFd() {
+ FileDescriptor fd = null;
+
+ try {
+ fd = NetlinkSocket.forProto(OsConstants.NETLINK_ROUTE);
+ Os.bind(fd, (SocketAddress)(new NetlinkSocketAddress(0, OsConstants.RTMGRP_NEIGH)));
+ Os.connect(fd, (SocketAddress)(new NetlinkSocketAddress(0, 0)));
+
+ if (VDBG) {
+ final NetlinkSocketAddress nlAddr = (NetlinkSocketAddress) Os.getsockname(fd);
+ Log.d(TAG, "bound to sockaddr_nl{"
+ + BitUtils.uint32(nlAddr.getPortId()) + ", "
+ + nlAddr.getGroupsMask()
+ + "}");
+ }
+ } catch (ErrnoException|SocketException e) {
+ logError("Failed to create rtnetlink socket", e);
+ IoUtils.closeQuietly(fd);
+ return null;
+ }
+
+ return fd;
+ }
+
+ @Override
+ protected void handlePacket(byte[] recvbuf, int length) {
+ final long whenMs = SystemClock.elapsedRealtime();
+
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(recvbuf, 0, length);
+ byteBuffer.order(ByteOrder.nativeOrder());
+
+ parseNetlinkMessageBuffer(byteBuffer, whenMs);
+ }
+
+ private void parseNetlinkMessageBuffer(ByteBuffer byteBuffer, long whenMs) {
+ while (byteBuffer.remaining() > 0) {
+ final int position = byteBuffer.position();
+ final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer);
+ if (nlMsg == null || nlMsg.getHeader() == null) {
+ byteBuffer.position(position);
+ mLog.e("unparsable netlink msg: " + NetlinkConstants.hexify(byteBuffer));
+ break;
+ }
+
+ final int srcPortId = nlMsg.getHeader().nlmsg_pid;
+ if (srcPortId != 0) {
+ mLog.e("non-kernel source portId: " + BitUtils.uint32(srcPortId));
+ break;
+ }
+
+ if (nlMsg instanceof NetlinkErrorMessage) {
+ mLog.e("netlink error: " + nlMsg);
+ continue;
+ } else if (!(nlMsg instanceof RtNetlinkNeighborMessage)) {
+ mLog.i("non-rtnetlink neighbor msg: " + nlMsg);
+ continue;
+ }
+
+ evaluateRtNetlinkNeighborMessage((RtNetlinkNeighborMessage) nlMsg, whenMs);
+ }
+ }
+
+ private void evaluateRtNetlinkNeighborMessage(
+ RtNetlinkNeighborMessage neighMsg, long whenMs) {
+ final short msgType = neighMsg.getHeader().nlmsg_type;
+ final StructNdMsg ndMsg = neighMsg.getNdHeader();
+ if (ndMsg == null) {
+ mLog.e("RtNetlinkNeighborMessage without ND message header!");
+ return;
+ }
+
+ final int ifindex = ndMsg.ndm_ifindex;
+ final InetAddress destination = neighMsg.getDestination();
+ final short nudState =
+ (msgType == NetlinkConstants.RTM_DELNEIGH)
+ ? StructNdMsg.NUD_NONE
+ : ndMsg.ndm_state;
+
+ final NeighborEvent event = new NeighborEvent(
+ whenMs, msgType, ifindex, destination, nudState, neighMsg.getLinkLayerAddress());
+
+ if (VDBG) {
+ Log.d(TAG, neighMsg.toString());
+ }
+ if (DBG) {
+ Log.d(TAG, event.toString());
+ }
+
+ mConsumer.accept(event);
+ }
+}
diff --git a/android/net/ip/IpReachabilityMonitor.java b/android/net/ip/IpReachabilityMonitor.java
index 714b35a0..b31ffbba 100644
--- a/android/net/ip/IpReachabilityMonitor.java
+++ b/android/net/ip/IpReachabilityMonitor.java
@@ -22,30 +22,27 @@ import android.net.LinkProperties;
import android.net.LinkProperties.ProvisioningChange;
import android.net.ProxyInfo;
import android.net.RouteInfo;
+import android.net.ip.IpNeighborMonitor.NeighborEvent;
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.IpReachabilityEvent;
-import android.net.netlink.NetlinkConstants;
-import android.net.netlink.NetlinkErrorMessage;
-import android.net.netlink.NetlinkMessage;
-import android.net.netlink.NetlinkSocket;
-import android.net.netlink.RtNetlinkNeighborMessage;
import android.net.netlink.StructNdMsg;
-import android.net.netlink.StructNdaCacheInfo;
-import android.net.netlink.StructNlMsgHdr;
import android.net.util.MultinetworkPolicyTracker;
import android.net.util.SharedLog;
+import android.os.Handler;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.SystemClock;
import android.system.ErrnoException;
-import android.system.NetlinkSocketAddress;
import android.system.OsConstants;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.DumpUtils.Dump;
import java.io.InterruptedIOException;
+import java.io.PrintWriter;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@@ -134,6 +131,8 @@ import java.util.Set;
* state it may be best for the link to disconnect completely and
* reconnect afresh.
*
+ * Accessing an instance of this class from multiple threads is NOT safe.
+ *
* @hide
*/
public class IpReachabilityMonitor {
@@ -169,64 +168,33 @@ public class IpReachabilityMonitor {
}
}
- private final Object mLock = new Object();
private final String mInterfaceName;
private final int mInterfaceIndex;
+ private final IpNeighborMonitor mIpNeighborMonitor;
private final SharedLog mLog;
private final Callback mCallback;
private final Dependencies mDependencies;
private final MultinetworkPolicyTracker mMultinetworkPolicyTracker;
- private final NetlinkSocketObserver mNetlinkSocketObserver;
- private final Thread mObserverThread;
private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
- @GuardedBy("mLock")
private LinkProperties mLinkProperties = new LinkProperties();
- // TODO: consider a map to a private NeighborState class holding more
- // information than a single NUD state entry.
- @GuardedBy("mLock")
- private Map<InetAddress, Short> mIpWatchList = new HashMap<>();
- @GuardedBy("mLock")
- private int mIpWatchListVersion;
- private volatile boolean mRunning;
+ private Map<InetAddress, NeighborEvent> mNeighborWatchList = new HashMap<>();
// Time in milliseconds of the last forced probe request.
private volatile long mLastProbeTimeMs;
- /**
- * Make the kernel perform neighbor reachability detection (IPv4 ARP or IPv6 ND)
- * for the given IP address on the specified interface index.
- *
- * @return 0 if the request was successfully passed to the kernel; otherwise return
- * a non-zero error code.
- */
- private static int probeNeighbor(int ifIndex, InetAddress ip) {
- final String msgSnippet = "probing ip=" + ip.getHostAddress() + "%" + ifIndex;
- if (DBG) { Log.d(TAG, msgSnippet); }
-
- final byte[] msg = RtNetlinkNeighborMessage.newNewNeighborMessage(
- 1, ip, StructNdMsg.NUD_PROBE, ifIndex, null);
-
- try {
- NetlinkSocket.sendOneShotKernelMessage(OsConstants.NETLINK_ROUTE, msg);
- } catch (ErrnoException e) {
- Log.e(TAG, "Error " + msgSnippet + ": " + e);
- return -e.errno;
- }
-
- return 0;
+ public IpReachabilityMonitor(
+ Context context, String ifName, Handler h, SharedLog log, Callback callback) {
+ this(context, ifName, h, log, callback, null);
}
- public IpReachabilityMonitor(Context context, String ifName, SharedLog log, Callback callback) {
- this(context, ifName, log, callback, null);
- }
-
- public IpReachabilityMonitor(Context context, String ifName, SharedLog log, Callback callback,
+ public IpReachabilityMonitor(
+ Context context, String ifName, Handler h, SharedLog log, Callback callback,
MultinetworkPolicyTracker tracker) {
- this(ifName, getInterfaceIndex(ifName), log, callback, tracker,
+ this(ifName, getInterfaceIndex(ifName), h, log, callback, tracker,
Dependencies.makeDefault(context, ifName));
}
@VisibleForTesting
- IpReachabilityMonitor(String ifName, int ifIndex, SharedLog log, Callback callback,
+ IpReachabilityMonitor(String ifName, int ifIndex, Handler h, SharedLog log, Callback callback,
MultinetworkPolicyTracker tracker, Dependencies dependencies) {
mInterfaceName = ifName;
mLog = log.forSubComponent(TAG);
@@ -234,45 +202,54 @@ public class IpReachabilityMonitor {
mMultinetworkPolicyTracker = tracker;
mInterfaceIndex = ifIndex;
mDependencies = dependencies;
- mNetlinkSocketObserver = new NetlinkSocketObserver();
- mObserverThread = new Thread(mNetlinkSocketObserver);
- mObserverThread.start();
+
+ mIpNeighborMonitor = new IpNeighborMonitor(h, mLog,
+ (NeighborEvent event) -> {
+ if (mInterfaceIndex != event.ifindex) return;
+ if (!mNeighborWatchList.containsKey(event.ip)) return;
+
+ final NeighborEvent prev = mNeighborWatchList.put(event.ip, event);
+
+ // TODO: Consider what to do with other states that are not within
+ // NeighborEvent#isValid() (i.e. NUD_NONE, NUD_INCOMPLETE).
+ if (event.nudState == StructNdMsg.NUD_FAILED) {
+ mLog.w("ALERT neighbor went from: " + prev + " to: " + event);
+ handleNeighborLost(event);
+ }
+ });
+ mIpNeighborMonitor.start();
}
public void stop() {
- mRunning = false;
+ mIpNeighborMonitor.stop();
clearLinkProperties();
- mNetlinkSocketObserver.clearNetlinkSocket();
}
- // TODO: add a public dump() method that can be called during a bug report.
-
- private String describeWatchList() {
- final String delimiter = ", ";
- StringBuilder sb = new StringBuilder();
- synchronized (mLock) {
- sb.append("iface{" + mInterfaceName + "/" + mInterfaceIndex + "}, ");
- sb.append("v{" + mIpWatchListVersion + "}, ");
- sb.append("ntable=[");
- boolean firstTime = true;
- for (Map.Entry<InetAddress, Short> entry : mIpWatchList.entrySet()) {
- if (firstTime) {
- firstTime = false;
- } else {
- sb.append(delimiter);
- }
- sb.append(entry.getKey().getHostAddress() + "/" +
- StructNdMsg.stringForNudState(entry.getValue()));
- }
- sb.append("]");
- }
- return sb.toString();
+ public void dump(PrintWriter pw) {
+ DumpUtils.dumpAsync(
+ mIpNeighborMonitor.getHandler(),
+ new Dump() {
+ @Override
+ public void dump(PrintWriter pw, String prefix) {
+ pw.println(describeWatchList("\n"));
+ }
+ },
+ pw, "", 1000);
}
- private boolean isWatching(InetAddress ip) {
- synchronized (mLock) {
- return mRunning && mIpWatchList.containsKey(ip);
+ private String describeWatchList() { return describeWatchList(" "); }
+
+ private String describeWatchList(String sep) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("iface{" + mInterfaceName + "/" + mInterfaceIndex + "}," + sep);
+ sb.append("ntable=[" + sep);
+ String delimiter = "";
+ for (Map.Entry<InetAddress, NeighborEvent> entry : mNeighborWatchList.entrySet()) {
+ sb.append(delimiter).append(entry.getKey().getHostAddress() + "/" + entry.getValue());
+ delimiter = "," + sep;
}
+ sb.append("]");
+ return sb.toString();
}
private static boolean isOnLink(List<RouteInfo> routes, InetAddress ip) {
@@ -284,13 +261,6 @@ public class IpReachabilityMonitor {
return false;
}
- private short getNeighborStateLocked(InetAddress ip) {
- if (mIpWatchList.containsKey(ip)) {
- return mIpWatchList.get(ip);
- }
- return StructNdMsg.NUD_NONE;
- }
-
public void updateLinkProperties(LinkProperties lp) {
if (!mInterfaceName.equals(lp.getInterfaceName())) {
// TODO: figure out whether / how to cope with interface changes.
@@ -299,70 +269,63 @@ public class IpReachabilityMonitor {
return;
}
- synchronized (mLock) {
- mLinkProperties = new LinkProperties(lp);
- Map<InetAddress, Short> newIpWatchList = new HashMap<>();
+ mLinkProperties = new LinkProperties(lp);
+ Map<InetAddress, NeighborEvent> newNeighborWatchList = new HashMap<>();
- final List<RouteInfo> routes = mLinkProperties.getRoutes();
- for (RouteInfo route : routes) {
- if (route.hasGateway()) {
- InetAddress gw = route.getGateway();
- if (isOnLink(routes, gw)) {
- newIpWatchList.put(gw, getNeighborStateLocked(gw));
- }
+ final List<RouteInfo> routes = mLinkProperties.getRoutes();
+ for (RouteInfo route : routes) {
+ if (route.hasGateway()) {
+ InetAddress gw = route.getGateway();
+ if (isOnLink(routes, gw)) {
+ newNeighborWatchList.put(gw, mNeighborWatchList.getOrDefault(gw, null));
}
}
+ }
- for (InetAddress nameserver : lp.getDnsServers()) {
- if (isOnLink(routes, nameserver)) {
- newIpWatchList.put(nameserver, getNeighborStateLocked(nameserver));
- }
+ for (InetAddress dns : lp.getDnsServers()) {
+ if (isOnLink(routes, dns)) {
+ newNeighborWatchList.put(dns, mNeighborWatchList.getOrDefault(dns, null));
}
-
- mIpWatchList = newIpWatchList;
- mIpWatchListVersion++;
}
+
+ mNeighborWatchList = newNeighborWatchList;
if (DBG) { Log.d(TAG, "watch: " + describeWatchList()); }
}
public void clearLinkProperties() {
- synchronized (mLock) {
- mLinkProperties.clear();
- mIpWatchList.clear();
- mIpWatchListVersion++;
- }
+ mLinkProperties.clear();
+ mNeighborWatchList.clear();
if (DBG) { Log.d(TAG, "clear: " + describeWatchList()); }
}
- private void handleNeighborLost(String msg) {
- InetAddress ip = null;
- final ProvisioningChange delta;
- synchronized (mLock) {
- LinkProperties whatIfLp = new LinkProperties(mLinkProperties);
+ private void handleNeighborLost(NeighborEvent event) {
+ final LinkProperties whatIfLp = new LinkProperties(mLinkProperties);
- for (Map.Entry<InetAddress, Short> entry : mIpWatchList.entrySet()) {
- if (entry.getValue() != StructNdMsg.NUD_FAILED) {
- continue;
- }
-
- ip = entry.getKey();
- for (RouteInfo route : mLinkProperties.getRoutes()) {
- if (ip.equals(route.getGateway())) {
- whatIfLp.removeRoute(route);
- }
- }
-
- if (avoidingBadLinks() || !(ip instanceof Inet6Address)) {
- // We should do this unconditionally, but alas we cannot: b/31827713.
- whatIfLp.removeDnsServer(ip);
+ InetAddress ip = null;
+ for (Map.Entry<InetAddress, NeighborEvent> entry : mNeighborWatchList.entrySet()) {
+ // TODO: Consider using NeighborEvent#isValid() here; it's more
+ // strict but may interact badly if other entries are somehow in
+ // NUD_INCOMPLETE (say, during network attach).
+ if (entry.getValue().nudState != StructNdMsg.NUD_FAILED) continue;
+
+ ip = entry.getKey();
+ for (RouteInfo route : mLinkProperties.getRoutes()) {
+ if (ip.equals(route.getGateway())) {
+ whatIfLp.removeRoute(route);
}
}
- delta = LinkProperties.compareProvisioning(mLinkProperties, whatIfLp);
+ if (avoidingBadLinks() || !(ip instanceof Inet6Address)) {
+ // We should do this unconditionally, but alas we cannot: b/31827713.
+ whatIfLp.removeDnsServer(ip);
+ }
}
+ final ProvisioningChange delta = LinkProperties.compareProvisioning(
+ mLinkProperties, whatIfLp);
+
if (delta == ProvisioningChange.LOST_PROVISIONING) {
- final String logMsg = "FAILURE: LOST_PROVISIONING, " + msg;
+ final String logMsg = "FAILURE: LOST_PROVISIONING, " + event;
Log.w(TAG, logMsg);
if (mCallback != null) {
// TODO: remove |ip| when the callback signature no longer has
@@ -378,12 +341,9 @@ public class IpReachabilityMonitor {
}
public void probeAll() {
- final List<InetAddress> ipProbeList;
- synchronized (mLock) {
- ipProbeList = new ArrayList<>(mIpWatchList.keySet());
- }
+ final List<InetAddress> ipProbeList = new ArrayList<>(mNeighborWatchList.keySet());
- if (!ipProbeList.isEmpty() && mRunning) {
+ if (!ipProbeList.isEmpty()) {
// Keep the CPU awake long enough to allow all ARP/ND
// probes a reasonable chance at success. See b/23197666.
//
@@ -394,13 +354,10 @@ public class IpReachabilityMonitor {
}
for (InetAddress target : ipProbeList) {
- if (!mRunning) {
- break;
- }
- final int returnValue = probeNeighbor(mInterfaceIndex, target);
+ final int rval = IpNeighborMonitor.startKernelNeighborProbe(mInterfaceIndex, target);
mLog.log(String.format("put neighbor %s into NUD_PROBE state (rval=%d)",
- target.getHostAddress(), returnValue));
- logEvent(IpReachabilityEvent.PROBE, returnValue);
+ target.getHostAddress(), rval));
+ logEvent(IpReachabilityEvent.PROBE, rval);
}
mLastProbeTimeMs = SystemClock.elapsedRealtime();
}
@@ -446,153 +403,4 @@ public class IpReachabilityMonitor {
int eventType = IpReachabilityEvent.nudFailureEventType(isFromProbe, isProvisioningLost);
mMetricsLog.log(mInterfaceName, new IpReachabilityEvent(eventType));
}
-
- // TODO: simplify the number of objects by making this extend Thread.
- private final class NetlinkSocketObserver implements Runnable {
- private NetlinkSocket mSocket;
-
- @Override
- public void run() {
- if (VDBG) { Log.d(TAG, "Starting observing thread."); }
- mRunning = true;
-
- try {
- setupNetlinkSocket();
- } catch (ErrnoException | SocketException e) {
- Log.e(TAG, "Failed to suitably initialize a netlink socket", e);
- mRunning = false;
- }
-
- while (mRunning) {
- final ByteBuffer byteBuffer;
- try {
- byteBuffer = recvKernelReply();
- } catch (ErrnoException e) {
- if (mRunning) { Log.w(TAG, "ErrnoException: ", e); }
- break;
- }
- final long whenMs = SystemClock.elapsedRealtime();
- if (byteBuffer == null) {
- continue;
- }
- parseNetlinkMessageBuffer(byteBuffer, whenMs);
- }
-
- clearNetlinkSocket();
-
- mRunning = false; // Not a no-op when ErrnoException happened.
- if (VDBG) { Log.d(TAG, "Finishing observing thread."); }
- }
-
- private void clearNetlinkSocket() {
- if (mSocket != null) {
- mSocket.close();
- }
- }
-
- // TODO: Refactor the main loop to recreate the socket upon recoverable errors.
- private void setupNetlinkSocket() throws ErrnoException, SocketException {
- clearNetlinkSocket();
- mSocket = new NetlinkSocket(OsConstants.NETLINK_ROUTE);
-
- final NetlinkSocketAddress listenAddr = new NetlinkSocketAddress(
- 0, OsConstants.RTMGRP_NEIGH);
- mSocket.bind(listenAddr);
-
- if (VDBG) {
- final NetlinkSocketAddress nlAddr = mSocket.getLocalAddress();
- Log.d(TAG, "bound to sockaddr_nl{"
- + ((long) (nlAddr.getPortId() & 0xffffffff)) + ", "
- + nlAddr.getGroupsMask()
- + "}");
- }
- }
-
- private ByteBuffer recvKernelReply() throws ErrnoException {
- try {
- return mSocket.recvMessage(0);
- } catch (InterruptedIOException e) {
- // Interruption or other error, e.g. another thread closed our file descriptor.
- } catch (ErrnoException e) {
- if (e.errno != OsConstants.EAGAIN) {
- throw e;
- }
- }
- return null;
- }
-
- private void parseNetlinkMessageBuffer(ByteBuffer byteBuffer, long whenMs) {
- while (byteBuffer.remaining() > 0) {
- final int position = byteBuffer.position();
- final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer);
- if (nlMsg == null || nlMsg.getHeader() == null) {
- byteBuffer.position(position);
- Log.e(TAG, "unparsable netlink msg: " + NetlinkConstants.hexify(byteBuffer));
- break;
- }
-
- final int srcPortId = nlMsg.getHeader().nlmsg_pid;
- if (srcPortId != 0) {
- Log.e(TAG, "non-kernel source portId: " + ((long) (srcPortId & 0xffffffff)));
- break;
- }
-
- if (nlMsg instanceof NetlinkErrorMessage) {
- Log.e(TAG, "netlink error: " + nlMsg);
- continue;
- } else if (!(nlMsg instanceof RtNetlinkNeighborMessage)) {
- if (DBG) {
- Log.d(TAG, "non-rtnetlink neighbor msg: " + nlMsg);
- }
- continue;
- }
-
- evaluateRtNetlinkNeighborMessage((RtNetlinkNeighborMessage) nlMsg, whenMs);
- }
- }
-
- private void evaluateRtNetlinkNeighborMessage(
- RtNetlinkNeighborMessage neighMsg, long whenMs) {
- final StructNdMsg ndMsg = neighMsg.getNdHeader();
- if (ndMsg == null || ndMsg.ndm_ifindex != mInterfaceIndex) {
- return;
- }
-
- final InetAddress destination = neighMsg.getDestination();
- if (!isWatching(destination)) {
- return;
- }
-
- final short msgType = neighMsg.getHeader().nlmsg_type;
- final short nudState = ndMsg.ndm_state;
- final String eventMsg = "NeighborEvent{"
- + "elapsedMs=" + whenMs + ", "
- + destination.getHostAddress() + ", "
- + "[" + NetlinkConstants.hexify(neighMsg.getLinkLayerAddress()) + "], "
- + NetlinkConstants.stringForNlMsgType(msgType) + ", "
- + StructNdMsg.stringForNudState(nudState)
- + "}";
-
- if (VDBG) {
- Log.d(TAG, neighMsg.toString());
- } else if (DBG) {
- Log.d(TAG, eventMsg);
- }
-
- synchronized (mLock) {
- if (mIpWatchList.containsKey(destination)) {
- final short value =
- (msgType == NetlinkConstants.RTM_DELNEIGH)
- ? StructNdMsg.NUD_NONE
- : nudState;
- mIpWatchList.put(destination, value);
- }
- }
-
- if (nudState == StructNdMsg.NUD_FAILED) {
- Log.w(TAG, "ALERT: " + eventMsg);
- handleNeighborLost(eventMsg);
- }
- }
- }
}
diff --git a/android/net/metrics/DefaultNetworkEvent.java b/android/net/metrics/DefaultNetworkEvent.java
index 8ff8e4f3..6f383b4d 100644
--- a/android/net/metrics/DefaultNetworkEvent.java
+++ b/android/net/metrics/DefaultNetworkEvent.java
@@ -74,7 +74,7 @@ public class DefaultNetworkEvent {
j.add("final_score=" + finalScore);
}
j.add(String.format("duration=%.0fs", durationMs / 1000.0));
- j.add(String.format("validation=%4.1f%%", (validatedMs * 100.0) / durationMs));
+ j.add(String.format("validation=%04.1f%%", (validatedMs * 100.0) / durationMs));
return j.toString();
}
diff --git a/android/net/metrics/WakeupStats.java b/android/net/metrics/WakeupStats.java
index 23c1f20f..7277ba34 100644
--- a/android/net/metrics/WakeupStats.java
+++ b/android/net/metrics/WakeupStats.java
@@ -16,6 +16,7 @@
package android.net.metrics;
+import android.net.MacAddress;
import android.os.Process;
import android.os.SystemClock;
import android.util.SparseIntArray;
@@ -80,13 +81,13 @@ public class WakeupStats {
}
switch (ev.dstHwAddr.addressType()) {
- case UNICAST:
+ case MacAddress.TYPE_UNICAST:
l2UnicastCount++;
break;
- case MULTICAST:
+ case MacAddress.TYPE_MULTICAST:
l2MulticastCount++;
break;
- case BROADCAST:
+ case MacAddress.TYPE_BROADCAST:
l2BroadcastCount++;
break;
default:
diff --git a/android/net/netlink/NetlinkSocket.java b/android/net/netlink/NetlinkSocket.java
index f5f211d8..5af3c299 100644
--- a/android/net/netlink/NetlinkSocket.java
+++ b/android/net/netlink/NetlinkSocket.java
@@ -16,16 +16,24 @@
package android.net.netlink;
+import static android.system.OsConstants.AF_NETLINK;
+import static android.system.OsConstants.EIO;
+import static android.system.OsConstants.EPROTO;
+import static android.system.OsConstants.ETIMEDOUT;
+import static android.system.OsConstants.SO_RCVBUF;
+import static android.system.OsConstants.SO_RCVTIMEO;
+import static android.system.OsConstants.SO_SNDTIMEO;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOL_SOCKET;
+
import android.system.ErrnoException;
import android.system.NetlinkSocketAddress;
import android.system.Os;
-import android.system.OsConstants;
import android.system.StructTimeval;
import android.util.Log;
import libcore.io.IoUtils;
import libcore.io.Libcore;
-import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.InterruptedIOException;
import java.net.SocketAddress;
@@ -37,28 +45,27 @@ import java.nio.ByteOrder;
/**
* NetlinkSocket
*
- * A small wrapper class to assist with AF_NETLINK socket operations.
+ * A small static class to assist with AF_NETLINK socket operations.
*
* @hide
*/
-public class NetlinkSocket implements Closeable {
+public class NetlinkSocket {
private static final String TAG = "NetlinkSocket";
- private static final int SOCKET_RECV_BUFSIZE = 64 * 1024;
- private static final int DEFAULT_RECV_BUFSIZE = 8 * 1024;
- final private FileDescriptor mDescriptor;
- private NetlinkSocketAddress mAddr;
- private long mLastRecvTimeoutMs;
- private long mLastSendTimeoutMs;
+ public static final int DEFAULT_RECV_BUFSIZE = 8 * 1024;
+ public static final int SOCKET_RECV_BUFSIZE = 64 * 1024;
public static void sendOneShotKernelMessage(int nlProto, byte[] msg) throws ErrnoException {
final String errPrefix = "Error in NetlinkSocket.sendOneShotKernelMessage";
+ final long IO_TIMEOUT = 300L;
+
+ FileDescriptor fd;
- try (NetlinkSocket nlSocket = new NetlinkSocket(nlProto)) {
- final long IO_TIMEOUT = 300L;
- nlSocket.connectToKernel();
- nlSocket.sendMessage(msg, 0, msg.length, IO_TIMEOUT);
- final ByteBuffer bytes = nlSocket.recvMessage(IO_TIMEOUT);
+ try {
+ fd = forProto(nlProto);
+ connectToKernel(fd);
+ sendMessage(fd, msg, 0, msg.length, IO_TIMEOUT);
+ final ByteBuffer bytes = recvMessage(fd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT);
// recvMessage() guaranteed to not return null if it did not throw.
final NetlinkMessage response = NetlinkMessage.parse(bytes);
if (response != null && response instanceof NetlinkErrorMessage &&
@@ -81,61 +88,30 @@ public class NetlinkSocket implements Closeable {
errmsg = response.toString();
}
Log.e(TAG, errPrefix + ", errmsg=" + errmsg);
- throw new ErrnoException(errmsg, OsConstants.EPROTO);
+ throw new ErrnoException(errmsg, EPROTO);
}
} catch (InterruptedIOException e) {
Log.e(TAG, errPrefix, e);
- throw new ErrnoException(errPrefix, OsConstants.ETIMEDOUT, e);
+ throw new ErrnoException(errPrefix, ETIMEDOUT, e);
} catch (SocketException e) {
Log.e(TAG, errPrefix, e);
- throw new ErrnoException(errPrefix, OsConstants.EIO, e);
+ throw new ErrnoException(errPrefix, EIO, e);
}
- }
-
- public NetlinkSocket(int nlProto) throws ErrnoException {
- mDescriptor = Os.socket(
- OsConstants.AF_NETLINK, OsConstants.SOCK_DGRAM, nlProto);
-
- Os.setsockoptInt(
- mDescriptor, OsConstants.SOL_SOCKET,
- OsConstants.SO_RCVBUF, SOCKET_RECV_BUFSIZE);
- }
-
- public NetlinkSocketAddress getLocalAddress() throws ErrnoException {
- return (NetlinkSocketAddress) Os.getsockname(mDescriptor);
- }
- public void bind(NetlinkSocketAddress localAddr) throws ErrnoException, SocketException {
- Os.bind(mDescriptor, (SocketAddress)localAddr);
+ IoUtils.closeQuietly(fd);
}
- public void connectTo(NetlinkSocketAddress peerAddr)
- throws ErrnoException, SocketException {
- Os.connect(mDescriptor, (SocketAddress) peerAddr);
+ public static FileDescriptor forProto(int nlProto) throws ErrnoException {
+ final FileDescriptor fd = Os.socket(AF_NETLINK, SOCK_DGRAM, nlProto);
+ Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, SOCKET_RECV_BUFSIZE);
+ return fd;
}
- public void connectToKernel() throws ErrnoException, SocketException {
- connectTo(new NetlinkSocketAddress(0, 0));
- }
-
- /**
- * Wait indefinitely (or until underlying socket error) for a
- * netlink message of at most DEFAULT_RECV_BUFSIZE size.
- */
- public ByteBuffer recvMessage()
- throws ErrnoException, InterruptedIOException {
- return recvMessage(DEFAULT_RECV_BUFSIZE, 0);
- }
-
- /**
- * Wait up to |timeoutMs| (or until underlying socket error) for a
- * netlink message of at most DEFAULT_RECV_BUFSIZE size.
- */
- public ByteBuffer recvMessage(long timeoutMs) throws ErrnoException, InterruptedIOException {
- return recvMessage(DEFAULT_RECV_BUFSIZE, timeoutMs);
+ public static void connectToKernel(FileDescriptor fd) throws ErrnoException, SocketException {
+ Os.connect(fd, (SocketAddress) (new NetlinkSocketAddress(0, 0)));
}
- private void checkTimeout(long timeoutMs) {
+ private static void checkTimeout(long timeoutMs) {
if (timeoutMs < 0) {
throw new IllegalArgumentException("Negative timeouts not permitted");
}
@@ -147,21 +123,14 @@ public class NetlinkSocket implements Closeable {
*
* Multi-threaded calls with different timeouts will cause unexpected results.
*/
- public ByteBuffer recvMessage(int bufsize, long timeoutMs)
+ public static ByteBuffer recvMessage(FileDescriptor fd, int bufsize, long timeoutMs)
throws ErrnoException, IllegalArgumentException, InterruptedIOException {
checkTimeout(timeoutMs);
- synchronized (mDescriptor) {
- if (mLastRecvTimeoutMs != timeoutMs) {
- Os.setsockoptTimeval(mDescriptor,
- OsConstants.SOL_SOCKET, OsConstants.SO_RCVTIMEO,
- StructTimeval.fromMillis(timeoutMs));
- mLastRecvTimeoutMs = timeoutMs;
- }
- }
+ Os.setsockoptTimeval(fd, SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(timeoutMs));
ByteBuffer byteBuffer = ByteBuffer.allocate(bufsize);
- int length = Os.read(mDescriptor, byteBuffer);
+ int length = Os.read(fd, byteBuffer);
if (length == bufsize) {
Log.w(TAG, "maximum read");
}
@@ -172,39 +141,16 @@ public class NetlinkSocket implements Closeable {
}
/**
- * Send a message to a peer to which this socket has previously connected.
- *
- * This blocks until completion or an error occurs.
- */
- public boolean sendMessage(byte[] bytes, int offset, int count)
- throws ErrnoException, InterruptedIOException {
- return sendMessage(bytes, offset, count, 0);
- }
-
- /**
* Send a message to a peer to which this socket has previously connected,
* waiting at most |timeoutMs| milliseconds for the send to complete.
*
* Multi-threaded calls with different timeouts will cause unexpected results.
*/
- public boolean sendMessage(byte[] bytes, int offset, int count, long timeoutMs)
+ public static int sendMessage(
+ FileDescriptor fd, byte[] bytes, int offset, int count, long timeoutMs)
throws ErrnoException, IllegalArgumentException, InterruptedIOException {
checkTimeout(timeoutMs);
-
- synchronized (mDescriptor) {
- if (mLastSendTimeoutMs != timeoutMs) {
- Os.setsockoptTimeval(mDescriptor,
- OsConstants.SOL_SOCKET, OsConstants.SO_SNDTIMEO,
- StructTimeval.fromMillis(timeoutMs));
- mLastSendTimeoutMs = timeoutMs;
- }
- }
-
- return (count == Os.write(mDescriptor, bytes, offset, count));
- }
-
- @Override
- public void close() {
- IoUtils.closeQuietly(mDescriptor);
+ Os.setsockoptTimeval(fd, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(timeoutMs));
+ return Os.write(fd, bytes, offset, count);
}
}
diff --git a/android/net/netlink/StructNdMsg.java b/android/net/netlink/StructNdMsg.java
index b68ec0bc..e34ec39a 100644
--- a/android/net/netlink/StructNdMsg.java
+++ b/android/net/netlink/StructNdMsg.java
@@ -63,6 +63,11 @@ public class StructNdMsg {
return ((nudState & (NUD_PERMANENT|NUD_NOARP|NUD_REACHABLE)) != 0);
}
+ public static boolean isNudStateValid(short nudState) {
+ return (isNudStateConnected(nudState) ||
+ ((nudState & (NUD_PROBE|NUD_STALE|NUD_DELAY)) != 0));
+ }
+
// Neighbor Cache Entry Flags
public static byte NTF_USE = (byte) 0x01;
public static byte NTF_SELF = (byte) 0x02;
@@ -143,7 +148,7 @@ public class StructNdMsg {
}
public boolean nudValid() {
- return (nudConnected() || ((ndm_state & (NUD_PROBE|NUD_STALE|NUD_DELAY)) != 0));
+ return isNudStateValid(ndm_state);
}
@Override
diff --git a/android/net/util/BlockingSocketReader.java b/android/net/util/PacketReader.java
index 99bf4695..10da2a55 100644
--- a/android/net/util/BlockingSocketReader.java
+++ b/android/net/util/PacketReader.java
@@ -67,7 +67,7 @@ import java.io.IOException;
*
* @hide
*/
-public abstract class BlockingSocketReader {
+public abstract class PacketReader {
private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
private static final int UNREGISTER_THIS_FD = 0;
@@ -83,11 +83,11 @@ public abstract class BlockingSocketReader {
IoUtils.closeQuietly(fd);
}
- protected BlockingSocketReader(Handler h) {
+ protected PacketReader(Handler h) {
this(h, DEFAULT_RECV_BUF_SIZE);
}
- protected BlockingSocketReader(Handler h, int recvbufsize) {
+ protected PacketReader(Handler h, int recvbufsize) {
mHandler = h;
mQueue = mHandler.getLooper().getQueue();
mPacket = new byte[Math.max(recvbufsize, DEFAULT_RECV_BUF_SIZE)];
@@ -115,6 +115,8 @@ public abstract class BlockingSocketReader {
}
}
+ public Handler getHandler() { return mHandler; }
+
public final int recvBufSize() { return mPacket.length; }
public final long numPacketsReceived() { return mPacketsReceived; }
diff --git a/android/net/wifi/WifiInfo.java b/android/net/wifi/WifiInfo.java
index bf8fed1c..3eb13ce6 100644
--- a/android/net/wifi/WifiInfo.java
+++ b/android/net/wifi/WifiInfo.java
@@ -22,7 +22,6 @@ import android.net.NetworkInfo.DetailedState;
import android.net.NetworkUtils;
import android.text.TextUtils;
-import java.lang.Math;
import java.net.InetAddress;
import java.net.Inet4Address;
import java.net.UnknownHostException;
@@ -126,143 +125,35 @@ public class WifiInfo implements Parcelable {
public long rxSuccess;
/**
- * Average rate of lost transmitted packets, in units of packets per 5 seconds.
+ * Average rate of lost transmitted packets, in units of packets per second.
* @hide
*/
public double txBadRate;
/**
- * Average rate of transmitted retry packets, in units of packets per 5 seconds.
+ * Average rate of transmitted retry packets, in units of packets per second.
* @hide
*/
public double txRetriesRate;
/**
- * Average rate of successfully transmitted unicast packets, in units of packets per 5 seconds.
+ * Average rate of successfully transmitted unicast packets, in units of packets per second.
* @hide
*/
public double txSuccessRate;
/**
- * Average rate of received unicast data packets, in units of packets per 5 seconds.
+ * Average rate of received unicast data packets, in units of packets per second.
* @hide
*/
public double rxSuccessRate;
- private static final long RESET_TIME_STAMP = Long.MIN_VALUE;
- private static final long FILTER_TIME_CONSTANT = 3000;
- /**
- * This factor is used to adjust the rate output under the new algorithm
- * such that the result is comparable to the previous algorithm.
- * This actually converts from unit 'packets per second' to 'packets per 5 seconds'.
- */
- private static final long OUTPUT_SCALE_FACTOR = 5;
- private long mLastPacketCountUpdateTimeStamp;
-
- /**
- * @hide
- */
- public int badRssiCount;
-
- /**
- * @hide
- */
- public int linkStuckCount;
-
- /**
- * @hide
- */
- public int lowRssiCount;
-
/**
* @hide
*/
public int score;
/**
- * @hide
- */
- public void updatePacketRates(WifiLinkLayerStats stats, long timeStamp) {
- if (stats != null) {
- long txgood = stats.txmpdu_be + stats.txmpdu_bk + stats.txmpdu_vi + stats.txmpdu_vo;
- long txretries = stats.retries_be + stats.retries_bk
- + stats.retries_vi + stats.retries_vo;
- long rxgood = stats.rxmpdu_be + stats.rxmpdu_bk + stats.rxmpdu_vi + stats.rxmpdu_vo;
- long txbad = stats.lostmpdu_be + stats.lostmpdu_bk
- + stats.lostmpdu_vi + stats.lostmpdu_vo;
-
- if (mLastPacketCountUpdateTimeStamp != RESET_TIME_STAMP
- && mLastPacketCountUpdateTimeStamp < timeStamp
- && txBad <= txbad
- && txSuccess <= txgood
- && rxSuccess <= rxgood
- && txRetries <= txretries) {
- long timeDelta = timeStamp - mLastPacketCountUpdateTimeStamp;
- double lastSampleWeight = Math.exp(-1.0 * timeDelta / FILTER_TIME_CONSTANT);
- double currentSampleWeight = 1.0 - lastSampleWeight;
-
- txBadRate = txBadRate * lastSampleWeight
- + (txbad - txBad) * OUTPUT_SCALE_FACTOR * 1000 / timeDelta
- * currentSampleWeight;
- txSuccessRate = txSuccessRate * lastSampleWeight
- + (txgood - txSuccess) * OUTPUT_SCALE_FACTOR * 1000 / timeDelta
- * currentSampleWeight;
- rxSuccessRate = rxSuccessRate * lastSampleWeight
- + (rxgood - rxSuccess) * OUTPUT_SCALE_FACTOR * 1000 / timeDelta
- * currentSampleWeight;
- txRetriesRate = txRetriesRate * lastSampleWeight
- + (txretries - txRetries) * OUTPUT_SCALE_FACTOR * 1000/ timeDelta
- * currentSampleWeight;
- } else {
- txBadRate = 0;
- txSuccessRate = 0;
- rxSuccessRate = 0;
- txRetriesRate = 0;
- }
- txBad = txbad;
- txSuccess = txgood;
- rxSuccess = rxgood;
- txRetries = txretries;
- mLastPacketCountUpdateTimeStamp = timeStamp;
- } else {
- txBad = 0;
- txSuccess = 0;
- rxSuccess = 0;
- txRetries = 0;
- txBadRate = 0;
- txSuccessRate = 0;
- rxSuccessRate = 0;
- txRetriesRate = 0;
- mLastPacketCountUpdateTimeStamp = RESET_TIME_STAMP;
- }
- }
-
-
- /**
- * This function is less powerful and used if the WifiLinkLayerStats API is not implemented
- * at the Wifi HAL
- * @hide
+ * Flag indicating that AP has hinted that upstream connection is metered,
+ * and sensitive to heavy data transfers.
*/
- public void updatePacketRates(long txPackets, long rxPackets) {
- //paranoia
- txBad = 0;
- txRetries = 0;
- txBadRate = 0;
- txRetriesRate = 0;
- if (txSuccess <= txPackets && rxSuccess <= rxPackets) {
- txSuccessRate = (txSuccessRate * 0.5)
- + ((double) (txPackets - txSuccess) * 0.5);
- rxSuccessRate = (rxSuccessRate * 0.5)
- + ((double) (rxPackets - rxSuccess) * 0.5);
- } else {
- txBadRate = 0;
- txRetriesRate = 0;
- }
- txSuccess = txPackets;
- rxSuccess = rxPackets;
- }
-
- /**
- * Flag indicating that AP has hinted that upstream connection is metered,
- * and sensitive to heavy data transfers.
- */
private boolean mMeteredHint;
/** @hide */
@@ -274,7 +165,6 @@ public class WifiInfo implements Parcelable {
mRssi = INVALID_RSSI;
mLinkSpeed = -1;
mFrequency = -1;
- mLastPacketCountUpdateTimeStamp = RESET_TIME_STAMP;
}
/** @hide */
@@ -296,11 +186,7 @@ public class WifiInfo implements Parcelable {
txSuccessRate = 0;
rxSuccessRate = 0;
txRetriesRate = 0;
- lowRssiCount = 0;
- badRssiCount = 0;
- linkStuckCount = 0;
score = 0;
- mLastPacketCountUpdateTimeStamp = RESET_TIME_STAMP;
}
/**
@@ -328,12 +214,7 @@ public class WifiInfo implements Parcelable {
txRetriesRate = source.txRetriesRate;
txSuccessRate = source.txSuccessRate;
rxSuccessRate = source.rxSuccessRate;
- mLastPacketCountUpdateTimeStamp =
- source.mLastPacketCountUpdateTimeStamp;
score = source.score;
- badRssiCount = source.badRssiCount;
- lowRssiCount = source.lowRssiCount;
- linkStuckCount = source.linkStuckCount;
}
}
@@ -452,22 +333,6 @@ public class WifiInfo implements Parcelable {
}
/**
- * @hide
- * This returns txSuccessRate in packets per second.
- */
- public double getTxSuccessRatePps() {
- return txSuccessRate / OUTPUT_SCALE_FACTOR;
- }
-
- /**
- * @hide
- * This returns rxSuccessRate in packets per second.
- */
- public double getRxSuccessRatePps() {
- return rxSuccessRate / OUTPUT_SCALE_FACTOR;
- }
-
- /**
* Record the MAC address of the WLAN interface
* @param macAddress the MAC address in {@code XX:XX:XX:XX:XX:XX} form
* @hide
@@ -658,8 +523,6 @@ public class WifiInfo implements Parcelable {
dest.writeDouble(txRetriesRate);
dest.writeDouble(txBadRate);
dest.writeDouble(rxSuccessRate);
- dest.writeInt(badRssiCount);
- dest.writeInt(lowRssiCount);
mSupplicantState.writeToParcel(dest, flags);
}
@@ -689,8 +552,6 @@ public class WifiInfo implements Parcelable {
info.txRetriesRate = in.readDouble();
info.txBadRate = in.readDouble();
info.rxSuccessRate = in.readDouble();
- info.badRssiCount = in.readInt();
- info.lowRssiCount = in.readInt();
info.mSupplicantState = SupplicantState.CREATOR.createFromParcel(in);
return info;
}
diff --git a/android/net/wifi/WifiLinkLayerStats.java b/android/net/wifi/WifiLinkLayerStats.java
deleted file mode 100644
index edd400b5..00000000
--- a/android/net/wifi/WifiLinkLayerStats.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Copyright (C) 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.
- */
-
-package android.net.wifi;
-
-import android.os.Parcelable;
-import android.os.Parcel;
-
-import java.util.Arrays;
-
-/**
- * A class representing link layer statistics collected over a Wifi Interface.
- */
-/** {@hide} */
-public class WifiLinkLayerStats implements Parcelable {
- private static final String TAG = "WifiLinkLayerStats";
-
- /**
- * The current status of this network configuration entry.
- * @see Status
- */
- /** {@hide} */
- public int status;
-
- /**
- * The network's SSID. Can either be an ASCII string,
- * which must be enclosed in double quotation marks
- * (e.g., {@code "MyNetwork"}, or a string of
- * hex digits,which are not enclosed in quotes
- * (e.g., {@code 01a243f405}).
- */
- /** {@hide} */
- public String SSID;
- /**
- * When set. this is the BSSID the radio is currently associated with.
- * The value is a string in the format of an Ethernet MAC address, e.g.,
- * <code>XX:XX:XX:XX:XX:XX</code> where each <code>X</code> is a hex digit.
- */
- /** {@hide} */
- public String BSSID;
-
- /* number beacons received from our own AP */
- /** {@hide} */
- public int beacon_rx;
-
- /* RSSI taken on management frames */
- /** {@hide} */
- public int rssi_mgmt;
-
- /* packets counters */
- /** {@hide} */
- /* WME Best Effort Access Category (receive mpdu, transmit mpdu, lost mpdu, number of retries)*/
- public long rxmpdu_be;
- /** {@hide} */
- public long txmpdu_be;
- /** {@hide} */
- public long lostmpdu_be;
- /** {@hide} */
- public long retries_be;
- /** {@hide} */
- /* WME Background Access Category (receive mpdu, transmit mpdu, lost mpdu, number of retries) */
- public long rxmpdu_bk;
- /** {@hide} */
- public long txmpdu_bk;
- /** {@hide} */
- public long lostmpdu_bk;
- /** {@hide} */
- public long retries_bk;
- /** {@hide} */
- /* WME Video Access Category (receive mpdu, transmit mpdu, lost mpdu, number of retries) */
- public long rxmpdu_vi;
- /** {@hide} */
- public long txmpdu_vi;
- /** {@hide} */
- public long lostmpdu_vi;
- /** {@hide} */
- public long retries_vi;
- /** {@hide} */
- /* WME Voice Access Category (receive mpdu, transmit mpdu, lost mpdu, number of retries) */
- public long rxmpdu_vo;
- /** {@hide} */
- public long txmpdu_vo;
- /** {@hide} */
- public long lostmpdu_vo;
- /** {@hide} */
- public long retries_vo;
-
- /** {@hide} */
- public int on_time;
- /** {@hide} */
- public int tx_time;
- /** {@hide} */
- public int[] tx_time_per_level;
- /** {@hide} */
- public int rx_time;
- /** {@hide} */
- public int on_time_scan;
-
- /** {@hide} */
- public WifiLinkLayerStats() {
- }
-
- @Override
- /** {@hide} */
- public String toString() {
- StringBuilder sbuf = new StringBuilder();
- sbuf.append(" WifiLinkLayerStats: ").append('\n');
-
- if (this.SSID != null) {
- sbuf.append(" SSID: ").append(this.SSID).append('\n');
- }
- if (this.BSSID != null) {
- sbuf.append(" BSSID: ").append(this.BSSID).append('\n');
- }
-
- sbuf.append(" my bss beacon rx: ").append(Integer.toString(this.beacon_rx)).append('\n');
- sbuf.append(" RSSI mgmt: ").append(Integer.toString(this.rssi_mgmt)).append('\n');
- sbuf.append(" BE : ").append(" rx=").append(Long.toString(this.rxmpdu_be))
- .append(" tx=").append(Long.toString(this.txmpdu_be))
- .append(" lost=").append(Long.toString(this.lostmpdu_be))
- .append(" retries=").append(Long.toString(this.retries_be)).append('\n');
- sbuf.append(" BK : ").append(" rx=").append(Long.toString(this.rxmpdu_bk))
- .append(" tx=").append(Long.toString(this.txmpdu_bk))
- .append(" lost=").append(Long.toString(this.lostmpdu_bk))
- .append(" retries=").append(Long.toString(this.retries_bk)).append('\n');
- sbuf.append(" VI : ").append(" rx=").append(Long.toString(this.rxmpdu_vi))
- .append(" tx=").append(Long.toString(this.txmpdu_vi))
- .append(" lost=").append(Long.toString(this.lostmpdu_vi))
- .append(" retries=").append(Long.toString(this.retries_vi)).append('\n');
- sbuf.append(" VO : ").append(" rx=").append(Long.toString(this.rxmpdu_vo))
- .append(" tx=").append(Long.toString(this.txmpdu_vo))
- .append(" lost=").append(Long.toString(this.lostmpdu_vo))
- .append(" retries=").append(Long.toString(this.retries_vo)).append('\n');
- sbuf.append(" on_time : ").append(Integer.toString(this.on_time))
- .append(" rx_time=").append(Integer.toString(this.rx_time))
- .append(" scan_time=").append(Integer.toString(this.on_time_scan)).append('\n')
- .append(" tx_time=").append(Integer.toString(this.tx_time))
- .append(" tx_time_per_level=" + Arrays.toString(tx_time_per_level));
- return sbuf.toString();
- }
-
- /** Implement the Parcelable interface {@hide} */
- public int describeContents() {
- return 0;
- }
-
- /** {@hide} */
- public String getPrintableSsid() {
- if (SSID == null) return "";
- final int length = SSID.length();
- if (length > 2 && (SSID.charAt(0) == '"') && SSID.charAt(length - 1) == '"') {
- return SSID.substring(1, length - 1);
- }
-
- /** The ascii-encoded string format is P"<ascii-encoded-string>"
- * The decoding is implemented in the supplicant for a newly configured
- * network.
- */
- if (length > 3 && (SSID.charAt(0) == 'P') && (SSID.charAt(1) == '"') &&
- (SSID.charAt(length-1) == '"')) {
- WifiSsid wifiSsid = WifiSsid.createFromAsciiEncoded(
- SSID.substring(2, length - 1));
- return wifiSsid.toString();
- }
- return SSID;
- }
-
- /** Implement the Parcelable interface {@hide} */
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(SSID);
- dest.writeString(BSSID);
- dest.writeInt(on_time);
- dest.writeInt(tx_time);
- dest.writeIntArray(tx_time_per_level);
- dest.writeInt(rx_time);
- dest.writeInt(on_time_scan);
- }
-
- /** Implement the Parcelable interface {@hide} */
- public static final Creator<WifiLinkLayerStats> CREATOR =
- new Creator<WifiLinkLayerStats>() {
- public WifiLinkLayerStats createFromParcel(Parcel in) {
- WifiLinkLayerStats stats = new WifiLinkLayerStats();
- stats.SSID = in.readString();
- stats.BSSID = in.readString();
- stats.on_time = in.readInt();
- stats.tx_time = in.readInt();
- stats.tx_time_per_level = in.createIntArray();
- stats.rx_time = in.readInt();
- stats.on_time_scan = in.readInt();
- return stats;
- };
- public WifiLinkLayerStats[] newArray(int size) {
- return new WifiLinkLayerStats[size];
- }
-
- };
-}
diff --git a/android/net/wifi/WifiManager.java b/android/net/wifi/WifiManager.java
index 558004ce..ea9be290 100644
--- a/android/net/wifi/WifiManager.java
+++ b/android/net/wifi/WifiManager.java
@@ -2846,8 +2846,7 @@ public class WifiManager {
* gets added to the list of configured networks for the foreground user.
*
* For a new network, this function is used instead of a
- * sequence of addNetwork(), enableNetwork(), saveConfiguration() and
- * reconnect()
+ * sequence of addNetwork(), enableNetwork(), and reconnect()
*
* @param config the set of variables that describe the configuration,
* contained in a {@link WifiConfiguration} object.
@@ -2869,8 +2868,7 @@ public class WifiManager {
/**
* Connect to a network with the given networkId.
*
- * This function is used instead of a enableNetwork(), saveConfiguration() and
- * reconnect()
+ * This function is used instead of a enableNetwork() and reconnect()
*
* @param networkId the ID of the network as returned by {@link #addNetwork} or {@link
* getConfiguredNetworks}.
@@ -2890,10 +2888,12 @@ public class WifiManager {
* is updated. Any new network is enabled by default.
*
* For a new network, this function is used instead of a
- * sequence of addNetwork(), enableNetwork() and saveConfiguration().
+ * sequence of addNetwork() and enableNetwork().
*
* For an existing network, it accomplishes the task of updateNetwork()
- * and saveConfiguration()
+ *
+ * This API will cause reconnect if the crecdentials of the current active
+ * connection has been changed.
*
* @param config the set of variables that describe the configuration,
* contained in a {@link WifiConfiguration} object.
@@ -2912,7 +2912,6 @@ public class WifiManager {
* foreground user.
*
* This function is used instead of a sequence of removeNetwork()
- * and saveConfiguration().
*
* @param config the set of variables that describe the configuration,
* contained in a {@link WifiConfiguration} object.
@@ -3488,27 +3487,23 @@ public class WifiManager {
}
/**
- * Set setting for allowing Scans when traffic is ongoing.
+ * Deprecated
+ * Does nothing
* @hide
+ * @deprecated
*/
public void setAllowScansWithTraffic(int enabled) {
- try {
- mService.setAllowScansWithTraffic(enabled);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return;
}
/**
- * Get setting for allowing Scans when traffic is ongoing.
+ * Deprecated
+ * returns value for 'disabled'
* @hide
+ * @deprecated
*/
public int getAllowScansWithTraffic() {
- try {
- return mService.getAllowScansWithTraffic();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return 0;
}
/**
@@ -3538,29 +3533,23 @@ public class WifiManager {
}
/**
- * Framework layer autojoin enable/disable when device is associated
- * this will enable/disable autojoin scan and switch network when connected
- * @return true -- if set successful false -- if set failed
+ * Deprecated
+ * returns false
* @hide
+ * @deprecated
*/
public boolean setEnableAutoJoinWhenAssociated(boolean enabled) {
- try {
- return mService.setEnableAutoJoinWhenAssociated(enabled);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return false;
}
/**
- * Get setting for Framework layer autojoin enable status
+ * Deprecated
+ * returns false
* @hide
+ * @deprecated
*/
public boolean getEnableAutoJoinWhenAssociated() {
- try {
- return mService.getEnableAutoJoinWhenAssociated();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return false;
}
/**
diff --git a/android/net/wifi/WifiScanner.java b/android/net/wifi/WifiScanner.java
index e3752ac7..928a1da8 100644
--- a/android/net/wifi/WifiScanner.java
+++ b/android/net/wifi/WifiScanner.java
@@ -160,6 +160,24 @@ public class WifiScanner {
*/
public static final int REPORT_EVENT_NO_BATCH = (1 << 2);
+ /**
+ * This is used to indicate the purpose of the scan to the wifi chip in
+ * {@link ScanSettings#type}.
+ * On devices with multiple hardware radio chains (and hence different modes of scan),
+ * this type serves as an indication to the hardware on what mode of scan to perform.
+ * Only apps holding android.Manifest.permission.NETWORK_STACK permission can set this value.
+ *
+ * Note: This serves as an intent and not as a stipulation, the wifi chip
+ * might honor or ignore the indication based on the current radio conditions. Always
+ * use the {@link ScanResult#radioChainInfos} to figure out the radio chain configuration used
+ * to receive the corresponding scan result.
+ */
+ /** {@hide} */
+ public static final int TYPE_LOW_LATENCY = 0;
+ /** {@hide} */
+ public static final int TYPE_LOW_POWER = 1;
+ /** {@hide} */
+ public static final int TYPE_HIGH_ACCURACY = 2;
/** {@hide} */
public static final String SCAN_PARAMS_SCAN_SETTINGS_KEY = "ScanSettings";
@@ -193,7 +211,8 @@ public class WifiScanner {
* list of hidden networks to scan for. Explicit probe requests are sent out for such
* networks during scan. Only valid for single scan requests.
* {@hide}
- * */
+ */
+ @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
public HiddenNetwork[] hiddenNetworks;
/** period of background scan; in millisecond, 0 => single shot scan */
public int periodInMs;
@@ -223,6 +242,13 @@ public class WifiScanner {
* {@hide}
*/
public boolean isPnoScan;
+ /**
+ * Indicate the type of scan to be performed by the wifi chip.
+ * Default value: {@link #TYPE_LOW_LATENCY}.
+ * {@hide}
+ */
+ @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
+ public int type = TYPE_LOW_LATENCY;
/** Implement the Parcelable interface {@hide} */
public int describeContents() {
@@ -239,6 +265,7 @@ public class WifiScanner {
dest.writeInt(maxPeriodInMs);
dest.writeInt(stepCount);
dest.writeInt(isPnoScan ? 1 : 0);
+ dest.writeInt(type);
if (channels != null) {
dest.writeInt(channels.length);
for (int i = 0; i < channels.length; i++) {
@@ -272,6 +299,7 @@ public class WifiScanner {
settings.maxPeriodInMs = in.readInt();
settings.stepCount = in.readInt();
settings.isPnoScan = in.readInt() == 1;
+ settings.type = in.readInt();
int num_channels = in.readInt();
settings.channels = new ChannelSpec[num_channels];
for (int i = 0; i < num_channels; i++) {
diff --git a/android/net/wifi/aware/PeerHandle.java b/android/net/wifi/aware/PeerHandle.java
index 1b0aba15..b525212e 100644
--- a/android/net/wifi/aware/PeerHandle.java
+++ b/android/net/wifi/aware/PeerHandle.java
@@ -33,7 +33,6 @@ public class PeerHandle {
/** @hide */
public int peerId;
- /** @hide RTT_API */
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -47,7 +46,6 @@ public class PeerHandle {
return peerId == ((PeerHandle) o).peerId;
}
- /** @hide RTT_API */
@Override
public int hashCode() {
return peerId;
diff --git a/android/net/wifi/aware/PublishConfig.java b/android/net/wifi/aware/PublishConfig.java
index e60f52f8..7a5049d7 100644
--- a/android/net/wifi/aware/PublishConfig.java
+++ b/android/net/wifi/aware/PublishConfig.java
@@ -182,7 +182,7 @@ public final class PublishConfig implements Parcelable {
*
* @hide
*/
- public void assertValid(Characteristics characteristics)
+ public void assertValid(Characteristics characteristics, boolean rttSupported)
throws IllegalArgumentException {
WifiAwareUtils.validateServiceName(mServiceName);
@@ -216,6 +216,10 @@ public final class PublishConfig implements Parcelable {
"Match filter longer than supported by device characteristics");
}
}
+
+ if (!rttSupported && mEnableRanging) {
+ throw new IllegalArgumentException("Ranging is not supported");
+ }
}
/**
@@ -364,6 +368,9 @@ public final class PublishConfig implements Parcelable {
* Optional. Disabled by default - i.e. any peer which attempts to measure distance to this
* device will be refused. If the peer has ranging enabled (using the
* {@link SubscribeConfig} APIs listed above, it will never discover this device.
+ * <p>
+ * The device must support Wi-Fi RTT for this feature to be used. Feature support is checked
+ * as described in {@link android.net.wifi.rtt}.
*
* @param enable If true, ranging is supported on request of the peer.
*
diff --git a/android/net/wifi/aware/SubscribeConfig.java b/android/net/wifi/aware/SubscribeConfig.java
index f6552a76..91f8e520 100644
--- a/android/net/wifi/aware/SubscribeConfig.java
+++ b/android/net/wifi/aware/SubscribeConfig.java
@@ -224,7 +224,7 @@ public final class SubscribeConfig implements Parcelable {
*
* @hide
*/
- public void assertValid(Characteristics characteristics)
+ public void assertValid(Characteristics characteristics, boolean rttSupported)
throws IllegalArgumentException {
WifiAwareUtils.validateServiceName(mServiceName);
@@ -269,6 +269,10 @@ public final class SubscribeConfig implements Parcelable {
throw new IllegalArgumentException(
"Maximum distance must be greater than minimum distance");
}
+
+ if (!rttSupported && (mMinDistanceMmSet || mMaxDistanceMmSet)) {
+ throw new IllegalArgumentException("Ranging is not supported");
+ }
}
/**
@@ -422,6 +426,9 @@ public final class SubscribeConfig implements Parcelable {
* peer must enable ranging using
* {@link PublishConfig.Builder#setRangingEnabled(boolean)}. Otherwise discovery will
* never be triggered.
+ * <p>
+ * The device must support Wi-Fi RTT for this feature to be used. Feature support is checked
+ * as described in {@link android.net.wifi.rtt}.
*
* @param minDistanceMm Minimum distance, in mm, to the publisher above which to trigger
* discovery.
@@ -450,6 +457,9 @@ public final class SubscribeConfig implements Parcelable {
* peer must enable ranging using
* {@link PublishConfig.Builder#setRangingEnabled(boolean)}. Otherwise discovery will
* never be triggered.
+ * <p>
+ * The device must support Wi-Fi RTT for this feature to be used. Feature support is checked
+ * as described in {@link android.net.wifi.rtt}.
*
* @param maxDistanceMm Maximum distance, in mm, to the publisher below which to trigger
* discovery.
diff --git a/android/net/wifi/aware/WifiAwareManager.java b/android/net/wifi/aware/WifiAwareManager.java
index 166da48e..d57d1524 100644
--- a/android/net/wifi/aware/WifiAwareManager.java
+++ b/android/net/wifi/aware/WifiAwareManager.java
@@ -301,7 +301,7 @@ public class WifiAwareManager {
if (VDBG) Log.v(TAG, "publish(): clientId=" + clientId + ", config=" + publishConfig);
try {
- mService.publish(clientId, publishConfig,
+ mService.publish(mContext.getOpPackageName(), clientId, publishConfig,
new WifiAwareDiscoverySessionCallbackProxy(this, looper, true, callback,
clientId));
} catch (RemoteException e) {
@@ -334,7 +334,7 @@ public class WifiAwareManager {
}
try {
- mService.subscribe(clientId, subscribeConfig,
+ mService.subscribe(mContext.getOpPackageName(), clientId, subscribeConfig,
new WifiAwareDiscoverySessionCallbackProxy(this, looper, false, callback,
clientId));
} catch (RemoteException e) {
diff --git a/android/net/wifi/hotspot2/ProvisioningCallback.java b/android/net/wifi/hotspot2/ProvisioningCallback.java
index 8b86cdde..2ea6e797 100644
--- a/android/net/wifi/hotspot2/ProvisioningCallback.java
+++ b/android/net/wifi/hotspot2/ProvisioningCallback.java
@@ -26,13 +26,49 @@ import android.os.Handler;
*/
public abstract class ProvisioningCallback {
- /**
+ /**
* The reason code for Provisioning Failure due to connection failure to OSU AP.
* @hide
*/
public static final int OSU_FAILURE_AP_CONNECTION = 1;
/**
+ * The reason code for Provisioning Failure due to connection failure to OSU AP.
+ * @hide
+ */
+ public static final int OSU_FAILURE_SERVER_URL_INVALID = 2;
+
+ /**
+ * The reason code for Provisioning Failure due to connection failure to OSU AP.
+ * @hide
+ */
+ public static final int OSU_FAILURE_SERVER_CONNECTION = 3;
+
+ /**
+ * The reason code for Provisioning Failure due to connection failure to OSU AP.
+ * @hide
+ */
+ public static final int OSU_FAILURE_SERVER_VALIDATION = 4;
+
+ /**
+ * The reason code for Provisioning Failure due to connection failure to OSU AP.
+ * @hide
+ */
+ public static final int OSU_FAILURE_PROVIDER_VERIFICATION = 5;
+
+ /**
+ * The reason code for Provisioning Failure when a provisioning flow is aborted.
+ * @hide
+ */
+ public static final int OSU_FAILURE_PROVISIONING_ABORTED = 6;
+
+ /**
+ * The reason code for Provisioning Failure when a provisioning flow is aborted.
+ * @hide
+ */
+ public static final int OSU_FAILURE_PROVISIONING_NOT_AVAILABLE = 7;
+
+ /**
* The status code for Provisioning flow to indicate connecting to OSU AP
* @hide
*/
@@ -45,6 +81,24 @@ public abstract class ProvisioningCallback {
public static final int OSU_STATUS_AP_CONNECTED = 2;
/**
+ * The status code for Provisioning flow to indicate connecting to OSU AP
+ * @hide
+ */
+ public static final int OSU_STATUS_SERVER_CONNECTED = 3;
+
+ /**
+ * The status code for Provisioning flow to indicate connecting to OSU AP
+ * @hide
+ */
+ public static final int OSU_STATUS_SERVER_VALIDATED = 4;
+
+ /**
+ * The status code for Provisioning flow to indicate connecting to OSU AP
+ * @hide
+ */
+ public static final int OSU_STATUS_PROVIDER_VERIFIED = 5;
+
+ /**
* Provisioning status for OSU failure
* @param status indicates error condition
*/
diff --git a/android/net/wifi/rtt/RangingRequest.java b/android/net/wifi/rtt/RangingRequest.java
index a396281f..b4e3097a 100644
--- a/android/net/wifi/rtt/RangingRequest.java
+++ b/android/net/wifi/rtt/RangingRequest.java
@@ -16,6 +16,8 @@
package android.net.wifi.rtt;
+import android.annotation.NonNull;
+import android.net.MacAddress;
import android.net.wifi.ScanResult;
import android.net.wifi.aware.AttachCallback;
import android.net.wifi.aware.DiscoverySessionCallback;
@@ -25,14 +27,9 @@ import android.net.wifi.aware.WifiAwareManager;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
-import android.text.TextUtils;
-
-import libcore.util.HexEncoding;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
-import java.util.Objects;
import java.util.StringJoiner;
/**
@@ -63,10 +60,10 @@ public final class RangingRequest implements Parcelable {
}
/** @hide */
- public final List<RttPeer> mRttPeers;
+ public final List<ResponderConfig> mRttPeers;
/** @hide */
- private RangingRequest(List<RttPeer> rttPeers) {
+ private RangingRequest(List<ResponderConfig> rttPeers) {
mRttPeers = rttPeers;
}
@@ -95,9 +92,9 @@ public final class RangingRequest implements Parcelable {
/** @hide */
@Override
public String toString() {
- StringJoiner sj = new StringJoiner(", ", "RangingRequest: mRttPeers=[", ",");
- for (RttPeer rp : mRttPeers) {
- sj.add(rp.toString());
+ StringJoiner sj = new StringJoiner(", ", "RangingRequest: mRttPeers=[", "]");
+ for (ResponderConfig rc : mRttPeers) {
+ sj.add(rc.toString());
}
return sj.toString();
}
@@ -109,26 +106,9 @@ public final class RangingRequest implements Parcelable {
"Ranging to too many peers requested. Use getMaxPeers() API to get limit.");
}
- for (RttPeer peer: mRttPeers) {
- if (peer instanceof RttPeerAp) {
- RttPeerAp apPeer = (RttPeerAp) peer;
- if (apPeer.scanResult == null || apPeer.scanResult.BSSID == null) {
- throw new IllegalArgumentException("Invalid AP peer specification");
- }
- } else if (peer instanceof RttPeerAware) {
- if (!awareSupported) {
- throw new IllegalArgumentException(
- "Request contains Aware peers - but Aware isn't supported on this "
- + "device");
- }
-
- RttPeerAware awarePeer = (RttPeerAware) peer;
- if (awarePeer.peerMacAddress == null && awarePeer.peerHandle == null) {
- throw new IllegalArgumentException("Invalid Aware peer specification");
- }
- } else {
- throw new IllegalArgumentException(
- "Request contains unknown peer specification types");
+ for (ResponderConfig peer: mRttPeers) {
+ if (!peer.isValid(awareSupported)) {
+ throw new IllegalArgumentException("Invalid Responder specification");
}
}
}
@@ -137,35 +117,34 @@ public final class RangingRequest implements Parcelable {
* Builder class used to construct {@link RangingRequest} objects.
*/
public static final class Builder {
- private List<RttPeer> mRttPeers = new ArrayList<>();
+ private List<ResponderConfig> mRttPeers = new ArrayList<>();
/**
* Add the device specified by the {@link ScanResult} to the list of devices with
- * which to measure range. The total number of results added to a request cannot exceed the
+ * which to measure range. The total number of peers added to a request cannot exceed the
* limit specified by {@link #getMaxPeers()}.
*
* @param apInfo Information of an Access Point (AP) obtained in a Scan Result.
* @return The builder to facilitate chaining
* {@code builder.setXXX(..).setXXX(..)}.
*/
- public Builder addAccessPoint(ScanResult apInfo) {
+ public Builder addAccessPoint(@NonNull ScanResult apInfo) {
if (apInfo == null) {
throw new IllegalArgumentException("Null ScanResult!");
}
- mRttPeers.add(new RttPeerAp(apInfo));
- return this;
+ return addResponder(ResponderConfig.fromScanResult(apInfo));
}
/**
* Add the devices specified by the {@link ScanResult}s to the list of devices with
- * which to measure range. The total number of results added to a request cannot exceed the
+ * which to measure range. The total number of peers added to a request cannot exceed the
* limit specified by {@link #getMaxPeers()}.
*
* @param apInfos Information of an Access Points (APs) obtained in a Scan Result.
* @return The builder to facilitate chaining
* {@code builder.setXXX(..).setXXX(..)}.
*/
- public Builder addAccessPoints(List<ScanResult> apInfos) {
+ public Builder addAccessPoints(@NonNull List<ScanResult> apInfos) {
if (apInfos == null) {
throw new IllegalArgumentException("Null list of ScanResults!");
}
@@ -190,9 +169,12 @@ public final class RangingRequest implements Parcelable {
* @param peerMacAddress The MAC address of the Wi-Fi Aware peer.
* @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
*/
- public Builder addWifiAwarePeer(byte[] peerMacAddress) {
- mRttPeers.add(new RttPeerAware(peerMacAddress));
- return this;
+ public Builder addWifiAwarePeer(@NonNull MacAddress peerMacAddress) {
+ if (peerMacAddress == null) {
+ throw new IllegalArgumentException("Null peer MAC address");
+ }
+ return addResponder(
+ ResponderConfig.fromWifiAwarePeerMacAddressWithDefaults(peerMacAddress));
}
/**
@@ -208,8 +190,30 @@ public final class RangingRequest implements Parcelable {
* @param peerHandle The peer handler of the peer Wi-Fi Aware device.
* @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
*/
- public Builder addWifiAwarePeer(PeerHandle peerHandle) {
- mRttPeers.add(new RttPeerAware(peerHandle));
+ public Builder addWifiAwarePeer(@NonNull PeerHandle peerHandle) {
+ if (peerHandle == null) {
+ throw new IllegalArgumentException("Null peer handler (identifier)");
+ }
+
+ return addResponder(ResponderConfig.fromWifiAwarePeerHandleWithDefaults(peerHandle));
+ }
+
+ /*
+ * Add the Responder device specified by the {@link ResponderConfig} to the list of devices
+ * with which to measure range. The total number of peers added to the request cannot exceed
+ * the limit specified by {@link #getMaxPeers()}.
+ *
+ * @param responder Information on the RTT Responder.
+ * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+ *
+ * @hide (SystemApi)
+ */
+ public Builder addResponder(@NonNull ResponderConfig responder) {
+ if (responder == null) {
+ throw new IllegalArgumentException("Null Responder!");
+ }
+
+ mRttPeers.add(responder);
return this;
}
@@ -241,152 +245,4 @@ public final class RangingRequest implements Parcelable {
public int hashCode() {
return mRttPeers.hashCode();
}
-
- /** @hide */
- public interface RttPeer {
- // empty (marker interface)
- }
-
- /** @hide */
- public static class RttPeerAp implements RttPeer, Parcelable {
- public final ScanResult scanResult;
-
- public RttPeerAp(ScanResult scanResult) {
- this.scanResult = scanResult;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- scanResult.writeToParcel(dest, flags);
- }
-
- public static final Creator<RttPeerAp> CREATOR = new Creator<RttPeerAp>() {
- @Override
- public RttPeerAp[] newArray(int size) {
- return new RttPeerAp[size];
- }
-
- @Override
- public RttPeerAp createFromParcel(Parcel in) {
- return new RttPeerAp(ScanResult.CREATOR.createFromParcel(in));
- }
- };
-
- @Override
- public String toString() {
- return new StringBuilder("RttPeerAp: scanResult=").append(
- scanResult.toString()).toString();
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
-
- if (!(o instanceof RttPeerAp)) {
- return false;
- }
-
- RttPeerAp lhs = (RttPeerAp) o;
-
- // Note: the only thing which matters for the request identity is the BSSID of the AP
- return TextUtils.equals(scanResult.BSSID, lhs.scanResult.BSSID);
- }
-
- @Override
- public int hashCode() {
- return scanResult.hashCode();
- }
- }
-
- /** @hide */
- public static class RttPeerAware implements RttPeer, Parcelable {
- public PeerHandle peerHandle;
- public byte[] peerMacAddress;
-
- public RttPeerAware(PeerHandle peerHandle) {
- if (peerHandle == null) {
- throw new IllegalArgumentException("Null peerHandle");
- }
- this.peerHandle = peerHandle;
- peerMacAddress = null;
- }
-
- public RttPeerAware(byte[] peerMacAddress) {
- if (peerMacAddress == null) {
- throw new IllegalArgumentException("Null peerMacAddress");
- }
-
- this.peerMacAddress = peerMacAddress;
- peerHandle = null;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- if (peerHandle == null) {
- dest.writeBoolean(false);
- dest.writeByteArray(peerMacAddress);
- } else {
- dest.writeBoolean(true);
- dest.writeInt(peerHandle.peerId);
- }
- }
-
- public static final Creator<RttPeerAware> CREATOR = new Creator<RttPeerAware>() {
- @Override
- public RttPeerAware[] newArray(int size) {
- return new RttPeerAware[size];
- }
-
- @Override
- public RttPeerAware createFromParcel(Parcel in) {
- boolean peerHandleAvail = in.readBoolean();
- if (peerHandleAvail) {
- return new RttPeerAware(new PeerHandle(in.readInt()));
- } else {
- return new RttPeerAware(in.createByteArray());
- }
- }
- };
-
- @Override
- public String toString() {
- return new StringBuilder("RttPeerAware: peerHandle=").append(
- peerHandle == null ? "<null>" : Integer.toString(peerHandle.peerId)).append(
- ", peerMacAddress=").append(peerMacAddress == null ? "<null>"
- : new String(HexEncoding.encode(peerMacAddress))).toString();
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
-
- if (!(o instanceof RttPeerAware)) {
- return false;
- }
-
- RttPeerAware lhs = (RttPeerAware) o;
-
- return Objects.equals(peerHandle, lhs.peerHandle) && Arrays.equals(peerMacAddress,
- lhs.peerMacAddress);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(peerHandle.peerId, peerMacAddress);
- }
- }
}
diff --git a/android/net/wifi/rtt/RangingResult.java b/android/net/wifi/rtt/RangingResult.java
index 93e52aeb..a380fae7 100644
--- a/android/net/wifi/rtt/RangingResult.java
+++ b/android/net/wifi/rtt/RangingResult.java
@@ -17,16 +17,15 @@
package android.net.wifi.rtt;
import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.net.MacAddress;
import android.net.wifi.aware.PeerHandle;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
-import libcore.util.HexEncoding;
-
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@@ -62,7 +61,7 @@ public final class RangingResult implements Parcelable {
public static final int STATUS_FAIL = 1;
private final int mStatus;
- private final byte[] mMac;
+ private final MacAddress mMac;
private final PeerHandle mPeerHandle;
private final int mDistanceMm;
private final int mDistanceStdDevMm;
@@ -70,7 +69,7 @@ public final class RangingResult implements Parcelable {
private final long mTimestamp;
/** @hide */
- public RangingResult(@RangeResultStatus int status, byte[] mac, int distanceMm,
+ public RangingResult(@RangeResultStatus int status, @NonNull MacAddress mac, int distanceMm,
int distanceStdDevMm, int rssi, long timestamp) {
mStatus = status;
mMac = mac;
@@ -109,7 +108,7 @@ public final class RangingResult implements Parcelable {
* Will return a {@code null} for results corresponding to requests issued using a {@code
* PeerHandle}, i.e. using the {@link RangingRequest.Builder#addWifiAwarePeer(PeerHandle)} API.
*/
- public byte[] getMacAddress() {
+ public MacAddress getMacAddress() {
return mMac;
}
@@ -193,7 +192,12 @@ public final class RangingResult implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mStatus);
- dest.writeByteArray(mMac);
+ if (mMac == null) {
+ dest.writeBoolean(false);
+ } else {
+ dest.writeBoolean(true);
+ mMac.writeToParcel(dest, flags);
+ }
if (mPeerHandle == null) {
dest.writeBoolean(false);
} else {
@@ -216,7 +220,11 @@ public final class RangingResult implements Parcelable {
@Override
public RangingResult createFromParcel(Parcel in) {
int status = in.readInt();
- byte[] mac = in.createByteArray();
+ boolean macAddressPresent = in.readBoolean();
+ MacAddress mac = null;
+ if (macAddressPresent) {
+ mac = MacAddress.CREATOR.createFromParcel(in);
+ }
boolean peerHandlePresent = in.readBoolean();
PeerHandle peerHandle = null;
if (peerHandlePresent) {
@@ -240,11 +248,11 @@ public final class RangingResult implements Parcelable {
@Override
public String toString() {
return new StringBuilder("RangingResult: [status=").append(mStatus).append(", mac=").append(
- mMac == null ? "<null>" : new String(HexEncoding.encodeToString(mMac))).append(
- ", peerHandle=").append(mPeerHandle == null ? "<null>" : mPeerHandle.peerId).append(
- ", distanceMm=").append(mDistanceMm).append(", distanceStdDevMm=").append(
- mDistanceStdDevMm).append(", rssi=").append(mRssi).append(", timestamp=").append(
- mTimestamp).append("]").toString();
+ mMac).append(", peerHandle=").append(
+ mPeerHandle == null ? "<null>" : mPeerHandle.peerId).append(", distanceMm=").append(
+ mDistanceMm).append(", distanceStdDevMm=").append(mDistanceStdDevMm).append(
+ ", rssi=").append(mRssi).append(", timestamp=").append(mTimestamp).append(
+ "]").toString();
}
@Override
@@ -259,7 +267,7 @@ public final class RangingResult implements Parcelable {
RangingResult lhs = (RangingResult) o;
- return mStatus == lhs.mStatus && Arrays.equals(mMac, lhs.mMac) && Objects.equals(
+ return mStatus == lhs.mStatus && Objects.equals(mMac, lhs.mMac) && Objects.equals(
mPeerHandle, lhs.mPeerHandle) && mDistanceMm == lhs.mDistanceMm
&& mDistanceStdDevMm == lhs.mDistanceStdDevMm && mRssi == lhs.mRssi
&& mTimestamp == lhs.mTimestamp;
diff --git a/android/net/wifi/rtt/ResponderConfig.java b/android/net/wifi/rtt/ResponderConfig.java
new file mode 100644
index 00000000..c3e10074
--- /dev/null
+++ b/android/net/wifi/rtt/ResponderConfig.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.rtt;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.net.MacAddress;
+import android.net.wifi.ScanResult;
+import android.net.wifi.aware.PeerHandle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Defines the configuration of an IEEE 802.11mc Responder. The Responder may be an Access Point
+ * (AP), a Wi-Fi Aware device, or a manually configured Responder.
+ * <p>
+ * A Responder configuration may be constructed from a {@link ScanResult} or manually (with the
+ * data obtained out-of-band from a peer).
+ *
+ * @hide (@SystemApi)
+ */
+public final class ResponderConfig implements Parcelable {
+ private static final int AWARE_BAND_2_DISCOVERY_CHANNEL = 2437;
+
+ /** @hide */
+ @IntDef({RESPONDER_AP, RESPONDER_STA, RESPONDER_P2P_GO, RESPONDER_P2P_CLIENT, RESPONDER_AWARE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ResponderType {
+ }
+
+ /**
+ * Responder is an AP.
+ */
+ public static final int RESPONDER_AP = 0;
+ /**
+ * Responder is a STA.
+ */
+ public static final int RESPONDER_STA = 1;
+ /**
+ * Responder is a Wi-Fi Direct Group Owner (GO).
+ */
+ public static final int RESPONDER_P2P_GO = 2;
+ /**
+ * Responder is a Wi-Fi Direct Group Client.
+ */
+ public static final int RESPONDER_P2P_CLIENT = 3;
+ /**
+ * Responder is a Wi-Fi Aware device.
+ */
+ public static final int RESPONDER_AWARE = 4;
+
+
+ /** @hide */
+ @IntDef({
+ CHANNEL_WIDTH_20MHZ, CHANNEL_WIDTH_40MHZ, CHANNEL_WIDTH_80MHZ, CHANNEL_WIDTH_160MHZ,
+ CHANNEL_WIDTH_80MHZ_PLUS_MHZ})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ChannelWidth {
+ }
+
+ /**
+ * Channel bandwidth is 20 MHZ
+ */
+ public static final int CHANNEL_WIDTH_20MHZ = 0;
+ /**
+ * Channel bandwidth is 40 MHZ
+ */
+ public static final int CHANNEL_WIDTH_40MHZ = 1;
+ /**
+ * Channel bandwidth is 80 MHZ
+ */
+ public static final int CHANNEL_WIDTH_80MHZ = 2;
+ /**
+ * Channel bandwidth is 160 MHZ
+ */
+ public static final int CHANNEL_WIDTH_160MHZ = 3;
+ /**
+ * Channel bandwidth is 160 MHZ, but 80MHZ + 80MHZ
+ */
+ public static final int CHANNEL_WIDTH_80MHZ_PLUS_MHZ = 4;
+
+ /** @hide */
+ @IntDef({PREAMBLE_LEGACY, PREAMBLE_HT, PREAMBLE_VHT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PreambleType {
+ }
+
+ /**
+ * Preamble type: Legacy.
+ */
+ public static final int PREAMBLE_LEGACY = 0;
+
+ /**
+ * Preamble type: HT.
+ */
+ public static final int PREAMBLE_HT = 1;
+
+ /**
+ * Preamble type: VHT.
+ */
+ public static final int PREAMBLE_VHT = 2;
+
+
+ /**
+ * The MAC address of the Responder. Will be null if a Wi-Fi Aware peer identifier (the
+ * peerHandle field) ise used to identify the Responder.
+ */
+ public final MacAddress macAddress;
+
+ /**
+ * The peer identifier of a Wi-Fi Aware Responder. Will be null if a MAC Address (the macAddress
+ * field) is used to identify the Responder.
+ */
+ public final PeerHandle peerHandle;
+
+ /**
+ * The device type of the Responder.
+ */
+ public final int responderType;
+
+ /**
+ * Indicates whether the Responder device supports IEEE 802.11mc.
+ */
+ public final boolean supports80211mc;
+
+ /**
+ * Responder channel bandwidth, specified using {@link ChannelWidth}.
+ */
+ public final int channelWidth;
+
+ /**
+ * The primary 20 MHz frequency (in MHz) of the channel of the Responder.
+ */
+ public final int frequency;
+
+ /**
+ * Not used if the {@link #channelWidth} is 20 MHz. If the Responder uses 40, 80 or 160 MHz,
+ * this is the center frequency (in MHz), if the Responder uses 80 + 80 MHz, this is the
+ * center frequency of the first segment (in MHz).
+ */
+ public final int centerFreq0;
+
+ /**
+ * Only used if the {@link #channelWidth} is 80 + 80 MHz. If the Responder uses 80 + 80 MHz,
+ * this is the center frequency of the second segment (in MHz).
+ */
+ public final int centerFreq1;
+
+ /**
+ * The preamble used by the Responder, specified using {@link PreambleType}.
+ */
+ public final int preamble;
+
+ /**
+ * Constructs Responder configuration, using a MAC address to identify the Responder.
+ *
+ * @param macAddress The MAC address of the Responder.
+ * @param responderType The type of the responder device, specified using
+ * {@link ResponderType}.
+ * @param supports80211mc Indicates whether the responder supports IEEE 802.11mc.
+ * @param channelWidth Responder channel bandwidth, specified using {@link ChannelWidth}.
+ * @param frequency The primary 20 MHz frequency (in MHz) of the channel of the Responder.
+ * @param centerFreq0 Not used if the {@code channelWidth} is 20 MHz. If the Responder uses
+ * 40, 80 or 160 MHz, this is the center frequency (in MHz), if the
+ * Responder uses 80 + 80 MHz, this is the center frequency of the first
+ * segment (in MHz).
+ * @param centerFreq1 Only used if the {@code channelWidth} is 80 + 80 MHz. If the
+ * Responder
+ * uses 80 + 80 MHz, this is the center frequency of the second segment
+ * (in
+ * MHz).
+ * @param preamble The preamble used by the Responder, specified using
+ * {@link PreambleType}.
+ */
+ public ResponderConfig(@NonNull MacAddress macAddress, @ResponderType int responderType,
+ boolean supports80211mc, @ChannelWidth int channelWidth, int frequency, int centerFreq0,
+ int centerFreq1, @PreambleType int preamble) {
+ if (macAddress == null) {
+ throw new IllegalArgumentException(
+ "Invalid ResponderConfig - must specify a MAC address");
+ }
+ this.macAddress = macAddress;
+ this.peerHandle = null;
+ this.responderType = responderType;
+ this.supports80211mc = supports80211mc;
+ this.channelWidth = channelWidth;
+ this.frequency = frequency;
+ this.centerFreq0 = centerFreq0;
+ this.centerFreq1 = centerFreq1;
+ this.preamble = preamble;
+ }
+
+ /**
+ * Constructs Responder configuration, using a Wi-Fi Aware PeerHandle to identify the Responder.
+ *
+ * @param peerHandle The Wi-Fi Aware peer identifier of the Responder.
+ * @param responderType The type of the responder device, specified using
+ * {@link ResponderType}.
+ * @param supports80211mc Indicates whether the responder supports IEEE 802.11mc.
+ * @param channelWidth Responder channel bandwidth, specified using {@link ChannelWidth}.
+ * @param frequency The primary 20 MHz frequency (in MHz) of the channel of the Responder.
+ * @param centerFreq0 Not used if the {@code channelWidth} is 20 MHz. If the Responder uses
+ * 40, 80 or 160 MHz, this is the center frequency (in MHz), if the
+ * Responder uses 80 + 80 MHz, this is the center frequency of the first
+ * segment (in MHz).
+ * @param centerFreq1 Only used if the {@code channelWidth} is 80 + 80 MHz. If the
+ * Responder
+ * uses 80 + 80 MHz, this is the center frequency of the second segment
+ * (in
+ * MHz).
+ * @param preamble The preamble used by the Responder, specified using
+ * {@link PreambleType}.
+ */
+ public ResponderConfig(@NonNull PeerHandle peerHandle, @ResponderType int responderType,
+ boolean supports80211mc, @ChannelWidth int channelWidth, int frequency, int centerFreq0,
+ int centerFreq1, @PreambleType int preamble) {
+ this.macAddress = null;
+ this.peerHandle = peerHandle;
+ this.responderType = responderType;
+ this.supports80211mc = supports80211mc;
+ this.channelWidth = channelWidth;
+ this.frequency = frequency;
+ this.centerFreq0 = centerFreq0;
+ this.centerFreq1 = centerFreq1;
+ this.preamble = preamble;
+ }
+
+ /**
+ * Constructs Responder configuration. This is an internal-only constructor which specifies both
+ * a MAC address and a Wi-Fi PeerHandle to identify the Responder.
+ *
+ * @param macAddress The MAC address of the Responder.
+ * @param peerHandle The Wi-Fi Aware peer identifier of the Responder.
+ * @param responderType The type of the responder device, specified using
+ * {@link ResponderType}.
+ * @param supports80211mc Indicates whether the responder supports IEEE 802.11mc.
+ * @param channelWidth Responder channel bandwidth, specified using {@link ChannelWidth}.
+ * @param frequency The primary 20 MHz frequency (in MHz) of the channel of the Responder.
+ * @param centerFreq0 Not used if the {@code channelWidth} is 20 MHz. If the Responder uses
+ * 40, 80 or 160 MHz, this is the center frequency (in MHz), if the
+ * Responder uses 80 + 80 MHz, this is the center frequency of the first
+ * segment (in MHz).
+ * @param centerFreq1 Only used if the {@code channelWidth} is 80 + 80 MHz. If the
+ * Responder
+ * uses 80 + 80 MHz, this is the center frequency of the second segment
+ * (in
+ * MHz).
+ * @param preamble The preamble used by the Responder, specified using
+ * {@link PreambleType}.
+ * @hide
+ */
+ public ResponderConfig(@NonNull MacAddress macAddress, @NonNull PeerHandle peerHandle,
+ @ResponderType int responderType, boolean supports80211mc,
+ @ChannelWidth int channelWidth, int frequency, int centerFreq0, int centerFreq1,
+ @PreambleType int preamble) {
+ this.macAddress = macAddress;
+ this.peerHandle = peerHandle;
+ this.responderType = responderType;
+ this.supports80211mc = supports80211mc;
+ this.channelWidth = channelWidth;
+ this.frequency = frequency;
+ this.centerFreq0 = centerFreq0;
+ this.centerFreq1 = centerFreq1;
+ this.preamble = preamble;
+ }
+
+ /**
+ * Creates a Responder configuration from a {@link ScanResult} corresponding to an Access
+ * Point (AP), which can be obtained from {@link android.net.wifi.WifiManager#getScanResults()}.
+ */
+ public static ResponderConfig fromScanResult(ScanResult scanResult) {
+ MacAddress macAddress = MacAddress.fromString(scanResult.BSSID);
+ int responderType = RESPONDER_AP;
+ boolean supports80211mc = scanResult.is80211mcResponder();
+ int channelWidth = translcateScanResultChannelWidth(scanResult.channelWidth);
+ int frequency = scanResult.frequency;
+ int centerFreq0 = scanResult.centerFreq0;
+ int centerFreq1 = scanResult.centerFreq1;
+
+ // TODO: b/68936111 - extract preamble info from IE
+ int preamble;
+ if (channelWidth == CHANNEL_WIDTH_80MHZ || channelWidth == CHANNEL_WIDTH_160MHZ) {
+ preamble = PREAMBLE_VHT;
+ } else {
+ preamble = PREAMBLE_HT;
+ }
+
+ return new ResponderConfig(macAddress, responderType, supports80211mc, channelWidth,
+ frequency, centerFreq0, centerFreq1, preamble);
+ }
+
+ /**
+ * Creates a Responder configuration from a MAC address corresponding to a Wi-Fi Aware
+ * Responder. The Responder parameters are set to defaults.
+ */
+ public static ResponderConfig fromWifiAwarePeerMacAddressWithDefaults(MacAddress macAddress) {
+ /* Note: the parameters are those of the Aware discovery channel (channel 6). A Responder
+ * is expected to be brought up and available to negotiate a maximum accuracy channel
+ * (i.e. Band 5 @ 80MHz). A Responder is brought up on the peer by starting an Aware
+ * Unsolicited Publisher with Ranging enabled.
+ */
+ return new ResponderConfig(macAddress, RESPONDER_AWARE, true, CHANNEL_WIDTH_20MHZ,
+ AWARE_BAND_2_DISCOVERY_CHANNEL, 0, 0, PREAMBLE_HT);
+ }
+
+ /**
+ * Creates a Responder configuration from a {@link PeerHandle} corresponding to a Wi-Fi Aware
+ * Responder. The Responder parameters are set to defaults.
+ */
+ public static ResponderConfig fromWifiAwarePeerHandleWithDefaults(PeerHandle peerHandle) {
+ /* Note: the parameters are those of the Aware discovery channel (channel 6). A Responder
+ * is expected to be brought up and available to negotiate a maximum accuracy channel
+ * (i.e. Band 5 @ 80MHz). A Responder is brought up on the peer by starting an Aware
+ * Unsolicited Publisher with Ranging enabled.
+ */
+ return new ResponderConfig(peerHandle, RESPONDER_AWARE, true, CHANNEL_WIDTH_20MHZ,
+ AWARE_BAND_2_DISCOVERY_CHANNEL, 0, 0, PREAMBLE_HT);
+ }
+
+ /**
+ * Check whether the Responder configuration is valid.
+ *
+ * @return true if valid, false otherwise.
+ * @hide
+ */
+ public boolean isValid(boolean awareSupported) {
+ if (macAddress == null && peerHandle == null || macAddress != null && peerHandle != null) {
+ return false;
+ }
+ if (!awareSupported && responderType == RESPONDER_AWARE) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (macAddress == null) {
+ dest.writeBoolean(false);
+ } else {
+ dest.writeBoolean(true);
+ macAddress.writeToParcel(dest, flags);
+ }
+ if (peerHandle == null) {
+ dest.writeBoolean(false);
+ } else {
+ dest.writeBoolean(true);
+ dest.writeInt(peerHandle.peerId);
+ }
+ dest.writeInt(responderType);
+ dest.writeInt(supports80211mc ? 1 : 0);
+ dest.writeInt(channelWidth);
+ dest.writeInt(frequency);
+ dest.writeInt(centerFreq0);
+ dest.writeInt(centerFreq1);
+ dest.writeInt(preamble);
+ }
+
+ public static final Creator<ResponderConfig> CREATOR = new Creator<ResponderConfig>() {
+ @Override
+ public ResponderConfig[] newArray(int size) {
+ return new ResponderConfig[size];
+ }
+
+ @Override
+ public ResponderConfig createFromParcel(Parcel in) {
+ boolean macAddressPresent = in.readBoolean();
+ MacAddress macAddress = null;
+ if (macAddressPresent) {
+ macAddress = MacAddress.CREATOR.createFromParcel(in);
+ }
+ boolean peerHandlePresent = in.readBoolean();
+ PeerHandle peerHandle = null;
+ if (peerHandlePresent) {
+ peerHandle = new PeerHandle(in.readInt());
+ }
+ int responderType = in.readInt();
+ boolean supports80211mc = in.readInt() == 1;
+ int channelWidth = in.readInt();
+ int frequency = in.readInt();
+ int centerFreq0 = in.readInt();
+ int centerFreq1 = in.readInt();
+ int preamble = in.readInt();
+
+ if (peerHandle == null) {
+ return new ResponderConfig(macAddress, responderType, supports80211mc, channelWidth,
+ frequency, centerFreq0, centerFreq1, preamble);
+ } else {
+ return new ResponderConfig(peerHandle, responderType, supports80211mc, channelWidth,
+ frequency, centerFreq0, centerFreq1, preamble);
+ }
+ }
+ };
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof ResponderConfig)) {
+ return false;
+ }
+
+ ResponderConfig lhs = (ResponderConfig) o;
+
+ return Objects.equals(macAddress, lhs.macAddress) && Objects.equals(peerHandle,
+ lhs.peerHandle) && responderType == lhs.responderType
+ && supports80211mc == lhs.supports80211mc && channelWidth == lhs.channelWidth
+ && frequency == lhs.frequency && centerFreq0 == lhs.centerFreq0
+ && centerFreq1 == lhs.centerFreq1 && preamble == lhs.preamble;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(macAddress, peerHandle, responderType, supports80211mc, channelWidth,
+ frequency, centerFreq0, centerFreq1, preamble);
+ }
+
+ /** @hide */
+ @Override
+ public String toString() {
+ return new StringBuffer("ResponderConfig: macAddress=").append(macAddress).append(
+ ", peerHandle=").append(peerHandle == null ? "<null>" : peerHandle.peerId).append(
+ ", responderType=").append(responderType).append(", supports80211mc=").append(
+ supports80211mc).append(", channelWidth=").append(channelWidth).append(
+ ", frequency=").append(frequency).append(", centerFreq0=").append(
+ centerFreq0).append(", centerFreq1=").append(centerFreq1).append(
+ ", preamble=").append(preamble).toString();
+ }
+
+ /** @hide */
+ static int translcateScanResultChannelWidth(int scanResultChannelWidth) {
+ switch (scanResultChannelWidth) {
+ case ScanResult.CHANNEL_WIDTH_20MHZ:
+ return CHANNEL_WIDTH_20MHZ;
+ case ScanResult.CHANNEL_WIDTH_40MHZ:
+ return CHANNEL_WIDTH_40MHZ;
+ case ScanResult.CHANNEL_WIDTH_80MHZ:
+ return CHANNEL_WIDTH_80MHZ;
+ case ScanResult.CHANNEL_WIDTH_160MHZ:
+ return CHANNEL_WIDTH_160MHZ;
+ case ScanResult.CHANNEL_WIDTH_80MHZ_PLUS_MHZ:
+ return CHANNEL_WIDTH_80MHZ_PLUS_MHZ;
+ default:
+ throw new IllegalArgumentException(
+ "translcateScanResultChannelWidth: bad " + scanResultChannelWidth);
+ }
+ }
+}
diff --git a/android/net/wifi/rtt/WifiRttManager.java b/android/net/wifi/rtt/WifiRttManager.java
index 735e872e..b4c690f4 100644
--- a/android/net/wifi/rtt/WifiRttManager.java
+++ b/android/net/wifi/rtt/WifiRttManager.java
@@ -41,7 +41,7 @@ import java.util.List;
*
* @hide RTT_API
*/
-@SystemService(Context.WIFI_RTT2_SERVICE)
+@SystemService(Context.WIFI_RTT_RANGING_SERVICE)
public class WifiRttManager {
private static final String TAG = "WifiRttManager";
private static final boolean VDBG = false;
diff --git a/android/os/BatteryStats.java b/android/os/BatteryStats.java
index 811091e3..1e847c59 100644
--- a/android/os/BatteryStats.java
+++ b/android/os/BatteryStats.java
@@ -16,6 +16,7 @@
package android.os;
+import android.app.ActivityManager;
import android.app.job.JobParameters;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -33,6 +34,7 @@ import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
import android.view.Display;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BatterySipper;
import com.android.internal.os.BatteryStatsHelper;
@@ -225,8 +227,11 @@ public abstract class BatteryStats implements Parcelable {
* New in version 28:
* - Light/Deep Doze power
* - WiFi Multicast Wakelock statistics (count & duration)
+ * New in version 29:
+ * - Process states re-ordered. TOP_SLEEPING now below BACKGROUND. HEAVY_WEIGHT introduced.
+ * - CPU times per UID process state
*/
- static final int CHECKIN_VERSION = 28;
+ static final int CHECKIN_VERSION = 29;
/**
* Old version, we hit 9 and ran out of room, need to remove.
@@ -327,7 +332,8 @@ public abstract class BatteryStats implements Parcelable {
*
* Other types might include times spent in foreground, background etc.
*/
- private final String UID_TIMES_TYPE_ALL = "A";
+ @VisibleForTesting
+ public static final String UID_TIMES_TYPE_ALL = "A";
/**
* State for keeping track of counting information.
@@ -507,6 +513,31 @@ public abstract class BatteryStats implements Parcelable {
}
/**
+ * Maps the ActivityManager procstate into corresponding BatteryStats procstate.
+ */
+ public static int mapToInternalProcessState(int procState) {
+ if (procState == ActivityManager.PROCESS_STATE_NONEXISTENT) {
+ return ActivityManager.PROCESS_STATE_NONEXISTENT;
+ } else if (procState == ActivityManager.PROCESS_STATE_TOP) {
+ return Uid.PROCESS_STATE_TOP;
+ } else if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+ // Persistent and other foreground states go here.
+ return Uid.PROCESS_STATE_FOREGROUND_SERVICE;
+ } else if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) {
+ // Persistent and other foreground states go here.
+ return Uid.PROCESS_STATE_FOREGROUND;
+ } else if (procState <= ActivityManager.PROCESS_STATE_RECEIVER) {
+ return Uid.PROCESS_STATE_BACKGROUND;
+ } else if (procState <= ActivityManager.PROCESS_STATE_TOP_SLEEPING) {
+ return Uid.PROCESS_STATE_TOP_SLEEPING;
+ } else if (procState <= ActivityManager.PROCESS_STATE_HEAVY_WEIGHT) {
+ return Uid.PROCESS_STATE_HEAVY_WEIGHT;
+ } else {
+ return Uid.PROCESS_STATE_CACHED;
+ }
+ }
+
+ /**
* The statistics associated with a particular uid.
*/
public static abstract class Uid {
@@ -645,6 +676,15 @@ public abstract class BatteryStats implements Parcelable {
public abstract long[] getCpuFreqTimes(int which);
public abstract long[] getScreenOffCpuFreqTimes(int which);
+ /**
+ * Returns cpu times of an uid at a particular process state.
+ */
+ public abstract long[] getCpuFreqTimes(int which, int procState);
+ /**
+ * Returns cpu times of an uid while the screen if off at a particular process state.
+ */
+ public abstract long[] getScreenOffCpuFreqTimes(int which, int procState);
+
// Note: the following times are disjoint. They can be added together to find the
// total time a uid has had any processes running at all.
@@ -658,32 +698,61 @@ public abstract class BatteryStats implements Parcelable {
*/
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 1;
/**
- * Time this uid has any process that is top while the device is sleeping, but none
- * in the "foreground service" or better state.
- */
- public static final int PROCESS_STATE_TOP_SLEEPING = 2;
- /**
* Time this uid has any process in an active foreground state, but none in the
* "top sleeping" or better state.
*/
- public static final int PROCESS_STATE_FOREGROUND = 3;
+ public static final int PROCESS_STATE_FOREGROUND = 2;
/**
* Time this uid has any process in an active background state, but none in the
* "foreground" or better state.
*/
- public static final int PROCESS_STATE_BACKGROUND = 4;
+ public static final int PROCESS_STATE_BACKGROUND = 3;
+ /**
+ * Time this uid has any process that is top while the device is sleeping, but not
+ * active for any other reason. We kind-of consider it a kind of cached process
+ * for execution restrictions.
+ */
+ public static final int PROCESS_STATE_TOP_SLEEPING = 4;
+ /**
+ * Time this uid has any process that is in the background but it has an activity
+ * marked as "can't save state". This is essentially a cached process, though the
+ * system will try much harder than normal to avoid killing it.
+ */
+ public static final int PROCESS_STATE_HEAVY_WEIGHT = 5;
/**
* Time this uid has any processes that are sitting around cached, not in one of the
* other active states.
*/
- public static final int PROCESS_STATE_CACHED = 5;
+ public static final int PROCESS_STATE_CACHED = 6;
/**
* Total number of process states we track.
*/
- public static final int NUM_PROCESS_STATE = 6;
+ public static final int NUM_PROCESS_STATE = 7;
+ // Used in dump
static final String[] PROCESS_STATE_NAMES = {
- "Top", "Fg Service", "Top Sleeping", "Foreground", "Background", "Cached"
+ "Top", "Fg Service", "Foreground", "Background", "Top Sleeping", "Heavy Weight",
+ "Cached"
+ };
+
+ // Used in checkin dump
+ @VisibleForTesting
+ public static final String[] UID_PROCESS_TYPES = {
+ "T", // TOP
+ "FS", // FOREGROUND_SERVICE
+ "F", // FOREGROUND
+ "B", // BACKGROUND
+ "TS", // TOP_SLEEPING
+ "HW", // HEAVY_WEIGHT
+ "C" // CACHED
+ };
+
+ /**
+ * When the process exits one of these states, we need to make sure cpu time in this state
+ * is not attributed to any non-critical process states.
+ */
+ public static final int[] CRITICAL_PROC_STATES = {
+ PROCESS_STATE_TOP, PROCESS_STATE_FOREGROUND_SERVICE, PROCESS_STATE_FOREGROUND
};
public abstract long getProcessStateTime(int state, long elapsedRealtimeUs, int which);
@@ -1180,7 +1249,7 @@ public abstract class BatteryStats implements Parcelable {
public static final class PackageChange {
public String mPackageName;
public boolean mUpdate;
- public int mVersionCode;
+ public long mVersionCode;
}
public static final class DailyItem {
@@ -3994,6 +4063,29 @@ public abstract class BatteryStats implements Parcelable {
dumpLine(pw, uid, category, CPU_TIMES_AT_FREQ_DATA, UID_TIMES_TYPE_ALL,
cpuFreqTimeMs.length, sb.toString());
}
+
+ for (int procState = 0; procState < Uid.NUM_PROCESS_STATE; ++procState) {
+ final long[] timesMs = u.getCpuFreqTimes(which, procState);
+ if (timesMs != null && timesMs.length == cpuFreqs.length) {
+ sb.setLength(0);
+ for (int i = 0; i < timesMs.length; ++i) {
+ sb.append((i == 0 ? "" : ",") + timesMs[i]);
+ }
+ final long[] screenOffTimesMs = u.getScreenOffCpuFreqTimes(
+ which, procState);
+ if (screenOffTimesMs != null) {
+ for (int i = 0; i < screenOffTimesMs.length; ++i) {
+ sb.append("," + screenOffTimesMs[i]);
+ }
+ } else {
+ for (int i = 0; i < timesMs.length; ++i) {
+ sb.append(",0");
+ }
+ }
+ dumpLine(pw, uid, category, CPU_TIMES_AT_FREQ_DATA,
+ Uid.UID_PROCESS_TYPES[procState], timesMs.length, sb.toString());
+ }
+ }
}
final ArrayMap<String, ? extends BatteryStats.Uid.Proc> processStats
@@ -5604,6 +5696,30 @@ public abstract class BatteryStats implements Parcelable {
pw.println(sb.toString());
}
+ for (int procState = 0; procState < Uid.NUM_PROCESS_STATE; ++procState) {
+ final long[] cpuTimes = u.getCpuFreqTimes(which, procState);
+ if (cpuTimes != null) {
+ sb.setLength(0);
+ sb.append(" Cpu times per freq at state "
+ + Uid.PROCESS_STATE_NAMES[procState] + ":");
+ for (int i = 0; i < cpuTimes.length; ++i) {
+ sb.append(" " + cpuTimes[i]);
+ }
+ pw.println(sb.toString());
+ }
+
+ final long[] screenOffCpuTimes = u.getScreenOffCpuFreqTimes(which, procState);
+ if (screenOffCpuTimes != null) {
+ sb.setLength(0);
+ sb.append(" Screen-off cpu times per freq at state "
+ + Uid.PROCESS_STATE_NAMES[procState] + ":");
+ for (int i = 0; i < screenOffCpuTimes.length; ++i) {
+ sb.append(" " + screenOffCpuTimes[i]);
+ }
+ pw.println(sb.toString());
+ }
+ }
+
final ArrayMap<String, ? extends BatteryStats.Uid.Proc> processStats
= u.getProcessStats();
for (int ipr=processStats.size()-1; ipr>=0; ipr--) {
@@ -6742,7 +6858,7 @@ public abstract class BatteryStats implements Parcelable {
/** Dump #STATS_SINCE_CHARGED batterystats data to a proto. @hide */
public void dumpProtoLocked(Context context, FileDescriptor fd, List<ApplicationInfo> apps,
- int flags, long historyStart) {
+ int flags) {
final ProtoOutputStream proto = new ProtoOutputStream(fd);
final long bToken = proto.start(BatteryStatsServiceDumpProto.BATTERYSTATS);
prepareForDumpLocked();
@@ -6752,13 +6868,7 @@ public abstract class BatteryStats implements Parcelable {
proto.write(BatteryStatsProto.START_PLATFORM_VERSION, getStartPlatformVersion());
proto.write(BatteryStatsProto.END_PLATFORM_VERSION, getEndPlatformVersion());
- long now = getHistoryBaseTime() + SystemClock.elapsedRealtime();
-
- if ((flags & (DUMP_INCLUDE_HISTORY | DUMP_HISTORY_ONLY)) != 0) {
- if (startIteratingHistoryLocked()) {
- // TODO: implement dumpProtoHistoryLocked(proto);
- }
- }
+ // History intentionally not included in proto dump.
if ((flags & (DUMP_HISTORY_ONLY | DUMP_DAILY_ONLY)) == 0) {
final BatteryStatsHelper helper = new BatteryStatsHelper(context, false,
diff --git a/android/os/Binder.java b/android/os/Binder.java
index b5bcd02c..33470f36 100644
--- a/android/os/Binder.java
+++ b/android/os/Binder.java
@@ -35,6 +35,9 @@ import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
/**
* Base class for a remotable object, the core part of a lightweight
@@ -298,7 +301,7 @@ public class Binder implements IBinder {
long callingIdentity = clearCallingIdentity();
Throwable throwableToPropagate = null;
try {
- action.run();
+ action.runOrThrow();
} catch (Throwable throwable) {
throwableToPropagate = throwable;
} finally {
@@ -322,7 +325,7 @@ public class Binder implements IBinder {
long callingIdentity = clearCallingIdentity();
Throwable throwableToPropagate = null;
try {
- return action.get();
+ return action.getOrThrow();
} catch (Throwable throwable) {
throwableToPropagate = throwable;
return null; // overridden by throwing in finally block
@@ -778,6 +781,8 @@ final class BinderProxy implements IBinder {
private static final int LOG_MAIN_INDEX_SIZE = 8;
private static final int MAIN_INDEX_SIZE = 1 << LOG_MAIN_INDEX_SIZE;
private static final int MAIN_INDEX_MASK = MAIN_INDEX_SIZE - 1;
+ // Debuggable builds will throw an AssertionError if the number of map entries exceeds:
+ private static final int CRASH_AT_SIZE = 5_000;
/**
* We next warn when we exceed this bucket size.
@@ -899,10 +904,60 @@ final class BinderProxy implements IBinder {
keyArray[size] = key;
}
if (size >= mWarnBucketSize) {
+ final int totalSize = size();
Log.v(Binder.TAG, "BinderProxy map growth! bucket size = " + size
- + " total = " + size());
+ + " total = " + totalSize);
mWarnBucketSize += WARN_INCREMENT;
+ if (Build.IS_DEBUGGABLE && totalSize > CRASH_AT_SIZE) {
+ diagnosticCrash();
+ }
+ }
+ }
+
+ /**
+ * Dump a histogram to the logcat, then throw an assertion error. Used to diagnose
+ * abnormally large proxy maps.
+ */
+ private void diagnosticCrash() {
+ Map<String, Integer> counts = new HashMap<>();
+ for (ArrayList<WeakReference<BinderProxy>> a : mMainIndexValues) {
+ if (a != null) {
+ for (WeakReference<BinderProxy> weakRef : a) {
+ BinderProxy bp = weakRef.get();
+ String key;
+ if (bp == null) {
+ key = "<cleared weak-ref>";
+ } else {
+ try {
+ key = bp.getInterfaceDescriptor();
+ } catch (Throwable t) {
+ key = "<exception during getDescriptor>";
+ }
+ }
+ Integer i = counts.get(key);
+ if (i == null) {
+ counts.put(key, 1);
+ } else {
+ counts.put(key, i + 1);
+ }
+ }
+ }
+ }
+ Map.Entry<String, Integer>[] sorted = counts.entrySet().toArray(
+ new Map.Entry[counts.size()]);
+ Arrays.sort(sorted, (Map.Entry<String, Integer> a, Map.Entry<String, Integer> b)
+ -> b.getValue().compareTo(a.getValue()));
+ Log.v(Binder.TAG, "BinderProxy descriptor histogram (top ten):");
+ int printLength = Math.min(10, sorted.length);
+ for (int i = 0; i < printLength; i++) {
+ Log.v(Binder.TAG, " #" + (i + 1) + ": " + sorted[i].getKey() + " x"
+ + sorted[i].getValue());
}
+
+ // Now throw an assertion.
+ final int totalSize = size();
+ throw new AssertionError("Binder ProxyMap has too many entries: " + totalSize
+ + ". BinderProxy leak?");
}
// Corresponding ArrayLists in the following two arrays always have the same size.
diff --git a/android/os/Build.java b/android/os/Build.java
index 02c7bd64..48f56847 100644
--- a/android/os/Build.java
+++ b/android/os/Build.java
@@ -117,8 +117,14 @@ public class Build {
public static final String SERIAL = getString("no.such.thing");
/**
- * Gets the hardware serial, if available.
- * @return The serial if specified.
+ * Gets the hardware serial number, if available.
+ *
+ * <p class="note"><b>Note:</b> Root access may allow you to modify device identifiers, such as
+ * the hardware serial number. If you change these identifiers, you can use
+ * <a href="/training/articles/security-key-attestation.html">key attestation</a> to obtain
+ * proof of the device's original identifiers.
+ *
+ * @return The serial number if specified.
*/
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
public static String getSerial() {
@@ -215,13 +221,31 @@ public class Build {
public static final String SDK = getString("ro.build.version.sdk");
/**
- * The user-visible SDK version of the framework; its possible
- * values are defined in {@link Build.VERSION_CODES}.
+ * The SDK version of the software currently running on this hardware
+ * device. This value never changes while a device is booted, but it may
+ * increase when the hardware manufacturer provides an OTA update.
+ * <p>
+ * Possible values are defined in {@link Build.VERSION_CODES}.
+ *
+ * @see #FIRST_SDK_INT
*/
public static final int SDK_INT = SystemProperties.getInt(
"ro.build.version.sdk", 0);
/**
+ * The SDK version of the software that <em>initially</em> shipped on
+ * this hardware device. It <em>never</em> changes during the lifetime
+ * of the device, even when {@link #SDK_INT} increases due to an OTA
+ * update.
+ * <p>
+ * Possible values are defined in {@link Build.VERSION_CODES}.
+ *
+ * @see #SDK_INT
+ */
+ public static final int FIRST_SDK_INT = SystemProperties
+ .getInt("ro.product.first_api_level", 0);
+
+ /**
* The developer preview revision of a prerelease SDK. This value will always
* be <code>0</code> on production platform builds/devices.
*
@@ -264,6 +288,14 @@ public class Build {
* @hide
*/
public static final int RESOURCES_SDK_INT = SDK_INT + ACTIVE_CODENAMES.length;
+
+ /**
+ * The current lowest supported value of app target SDK. Applications targeting
+ * lower values will fail to install and run. Its possible values are defined
+ * in {@link Build.VERSION_CODES}.
+ */
+ public static final int MIN_SUPPORTED_TARGET_SDK_INT = SystemProperties.getInt(
+ "ro.build.version.min_supported_target_sdk", 0);
}
/**
@@ -937,7 +969,9 @@ public class Build {
if (IS_ENG) return true;
if (IS_TREBLE_ENABLED) {
- int result = VintfObject.verify(new String[0]);
+ // If we can run this code, the device should already pass AVB.
+ // So, we don't need to check AVB here.
+ int result = VintfObject.verifyWithoutAvb();
if (result != 0) {
Slog.e(TAG, "Vendor interface is incompatible, error="
diff --git a/android/os/Debug.java b/android/os/Debug.java
index 2acf36fe..848ab88d 100644
--- a/android/os/Debug.java
+++ b/android/os/Debug.java
@@ -1136,7 +1136,7 @@ public final class Debug
int intervalUs) {
VMDebug.startMethodTracing(fixTracePath(tracePath), bufferSize, 0, true, intervalUs);
}
-
+
/**
* Formats name of trace log file for method tracing.
*/
@@ -1706,11 +1706,11 @@ public final class Debug
* Retrieves information about this processes memory usages. This information is broken down by
* how much is in use by dalvik, the native heap, and everything else.
*
- * <p><b>Note:</b> this method directly retrieves memory information for the give process
+ * <p><b>Note:</b> this method directly retrieves memory information for the given process
* from low-level data available to it. It may not be able to retrieve information about
* some protected allocations, such as graphics. If you want to be sure you can see
- * all information about allocations by the process, use instead
- * {@link android.app.ActivityManager#getProcessMemoryInfo(int[])}.</p>
+ * all information about allocations by the process, use
+ * {@link android.app.ActivityManager#getProcessMemoryInfo(int[])} instead.</p>
*/
public static native void getMemoryInfo(MemoryInfo memoryInfo);
diff --git a/android/os/Environment.java b/android/os/Environment.java
index f977c1de..b1794a6d 100644
--- a/android/os/Environment.java
+++ b/android/os/Environment.java
@@ -24,6 +24,7 @@ import android.text.TextUtils;
import android.util.Log;
import java.io.File;
+import java.util.LinkedList;
/**
* Provides access to environment variables.
@@ -291,8 +292,9 @@ public class Environment {
}
/** {@hide} */
- public static File getReferenceProfile(String packageName) {
- return buildPath(getDataDirectory(), "misc", "profiles", "ref", packageName);
+ public static File getProfileSnapshotPath(String packageName, String codePath) {
+ return buildPath(buildPath(getDataDirectory(), "misc", "profiles", "ref", packageName,
+ "primary.prof.snapshot"));
}
/** {@hide} */
@@ -608,6 +610,79 @@ public class Environment {
return false;
}
+ /** {@hide} */ public static final int HAS_MUSIC = 1 << 0;
+ /** {@hide} */ public static final int HAS_PODCASTS = 1 << 1;
+ /** {@hide} */ public static final int HAS_RINGTONES = 1 << 2;
+ /** {@hide} */ public static final int HAS_ALARMS = 1 << 3;
+ /** {@hide} */ public static final int HAS_NOTIFICATIONS = 1 << 4;
+ /** {@hide} */ public static final int HAS_PICTURES = 1 << 5;
+ /** {@hide} */ public static final int HAS_MOVIES = 1 << 6;
+ /** {@hide} */ public static final int HAS_DOWNLOADS = 1 << 7;
+ /** {@hide} */ public static final int HAS_DCIM = 1 << 8;
+ /** {@hide} */ public static final int HAS_DOCUMENTS = 1 << 9;
+
+ /** {@hide} */ public static final int HAS_ANDROID = 1 << 16;
+ /** {@hide} */ public static final int HAS_OTHER = 1 << 17;
+
+ /**
+ * Classify the content types present on the given external storage device.
+ * <p>
+ * This is typically useful for deciding if an inserted SD card is empty, or
+ * if it contains content like photos that should be preserved.
+ *
+ * @hide
+ */
+ public static int classifyExternalStorageDirectory(File dir) {
+ int res = 0;
+ for (File f : FileUtils.listFilesOrEmpty(dir)) {
+ if (f.isFile() && isInterestingFile(f)) {
+ res |= HAS_OTHER;
+ } else if (f.isDirectory() && hasInterestingFiles(f)) {
+ final String name = f.getName();
+ if (DIRECTORY_MUSIC.equals(name)) res |= HAS_MUSIC;
+ else if (DIRECTORY_PODCASTS.equals(name)) res |= HAS_PODCASTS;
+ else if (DIRECTORY_RINGTONES.equals(name)) res |= HAS_RINGTONES;
+ else if (DIRECTORY_ALARMS.equals(name)) res |= HAS_ALARMS;
+ else if (DIRECTORY_NOTIFICATIONS.equals(name)) res |= HAS_NOTIFICATIONS;
+ else if (DIRECTORY_PICTURES.equals(name)) res |= HAS_PICTURES;
+ else if (DIRECTORY_MOVIES.equals(name)) res |= HAS_MOVIES;
+ else if (DIRECTORY_DOWNLOADS.equals(name)) res |= HAS_DOWNLOADS;
+ else if (DIRECTORY_DCIM.equals(name)) res |= HAS_DCIM;
+ else if (DIRECTORY_DOCUMENTS.equals(name)) res |= HAS_DOCUMENTS;
+ else if (DIRECTORY_ANDROID.equals(name)) res |= HAS_ANDROID;
+ else res |= HAS_OTHER;
+ }
+ }
+ return res;
+ }
+
+ private static boolean hasInterestingFiles(File dir) {
+ final LinkedList<File> explore = new LinkedList<>();
+ explore.add(dir);
+ while (!explore.isEmpty()) {
+ dir = explore.pop();
+ for (File f : FileUtils.listFilesOrEmpty(dir)) {
+ if (isInterestingFile(f)) return true;
+ if (f.isDirectory()) explore.add(f);
+ }
+ }
+ return false;
+ }
+
+ private static boolean isInterestingFile(File file) {
+ if (file.isFile()) {
+ final String name = file.getName().toLowerCase();
+ if (name.endsWith(".exe") || name.equals("autorun.inf")
+ || name.equals("launchpad.zip") || name.equals(".nomedia")) {
+ return false;
+ } else {
+ return true;
+ }
+ } else {
+ return false;
+ }
+ }
+
/**
* Get a top-level shared/external storage directory for placing files of a
* particular type. This is where the user will typically place and manage
diff --git a/android/os/HandlerExecutor.java b/android/os/HandlerExecutor.java
new file mode 100644
index 00000000..416b24b5
--- /dev/null
+++ b/android/os/HandlerExecutor.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import android.annotation.NonNull;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * An adapter {@link Executor} that posts all executed tasks onto the given
+ * {@link Handler}.
+ *
+ * @hide
+ */
+public class HandlerExecutor implements Executor {
+ private final Handler mHandler;
+
+ public HandlerExecutor(@NonNull Handler handler) {
+ mHandler = Preconditions.checkNotNull(handler);
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ if (!mHandler.post(command)) {
+ throw new RejectedExecutionException(mHandler + " is shutting down");
+ }
+ }
+}
diff --git a/android/os/HardwarePropertiesManager.java b/android/os/HardwarePropertiesManager.java
index aad202e7..eae7d701 100644
--- a/android/os/HardwarePropertiesManager.java
+++ b/android/os/HardwarePropertiesManager.java
@@ -40,9 +40,11 @@ public class HardwarePropertiesManager {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({
- DEVICE_TEMPERATURE_CPU, DEVICE_TEMPERATURE_GPU, DEVICE_TEMPERATURE_BATTERY,
- DEVICE_TEMPERATURE_SKIN
+ @IntDef(prefix = { "DEVICE_TEMPERATURE_" }, value = {
+ DEVICE_TEMPERATURE_CPU,
+ DEVICE_TEMPERATURE_GPU,
+ DEVICE_TEMPERATURE_BATTERY,
+ DEVICE_TEMPERATURE_SKIN
})
public @interface DeviceTemperatureType {}
@@ -50,9 +52,11 @@ public class HardwarePropertiesManager {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({
- TEMPERATURE_CURRENT, TEMPERATURE_THROTTLING, TEMPERATURE_SHUTDOWN,
- TEMPERATURE_THROTTLING_BELOW_VR_MIN
+ @IntDef(prefix = { "TEMPERATURE_" }, value = {
+ TEMPERATURE_CURRENT,
+ TEMPERATURE_THROTTLING,
+ TEMPERATURE_SHUTDOWN,
+ TEMPERATURE_THROTTLING_BELOW_VR_MIN
})
public @interface TemperatureSource {}
diff --git a/android/os/Message.java b/android/os/Message.java
index d066db1f..b303e10f 100644
--- a/android/os/Message.java
+++ b/android/os/Message.java
@@ -109,7 +109,9 @@ public final class Message implements Parcelable {
// sometimes we store linked lists of these things
/*package*/ Message next;
- private static final Object sPoolSync = new Object();
+
+ /** @hide */
+ public static final Object sPoolSync = new Object();
private static Message sPool;
private static int sPoolSize = 0;
@@ -370,6 +372,12 @@ public final class Message implements Parcelable {
return callback;
}
+ /** @hide */
+ public Message setCallback(Runnable r) {
+ callback = r;
+ return this;
+ }
+
/**
* Obtains a Bundle of arbitrary data associated with this
* event, lazily creating it if necessary. Set this value by calling
@@ -411,6 +419,16 @@ public final class Message implements Parcelable {
}
/**
+ * Chainable setter for {@link #what}
+ *
+ * @hide
+ */
+ public Message setWhat(int what) {
+ this.what = what;
+ return this;
+ }
+
+ /**
* Sends this Message to the Handler specified by {@link #getTarget}.
* Throws a null pointer exception if this field has not been set.
*/
diff --git a/android/os/MessageQueue.java b/android/os/MessageQueue.java
index 624e28a6..96e7a598 100644
--- a/android/os/MessageQueue.java
+++ b/android/os/MessageQueue.java
@@ -871,7 +871,11 @@ public final class MessageQueue {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag=true, value={EVENT_INPUT, EVENT_OUTPUT, EVENT_ERROR})
+ @IntDef(flag = true, prefix = { "EVENT_" }, value = {
+ EVENT_INPUT,
+ EVENT_OUTPUT,
+ EVENT_ERROR
+ })
public @interface Events {}
/**
diff --git a/android/os/PowerManager.java b/android/os/PowerManager.java
index 068f5f7f..cd6d41b3 100644
--- a/android/os/PowerManager.java
+++ b/android/os/PowerManager.java
@@ -466,7 +466,7 @@ public final class PowerManager {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({
+ @IntDef(prefix = { "SHUTDOWN_REASON_" }, value = {
SHUTDOWN_REASON_UNKNOWN,
SHUTDOWN_REASON_SHUTDOWN,
SHUTDOWN_REASON_REBOOT,
@@ -680,6 +680,26 @@ public final class PowerManager {
* as the user moves between applications and doesn't require a special permission.
* </p>
*
+ * <p>
+ * Recommended naming conventions for tags to make debugging easier:
+ * <ul>
+ * <li>use a unique prefix delimited by a colon for your app/library (e.g.
+ * gmail:mytag) to make it easier to understand where the wake locks comes
+ * from. This namespace will also avoid collision for tags inside your app
+ * coming from different libraries which will make debugging easier.
+ * <li>use constants (e.g. do not include timestamps in the tag) to make it
+ * easier for tools to aggregate similar wake locks. When collecting
+ * debugging data, the platform only monitors a finite number of tags,
+ * using constants will help tools to provide better debugging data.
+ * <li>avoid using Class#getName() or similar method since this class name
+ * can be transformed by java optimizer and obfuscator tools.
+ * <li>avoid wrapping the tag or a prefix to avoid collision with wake lock
+ * tags from the platform (e.g. *alarm*).
+ * <li>never include personnally identifiable information for privacy
+ * reasons.
+ * </ul>
+ * </p>
+ *
* @param levelAndFlags Combination of wake lock level and flag values defining
* the requested behavior of the WakeLock.
* @param tag Your class name (or other tag) for debugging purposes.
@@ -1509,11 +1529,18 @@ public final class PowerManager {
* cost of that work can be accounted to the application.
* </p>
*
+ * <p>
+ * Make sure to follow the tag naming convention when using WorkSource
+ * to make it easier for app developers to understand wake locks
+ * attributed to them. See {@link PowerManager#newWakeLock(int, String)}
+ * documentation.
+ * </p>
+ *
* @param ws The work source, or null if none.
*/
public void setWorkSource(WorkSource ws) {
synchronized (mToken) {
- if (ws != null && ws.size() == 0) {
+ if (ws != null && ws.isEmpty()) {
ws = null;
}
@@ -1525,7 +1552,7 @@ public final class PowerManager {
changed = true;
mWorkSource = new WorkSource(ws);
} else {
- changed = mWorkSource.diff(ws);
+ changed = !mWorkSource.equals(ws);
if (changed) {
mWorkSource.set(ws);
}
diff --git a/android/os/PowerManagerInternal.java b/android/os/PowerManagerInternal.java
index 77ac2651..3ef0961f 100644
--- a/android/os/PowerManagerInternal.java
+++ b/android/os/PowerManagerInternal.java
@@ -110,7 +110,7 @@ public abstract class PowerManagerInternal {
*
* This method must only be called by the device administration policy manager.
*/
- public abstract void setMaximumScreenOffTimeoutFromDeviceAdmin(int timeMs);
+ public abstract void setMaximumScreenOffTimeoutFromDeviceAdmin(int userId, long timeMs);
/**
* Used by the dream manager to override certain properties while dozing.
diff --git a/android/os/RemoteCallbackList.java b/android/os/RemoteCallbackList.java
index b9b9a18e..bbb8a7b5 100644
--- a/android/os/RemoteCallbackList.java
+++ b/android/os/RemoteCallbackList.java
@@ -334,6 +334,23 @@ public class RemoteCallbackList<E extends IInterface> {
}
/**
+ * Performs {@code action} for each cookie associated with a callback, calling
+ * {@link #beginBroadcast()}/{@link #finishBroadcast()} before/after looping
+ *
+ * @hide
+ */
+ public <C> void broadcastForEachCookie(Consumer<C> action) {
+ int itemCount = beginBroadcast();
+ try {
+ for (int i = 0; i < itemCount; i++) {
+ action.accept((C) getBroadcastCookie(i));
+ }
+ } finally {
+ finishBroadcast();
+ }
+ }
+
+ /**
* Returns the number of registered callbacks. Note that the number of registered
* callbacks may differ from the value returned by {@link #beginBroadcast()} since
* the former returns the number of callbacks registered at the time of the call
diff --git a/android/os/ServiceManager.java b/android/os/ServiceManager.java
index 42ec315c..34c78455 100644
--- a/android/os/ServiceManager.java
+++ b/android/os/ServiceManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2007 The Android Open Source Project
+ * Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,29 +16,9 @@
package android.os;
-import android.util.Log;
-
-import com.android.internal.os.BinderInternal;
-
-import java.util.HashMap;
import java.util.Map;
-/** @hide */
public final class ServiceManager {
- private static final String TAG = "ServiceManager";
- private static IServiceManager sServiceManager;
- private static HashMap<String, IBinder> sCache = new HashMap<String, IBinder>();
-
- private static IServiceManager getIServiceManager() {
- if (sServiceManager != null) {
- return sServiceManager;
- }
-
- // Find the service manager
- sServiceManager = ServiceManagerNative
- .asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));
- return sServiceManager;
- }
/**
* Returns a reference to a service with the given name.
@@ -47,32 +27,14 @@ public final class ServiceManager {
* @return a reference to the service, or <code>null</code> if the service doesn't exist
*/
public static IBinder getService(String name) {
- try {
- IBinder service = sCache.get(name);
- if (service != null) {
- return service;
- } else {
- return Binder.allowBlocking(getIServiceManager().getService(name));
- }
- } catch (RemoteException e) {
- Log.e(TAG, "error in getService", e);
- }
return null;
}
/**
- * Returns a reference to a service with the given name, or throws
- * {@link NullPointerException} if none is found.
- *
- * @hide
+ * Is not supposed to return null, but that is fine for layoutlib.
*/
public static IBinder getServiceOrThrow(String name) throws ServiceNotFoundException {
- final IBinder binder = getService(name);
- if (binder != null) {
- return binder;
- } else {
- throw new ServiceNotFoundException(name);
- }
+ throw new ServiceNotFoundException(name);
}
/**
@@ -83,39 +45,7 @@ public final class ServiceManager {
* @param service the service object
*/
public static void addService(String name, IBinder service) {
- addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_NORMAL);
- }
-
- /**
- * Place a new @a service called @a name into the service
- * manager.
- *
- * @param name the name of the new service
- * @param service the service object
- * @param allowIsolated set to true to allow isolated sandboxed processes
- * to access this service
- */
- public static void addService(String name, IBinder service, boolean allowIsolated) {
- addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_NORMAL);
- }
-
- /**
- * Place a new @a service called @a name into the service
- * manager.
- *
- * @param name the name of the new service
- * @param service the service object
- * @param allowIsolated set to true to allow isolated sandboxed processes
- * @param dumpPriority supported dump priority levels as a bitmask
- * to access this service
- */
- public static void addService(String name, IBinder service, boolean allowIsolated,
- int dumpPriority) {
- try {
- getIServiceManager().addService(name, service, allowIsolated, dumpPriority);
- } catch (RemoteException e) {
- Log.e(TAG, "error in addService", e);
- }
+ // pass
}
/**
@@ -123,17 +53,7 @@ public final class ServiceManager {
* service manager. Non-blocking.
*/
public static IBinder checkService(String name) {
- try {
- IBinder service = sCache.get(name);
- if (service != null) {
- return service;
- } else {
- return Binder.allowBlocking(getIServiceManager().checkService(name));
- }
- } catch (RemoteException e) {
- Log.e(TAG, "error in checkService", e);
- return null;
- }
+ return null;
}
/**
@@ -142,12 +62,9 @@ public final class ServiceManager {
* case of an exception
*/
public static String[] listServices() {
- try {
- return getIServiceManager().listServices(IServiceManager.DUMP_FLAG_PRIORITY_ALL);
- } catch (RemoteException e) {
- Log.e(TAG, "error in listServices", e);
- return null;
- }
+ // actual implementation returns null sometimes, so it's ok
+ // to return null instead of an empty list.
+ return null;
}
/**
@@ -159,10 +76,7 @@ public final class ServiceManager {
* @hide
*/
public static void initServiceCache(Map<String, IBinder> cache) {
- if (sCache.size() != 0) {
- throw new IllegalStateException("setServiceCache may only be called once");
- }
- sCache.putAll(cache);
+ // pass
}
/**
@@ -173,6 +87,7 @@ public final class ServiceManager {
* @hide
*/
public static class ServiceNotFoundException extends Exception {
+ // identical to the original implementation
public ServiceNotFoundException(String name) {
super("No service published for: " + name);
}
diff --git a/android/os/StatsLogEventWrapper.java b/android/os/StatsLogEventWrapper.java
index 9491bec6..3e8161f2 100644
--- a/android/os/StatsLogEventWrapper.java
+++ b/android/os/StatsLogEventWrapper.java
@@ -33,6 +33,8 @@ public final class StatsLogEventWrapper implements Parcelable {
private static final int EVENT_TYPE_LIST = 3;
private static final int EVENT_TYPE_FLOAT = 4;
+ // Keep this in sync with system/core/logcat/event.logtags
+ private static final int STATS_BUFFER_TAG_ID = 1937006964;
/**
* Creates a log_event that is binary-encoded as implemented in
* system/core/liblog/log_event_list.c; this allows us to use the same parsing logic in statsd
@@ -46,9 +48,14 @@ public final class StatsLogEventWrapper implements Parcelable {
*/
public StatsLogEventWrapper(int tag, int fields) {
// Write four bytes from tag, starting with least-significant bit.
- write4Bytes(tag);
+ // For pulled data, this tag number is not really used. We use the same tag number as
+ // pushed ones to be consistent.
+ write4Bytes(STATS_BUFFER_TAG_ID);
mStorage.write(EVENT_TYPE_LIST); // This is required to start the log entry.
- mStorage.write(fields); // Indicate number of elements in this list.
+ mStorage.write(fields + 1); // Indicate number of elements in this list. +1 for the tag
+ mStorage.write(EVENT_TYPE_INT);
+ // The first element is the real atom tag number
+ write4Bytes(tag);
}
/**
diff --git a/android/os/UserManager.java b/android/os/UserManager.java
index 22967af7..dd9fd93e 100644
--- a/android/os/UserManager.java
+++ b/android/os/UserManager.java
@@ -103,8 +103,12 @@ public class UserManager {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag=true, value={RESTRICTION_NOT_SET, RESTRICTION_SOURCE_SYSTEM,
- RESTRICTION_SOURCE_DEVICE_OWNER, RESTRICTION_SOURCE_PROFILE_OWNER})
+ @IntDef(flag = true, prefix = { "RESTRICTION_" }, value = {
+ RESTRICTION_NOT_SET,
+ RESTRICTION_SOURCE_SYSTEM,
+ RESTRICTION_SOURCE_DEVICE_OWNER,
+ RESTRICTION_SOURCE_PROFILE_OWNER
+ })
@SystemApi
public @interface UserRestrictionSource {}
@@ -190,6 +194,21 @@ public class UserManager {
public static final String DISALLOW_SHARE_LOCATION = "no_share_location";
/**
+ * Specifies if airplane mode is disallowed on the device.
+ *
+ * <p> This restriction can only be set by the device owner and the profile owner on the
+ * primary user and it applies globally - i.e. it disables airplane mode on the entire device.
+ * <p>The default value is <code>false</code>.
+ *
+ * <p>Key for user restrictions.
+ * <p>Type: Boolean
+ * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
+ * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
+ * @see #getUserRestrictions()
+ */
+ public static final String DISALLOW_AIRPLANE_MODE = "no_airplane_mode";
+
+ /**
* Specifies if a user is disallowed from enabling the
* "Unknown Sources" setting, that allows installation of apps from unknown sources.
* The default value is <code>false</code>.
@@ -331,6 +350,28 @@ public class UserManager {
public static final String DISALLOW_CONFIG_VPN = "no_config_vpn";
/**
+ * Specifies if a user is disallowed from configuring location mode. Device owner and profile
+ * owners can set this restriction and it only applies on the managed user.
+ *
+ * <p>In a managed profile, location sharing is forced off when it's off on primary user, so
+ * user can still turn off location sharing on managed profile when the restriction is set by
+ * profile owner on managed profile.
+ *
+ * <p>This user restriction is different from {@link #DISALLOW_SHARE_LOCATION},
+ * as the device owner or profile owner can still enable or disable location mode via
+ * {@link DevicePolicyManager#setSecureSetting} when this restriction is on.
+ *
+ * <p>The default value is <code>false</code>.
+ *
+ * <p>Key for user restrictions.
+ * <p>Type: Boolean
+ * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
+ * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
+ * @see #getUserRestrictions()
+ */
+ public static final String DISALLOW_CONFIG_LOCATION_MODE = "no_config_location_mode";
+
+ /**
* Specifies if date, time and timezone configuring is disallowed.
*
* <p>When restriction is set by device owners, it applies globally - i.e., it disables date,
@@ -770,6 +811,25 @@ public class UserManager {
public static final String DISALLOW_OEM_UNLOCK = "no_oem_unlock";
/**
+ * Specifies that the managed profile is not allowed to have unified lock screen challenge with
+ * the primary user.
+ *
+ * <p><strong>Note:</strong> Setting this restriction alone doesn't automatically set a
+ * separate challenge. Profile owner can ask the user to set a new password using
+ * {@link DevicePolicyManager#ACTION_SET_NEW_PASSWORD} and verify it using
+ * {@link DevicePolicyManager#isUsingUnifiedPassword(ComponentName)}.
+ *
+ * <p>Can be set by profile owners. It only has effect on managed profiles when set by managed
+ * profile owner. Has no effect on non-managed profiles or users.
+ * <p>Key for user restrictions.
+ * <p>Type: Boolean
+ * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
+ * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
+ * @see #getUserRestrictions()
+ */
+ public static final String DISALLOW_UNIFIED_PASSWORD = "no_unified_password";
+
+ /**
* Allows apps in the parent profile to handle web links from the managed profile.
*
* This user restriction has an effect only in a managed profile.
@@ -2107,15 +2167,46 @@ public class UserManager {
}
/**
- * Set quiet mode of a managed profile.
+ * Enables or disables quiet mode for a managed profile. If quiet mode is enabled, apps in a
+ * managed profile don't run, generate notifications, or consume data or battery.
+ * <p>
+ * If a user's credential is needed to turn off quiet mode, a confirm credential screen will be
+ * shown to the user.
+ * <p>
+ * The change may not happen instantly, however apps can listen for
+ * {@link Intent#ACTION_MANAGED_PROFILE_AVAILABLE} and
+ * {@link Intent#ACTION_MANAGED_PROFILE_UNAVAILABLE} broadcasts in order to be notified of
+ * the change of the quiet mode. Apps can also check the current state of quiet mode by
+ * calling {@link #isQuietModeEnabled(UserHandle)}.
+ * <p>
+ * The caller must either be the foreground default launcher or have one of these permissions:
+ * {@code MANAGE_USERS} or {@code MODIFY_QUIET_MODE}.
*
- * @param userHandle The user handle of the profile.
- * @param enableQuietMode Whether quiet mode should be enabled or disabled.
+ * @param enableQuietMode whether quiet mode should be enabled or disabled
+ * @param userHandle user handle of the profile
+ * @return {@code false} if user's credential is needed in order to turn off quiet mode,
+ * {@code true} otherwise
+ * @throws SecurityException if the caller is invalid
+ * @throws IllegalArgumentException if {@code userHandle} is not a managed profile
+ *
+ * @see #isQuietModeEnabled(UserHandle)
+ */
+ public boolean trySetQuietModeEnabled(boolean enableQuietMode, @NonNull UserHandle userHandle) {
+ return trySetQuietModeEnabled(enableQuietMode, userHandle, null);
+ }
+
+ /**
+ * Similar to {@link #trySetQuietModeEnabled(boolean, UserHandle)}, except you can specify
+ * a target to start when user is unlocked.
+ *
+ * @see {@link #trySetQuietModeEnabled(boolean, UserHandle)}
* @hide
*/
- public void setQuietModeEnabled(@UserIdInt int userHandle, boolean enableQuietMode) {
+ public boolean trySetQuietModeEnabled(
+ boolean enableQuietMode, @NonNull UserHandle userHandle, IntentSender target) {
try {
- mService.setQuietModeEnabled(userHandle, enableQuietMode);
+ return mService.trySetQuietModeEnabled(
+ mContext.getPackageName(), enableQuietMode, userHandle.getIdentifier(), target);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
@@ -2137,23 +2228,6 @@ public class UserManager {
}
/**
- * Tries disabling quiet mode for a given user. If the user is still locked, we unlock the user
- * first by showing the confirm credentials screen and disable quiet mode upon successful
- * unlocking. If the user is already unlocked, we call through to {@link #setQuietModeEnabled}
- * directly.
- *
- * @return true if the quiet mode was disabled immediately
- * @hide
- */
- public boolean trySetQuietModeDisabled(@UserIdInt int userHandle, IntentSender target) {
- try {
- return mService.trySetQuietModeDisabled(userHandle, target);
- } catch (RemoteException re) {
- throw re.rethrowFromSystemServer();
- }
- }
-
- /**
* If the target user is a managed profile of the calling user or the caller
* is itself a managed profile, then this returns a badged copy of the given
* icon to be able to distinguish it from the original icon. For badging an
diff --git a/android/os/UserManagerInternal.java b/android/os/UserManagerInternal.java
index 9369eebf..6c9f1b25 100644
--- a/android/os/UserManagerInternal.java
+++ b/android/os/UserManagerInternal.java
@@ -130,7 +130,8 @@ public abstract class UserManagerInternal {
* <p>Called by the {@link com.android.server.devicepolicy.DevicePolicyManagerService} when
* createAndManageUser is called by the device owner.
*/
- public abstract UserInfo createUserEvenWhenDisallowed(String name, int flags);
+ public abstract UserInfo createUserEvenWhenDisallowed(String name, int flags,
+ String[] disallowedPackages);
/**
* Same as {@link UserManager#removeUser(int userHandle)}, but bypasses the check for
diff --git a/android/os/VintfObject.java b/android/os/VintfObject.java
index 65b33e59..340f3fb8 100644
--- a/android/os/VintfObject.java
+++ b/android/os/VintfObject.java
@@ -18,7 +18,6 @@ package android.os;
import java.util.Map;
-import android.util.Log;
/**
* Java API for libvintf.
@@ -40,7 +39,7 @@ public class VintfObject {
* Verify that the given metadata for an OTA package is compatible with
* this device.
*
- * @param packageInfo a list of serialized form of HalMaanifest's /
+ * @param packageInfo a list of serialized form of HalManifest's /
* CompatibilityMatri'ces (XML).
* @return = 0 if success (compatible)
* > 0 if incompatible
@@ -48,6 +47,17 @@ public class VintfObject {
*/
public static native int verify(String[] packageInfo);
+ /**
+ * Verify Vintf compatibility on the device without checking AVB
+ * (Android Verified Boot). It is useful to verify a running system
+ * image where AVB check is irrelevant.
+ *
+ * @return = 0 if success (compatible)
+ * > 0 if incompatible
+ * < 0 if any error (mount partition fails, illformed XML, etc.)
+ */
+ public static native int verifyWithoutAvb();
+
/// ---------- CTS Device Info
/**
diff --git a/android/os/WorkSource.java b/android/os/WorkSource.java
index ecec448a..401b4a36 100644
--- a/android/os/WorkSource.java
+++ b/android/os/WorkSource.java
@@ -1,10 +1,13 @@
package android.os;
+import android.annotation.Nullable;
import android.os.WorkSourceProto;
import android.util.Log;
import android.util.proto.ProtoOutputStream;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Objects;
/**
* Describes the source of some work that may be done by someone else.
@@ -19,6 +22,8 @@ public class WorkSource implements Parcelable {
int[] mUids;
String[] mNames;
+ private ArrayList<WorkChain> mChains;
+
/**
* Internal statics to avoid object allocations in some operations.
* The WorkSource object itself is not thread safe, but we need to
@@ -39,6 +44,7 @@ public class WorkSource implements Parcelable {
*/
public WorkSource() {
mNum = 0;
+ mChains = null;
}
/**
@@ -48,6 +54,7 @@ public class WorkSource implements Parcelable {
public WorkSource(WorkSource orig) {
if (orig == null) {
mNum = 0;
+ mChains = null;
return;
}
mNum = orig.mNum;
@@ -58,6 +65,16 @@ public class WorkSource implements Parcelable {
mUids = null;
mNames = null;
}
+
+ if (orig.mChains != null) {
+ // Make a copy of all WorkChains that exist on |orig| since they are mutable.
+ mChains = new ArrayList<>(orig.mChains.size());
+ for (WorkChain chain : orig.mChains) {
+ mChains.add(new WorkChain(chain));
+ }
+ } else {
+ mChains = null;
+ }
}
/** @hide */
@@ -65,6 +82,7 @@ public class WorkSource implements Parcelable {
mNum = 1;
mUids = new int[] { uid, 0 };
mNames = null;
+ mChains = null;
}
/** @hide */
@@ -75,12 +93,21 @@ public class WorkSource implements Parcelable {
mNum = 1;
mUids = new int[] { uid, 0 };
mNames = new String[] { name, null };
+ mChains = null;
}
WorkSource(Parcel in) {
mNum = in.readInt();
mUids = in.createIntArray();
mNames = in.createStringArray();
+
+ int numChains = in.readInt();
+ if (numChains > 0) {
+ mChains = new ArrayList<>(numChains);
+ in.readParcelableList(mChains, WorkChain.class.getClassLoader());
+ } else {
+ mChains = null;
+ }
}
/** @hide */
@@ -99,7 +126,8 @@ public class WorkSource implements Parcelable {
}
/**
- * Clear names from this WorkSource. Uids are left intact.
+ * Clear names from this WorkSource. Uids are left intact. WorkChains if any, are left
+ * intact.
*
* <p>Useful when combining with another WorkSource that doesn't have names.
* @hide
@@ -127,11 +155,16 @@ public class WorkSource implements Parcelable {
*/
public void clear() {
mNum = 0;
+ if (mChains != null) {
+ mChains.clear();
+ }
}
@Override
public boolean equals(Object o) {
- return o instanceof WorkSource && !diff((WorkSource)o);
+ return o instanceof WorkSource
+ && !diff((WorkSource) o)
+ && Objects.equals(mChains, ((WorkSource) o).mChains);
}
@Override
@@ -145,6 +178,11 @@ public class WorkSource implements Parcelable {
result = ((result << 4) | (result >>> 28)) ^ mNames[i].hashCode();
}
}
+
+ if (mChains != null) {
+ result = ((result << 4) | (result >>> 28)) ^ mChains.hashCode();
+ }
+
return result;
}
@@ -153,6 +191,8 @@ public class WorkSource implements Parcelable {
* @param other The WorkSource to compare against.
* @return If there is a difference, true is returned.
*/
+ // TODO: This is a public API so it cannot be renamed. Because it is used in several places,
+ // we keep its semantics the same and ignore any differences in WorkChains (if any).
public boolean diff(WorkSource other) {
int N = mNum;
if (N != other.mNum) {
@@ -175,12 +215,15 @@ public class WorkSource implements Parcelable {
/**
* Replace the current contents of this work source with the given
- * work source. If <var>other</var> is null, the current work source
+ * work source. If {@code other} is null, the current work source
* will be made empty.
*/
public void set(WorkSource other) {
if (other == null) {
mNum = 0;
+ if (mChains != null) {
+ mChains.clear();
+ }
return;
}
mNum = other.mNum;
@@ -203,6 +246,18 @@ public class WorkSource implements Parcelable {
mUids = null;
mNames = null;
}
+
+ if (other.mChains != null) {
+ if (mChains != null) {
+ mChains.clear();
+ } else {
+ mChains = new ArrayList<>(other.mChains.size());
+ }
+
+ for (WorkChain chain : other.mChains) {
+ mChains.add(new WorkChain(chain));
+ }
+ }
}
/** @hide */
@@ -211,6 +266,9 @@ public class WorkSource implements Parcelable {
if (mUids == null) mUids = new int[2];
mUids[0] = uid;
mNames = null;
+ if (mChains != null) {
+ mChains.clear();
+ }
}
/** @hide */
@@ -225,9 +283,23 @@ public class WorkSource implements Parcelable {
}
mUids[0] = uid;
mNames[0] = name;
+ if (mChains != null) {
+ mChains.clear();
+ }
}
- /** @hide */
+ /**
+ * Legacy API, DO NOT USE: Only deals with flat UIDs and tags. No chains are transferred, and no
+ * differences in chains are returned. This will be removed once its callers have been
+ * rewritten.
+ *
+ * NOTE: This is currently only used in GnssLocationProvider.
+ *
+ * @hide
+ * @deprecated for internal use only. WorkSources are opaque and no external callers should need
+ * to be aware of internal differences.
+ */
+ @Deprecated
public WorkSource[] setReturningDiffs(WorkSource other) {
synchronized (sTmpWorkSource) {
sNewbWork = null;
@@ -251,11 +323,34 @@ public class WorkSource implements Parcelable {
*/
public boolean add(WorkSource other) {
synchronized (sTmpWorkSource) {
- return updateLocked(other, false, false);
+ boolean uidAdded = updateLocked(other, false, false);
+
+ boolean chainAdded = false;
+ if (other.mChains != null) {
+ // NOTE: This is quite an expensive operation, especially if the number of chains
+ // is large. We could look into optimizing it if it proves problematic.
+ if (mChains == null) {
+ mChains = new ArrayList<>(other.mChains.size());
+ }
+
+ for (WorkChain wc : other.mChains) {
+ if (!mChains.contains(wc)) {
+ mChains.add(new WorkChain(wc));
+ }
+ }
+ }
+
+ return uidAdded || chainAdded;
}
}
- /** @hide */
+ /**
+ * Legacy API: DO NOT USE. Only in use from unit tests.
+ *
+ * @hide
+ * @deprecated meant for unit testing use only. Will be removed in a future API revision.
+ */
+ @Deprecated
public WorkSource addReturningNewbs(WorkSource other) {
synchronized (sTmpWorkSource) {
sNewbWork = null;
@@ -311,22 +406,14 @@ public class WorkSource implements Parcelable {
return true;
}
- /** @hide */
- public WorkSource addReturningNewbs(int uid) {
- synchronized (sTmpWorkSource) {
- sNewbWork = null;
- sTmpWorkSource.mUids[0] = uid;
- updateLocked(sTmpWorkSource, false, true);
- return sNewbWork;
- }
- }
-
public boolean remove(WorkSource other) {
if (mNum <= 0 || other.mNum <= 0) {
return false;
}
+
+ boolean uidRemoved = false;
if (mNames == null && other.mNames == null) {
- return removeUids(other);
+ uidRemoved = removeUids(other);
} else {
if (mNames == null) {
throw new IllegalArgumentException("Other " + other + " has names, but target "
@@ -336,24 +423,54 @@ public class WorkSource implements Parcelable {
throw new IllegalArgumentException("Target " + this + " has names, but other "
+ other + " does not");
}
- return removeUidsAndNames(other);
+ uidRemoved = removeUidsAndNames(other);
}
- }
- /** @hide */
- public WorkSource stripNames() {
- if (mNum <= 0) {
- return new WorkSource();
- }
- WorkSource result = new WorkSource();
- int lastUid = -1;
- for (int i=0; i<mNum; i++) {
- int uid = mUids[i];
- if (i == 0 || lastUid != uid) {
- result.add(uid);
+ boolean chainRemoved = false;
+ if (other.mChains != null) {
+ if (mChains != null) {
+ chainRemoved = mChains.removeAll(other.mChains);
}
+ } else if (mChains != null) {
+ mChains.clear();
+ chainRemoved = true;
}
- return result;
+
+ return uidRemoved || chainRemoved;
+ }
+
+ /**
+ * Create a new {@code WorkChain} associated with this WorkSource and return it.
+ *
+ * @hide
+ */
+ public WorkChain createWorkChain() {
+ if (mChains == null) {
+ mChains = new ArrayList<>(4);
+ }
+
+ final WorkChain wc = new WorkChain();
+ mChains.add(wc);
+
+ return wc;
+ }
+
+ /**
+ * Returns {@code true} iff. this work source contains zero UIDs and zero WorkChains to
+ * attribute usage to.
+ *
+ * @hide for internal use only.
+ */
+ public boolean isEmpty() {
+ return mNum == 0 && (mChains == null || mChains.isEmpty());
+ }
+
+ /**
+ * @return the list of {@code WorkChains} associated with this {@code WorkSource}.
+ * @hide
+ */
+ public ArrayList<WorkChain> getWorkChains() {
+ return mChains;
}
private boolean removeUids(WorkSource other) {
@@ -664,6 +781,224 @@ public class WorkSource implements Parcelable {
}
}
+ /**
+ * Represents an attribution chain for an item of work being performed. An attribution chain is
+ * an indexed list of {code (uid, tag)} nodes. The node at {@code index == 0} is the initiator
+ * of the work, and the node at the highest index performs the work. Nodes at other indices
+ * are intermediaries that facilitate the work. Examples :
+ *
+ * (1) Work being performed by uid=2456 (no chaining):
+ * <pre>
+ * WorkChain {
+ * mUids = { 2456 }
+ * mTags = { null }
+ * mSize = 1;
+ * }
+ * </pre>
+ *
+ * (2) Work being performed by uid=2456 (from component "c1") on behalf of uid=5678:
+ *
+ * <pre>
+ * WorkChain {
+ * mUids = { 5678, 2456 }
+ * mTags = { null, "c1" }
+ * mSize = 1
+ * }
+ * </pre>
+ *
+ * Attribution chains are mutable, though the only operation that can be performed on them
+ * is the addition of a new node at the end of the attribution chain to represent
+ *
+ * @hide
+ */
+ public static class WorkChain implements Parcelable {
+ private int mSize;
+ private int[] mUids;
+ private String[] mTags;
+
+ // @VisibleForTesting
+ public WorkChain() {
+ mSize = 0;
+ mUids = new int[4];
+ mTags = new String[4];
+ }
+
+ // @VisibleForTesting
+ public WorkChain(WorkChain other) {
+ mSize = other.mSize;
+ mUids = other.mUids.clone();
+ mTags = other.mTags.clone();
+ }
+
+ private WorkChain(Parcel in) {
+ mSize = in.readInt();
+ mUids = in.createIntArray();
+ mTags = in.createStringArray();
+ }
+
+ /**
+ * Append a node whose uid is {@code uid} and whose optional tag is {@code tag} to this
+ * {@code WorkChain}.
+ */
+ public WorkChain addNode(int uid, @Nullable String tag) {
+ if (mSize == mUids.length) {
+ resizeArrays();
+ }
+
+ mUids[mSize] = uid;
+ mTags[mSize] = tag;
+ mSize++;
+
+ return this;
+ }
+
+ /**
+ * Return the UID to which this WorkChain should be attributed to, i.e, the UID that
+ * initiated the work and not the UID performing it.
+ */
+ public int getAttributionUid() {
+ return mUids[0];
+ }
+
+ // TODO: The following three trivial getters are purely for testing and will be removed
+ // once we have higher level logic in place, e.g for serializing this WorkChain to a proto,
+ // diffing it etc.
+ //
+ // @VisibleForTesting
+ public int[] getUids() {
+ return mUids;
+ }
+ // @VisibleForTesting
+ public String[] getTags() {
+ return mTags;
+ }
+ // @VisibleForTesting
+ public int getSize() {
+ return mSize;
+ }
+
+ private void resizeArrays() {
+ final int newSize = mSize * 2;
+ int[] uids = new int[newSize];
+ String[] tags = new String[newSize];
+
+ System.arraycopy(mUids, 0, uids, 0, mSize);
+ System.arraycopy(mTags, 0, tags, 0, mSize);
+
+ mUids = uids;
+ mTags = tags;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder("WorkChain{");
+ for (int i = 0; i < mSize; ++i) {
+ if (i != 0) {
+ result.append(", ");
+ }
+ result.append("(");
+ result.append(mUids[i]);
+ if (mTags[i] != null) {
+ result.append(", ");
+ result.append(mTags[i]);
+ }
+ result.append(")");
+ }
+
+ result.append("}");
+ return result.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return (mSize + 31 * Arrays.hashCode(mUids)) * 31 + Arrays.hashCode(mTags);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof WorkChain) {
+ WorkChain other = (WorkChain) o;
+
+ return mSize == other.mSize
+ && Arrays.equals(mUids, other.mUids)
+ && Arrays.equals(mTags, other.mTags);
+ }
+
+ return false;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mSize);
+ dest.writeIntArray(mUids);
+ dest.writeStringArray(mTags);
+ }
+
+ public static final Parcelable.Creator<WorkChain> CREATOR =
+ new Parcelable.Creator<WorkChain>() {
+ public WorkChain createFromParcel(Parcel in) {
+ return new WorkChain(in);
+ }
+ public WorkChain[] newArray(int size) {
+ return new WorkChain[size];
+ }
+ };
+ }
+
+ /**
+ * Computes the differences in WorkChains contained between {@code oldWs} and {@code newWs}.
+ *
+ * Returns {@code null} if no differences exist, otherwise returns a two element array. The
+ * first element is a list of "new" chains, i.e WorkChains present in {@code newWs} but not in
+ * {@code oldWs}. The second element is a list of "gone" chains, i.e WorkChains present in
+ * {@code oldWs} but not in {@code newWs}.
+ *
+ * @hide
+ */
+ public static ArrayList<WorkChain>[] diffChains(WorkSource oldWs, WorkSource newWs) {
+ ArrayList<WorkChain> newChains = null;
+ ArrayList<WorkChain> goneChains = null;
+
+ // TODO(narayan): This is a dumb O(M*N) algorithm that determines what has changed across
+ // WorkSource objects. We can replace this with something smarter, for e.g by defining
+ // a Comparator between WorkChains. It's unclear whether that will be more efficient if
+ // the number of chains associated with a WorkSource is expected to be small
+ if (oldWs.mChains != null) {
+ for (int i = 0; i < oldWs.mChains.size(); ++i) {
+ final WorkChain wc = oldWs.mChains.get(i);
+ if (newWs.mChains == null || !newWs.mChains.contains(wc)) {
+ if (goneChains == null) {
+ goneChains = new ArrayList<>(oldWs.mChains.size());
+ }
+ goneChains.add(wc);
+ }
+ }
+ }
+
+ if (newWs.mChains != null) {
+ for (int i = 0; i < newWs.mChains.size(); ++i) {
+ final WorkChain wc = newWs.mChains.get(i);
+ if (oldWs.mChains == null || !oldWs.mChains.contains(wc)) {
+ if (newChains == null) {
+ newChains = new ArrayList<>(newWs.mChains.size());
+ }
+ newChains.add(wc);
+ }
+ }
+ }
+
+ if (newChains != null || goneChains != null) {
+ return new ArrayList[] { newChains, goneChains };
+ }
+
+ return null;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -674,6 +1009,13 @@ public class WorkSource implements Parcelable {
dest.writeInt(mNum);
dest.writeIntArray(mUids);
dest.writeStringArray(mNames);
+
+ if (mChains == null) {
+ dest.writeInt(-1);
+ } else {
+ dest.writeInt(mChains.size());
+ dest.writeParcelableList(mChains, flags);
+ }
}
@Override
@@ -690,6 +1032,17 @@ public class WorkSource implements Parcelable {
result.append(mNames[i]);
}
}
+
+ if (mChains != null) {
+ result.append(" chains=");
+ for (int i = 0; i < mChains.size(); ++i) {
+ if (i != 0) {
+ result.append(", ");
+ }
+ result.append(mChains.get(i));
+ }
+ }
+
result.append("}");
return result.toString();
}
@@ -705,6 +1058,25 @@ public class WorkSource implements Parcelable {
}
proto.end(contentProto);
}
+
+ if (mChains != null) {
+ for (int i = 0; i < mChains.size(); i++) {
+ final WorkChain wc = mChains.get(i);
+ final long workChain = proto.start(WorkSourceProto.WORK_CHAINS);
+
+ final String[] tags = wc.getTags();
+ final int[] uids = wc.getUids();
+ for (int j = 0; j < tags.length; j++) {
+ final long contentProto = proto.start(WorkSourceProto.WORK_SOURCE_CONTENTS);
+ proto.write(WorkSourceProto.WorkSourceContentProto.UID, uids[j]);
+ proto.write(WorkSourceProto.WorkSourceContentProto.NAME, tags[j]);
+ proto.end(contentProto);
+ }
+
+ proto.end(workChain);
+ }
+ }
+
proto.end(workSourceToken);
}
diff --git a/android/os/connectivity/CellularBatteryStats.java b/android/os/connectivity/CellularBatteryStats.java
new file mode 100644
index 00000000..2593c85c
--- /dev/null
+++ b/android/os/connectivity/CellularBatteryStats.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.os.connectivity;
+
+import android.os.BatteryStats;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import android.telephony.ModemActivityInfo;
+import android.telephony.SignalStrength;
+
+import java.util.Arrays;
+
+/**
+ * API for Cellular power stats
+ *
+ * @hide
+ */
+public final class CellularBatteryStats implements Parcelable {
+
+ private long mLoggingDurationMs;
+ private long mKernelActiveTimeMs;
+ private long mNumPacketsTx;
+ private long mNumBytesTx;
+ private long mNumPacketsRx;
+ private long mNumBytesRx;
+ private long mSleepTimeMs;
+ private long mIdleTimeMs;
+ private long mRxTimeMs;
+ private long mEnergyConsumedMaMs;
+ private long[] mTimeInRatMs;
+ private long[] mTimeInRxSignalStrengthLevelMs;
+ private long[] mTxTimeMs;
+
+ public static final Parcelable.Creator<CellularBatteryStats> CREATOR = new
+ Parcelable.Creator<CellularBatteryStats>() {
+ public CellularBatteryStats createFromParcel(Parcel in) {
+ return new CellularBatteryStats(in);
+ }
+
+ public CellularBatteryStats[] newArray(int size) {
+ return new CellularBatteryStats[size];
+ }
+ };
+
+ public CellularBatteryStats() {
+ initialize();
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeLong(mLoggingDurationMs);
+ out.writeLong(mKernelActiveTimeMs);
+ out.writeLong(mNumPacketsTx);
+ out.writeLong(mNumBytesTx);
+ out.writeLong(mNumPacketsRx);
+ out.writeLong(mNumBytesRx);
+ out.writeLong(mSleepTimeMs);
+ out.writeLong(mIdleTimeMs);
+ out.writeLong(mRxTimeMs);
+ out.writeLong(mEnergyConsumedMaMs);
+ out.writeLongArray(mTimeInRatMs);
+ out.writeLongArray(mTimeInRxSignalStrengthLevelMs);
+ out.writeLongArray(mTxTimeMs);
+ }
+
+ public void readFromParcel(Parcel in) {
+ mLoggingDurationMs = in.readLong();
+ mKernelActiveTimeMs = in.readLong();
+ mNumPacketsTx = in.readLong();
+ mNumBytesTx = in.readLong();
+ mNumPacketsRx = in.readLong();
+ mNumBytesRx = in.readLong();
+ mSleepTimeMs = in.readLong();
+ mIdleTimeMs = in.readLong();
+ mRxTimeMs = in.readLong();
+ mEnergyConsumedMaMs = in.readLong();
+ in.readLongArray(mTimeInRatMs);
+ in.readLongArray(mTimeInRxSignalStrengthLevelMs);
+ in.readLongArray(mTxTimeMs);
+ }
+
+ public long getLoggingDurationMs() {
+ return mLoggingDurationMs;
+ }
+
+ public long getKernelActiveTimeMs() {
+ return mKernelActiveTimeMs;
+ }
+
+ public long getNumPacketsTx() {
+ return mNumPacketsTx;
+ }
+
+ public long getNumBytesTx() {
+ return mNumBytesTx;
+ }
+
+ public long getNumPacketsRx() {
+ return mNumPacketsRx;
+ }
+
+ public long getNumBytesRx() {
+ return mNumBytesRx;
+ }
+
+ public long getSleepTimeMs() {
+ return mSleepTimeMs;
+ }
+
+ public long getIdleTimeMs() {
+ return mIdleTimeMs;
+ }
+
+ public long getRxTimeMs() {
+ return mRxTimeMs;
+ }
+
+ public long getEnergyConsumedMaMs() {
+ return mEnergyConsumedMaMs;
+ }
+
+ public long[] getTimeInRatMs() {
+ return mTimeInRatMs;
+ }
+
+ public long[] getTimeInRxSignalStrengthLevelMs() {
+ return mTimeInRxSignalStrengthLevelMs;
+ }
+
+ public long[] getTxTimeMs() {
+ return mTxTimeMs;
+ }
+
+ public void setLoggingDurationMs(long t) {
+ mLoggingDurationMs = t;
+ return;
+ }
+
+ public void setKernelActiveTimeMs(long t) {
+ mKernelActiveTimeMs = t;
+ return;
+ }
+
+ public void setNumPacketsTx(long n) {
+ mNumPacketsTx = n;
+ return;
+ }
+
+ public void setNumBytesTx(long b) {
+ mNumBytesTx = b;
+ return;
+ }
+
+ public void setNumPacketsRx(long n) {
+ mNumPacketsRx = n;
+ return;
+ }
+
+ public void setNumBytesRx(long b) {
+ mNumBytesRx = b;
+ return;
+ }
+
+ public void setSleepTimeMs(long t) {
+ mSleepTimeMs = t;
+ return;
+ }
+
+ public void setIdleTimeMs(long t) {
+ mIdleTimeMs = t;
+ return;
+ }
+
+ public void setRxTimeMs(long t) {
+ mRxTimeMs = t;
+ return;
+ }
+
+ public void setEnergyConsumedMaMs(long e) {
+ mEnergyConsumedMaMs = e;
+ return;
+ }
+
+ public void setTimeInRatMs(long[] t) {
+ mTimeInRatMs = Arrays.copyOfRange(t, 0,
+ Math.min(t.length, BatteryStats.NUM_DATA_CONNECTION_TYPES));
+ return;
+ }
+
+ public void setTimeInRxSignalStrengthLevelMs(long[] t) {
+ mTimeInRxSignalStrengthLevelMs = Arrays.copyOfRange(t, 0,
+ Math.min(t.length, SignalStrength.NUM_SIGNAL_STRENGTH_BINS));
+ return;
+ }
+
+ public void setTxTimeMs(long[] t) {
+ mTxTimeMs = Arrays.copyOfRange(t, 0, Math.min(t.length, ModemActivityInfo.TX_POWER_LEVELS));
+ return;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ private CellularBatteryStats(Parcel in) {
+ initialize();
+ readFromParcel(in);
+ }
+
+ private void initialize() {
+ mLoggingDurationMs = 0;
+ mKernelActiveTimeMs = 0;
+ mNumPacketsTx = 0;
+ mNumBytesTx = 0;
+ mNumPacketsRx = 0;
+ mNumBytesRx = 0;
+ mSleepTimeMs = 0;
+ mIdleTimeMs = 0;
+ mRxTimeMs = 0;
+ mEnergyConsumedMaMs = 0;
+ mTimeInRatMs = new long[BatteryStats.NUM_DATA_CONNECTION_TYPES];
+ Arrays.fill(mTimeInRatMs, 0);
+ mTimeInRxSignalStrengthLevelMs = new long[SignalStrength.NUM_SIGNAL_STRENGTH_BINS];
+ Arrays.fill(mTimeInRxSignalStrengthLevelMs, 0);
+ mTxTimeMs = new long[ModemActivityInfo.TX_POWER_LEVELS];
+ Arrays.fill(mTxTimeMs, 0);
+ return;
+ }
+} \ No newline at end of file
diff --git a/android/os/storage/StorageManager.java b/android/os/storage/StorageManager.java
index 4796712f..4c587a83 100644
--- a/android/os/storage/StorageManager.java
+++ b/android/os/storage/StorageManager.java
@@ -1115,12 +1115,14 @@ public class StorageManager {
/** {@hide} */
public static Pair<String, Long> getPrimaryStoragePathAndSize() {
return Pair.create(null,
- FileUtils.roundStorageSize(Environment.getDataDirectory().getTotalSpace()));
+ FileUtils.roundStorageSize(Environment.getDataDirectory().getTotalSpace()
+ + Environment.getRootDirectory().getTotalSpace()));
}
/** {@hide} */
public long getPrimaryStorageSize() {
- return FileUtils.roundStorageSize(Environment.getDataDirectory().getTotalSpace());
+ return FileUtils.roundStorageSize(Environment.getDataDirectory().getTotalSpace()
+ + Environment.getRootDirectory().getTotalSpace());
}
/** {@hide} */
@@ -1690,7 +1692,7 @@ public class StorageManager {
public static final int FLAG_ALLOCATE_DEFY_HALF_RESERVED = 1 << 2;
/** @hide */
- @IntDef(flag = true, value = {
+ @IntDef(flag = true, prefix = { "FLAG_ALLOCATE_" }, value = {
FLAG_ALLOCATE_AGGRESSIVE,
FLAG_ALLOCATE_DEFY_ALL_RESERVED,
FLAG_ALLOCATE_DEFY_HALF_RESERVED,
diff --git a/android/os/storage/StorageVolume.java b/android/os/storage/StorageVolume.java
index 1fc0b820..070b8c1b 100644
--- a/android/os/storage/StorageVolume.java
+++ b/android/os/storage/StorageVolume.java
@@ -19,7 +19,6 @@ package android.os.storage;
import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
-import android.net.TrafficStats;
import android.net.Uri;
import android.os.Environment;
import android.os.Parcel;
@@ -78,13 +77,11 @@ import java.io.File;
public final class StorageVolume implements Parcelable {
private final String mId;
- private final int mStorageId;
private final File mPath;
private final String mDescription;
private final boolean mPrimary;
private final boolean mRemovable;
private final boolean mEmulated;
- private final long mMtpReserveSize;
private final boolean mAllowMassStorage;
private final long mMaxFileSize;
private final UserHandle mOwner;
@@ -121,17 +118,15 @@ public final class StorageVolume implements Parcelable {
public static final int STORAGE_ID_PRIMARY = 0x00010001;
/** {@hide} */
- public StorageVolume(String id, int storageId, File path, String description, boolean primary,
- boolean removable, boolean emulated, long mtpReserveSize, boolean allowMassStorage,
+ public StorageVolume(String id, File path, String description, boolean primary,
+ boolean removable, boolean emulated, boolean allowMassStorage,
long maxFileSize, UserHandle owner, String fsUuid, String state) {
mId = Preconditions.checkNotNull(id);
- mStorageId = storageId;
mPath = Preconditions.checkNotNull(path);
mDescription = Preconditions.checkNotNull(description);
mPrimary = primary;
mRemovable = removable;
mEmulated = emulated;
- mMtpReserveSize = mtpReserveSize;
mAllowMassStorage = allowMassStorage;
mMaxFileSize = maxFileSize;
mOwner = Preconditions.checkNotNull(owner);
@@ -141,13 +136,11 @@ public final class StorageVolume implements Parcelable {
private StorageVolume(Parcel in) {
mId = in.readString();
- mStorageId = in.readInt();
mPath = new File(in.readString());
mDescription = in.readString();
mPrimary = in.readInt() != 0;
mRemovable = in.readInt() != 0;
mEmulated = in.readInt() != 0;
- mMtpReserveSize = in.readLong();
mAllowMassStorage = in.readInt() != 0;
mMaxFileSize = in.readLong();
mOwner = in.readParcelable(null);
@@ -211,34 +204,6 @@ public final class StorageVolume implements Parcelable {
}
/**
- * Returns the MTP storage ID for the volume.
- * this is also used for the storage_id column in the media provider.
- *
- * @return MTP storage ID
- * @hide
- */
- public int getStorageId() {
- return mStorageId;
- }
-
- /**
- * Number of megabytes of space to leave unallocated by MTP.
- * MTP will subtract this value from the free space it reports back
- * to the host via GetStorageInfo, and will not allow new files to
- * be added via MTP if there is less than this amount left free in the storage.
- * If MTP has dedicated storage this value should be zero, but if MTP is
- * sharing storage with the rest of the system, set this to a positive value
- * to ensure that MTP activity does not result in the storage being
- * too close to full.
- *
- * @return MTP reserve space
- * @hide
- */
- public int getMtpReserveSpace() {
- return (int) (mMtpReserveSize / TrafficStats.MB_IN_BYTES);
- }
-
- /**
* Returns true if this volume can be shared via USB mass storage.
*
* @return whether mass storage is allowed
@@ -385,13 +350,11 @@ public final class StorageVolume implements Parcelable {
pw.println("StorageVolume:");
pw.increaseIndent();
pw.printPair("mId", mId);
- pw.printPair("mStorageId", mStorageId);
pw.printPair("mPath", mPath);
pw.printPair("mDescription", mDescription);
pw.printPair("mPrimary", mPrimary);
pw.printPair("mRemovable", mRemovable);
pw.printPair("mEmulated", mEmulated);
- pw.printPair("mMtpReserveSize", mMtpReserveSize);
pw.printPair("mAllowMassStorage", mAllowMassStorage);
pw.printPair("mMaxFileSize", mMaxFileSize);
pw.printPair("mOwner", mOwner);
@@ -420,13 +383,11 @@ public final class StorageVolume implements Parcelable {
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(mId);
- parcel.writeInt(mStorageId);
parcel.writeString(mPath.toString());
parcel.writeString(mDescription);
parcel.writeInt(mPrimary ? 1 : 0);
parcel.writeInt(mRemovable ? 1 : 0);
parcel.writeInt(mEmulated ? 1 : 0);
- parcel.writeLong(mMtpReserveSize);
parcel.writeInt(mAllowMassStorage ? 1 : 0);
parcel.writeLong(mMaxFileSize);
parcel.writeParcelable(mOwner, flags);
diff --git a/android/os/storage/VolumeInfo.java b/android/os/storage/VolumeInfo.java
index 76f79f13..d3877cac 100644
--- a/android/os/storage/VolumeInfo.java
+++ b/android/os/storage/VolumeInfo.java
@@ -343,9 +343,7 @@ public class VolumeInfo implements Parcelable {
String description = null;
String derivedFsUuid = fsUuid;
- long mtpReserveSize = 0;
long maxFileSize = 0;
- int mtpStorageId = StorageVolume.STORAGE_ID_INVALID;
if (type == TYPE_EMULATED) {
emulated = true;
@@ -356,12 +354,6 @@ public class VolumeInfo implements Parcelable {
derivedFsUuid = privateVol.fsUuid;
}
- if (isPrimary()) {
- mtpStorageId = StorageVolume.STORAGE_ID_PRIMARY;
- }
-
- mtpReserveSize = storage.getStorageLowBytes(userPath);
-
if (ID_EMULATED_INTERNAL.equals(id)) {
removable = false;
} else {
@@ -374,14 +366,6 @@ public class VolumeInfo implements Parcelable {
description = storage.getBestVolumeDescription(this);
- if (isPrimary()) {
- mtpStorageId = StorageVolume.STORAGE_ID_PRIMARY;
- } else {
- // Since MediaProvider currently persists this value, we need a
- // value that is stable over time.
- mtpStorageId = buildStableMtpStorageId(fsUuid);
- }
-
if ("vfat".equals(fsType)) {
maxFileSize = 4294967295L;
}
@@ -394,8 +378,8 @@ public class VolumeInfo implements Parcelable {
description = context.getString(android.R.string.unknownName);
}
- return new StorageVolume(id, mtpStorageId, userPath, description, isPrimary(), removable,
- emulated, mtpReserveSize, allowMassStorage, maxFileSize, new UserHandle(userId),
+ return new StorageVolume(id, userPath, description, isPrimary(), removable,
+ emulated, allowMassStorage, maxFileSize, new UserHandle(userId),
derivedFsUuid, envState);
}
diff --git a/android/print/PrintAttributes.java b/android/print/PrintAttributes.java
index ce5b11ee..f0d9a0ce 100644
--- a/android/print/PrintAttributes.java
+++ b/android/print/PrintAttributes.java
@@ -49,8 +49,9 @@ import java.util.Map;
public final class PrintAttributes implements Parcelable {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true, value = {
- COLOR_MODE_MONOCHROME, COLOR_MODE_COLOR
+ @IntDef(flag = true, prefix = { "COLOR_MODE_" }, value = {
+ COLOR_MODE_MONOCHROME,
+ COLOR_MODE_COLOR
})
@interface ColorMode {
}
@@ -64,8 +65,10 @@ public final class PrintAttributes implements Parcelable {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true, value = {
- DUPLEX_MODE_NONE, DUPLEX_MODE_LONG_EDGE, DUPLEX_MODE_SHORT_EDGE
+ @IntDef(flag = true, prefix = { "DUPLEX_MODE_" }, value = {
+ DUPLEX_MODE_NONE,
+ DUPLEX_MODE_LONG_EDGE,
+ DUPLEX_MODE_SHORT_EDGE
})
@interface DuplexMode {
}
diff --git a/android/print/PrintDocumentInfo.java b/android/print/PrintDocumentInfo.java
index 61434041..55c902e1 100644
--- a/android/print/PrintDocumentInfo.java
+++ b/android/print/PrintDocumentInfo.java
@@ -83,11 +83,14 @@ public final class PrintDocumentInfo implements Parcelable {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({
- CONTENT_TYPE_UNKNOWN, CONTENT_TYPE_DOCUMENT, CONTENT_TYPE_PHOTO
+ @IntDef(prefix = { "CONTENT_TYPE_" }, value = {
+ CONTENT_TYPE_UNKNOWN,
+ CONTENT_TYPE_DOCUMENT,
+ CONTENT_TYPE_PHOTO
})
public @interface ContentType {
}
+
/**
* Content type: unknown.
*/
diff --git a/android/print/PrintJobInfo.java b/android/print/PrintJobInfo.java
index 94686a8e..85fdd642 100644
--- a/android/print/PrintJobInfo.java
+++ b/android/print/PrintJobInfo.java
@@ -45,9 +45,14 @@ import java.util.Arrays;
public final class PrintJobInfo implements Parcelable {
/** @hide */
- @IntDef({
- STATE_CREATED, STATE_QUEUED, STATE_STARTED, STATE_BLOCKED, STATE_COMPLETED,
- STATE_FAILED, STATE_CANCELED
+ @IntDef(prefix = { "STATE_" }, value = {
+ STATE_CREATED,
+ STATE_QUEUED,
+ STATE_STARTED,
+ STATE_BLOCKED,
+ STATE_COMPLETED,
+ STATE_FAILED,
+ STATE_CANCELED
})
@Retention(RetentionPolicy.SOURCE)
public @interface State {
diff --git a/android/print/PrinterInfo.java b/android/print/PrinterInfo.java
index 88feab7f..e79cc651 100644
--- a/android/print/PrinterInfo.java
+++ b/android/print/PrinterInfo.java
@@ -53,12 +53,15 @@ import java.lang.annotation.RetentionPolicy;
public final class PrinterInfo implements Parcelable {
/** @hide */
- @IntDef({
- STATUS_IDLE, STATUS_BUSY, STATUS_UNAVAILABLE
+ @IntDef(prefix = { "STATUS_" }, value = {
+ STATUS_IDLE,
+ STATUS_BUSY,
+ STATUS_UNAVAILABLE
})
@Retention(RetentionPolicy.SOURCE)
public @interface Status {
}
+
/** Printer status: the printer is idle and ready to print. */
public static final int STATUS_IDLE = PrinterInfoProto.STATUS_IDLE;
diff --git a/android/privacy/DifferentialPrivacyConfig.java b/android/privacy/DifferentialPrivacyConfig.java
new file mode 100644
index 00000000..e14893e7
--- /dev/null
+++ b/android/privacy/DifferentialPrivacyConfig.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.privacy;
+
+/**
+ * An interface for differential privacy configuration.
+ * {@link DifferentialPrivacyEncoder} will apply this configuration to do differential privacy
+ * encoding.
+ *
+ * @hide
+ */
+public interface DifferentialPrivacyConfig {
+
+ /**
+ * Returns the name of the algorithm used in differential privacy config.
+ *
+ * @return The name of the algorithm
+ */
+ String getAlgorithm();
+}
diff --git a/android/privacy/DifferentialPrivacyEncoder.java b/android/privacy/DifferentialPrivacyEncoder.java
new file mode 100644
index 00000000..9355d6a5
--- /dev/null
+++ b/android/privacy/DifferentialPrivacyEncoder.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.privacy;
+
+/**
+ * An interface for differential privacy encoder.
+ * Applications can use it to convert privacy sensitive data to privacy protected report.
+ * There is no decoder implemented in Android as it is not possible decode a single report by
+ * design.
+ *
+ * <p>Each type of log should have its own encoder, otherwise it may leak
+ * some information about Permanent Randomized Response(PRR, is used to create a “noisy”
+ * answer which is memoized by the client and permanently reused in place of the real answer).
+ *
+ * <p>Some encoders may not support all encoding methods, and it will throw {@link
+ * UnsupportedOperationException} if you call unsupported encoding method.
+ *
+ * <p><b>WARNING:</b> Privacy protection works only when encoder uses a suitable DP configuration,
+ * and the configuration and algorithm that is suitable is highly dependent on the use case.
+ * If the configuration is not suitable for the use case, it may hurt privacy or utility or both.
+ *
+ * @hide
+ */
+public interface DifferentialPrivacyEncoder {
+
+ /**
+ * Apply differential privacy to encode a string.
+ *
+ * @param original An arbitrary string
+ * @return Differential privacy encoded bytes derived from the string
+ */
+ byte[] encodeString(String original);
+
+ /**
+ * Apply differential privacy to encode a boolean.
+ *
+ * @param original An arbitrary boolean.
+ * @return Differential privacy encoded bytes derived from the boolean
+ */
+ byte[] encodeBoolean(boolean original);
+
+ /**
+ * Apply differential privacy to encode sequence of bytes.
+ *
+ * @param original An arbitrary byte array.
+ * @return Differential privacy encoded bytes derived from the bytes
+ */
+ byte[] encodeBits(byte[] original);
+
+ /**
+ * Returns the configuration that this encoder is using.
+ */
+ DifferentialPrivacyConfig getConfig();
+
+ /**
+ * Return True if the output from encoder is NOT securely randomized, otherwise encoder should
+ * be secure to randomize input.
+ *
+ * <b> A non-secure encoder is intended only for testing only and must not be used to process
+ * real data.
+ * </b>
+ */
+ boolean isInsecureEncoderForTest();
+}
diff --git a/android/privacy/internal/longitudinalreporting/LongitudinalReportingConfig.java b/android/privacy/internal/longitudinalreporting/LongitudinalReportingConfig.java
new file mode 100644
index 00000000..ee910fc1
--- /dev/null
+++ b/android/privacy/internal/longitudinalreporting/LongitudinalReportingConfig.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.privacy.internal.longitudinalreporting;
+
+import android.privacy.DifferentialPrivacyConfig;
+import android.privacy.internal.rappor.RapporConfig;
+import android.text.TextUtils;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * A class to store {@link LongitudinalReportingEncoder} configuration.
+ *
+ * <ul>
+ * <li> f is probability to flip input value, used in IRR.
+ * <li> p is probability to override input value, used in PRR1.
+ * <li> q is probability to set input value as 1 when result of PRR(p) is true, used in PRR2.
+ * </ul>
+ *
+ * @hide
+ */
+public class LongitudinalReportingConfig implements DifferentialPrivacyConfig {
+
+ private static final String ALGORITHM_NAME = "LongitudinalReporting";
+
+ // Probability to flip input value.
+ private final double mProbabilityF;
+
+ // Probability to override original value.
+ private final double mProbabilityP;
+ // Probability to override value with 1.
+ private final double mProbabilityQ;
+
+ // IRR config to randomize original value
+ private final RapporConfig mIRRConfig;
+
+ private final String mEncoderId;
+
+ /**
+ * Constructor to create {@link LongitudinalReportingConfig} used for {@link
+ * LongitudinalReportingEncoder}
+ *
+ * @param encoderId Unique encoder id.
+ * @param probabilityF Probability F used in Longitudinal Reporting algorithm.
+ * @param probabilityP Probability P used in Longitudinal Reporting algorithm. This will be
+ * quantized to the nearest 1/256.
+ * @param probabilityQ Probability Q used in Longitudinal Reporting algorithm. This will be
+ * quantized to the nearest 1/256.
+ */
+ public LongitudinalReportingConfig(String encoderId, double probabilityF,
+ double probabilityP, double probabilityQ) {
+ Preconditions.checkArgument(probabilityF >= 0 && probabilityF <= 1,
+ "probabilityF must be in range [0.0, 1.0]");
+ this.mProbabilityF = probabilityF;
+ Preconditions.checkArgument(probabilityP >= 0 && probabilityP <= 1,
+ "probabilityP must be in range [0.0, 1.0]");
+ this.mProbabilityP = probabilityP;
+ Preconditions.checkArgument(probabilityQ >= 0 && probabilityQ <= 1,
+ "probabilityQ must be in range [0.0, 1.0]");
+ this.mProbabilityQ = probabilityQ;
+ Preconditions.checkArgument(!TextUtils.isEmpty(encoderId), "encoderId cannot be empty");
+ mEncoderId = encoderId;
+ mIRRConfig = new RapporConfig(encoderId, 1, 0.0, probabilityF, 1 - probabilityF, 1, 1);
+ }
+
+ @Override
+ public String getAlgorithm() {
+ return ALGORITHM_NAME;
+ }
+
+ RapporConfig getIRRConfig() {
+ return mIRRConfig;
+ }
+
+ double getProbabilityP() {
+ return mProbabilityP;
+ }
+
+ double getProbabilityQ() {
+ return mProbabilityQ;
+ }
+
+ String getEncoderId() {
+ return mEncoderId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("EncoderId: %s, ProbabilityF: %.3f, ProbabilityP: %.3f"
+ + ", ProbabilityQ: %.3f",
+ mEncoderId, mProbabilityF, mProbabilityP, mProbabilityQ);
+ }
+}
diff --git a/android/privacy/internal/longitudinalreporting/LongitudinalReportingEncoder.java b/android/privacy/internal/longitudinalreporting/LongitudinalReportingEncoder.java
new file mode 100644
index 00000000..219868d6
--- /dev/null
+++ b/android/privacy/internal/longitudinalreporting/LongitudinalReportingEncoder.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.privacy.internal.longitudinalreporting;
+
+import android.privacy.DifferentialPrivacyEncoder;
+import android.privacy.internal.rappor.RapporConfig;
+import android.privacy.internal.rappor.RapporEncoder;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Differential privacy encoder by using Longitudinal Reporting algorithm.
+ *
+ * <b>
+ * Notes: It supports encodeBoolean() only for now.
+ * </b>
+ *
+ * <p>
+ * Definition:
+ * PRR = Permanent Randomized Response
+ * IRR = Instantaneous Randomized response
+ *
+ * Algorithm:
+ * Step 1: Create long-term secrets x(ignoreOriginalInput)=Ber(P), y=Ber(Q), where Ber denotes
+ * Bernoulli distribution on {0, 1}, and we use it as a long-term secret, we implement Ber(x) by
+ * using PRR(2x, 0) when x < 1/2, PRR(2(1-x), 1) when x >= 1/2.
+ * Step 2: If x is 0, report IRR(F, original), otherwise report IRR(F, y)
+ * </p>
+ *
+ * Reference: go/bit-reporting-with-longitudinal-privacy
+ * TODO: Add a public blog / site to explain how it works.
+ *
+ * @hide
+ */
+public class LongitudinalReportingEncoder implements DifferentialPrivacyEncoder {
+
+ // Suffix that will be added to Rappor's encoder id. There's a (relatively) small risk some
+ // other Rappor encoder may re-use the same encoder id.
+ private static final String PRR1_ENCODER_ID = "prr1_encoder_id";
+ private static final String PRR2_ENCODER_ID = "prr2_encoder_id";
+
+ private final LongitudinalReportingConfig mConfig;
+
+ // IRR encoder to encode input value.
+ private final RapporEncoder mIRREncoder;
+
+ // A value that used to replace original value as input, so there's always a chance we are
+ // doing IRR on a fake value not actual original value.
+ // Null if original value does not need to be replaced.
+ private final Boolean mFakeValue;
+
+ // True if encoder is securely randomized.
+ private final boolean mIsSecure;
+
+ /**
+ * Create {@link LongitudinalReportingEncoder} with
+ * {@link LongitudinalReportingConfig} provided.
+ *
+ * @param config Longitudinal Reporting parameters to encode input
+ * @param userSecret User generated secret that used to generate PRR
+ * @return {@link LongitudinalReportingEncoder} instance
+ */
+ public static LongitudinalReportingEncoder createEncoder(LongitudinalReportingConfig config,
+ byte[] userSecret) {
+ return new LongitudinalReportingEncoder(config, true, userSecret);
+ }
+
+ /**
+ * Create <strong>insecure</strong> {@link LongitudinalReportingEncoder} with
+ * {@link LongitudinalReportingConfig} provided.
+ * Should not use it to process sensitive data.
+ *
+ * @param config Rappor parameters to encode input.
+ * @return {@link LongitudinalReportingEncoder} instance.
+ */
+ @VisibleForTesting
+ public static LongitudinalReportingEncoder createInsecureEncoderForTest(
+ LongitudinalReportingConfig config) {
+ return new LongitudinalReportingEncoder(config, false, null);
+ }
+
+ private LongitudinalReportingEncoder(LongitudinalReportingConfig config,
+ boolean secureEncoder, byte[] userSecret) {
+ mConfig = config;
+ mIsSecure = secureEncoder;
+ final boolean ignoreOriginalInput = getLongTermRandomizedResult(config.getProbabilityP(),
+ secureEncoder, userSecret, config.getEncoderId() + PRR1_ENCODER_ID);
+
+ if (ignoreOriginalInput) {
+ mFakeValue = getLongTermRandomizedResult(config.getProbabilityQ(),
+ secureEncoder, userSecret, config.getEncoderId() + PRR2_ENCODER_ID);
+ } else {
+ // Not using fake value, so IRR will be processed on real input value.
+ mFakeValue = null;
+ }
+
+ final RapporConfig irrConfig = config.getIRRConfig();
+ mIRREncoder = secureEncoder
+ ? RapporEncoder.createEncoder(irrConfig, userSecret)
+ : RapporEncoder.createInsecureEncoderForTest(irrConfig);
+ }
+
+ @Override
+ public byte[] encodeString(String original) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public byte[] encodeBoolean(boolean original) {
+ if (mFakeValue != null) {
+ // Use the fake value generated in PRR.
+ original = mFakeValue.booleanValue();
+ }
+ return mIRREncoder.encodeBoolean(original);
+ }
+
+ @Override
+ public byte[] encodeBits(byte[] bits) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public LongitudinalReportingConfig getConfig() {
+ return mConfig;
+ }
+
+ @Override
+ public boolean isInsecureEncoderForTest() {
+ return !mIsSecure;
+ }
+
+ /**
+ * Get PRR result that with probability p is 1, probability 1-p is 0.
+ */
+ @VisibleForTesting
+ public static boolean getLongTermRandomizedResult(double p, boolean secureEncoder,
+ byte[] userSecret, String encoderId) {
+ // Use Rappor to get PRR result. Rappor's P and Q are set to 0 and 1 so IRR will not be
+ // effective.
+ // As Rappor has rapporF/2 chance returns 0, rapporF/2 chance returns 1, and 1-rapporF
+ // chance returns original input.
+ // If p < 0.5, setting rapporF=2p and input=0 will make Rappor has p chance to return 1
+ // P(output=1 | input=0) = rapporF/2 = 2p/2 = p.
+ // If p >= 0.5, setting rapporF=2(1-p) and input=1 will make Rappor has p chance
+ // to return 1.
+ // P(output=1 | input=1) = rapporF/2 + (1 - rapporF) = 2(1-p)/2 + (1 - 2(1-p)) = p.
+ final double effectiveF = p < 0.5f ? p * 2 : (1 - p) * 2;
+ final boolean prrInput = p < 0.5f ? false : true;
+ final RapporConfig prrConfig = new RapporConfig(encoderId, 1, effectiveF,
+ 0, 1, 1, 1);
+ final RapporEncoder encoder = secureEncoder
+ ? RapporEncoder.createEncoder(prrConfig, userSecret)
+ : RapporEncoder.createInsecureEncoderForTest(prrConfig);
+ return encoder.encodeBoolean(prrInput)[0] > 0;
+ }
+}
diff --git a/android/privacy/internal/rappor/RapporConfig.java b/android/privacy/internal/rappor/RapporConfig.java
new file mode 100644
index 00000000..221999bf
--- /dev/null
+++ b/android/privacy/internal/rappor/RapporConfig.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.privacy.internal.rappor;
+
+import android.privacy.DifferentialPrivacyConfig;
+import android.text.TextUtils;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * A class to store {@link RapporEncoder} config.
+ *
+ * @hide
+ */
+public class RapporConfig implements DifferentialPrivacyConfig {
+
+ private static final String ALGORITHM_NAME = "Rappor";
+
+ final String mEncoderId;
+ final int mNumBits;
+ final double mProbabilityF;
+ final double mProbabilityP;
+ final double mProbabilityQ;
+ final int mNumCohorts;
+ final int mNumBloomHashes;
+
+ /**
+ * Constructor for {@link RapporConfig}.
+ *
+ * @param encoderId Unique id for encoder.
+ * @param numBits Number of bits to be encoded in Rappor algorithm.
+ * @param probabilityF Probability F that used in Rappor algorithm. This will be
+ * quantized to the nearest 1/128.
+ * @param probabilityP Probability P that used in Rappor algorithm.
+ * @param probabilityQ Probability Q that used in Rappor algorithm.
+ * @param numCohorts Number of cohorts that used in Rappor algorithm.
+ * @param numBloomHashes Number of bloom hashes that used in Rappor algorithm.
+ */
+ public RapporConfig(String encoderId, int numBits, double probabilityF,
+ double probabilityP, double probabilityQ, int numCohorts, int numBloomHashes) {
+ Preconditions.checkArgument(!TextUtils.isEmpty(encoderId), "encoderId cannot be empty");
+ this.mEncoderId = encoderId;
+ Preconditions.checkArgument(numBits > 0, "numBits needs to be > 0");
+ this.mNumBits = numBits;
+ Preconditions.checkArgument(probabilityF >= 0 && probabilityF <= 1,
+ "probabilityF must be in range [0.0, 1.0]");
+ this.mProbabilityF = probabilityF;
+ Preconditions.checkArgument(probabilityP >= 0 && probabilityP <= 1,
+ "probabilityP must be in range [0.0, 1.0]");
+ this.mProbabilityP = probabilityP;
+ Preconditions.checkArgument(probabilityQ >= 0 && probabilityQ <= 1,
+ "probabilityQ must be in range [0.0, 1.0]");
+ this.mProbabilityQ = probabilityQ;
+ Preconditions.checkArgument(numCohorts > 0, "numCohorts needs to be > 0");
+ this.mNumCohorts = numCohorts;
+ Preconditions.checkArgument(numBloomHashes > 0, "numBloomHashes needs to be > 0");
+ this.mNumBloomHashes = numBloomHashes;
+ }
+
+ @Override
+ public String getAlgorithm() {
+ return ALGORITHM_NAME;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "EncoderId: %s, NumBits: %d, ProbabilityF: %.3f, ProbabilityP: %.3f"
+ + ", ProbabilityQ: %.3f, NumCohorts: %d, NumBloomHashes: %d",
+ mEncoderId, mNumBits, mProbabilityF, mProbabilityP, mProbabilityQ,
+ mNumCohorts, mNumBloomHashes);
+ }
+}
diff --git a/android/privacy/internal/rappor/RapporEncoder.java b/android/privacy/internal/rappor/RapporEncoder.java
new file mode 100644
index 00000000..2eca4c98
--- /dev/null
+++ b/android/privacy/internal/rappor/RapporEncoder.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.privacy.internal.rappor;
+
+import android.privacy.DifferentialPrivacyEncoder;
+
+import com.google.android.rappor.Encoder;
+
+import java.security.SecureRandom;
+import java.util.Random;
+
+/**
+ * Differential privacy encoder by using
+ * <a href="https://research.google.com/pubs/pub42852.html">RAPPOR</a>
+ * algorithm.
+ *
+ * @hide
+ */
+public class RapporEncoder implements DifferentialPrivacyEncoder {
+
+ // Hard-coded seed and secret for insecure encoder
+ private static final long INSECURE_RANDOM_SEED = 0x12345678L;
+ private static final byte[] INSECURE_SECRET = new byte[]{
+ (byte) 0xD7, (byte) 0x68, (byte) 0x99, (byte) 0x93,
+ (byte) 0x94, (byte) 0x13, (byte) 0x53, (byte) 0x54,
+ (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54,
+ (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54,
+ (byte) 0xD7, (byte) 0x68, (byte) 0x99, (byte) 0x93,
+ (byte) 0x94, (byte) 0x13, (byte) 0x53, (byte) 0x54,
+ (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54,
+ (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54,
+ (byte) 0xD7, (byte) 0x68, (byte) 0x99, (byte) 0x93,
+ (byte) 0x94, (byte) 0x13, (byte) 0x53, (byte) 0x54,
+ (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54,
+ (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54
+ };
+ private static final SecureRandom sSecureRandom = new SecureRandom();
+
+ private final RapporConfig mConfig;
+
+ // Rappor encoder
+ private final Encoder mEncoder;
+ // True if encoder is secure (seed is securely randomized)
+ private final boolean mIsSecure;
+
+
+ private RapporEncoder(RapporConfig config, boolean secureEncoder, byte[] userSecret) {
+ mConfig = config;
+ mIsSecure = secureEncoder;
+ final Random random;
+ if (secureEncoder) {
+ // Use SecureRandom as random generator.
+ random = sSecureRandom;
+ } else {
+ // Hard-coded random generator, to have deterministic result.
+ random = new Random(INSECURE_RANDOM_SEED);
+ userSecret = INSECURE_SECRET;
+ }
+ mEncoder = new Encoder(random, null, null,
+ userSecret, config.mEncoderId, config.mNumBits,
+ config.mProbabilityF, config.mProbabilityP, config.mProbabilityQ,
+ config.mNumCohorts, config.mNumBloomHashes);
+ }
+
+ /**
+ * Create {@link RapporEncoder} with {@link RapporConfig} and user secret provided.
+ *
+ * @param config Rappor parameters to encode input.
+ * @param userSecret Per device unique secret key.
+ * @return {@link RapporEncoder} instance.
+ */
+ public static RapporEncoder createEncoder(RapporConfig config, byte[] userSecret) {
+ return new RapporEncoder(config, true, userSecret);
+ }
+
+ /**
+ * Create <strong>insecure</strong> {@link RapporEncoder} with {@link RapporConfig} provided.
+ * Should not use it to process sensitive data.
+ *
+ * @param config Rappor parameters to encode input.
+ * @return {@link RapporEncoder} instance.
+ */
+ public static RapporEncoder createInsecureEncoderForTest(RapporConfig config) {
+ return new RapporEncoder(config, false, null);
+ }
+
+ @Override
+ public byte[] encodeString(String original) {
+ return mEncoder.encodeString(original);
+ }
+
+ @Override
+ public byte[] encodeBoolean(boolean original) {
+ return mEncoder.encodeBoolean(original);
+ }
+
+ @Override
+ public byte[] encodeBits(byte[] bits) {
+ return mEncoder.encodeBits(bits);
+ }
+
+ @Override
+ public RapporConfig getConfig() {
+ return mConfig;
+ }
+
+ @Override
+ public boolean isInsecureEncoderForTest() {
+ return !mIsSecure;
+ }
+}
diff --git a/android/provider/AlarmClock.java b/android/provider/AlarmClock.java
index f9030124..21694575 100644
--- a/android/provider/AlarmClock.java
+++ b/android/provider/AlarmClock.java
@@ -158,7 +158,6 @@ public final class AlarmClock {
* <p>
* Dismiss all currently expired timers. If there are no expired timers, then this is a no-op.
* </p>
- * @hide
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_DISMISS_TIMER = "android.intent.action.DISMISS_TIMER";
diff --git a/android/provider/CallLog.java b/android/provider/CallLog.java
index a8acb976..60df467b 100644
--- a/android/provider/CallLog.java
+++ b/android/provider/CallLog.java
@@ -212,16 +212,25 @@ public class CallLog {
public static final String FEATURES = "features";
/** Call had video. */
- public static final int FEATURES_VIDEO = 0x1;
+ public static final int FEATURES_VIDEO = 1 << 0;
/** Call was pulled externally. */
- public static final int FEATURES_PULLED_EXTERNALLY = 0x2;
+ public static final int FEATURES_PULLED_EXTERNALLY = 1 << 1;
/** Call was HD. */
- public static final int FEATURES_HD_CALL = 0x4;
+ public static final int FEATURES_HD_CALL = 1 << 2;
/** Call was WIFI call. */
- public static final int FEATURES_WIFI = 0x8;
+ public static final int FEATURES_WIFI = 1 << 3;
+
+ /** Call was on RTT at some point */
+ public static final int FEATURES_RTT = 1 << 4;
+
+ /**
+ * Indicates the call underwent Assisted Dialing.
+ * @hide
+ */
+ public static final Integer FEATURES_ASSISTED_DIALING_USED = 0x10;
/**
* The phone number as the user entered it.
diff --git a/android/provider/ContactsContract.java b/android/provider/ContactsContract.java
index cc1c0677..c94da9a1 100644
--- a/android/provider/ContactsContract.java
+++ b/android/provider/ContactsContract.java
@@ -22,6 +22,7 @@ import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.SystemApi;
import android.app.Activity;
import android.content.BroadcastReceiver;
+import android.content.ComponentName;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
@@ -42,10 +43,12 @@ import android.database.DatabaseUtils;
import android.graphics.Rect;
import android.net.Uri;
import android.os.RemoteException;
+import android.telecom.PhoneAccountHandle;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Pair;
import android.view.View;
+
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -4237,6 +4240,45 @@ public final class ContactsContract {
* current carrier. An allowed bitmask of {@link #CARRIER_PRESENCE}.
*/
public static final int CARRIER_PRESENCE_VT_CAPABLE = 0x01;
+
+ /**
+ * The flattened {@link android.content.ComponentName} of a {@link
+ * android.telecom.PhoneAccountHandle} that is the preferred {@code PhoneAccountHandle} to
+ * call the contact with.
+ *
+ * <p> On a multi-SIM device this field can be used in a {@link CommonDataKinds.Phone} row
+ * to indicate the {@link PhoneAccountHandle} to call the number with, instead of using
+ * {@link android.telecom.TelecomManager#getDefaultOutgoingPhoneAccount(String)} or asking
+ * every time.
+ *
+ * <p>{@link android.telecom.TelecomManager#placeCall(Uri, android.os.Bundle)}
+ * should be called with {@link android.telecom.TelecomManager#EXTRA_PHONE_ACCOUNT_HANDLE}
+ * set to the {@link PhoneAccountHandle} using the {@link ComponentName} from this field.
+ *
+ * @see #PREFERRED_PHONE_ACCOUNT_ID
+ * @see PhoneAccountHandle#getComponentName()
+ * @see ComponentName#flattenToString()
+ */
+ String PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME = "preferred_phone_account_component_name";
+
+ /**
+ * The ID of a {@link
+ * android.telecom.PhoneAccountHandle} that is the preferred {@code PhoneAccountHandle} to
+ * call the contact with. Used by {@link CommonDataKinds.Phone}.
+ *
+ * <p> On a multi-SIM device this field can be used in a {@link CommonDataKinds.Phone} row
+ * to indicate the {@link PhoneAccountHandle} to call the number with, instead of using
+ * {@link android.telecom.TelecomManager#getDefaultOutgoingPhoneAccount(String)} or asking
+ * every time.
+ *
+ * <p>{@link android.telecom.TelecomManager#placeCall(Uri, android.os.Bundle)}
+ * should be called with {@link android.telecom.TelecomManager#EXTRA_PHONE_ACCOUNT_HANDLE}
+ * set to the {@link PhoneAccountHandle} using the id from this field.
+ *
+ * @see #PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME
+ * @see PhoneAccountHandle#getId()
+ */
+ String PREFERRED_PHONE_ACCOUNT_ID = "preferred_phone_account_id";
}
/**
diff --git a/android/provider/FontsContract.java b/android/provider/FontsContract.java
index d8540ffd..a1d1c573 100644
--- a/android/provider/FontsContract.java
+++ b/android/provider/FontsContract.java
@@ -283,7 +283,11 @@ public class FontsContract {
public static final int STATUS_REJECTED = 3;
/** @hide */
- @IntDef({STATUS_OK, STATUS_WRONG_CERTIFICATES, STATUS_UNEXPECTED_DATA_PROVIDED})
+ @IntDef(prefix = { "STATUS_" }, value = {
+ STATUS_OK,
+ STATUS_WRONG_CERTIFICATES,
+ STATUS_UNEXPECTED_DATA_PROVIDED
+ })
@Retention(RetentionPolicy.SOURCE)
@interface FontResultStatus {}
@@ -438,9 +442,13 @@ public class FontsContract {
public static final int FAIL_REASON_MALFORMED_QUERY = Columns.RESULT_CODE_MALFORMED_QUERY;
/** @hide */
- @IntDef({ FAIL_REASON_PROVIDER_NOT_FOUND, FAIL_REASON_FONT_LOAD_ERROR,
- FAIL_REASON_FONT_NOT_FOUND, FAIL_REASON_FONT_UNAVAILABLE,
- FAIL_REASON_MALFORMED_QUERY })
+ @IntDef(prefix = { "FAIL_" }, value = {
+ FAIL_REASON_PROVIDER_NOT_FOUND,
+ FAIL_REASON_FONT_LOAD_ERROR,
+ FAIL_REASON_FONT_NOT_FOUND,
+ FAIL_REASON_FONT_UNAVAILABLE,
+ FAIL_REASON_MALFORMED_QUERY
+ })
@Retention(RetentionPolicy.SOURCE)
@interface FontRequestFailReason {}
diff --git a/android/provider/MediaStore.java b/android/provider/MediaStore.java
index 32d68cd9..d9808a3d 100644
--- a/android/provider/MediaStore.java
+++ b/android/provider/MediaStore.java
@@ -63,15 +63,6 @@ public final class MediaStore {
private static final String CONTENT_AUTHORITY_SLASH = "content://" + AUTHORITY + "/";
- /**
- * Broadcast Action: A broadcast to indicate the end of an MTP session with the host.
- * This broadcast is only sent if MTP activity has modified the media database during the
- * most recent MTP session.
- *
- * @hide
- */
- public static final String ACTION_MTP_SESSION_END = "android.provider.action.MTP_SESSION_END";
-
/**
* 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
diff --git a/android/provider/Settings.java b/android/provider/Settings.java
index 1e0948a4..2f865141 100644
--- a/android/provider/Settings.java
+++ b/android/provider/Settings.java
@@ -1689,7 +1689,7 @@ public final class Settings {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({
+ @IntDef(prefix = { "RESET_MODE_" }, value = {
RESET_MODE_PACKAGE_DEFAULTS,
RESET_MODE_UNTRUSTED_DEFAULTS,
RESET_MODE_UNTRUSTED_CHANGES,
@@ -3123,6 +3123,10 @@ public final class Settings {
* to dream after a period of inactivity. This value is also known as the
* user activity timeout period since the screen isn't necessarily turned off
* when it expires.
+ *
+ * <p>
+ * This value is bounded by maximum timeout set by
+ * {@link android.app.admin.DevicePolicyManager#setMaximumTimeToLock(ComponentName, long)}.
*/
public static final String SCREEN_OFF_TIMEOUT = "screen_off_timeout";
@@ -5324,13 +5328,57 @@ public final class Settings {
public static final String AUTOFILL_SERVICE = "autofill_service";
/**
- * Experimental autofill feature.
+ * Boolean indicating if Autofill supports field classification.
+ *
+ * @see android.service.autofill.AutofillService
+ *
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public static final String AUTOFILL_FEATURE_FIELD_CLASSIFICATION =
+ "autofill_field_classification";
+
+ /**
+ * Defines value returned by {@link android.service.autofill.UserData#getMaxUserDataSize()}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public static final String AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE =
+ "autofill_user_data_max_user_data_size";
+
+ /**
+ * Defines value returned by
+ * {@link android.service.autofill.UserData#getMaxFieldClassificationIdsSize()}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public static final String AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE =
+ "autofill_user_data_max_field_classification_size";
+
+ /**
+ * Defines value returned by {@link android.service.autofill.UserData#getMaxValueLength()}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public static final String AUTOFILL_USER_DATA_MAX_VALUE_LENGTH =
+ "autofill_user_data_max_value_length";
+
+ /**
+ * Defines value returned by {@link android.service.autofill.UserData#getMinValueLength()}.
*
- * <p>TODO(b/67867469): remove once feature is finished
* @hide
*/
+ @SystemApi
@TestApi
- public static final String AUTOFILL_FEATURE_FIELD_DETECTION = "autofill_field_detection";
+ public static final String AUTOFILL_USER_DATA_MIN_VALUE_LENGTH =
+ "autofill_user_data_min_value_length";
/**
* @deprecated Use {@link android.provider.Settings.Global#DEVICE_PROVISIONED} instead
@@ -5730,6 +5778,14 @@ public final class Settings {
"touch_exploration_granted_accessibility_services";
/**
+ * Uri of the slice that's presented on the keyguard.
+ * Defaults to a slice with the date and next alarm.
+ *
+ * @hide
+ */
+ public static final String KEYGUARD_SLICE_URI = "keyguard_slice_uri";
+
+ /**
* Whether to speak passwords while in accessibility mode.
*
* @deprecated The speaking of passwords is controlled by individual accessibility services.
@@ -7204,8 +7260,11 @@ public final class Settings {
* full_backup_interval_milliseconds (long)
* full_backup_require_charging (boolean)
* full_backup_required_network_type (int)
+ * backup_finished_notification_receivers (String[])
* </pre>
*
+ * backup_finished_notification_receivers uses ":" as delimeter for values.
+ *
* <p>
* Type: string
* @hide
@@ -8651,6 +8710,12 @@ public final class Settings {
"wifi_scan_always_enabled";
/**
+ * Whether soft AP will shut down after a timeout period when no devices are connected.
+ * @hide
+ */
+ public static final String SOFT_AP_TIMEOUT_ENABLED = "soft_ap_timeout_enabled";
+
+ /**
* Value to specify if Wi-Fi Wakeup feature is enabled.
*
* Type: int (0 for false, 1 for true)
@@ -9416,6 +9481,7 @@ public final class Settings {
* service_min_restart_time_between (long)
* service_max_inactivity (long)
* service_bg_start_timeout (long)
+ * process_start_async (boolean)
* </pre>
*
* <p>
@@ -9516,6 +9582,31 @@ public final class Settings {
public static final String ANOMALY_DETECTION_CONSTANTS = "anomaly_detection_constants";
/**
+ * Battery tip specific settings
+ * This is encoded as a key=value list, separated by commas. Ex:
+ *
+ * "battery_tip_enabled=true,summary_enabled=true,high_usage_enabled=true,"
+ * "high_usage_app_count=3,reduced_battery_enabled=false,reduced_battery_percent=50"
+ *
+ * The following keys are supported:
+ *
+ * <pre>
+ * battery_tip_enabled (boolean)
+ * summary_enabled (boolean)
+ * battery_saver_tip_enabled (boolean)
+ * high_usage_enabled (boolean)
+ * high_usage_app_count (int)
+ * app_restriction_enabled (boolean)
+ * reduced_battery_enabled (boolean)
+ * reduced_battery_percent (int)
+ * low_battery_enabled (boolean)
+ * low_battery_hour (int)
+ * </pre>
+ * @hide
+ */
+ public static final String BATTERY_TIP_CONSTANTS = "battery_tip_constants";
+
+ /**
* Always on display(AOD) specific settings
* This is encoded as a key=value list, separated by commas. Ex:
*
@@ -9524,8 +9615,8 @@ public final class Settings {
* The following keys are supported:
*
* <pre>
- * screen_brightness_array (string)
- * dimming_scrim_array (string)
+ * screen_brightness_array (int[])
+ * dimming_scrim_array (int[])
* prox_screen_off_delay (long)
* prox_cooldown_trigger (long)
* prox_cooldown_period (long)
@@ -9537,9 +9628,10 @@ public final class Settings {
/**
* App standby (app idle) specific settings.
* This is encoded as a key=value list, separated by commas. Ex:
- *
+ * <p>
* "idle_duration=5000,parole_interval=4500"
- *
+ * <p>
+ * All durations are in millis.
* The following keys are supported:
*
* <pre>
@@ -9689,6 +9781,15 @@ public final class Settings {
public static final String TEXT_CLASSIFIER_CONSTANTS = "text_classifier_constants";
/**
+ * Whether or not App Standby feature is enabled. This controls throttling of apps
+ * based on usage patterns and predictions.
+ * Type: int (0 for false, 1 for true)
+ * Default: 1
+ * @hide
+ */
+ public static final java.lang.String APP_STANDBY_ENABLED = "app_standby_enabled";
+
+ /**
* Get the key that retrieves a bluetooth headset's priority.
* @hide
*/
@@ -10117,6 +10218,16 @@ public final class Settings {
public static final String POLICY_CONTROL = "policy_control";
/**
+ * {@link android.view.DisplayCutout DisplayCutout} emulation mode.
+ *
+ * @hide
+ */
+ public static final String EMULATE_DISPLAY_CUTOUT = "emulate_display_cutout";
+
+ /** @hide */ public static final int EMULATE_DISPLAY_CUTOUT_OFF = 0;
+ /** @hide */ public static final int EMULATE_DISPLAY_CUTOUT_ON = 1;
+
+ /**
* Defines global zen mode. ZEN_MODE_OFF, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
* or ZEN_MODE_NO_INTERRUPTIONS.
*
@@ -10430,14 +10541,6 @@ public final class Settings {
"location_settings_link_to_permissions_enabled";
/**
- * Flag to enable use of RefactoredBackupManagerService.
- *
- * @hide
- */
- public static final String BACKUP_REFACTORED_SERVICE_DISABLED =
- "backup_refactored_service_disabled";
-
- /**
* Flag to set the waiting time for euicc factory reset inside System > Settings
* Type: long
*
@@ -10501,7 +10604,17 @@ public final class Settings {
LOW_POWER_MODE_TRIGGER_LEVEL,
BLUETOOTH_ON,
PRIVATE_DNS_MODE,
- PRIVATE_DNS_SPECIFIER
+ PRIVATE_DNS_SPECIFIER,
+ SOFT_AP_TIMEOUT_ENABLED
+ };
+
+ /**
+ * Global settings that shouldn't be persisted.
+ *
+ * @hide
+ */
+ public static final String[] TRANSIENT_SETTINGS = {
+ LOCATION_GLOBAL_KILL_SWITCH,
};
/** @hide */
@@ -11031,6 +11144,7 @@ public final class Settings {
INSTANT_APP_SETTINGS.add(DEBUG_VIEW_ATTRIBUTES);
INSTANT_APP_SETTINGS.add(WTF_IS_FATAL);
INSTANT_APP_SETTINGS.add(SEND_ACTION_APP_ERROR);
+ INSTANT_APP_SETTINGS.add(ZEN_MODE);
}
/**
@@ -11075,7 +11189,7 @@ public final class Settings {
*
* <pre>
* default (int)
- * options_array (string)
+ * options_array (int[])
* </pre>
*
* All delays in integer minutes. Array order is respected.
@@ -11084,6 +11198,28 @@ public final class Settings {
*/
public static final String NOTIFICATION_SNOOZE_OPTIONS =
"notification_snooze_options";
+
+ /**
+ * Configuration flags for SQLite Compatibility WAL. Encoded as a key-value list, separated
+ * by commas. E.g.: compatibility_wal_supported=true, wal_syncmode=OFF
+ *
+ * Supported keys:
+ * compatibility_wal_supported (boolean)
+ * wal_syncmode (String)
+ *
+ * @hide
+ */
+ public static final String SQLITE_COMPATIBILITY_WAL_FLAGS =
+ "sqlite_compatibility_wal_flags";
+
+ /**
+ * Enable GNSS Raw Measurements Full Tracking?
+ * 0 = no
+ * 1 = yes
+ * @hide
+ */
+ public static final String ENABLE_GNSS_RAW_MEAS_FULL_TRACKING =
+ "enable_gnss_raw_meas_full_tracking";
}
/**
diff --git a/android/provider/Telephony.java b/android/provider/Telephony.java
index d7b6142a..942ea009 100644
--- a/android/provider/Telephony.java
+++ b/android/provider/Telephony.java
@@ -3342,6 +3342,12 @@ public final class Telephony {
public static final String APN = "apn";
/**
+ * Prefix of Integrated Circuit Card Identifier.
+ * <P>Type: TEXT </P>
+ */
+ public static final String ICCID_PREFIX = "iccid_prefix";
+
+ /**
* User facing carrier name.
* <P>Type: TEXT </P>
*/
diff --git a/android/provider/VoicemailContract.java b/android/provider/VoicemailContract.java
index 864a0fd7..6a3c55ef 100644
--- a/android/provider/VoicemailContract.java
+++ b/android/provider/VoicemailContract.java
@@ -16,7 +16,6 @@
package android.provider;
-import android.Manifest;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.content.ComponentName;
@@ -50,7 +49,7 @@ import java.util.List;
* </ul>
*
* <P> The minimum permission needed to access this content provider is
- * {@link Manifest.permission#ADD_VOICEMAIL}
+ * {@link android.Manifest.permission#ADD_VOICEMAIL}
*
* <P>Voicemails are inserted by what is called as a "voicemail source"
* application, which is responsible for syncing voicemail data between a remote
@@ -293,11 +292,26 @@ public class VoicemailContract {
* Flag used to indicate that local, unsynced changes are present.
* Currently, this is used to indicate that the voicemail was read or deleted.
* The value will be 1 if dirty is true, 0 if false.
+ *
+ * <p>When a caller updates a voicemail row (either with {@link ContentResolver#update} or
+ * {@link ContentResolver#applyBatch}), and if the {@link ContentValues} doesn't contain
+ * this column, the voicemail provider implicitly sets it to 0 if the calling package is
+ * the {@link #SOURCE_PACKAGE} or to 1 otherwise. To prevent this behavior, explicitly set
+ * {@link #DIRTY_RETAIN} to DIRTY in the {@link ContentValues}.
+ *
* <P>Type: INTEGER (boolean)</P>
+ *
+ * @see #DIRTY_RETAIN
*/
public static final String DIRTY = "dirty";
/**
+ * Value of {@link #DIRTY} when updating to indicate that the value should not be updated
+ * during this operation.
+ */
+ public static final int DIRTY_RETAIN = -1;
+
+ /**
* Flag used to indicate that the voicemail was deleted but not synced to the server.
* A deleted row should be ignored.
* The value will be 1 if deleted is true, 0 if false.
diff --git a/android/security/AttestedKeyPair.java b/android/security/AttestedKeyPair.java
new file mode 100644
index 00000000..c6bff5c1
--- /dev/null
+++ b/android/security/AttestedKeyPair.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security;
+
+import java.security.KeyPair;
+import java.security.cert.Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * The {@code AttestedKeyPair} class contains a {@code KeyPair} instance of
+ * keys generated by Keystore and owned by KeyChain, as well as an attestation
+ * record for the key.
+ *
+ * <p>Such keys can be obtained by calling
+ * {@link android.app.admin.DevicePolicyManager#generateKeyPair}.
+ */
+
+public final class AttestedKeyPair {
+ private final KeyPair mKeyPair;
+ private final Certificate[] mAttestationRecord;
+
+ /**
+ * @hide Only created by the platform, no need to expose as public API.
+ */
+ public AttestedKeyPair(KeyPair keyPair, Certificate[] attestationRecord) {
+ mKeyPair = keyPair;
+ mAttestationRecord = attestationRecord;
+ }
+
+ /**
+ * Returns the generated key pair associated with the attestation record
+ * in this instance.
+ */
+ public KeyPair getKeyPair() {
+ return mKeyPair;
+ }
+
+ /**
+ * Returns the attestation record for the key pair in this instance.
+ *
+ * The attestation record is a chain of certificates. The leaf certificate links to the public
+ * key of this key pair and other properties of the key or the device. If the key is in secure
+ * hardware, and if the secure hardware supports attestation, the leaf certificate will be
+ * signed by a chain of certificates rooted at a trustworthy CA key. Otherwise the chain will be
+ * rooted at an untrusted certificate.
+ *
+ * The attestation record could be for properties of the key, or include device identifiers.
+ *
+ * See {@link android.security.keystore.KeyGenParameterSpec.Builder#setAttestationChallenge}
+ * and <a href="https://developer.android.com/training/articles/security-key-attestation.html">
+ * Key Attestation</a> for the format of the attestation record inside the certificate.
+ */
+ public List<Certificate> getAttestationRecord() {
+ if (mAttestationRecord == null) {
+ return new ArrayList();
+ }
+ return Arrays.asList(mAttestationRecord);
+ }
+}
diff --git a/android/security/Credentials.java b/android/security/Credentials.java
index 6830a748..57db20be 100644
--- a/android/security/Credentials.java
+++ b/android/security/Credentials.java
@@ -60,10 +60,12 @@ public class Credentials {
/** Key prefix for user certificates. */
public static final String USER_CERTIFICATE = "USRCERT_";
- /** Key prefix for user private keys. */
+ /** Key prefix for user private and secret keys. */
public static final String USER_PRIVATE_KEY = "USRPKEY_";
- /** Key prefix for user secret keys. */
+ /** Key prefix for user secret keys.
+ * @deprecated use {@code USER_PRIVATE_KEY} for this category instead.
+ */
public static final String USER_SECRET_KEY = "USRSKEY_";
/** Key prefix for VPN. */
@@ -235,8 +237,7 @@ public class Credentials {
* Make sure every type is deleted. There can be all three types, so
* don't use a conditional here.
*/
- return deletePrivateKeyTypeForAlias(keystore, alias, uid)
- & deleteSecretKeyTypeForAlias(keystore, alias, uid)
+ return deleteUserKeyTypeForAlias(keystore, alias, uid)
& deleteCertificateTypesForAlias(keystore, alias, uid);
}
@@ -264,34 +265,27 @@ public class Credentials {
}
/**
- * Delete private key for a particular {@code alias}.
- * Returns {@code true} if the entry no longer exists.
- */
- static boolean deletePrivateKeyTypeForAlias(KeyStore keystore, String alias) {
- return deletePrivateKeyTypeForAlias(keystore, alias, KeyStore.UID_SELF);
- }
-
- /**
- * Delete private key for a particular {@code alias}.
+ * Delete user key for a particular {@code alias}.
* Returns {@code true} if the entry no longer exists.
*/
- static boolean deletePrivateKeyTypeForAlias(KeyStore keystore, String alias, int uid) {
- return keystore.delete(Credentials.USER_PRIVATE_KEY + alias, uid);
+ public static boolean deleteUserKeyTypeForAlias(KeyStore keystore, String alias) {
+ return deleteUserKeyTypeForAlias(keystore, alias, KeyStore.UID_SELF);
}
/**
- * Delete secret key for a particular {@code alias}.
+ * Delete user key for a particular {@code alias}.
* Returns {@code true} if the entry no longer exists.
*/
- public static boolean deleteSecretKeyTypeForAlias(KeyStore keystore, String alias) {
- return deleteSecretKeyTypeForAlias(keystore, alias, KeyStore.UID_SELF);
+ public static boolean deleteUserKeyTypeForAlias(KeyStore keystore, String alias, int uid) {
+ return keystore.delete(Credentials.USER_PRIVATE_KEY + alias, uid) ||
+ keystore.delete(Credentials.USER_SECRET_KEY + alias, uid);
}
/**
- * Delete secret key for a particular {@code alias}.
+ * Delete legacy prefixed entry for a particular {@code alias}
* Returns {@code true} if the entry no longer exists.
*/
- public static boolean deleteSecretKeyTypeForAlias(KeyStore keystore, String alias, int uid) {
+ public static boolean deleteLegacyKeyForAlias(KeyStore keystore, String alias, int uid) {
return keystore.delete(Credentials.USER_SECRET_KEY + alias, uid);
}
}
diff --git a/android/security/KeyStore.java b/android/security/KeyStore.java
index 399dddd7..fabcdf00 100644
--- a/android/security/KeyStore.java
+++ b/android/security/KeyStore.java
@@ -95,6 +95,16 @@ public class KeyStore {
public static final int FLAG_ENCRYPTED = 1;
/**
+ * Select Software keymaster device, which as of this writing is the lowest security
+ * level available on an android device. If neither FLAG_STRONGBOX nor FLAG_SOFTWARE is provided
+ * A TEE based keymaster implementation is implied.
+ *
+ * Need to be in sync with KeyStoreFlag in system/security/keystore/include/keystore/keystore.h
+ * For historical reasons this corresponds to the KEYSTORE_FLAG_FALLBACK flag.
+ */
+ public static final int FLAG_SOFTWARE = 1 << 1;
+
+ /**
* A private flag that's only available to system server to indicate that this key is part of
* device encryption flow so it receives special treatment from keystore. For example this key
* will not be super encrypted, and it will be stored separately under an unique UID instead
@@ -104,6 +114,16 @@ public class KeyStore {
*/
public static final int FLAG_CRITICAL_TO_DEVICE_ENCRYPTION = 1 << 3;
+ /**
+ * Select Strongbox keymaster device, which as of this writing the the highest security level
+ * available an android devices. If neither FLAG_STRONGBOX nor FLAG_SOFTWARE is provided
+ * A TEE based keymaster implementation is implied.
+ *
+ * Need to be in sync with KeyStoreFlag in system/security/keystore/include/keystore/keystore.h
+ */
+ public static final int FLAG_STRONGBOX = 1 << 4;
+
+
// States
public enum State { UNLOCKED, LOCKED, UNINITIALIZED };
@@ -440,9 +460,9 @@ public class KeyStore {
return mError;
}
- public boolean addRngEntropy(byte[] data) {
+ public boolean addRngEntropy(byte[] data, int flags) {
try {
- return mBinder.addRngEntropy(data) == NO_ERROR;
+ return mBinder.addRngEntropy(data, flags) == NO_ERROR;
} catch (RemoteException e) {
Log.w(TAG, "Cannot connect to keystore", e);
return false;
diff --git a/android/security/keymaster/KeyAttestationPackageInfo.java b/android/security/keymaster/KeyAttestationPackageInfo.java
index 5a3f3907..a93d1e11 100644
--- a/android/security/keymaster/KeyAttestationPackageInfo.java
+++ b/android/security/keymaster/KeyAttestationPackageInfo.java
@@ -28,7 +28,7 @@ import android.os.Parcelable;
*/
public class KeyAttestationPackageInfo implements Parcelable {
private final String mPackageName;
- private final int mPackageVersionCode;
+ private final long mPackageVersionCode;
private final Signature[] mPackageSignatures;
/**
@@ -37,7 +37,7 @@ public class KeyAttestationPackageInfo implements Parcelable {
* @param mPackageSignatures
*/
public KeyAttestationPackageInfo(
- String mPackageName, int mPackageVersionCode, Signature[] mPackageSignatures) {
+ String mPackageName, long mPackageVersionCode, Signature[] mPackageSignatures) {
super();
this.mPackageName = mPackageName;
this.mPackageVersionCode = mPackageVersionCode;
@@ -52,7 +52,7 @@ public class KeyAttestationPackageInfo implements Parcelable {
/**
* @return the mPackageVersionCode
*/
- public int getPackageVersionCode() {
+ public long getPackageVersionCode() {
return mPackageVersionCode;
}
/**
@@ -70,7 +70,7 @@ public class KeyAttestationPackageInfo implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mPackageName);
- dest.writeInt(mPackageVersionCode);
+ dest.writeLong(mPackageVersionCode);
dest.writeTypedArray(mPackageSignatures, flags);
}
@@ -89,7 +89,7 @@ public class KeyAttestationPackageInfo implements Parcelable {
private KeyAttestationPackageInfo(Parcel source) {
mPackageName = source.readString();
- mPackageVersionCode = source.readInt();
+ mPackageVersionCode = source.readLong();
mPackageSignatures = source.createTypedArray(Signature.CREATOR);
}
}
diff --git a/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java b/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java
index 988e32cf..f1d1e166 100644
--- a/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java
+++ b/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java
@@ -305,7 +305,7 @@ public abstract class AndroidKeyStoreKeyGeneratorSpi extends KeyGeneratorSpi {
KeyStoreCryptoOperationUtils.getRandomBytesToMixIntoKeystoreRng(
mRng, (mKeySizeBits + 7) / 8);
int flags = 0;
- String keyAliasInKeystore = Credentials.USER_SECRET_KEY + spec.getKeystoreAlias();
+ String keyAliasInKeystore = Credentials.USER_PRIVATE_KEY + spec.getKeystoreAlias();
KeyCharacteristics resultingKeyCharacteristics = new KeyCharacteristics();
boolean success = false;
try {
diff --git a/android/security/keystore/AndroidKeyStoreProvider.java b/android/security/keystore/AndroidKeyStoreProvider.java
index f36c00ce..55e6519d 100644
--- a/android/security/keystore/AndroidKeyStoreProvider.java
+++ b/android/security/keystore/AndroidKeyStoreProvider.java
@@ -196,7 +196,7 @@ public class AndroidKeyStoreProvider extends Provider {
}
@NonNull
- public static AndroidKeyStorePrivateKey getAndroidKeyStorePrivateKey(
+ private static AndroidKeyStorePrivateKey getAndroidKeyStorePrivateKey(
@NonNull AndroidKeyStorePublicKey publicKey) {
String keyAlgorithm = publicKey.getAlgorithm();
if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(keyAlgorithm)) {
@@ -212,17 +212,25 @@ public class AndroidKeyStoreProvider extends Provider {
}
@NonNull
- public static AndroidKeyStorePublicKey loadAndroidKeyStorePublicKeyFromKeystore(
- @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid)
+ private static KeyCharacteristics getKeyCharacteristics(@NonNull KeyStore keyStore,
+ @NonNull String alias, int uid)
throws UnrecoverableKeyException {
KeyCharacteristics keyCharacteristics = new KeyCharacteristics();
int errorCode = keyStore.getKeyCharacteristics(
- privateKeyAlias, null, null, uid, keyCharacteristics);
+ alias, null, null, uid, keyCharacteristics);
if (errorCode != KeyStore.NO_ERROR) {
throw (UnrecoverableKeyException)
- new UnrecoverableKeyException("Failed to obtain information about private key")
- .initCause(KeyStore.getKeyStoreException(errorCode));
+ new UnrecoverableKeyException("Failed to obtain information about key")
+ .initCause(KeyStore.getKeyStoreException(errorCode));
}
+ return keyCharacteristics;
+ }
+
+ @NonNull
+ private static AndroidKeyStorePublicKey loadAndroidKeyStorePublicKeyFromKeystore(
+ @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid,
+ KeyCharacteristics keyCharacteristics)
+ throws UnrecoverableKeyException {
ExportResult exportResult = keyStore.exportKey(
privateKeyAlias, KeymasterDefs.KM_KEY_FORMAT_X509, null, null, uid);
if (exportResult.resultCode != KeyStore.NO_ERROR) {
@@ -252,37 +260,56 @@ public class AndroidKeyStoreProvider extends Provider {
}
@NonNull
- public static KeyPair loadAndroidKeyStoreKeyPairFromKeystore(
+ public static AndroidKeyStorePublicKey loadAndroidKeyStorePublicKeyFromKeystore(
@NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid)
throws UnrecoverableKeyException {
+ return loadAndroidKeyStorePublicKeyFromKeystore(keyStore, privateKeyAlias, uid,
+ getKeyCharacteristics(keyStore, privateKeyAlias, uid));
+ }
+
+ @NonNull
+ private static KeyPair loadAndroidKeyStoreKeyPairFromKeystore(
+ @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid,
+ @NonNull KeyCharacteristics keyCharacteristics)
+ throws UnrecoverableKeyException {
AndroidKeyStorePublicKey publicKey =
- loadAndroidKeyStorePublicKeyFromKeystore(keyStore, privateKeyAlias, uid);
+ loadAndroidKeyStorePublicKeyFromKeystore(keyStore, privateKeyAlias, uid,
+ keyCharacteristics);
AndroidKeyStorePrivateKey privateKey =
AndroidKeyStoreProvider.getAndroidKeyStorePrivateKey(publicKey);
return new KeyPair(publicKey, privateKey);
}
@NonNull
- public static AndroidKeyStorePrivateKey loadAndroidKeyStorePrivateKeyFromKeystore(
+ public static KeyPair loadAndroidKeyStoreKeyPairFromKeystore(
@NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid)
throws UnrecoverableKeyException {
- KeyPair keyPair = loadAndroidKeyStoreKeyPairFromKeystore(keyStore, privateKeyAlias, uid);
+ return loadAndroidKeyStoreKeyPairFromKeystore(keyStore, privateKeyAlias, uid,
+ getKeyCharacteristics(keyStore, privateKeyAlias, uid));
+ }
+
+ @NonNull
+ private static AndroidKeyStorePrivateKey loadAndroidKeyStorePrivateKeyFromKeystore(
+ @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid,
+ @NonNull KeyCharacteristics keyCharacteristics)
+ throws UnrecoverableKeyException {
+ KeyPair keyPair = loadAndroidKeyStoreKeyPairFromKeystore(keyStore, privateKeyAlias, uid,
+ keyCharacteristics);
return (AndroidKeyStorePrivateKey) keyPair.getPrivate();
}
@NonNull
- public static AndroidKeyStoreSecretKey loadAndroidKeyStoreSecretKeyFromKeystore(
- @NonNull KeyStore keyStore, @NonNull String secretKeyAlias, int uid)
+ public static AndroidKeyStorePrivateKey loadAndroidKeyStorePrivateKeyFromKeystore(
+ @NonNull KeyStore keyStore, @NonNull String privateKeyAlias, int uid)
throws UnrecoverableKeyException {
- KeyCharacteristics keyCharacteristics = new KeyCharacteristics();
- int errorCode = keyStore.getKeyCharacteristics(
- secretKeyAlias, null, null, uid, keyCharacteristics);
- if (errorCode != KeyStore.NO_ERROR) {
- throw (UnrecoverableKeyException)
- new UnrecoverableKeyException("Failed to obtain information about key")
- .initCause(KeyStore.getKeyStoreException(errorCode));
- }
+ return loadAndroidKeyStorePrivateKeyFromKeystore(keyStore, privateKeyAlias, uid,
+ getKeyCharacteristics(keyStore, privateKeyAlias, uid));
+ }
+ @NonNull
+ private static AndroidKeyStoreSecretKey loadAndroidKeyStoreSecretKeyFromKeystore(
+ @NonNull String secretKeyAlias, int uid, @NonNull KeyCharacteristics keyCharacteristics)
+ throws UnrecoverableKeyException {
Integer keymasterAlgorithm = keyCharacteristics.getEnum(KeymasterDefs.KM_TAG_ALGORITHM);
if (keymasterAlgorithm == null) {
throw new UnrecoverableKeyException("Key algorithm unknown");
@@ -310,6 +337,29 @@ public class AndroidKeyStoreProvider extends Provider {
return new AndroidKeyStoreSecretKey(secretKeyAlias, uid, keyAlgorithmString);
}
+ public static AndroidKeyStoreKey loadAndroidKeyStoreKeyFromKeystore(
+ @NonNull KeyStore keyStore, @NonNull String userKeyAlias, int uid)
+ throws UnrecoverableKeyException {
+ KeyCharacteristics keyCharacteristics = getKeyCharacteristics(keyStore, userKeyAlias, uid);
+
+ Integer keymasterAlgorithm = keyCharacteristics.getEnum(KeymasterDefs.KM_TAG_ALGORITHM);
+ if (keymasterAlgorithm == null) {
+ throw new UnrecoverableKeyException("Key algorithm unknown");
+ }
+
+ if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC ||
+ keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_AES) {
+ return loadAndroidKeyStoreSecretKeyFromKeystore(userKeyAlias, uid,
+ keyCharacteristics);
+ } else if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_RSA ||
+ keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_EC) {
+ return loadAndroidKeyStorePrivateKeyFromKeystore(keyStore, userKeyAlias, uid,
+ keyCharacteristics);
+ } else {
+ throw new UnrecoverableKeyException("Key algorithm unknown");
+ }
+ }
+
/**
* Returns an {@code AndroidKeyStore} {@link java.security.KeyStore}} of the specified UID.
* The {@code KeyStore} contains keys and certificates owned by that UID. Such cross-UID
diff --git a/android/security/keystore/AndroidKeyStoreSecretKeyFactorySpi.java b/android/security/keystore/AndroidKeyStoreSecretKeyFactorySpi.java
index 0379863e..fdb885db 100644
--- a/android/security/keystore/AndroidKeyStoreSecretKeyFactorySpi.java
+++ b/android/security/keystore/AndroidKeyStoreSecretKeyFactorySpi.java
@@ -64,7 +64,10 @@ public class AndroidKeyStoreSecretKeyFactorySpi extends SecretKeyFactorySpi {
AndroidKeyStoreKey keystoreKey = (AndroidKeyStoreKey) key;
String keyAliasInKeystore = keystoreKey.getAlias();
String entryAlias;
- if (keyAliasInKeystore.startsWith(Credentials.USER_SECRET_KEY)) {
+ if (keyAliasInKeystore.startsWith(Credentials.USER_PRIVATE_KEY)) {
+ entryAlias = keyAliasInKeystore.substring(Credentials.USER_PRIVATE_KEY.length());
+ } else if (keyAliasInKeystore.startsWith(Credentials.USER_SECRET_KEY)){
+ // key has legacy prefix
entryAlias = keyAliasInKeystore.substring(Credentials.USER_SECRET_KEY.length());
} else {
throw new InvalidKeySpecException("Invalid key alias: " + keyAliasInKeystore);
diff --git a/android/security/keystore/AndroidKeyStoreSpi.java b/android/security/keystore/AndroidKeyStoreSpi.java
index bab4010b..d73a9e29 100644
--- a/android/security/keystore/AndroidKeyStoreSpi.java
+++ b/android/security/keystore/AndroidKeyStoreSpi.java
@@ -89,18 +89,14 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi {
@Override
public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException,
UnrecoverableKeyException {
- if (isPrivateKeyEntry(alias)) {
- String privateKeyAlias = Credentials.USER_PRIVATE_KEY + alias;
- return AndroidKeyStoreProvider.loadAndroidKeyStorePrivateKeyFromKeystore(
- mKeyStore, privateKeyAlias, mUid);
- } else if (isSecretKeyEntry(alias)) {
- String secretKeyAlias = Credentials.USER_SECRET_KEY + alias;
- return AndroidKeyStoreProvider.loadAndroidKeyStoreSecretKeyFromKeystore(
- mKeyStore, secretKeyAlias, mUid);
- } else {
- // Key not found
- return null;
+ String userKeyAlias = Credentials.USER_PRIVATE_KEY + alias;
+ if (!mKeyStore.contains(userKeyAlias, mUid)) {
+ // try legacy prefix for backward compatibility
+ userKeyAlias = Credentials.USER_SECRET_KEY + alias;
+ if (!mKeyStore.contains(userKeyAlias, mUid)) return null;
}
+ return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(mKeyStore, userKeyAlias,
+ mUid);
}
@Override
@@ -540,7 +536,7 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi {
} else {
// Keep the stored private key around -- delete all other entry types
Credentials.deleteCertificateTypesForAlias(mKeyStore, alias, mUid);
- Credentials.deleteSecretKeyTypeForAlias(mKeyStore, alias, mUid);
+ Credentials.deleteLegacyKeyForAlias(mKeyStore, alias, mUid);
}
// Store the leaf certificate
@@ -565,7 +561,7 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi {
Credentials.deleteAllTypesForAlias(mKeyStore, alias, mUid);
} else {
Credentials.deleteCertificateTypesForAlias(mKeyStore, alias, mUid);
- Credentials.deleteSecretKeyTypeForAlias(mKeyStore, alias, mUid);
+ Credentials.deleteLegacyKeyForAlias(mKeyStore, alias, mUid);
}
}
}
@@ -588,12 +584,17 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi {
if (keyAliasInKeystore == null) {
throw new KeyStoreException("KeyStore-backed secret key does not have an alias");
}
- if (!keyAliasInKeystore.startsWith(Credentials.USER_SECRET_KEY)) {
- throw new KeyStoreException("KeyStore-backed secret key has invalid alias: "
- + keyAliasInKeystore);
+ String keyAliasPrefix = Credentials.USER_PRIVATE_KEY;
+ if (!keyAliasInKeystore.startsWith(keyAliasPrefix)) {
+ // try legacy prefix
+ keyAliasPrefix = Credentials.USER_SECRET_KEY;
+ if (!keyAliasInKeystore.startsWith(keyAliasPrefix)) {
+ throw new KeyStoreException("KeyStore-backed secret key has invalid alias: "
+ + keyAliasInKeystore);
+ }
}
String keyEntryAlias =
- keyAliasInKeystore.substring(Credentials.USER_SECRET_KEY.length());
+ keyAliasInKeystore.substring(keyAliasPrefix.length());
if (!entryAlias.equals(keyEntryAlias)) {
throw new KeyStoreException("Can only replace KeyStore-backed keys with same"
+ " alias: " + entryAlias + " != " + keyEntryAlias);
@@ -728,7 +729,7 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi {
}
Credentials.deleteAllTypesForAlias(mKeyStore, entryAlias, mUid);
- String keyAliasInKeystore = Credentials.USER_SECRET_KEY + entryAlias;
+ String keyAliasInKeystore = Credentials.USER_PRIVATE_KEY + entryAlias;
int errorCode = mKeyStore.importKey(
keyAliasInKeystore,
args,
@@ -827,24 +828,10 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi {
}
private boolean isKeyEntry(String alias) {
- return isPrivateKeyEntry(alias) || isSecretKeyEntry(alias);
- }
-
- private boolean isPrivateKeyEntry(String alias) {
- if (alias == null) {
- throw new NullPointerException("alias == null");
- }
-
- return mKeyStore.contains(Credentials.USER_PRIVATE_KEY + alias, mUid);
+ return mKeyStore.contains(Credentials.USER_PRIVATE_KEY + alias, mUid) ||
+ mKeyStore.contains(Credentials.USER_SECRET_KEY + alias, mUid);
}
- private boolean isSecretKeyEntry(String alias) {
- if (alias == null) {
- throw new NullPointerException("alias == null");
- }
-
- return mKeyStore.contains(Credentials.USER_SECRET_KEY + alias, mUid);
- }
private boolean isCertificateEntry(String alias) {
if (alias == null) {
diff --git a/android/security/keystore/AttestationUtils.java b/android/security/keystore/AttestationUtils.java
index cf4347d1..0811100f 100644
--- a/android/security/keystore/AttestationUtils.java
+++ b/android/security/keystore/AttestationUtils.java
@@ -73,6 +73,33 @@ public abstract class AttestationUtils {
public static final int ID_TYPE_MEID = 3;
/**
+ * Creates an array of X509Certificates from the provided KeymasterCertificateChain.
+ *
+ * @hide Only called by the DevicePolicyManager.
+ */
+ @NonNull public static X509Certificate[] parseCertificateChain(
+ final KeymasterCertificateChain kmChain) throws
+ KeyAttestationException {
+ // Extract certificate chain.
+ final Collection<byte[]> rawChain = kmChain.getCertificates();
+ if (rawChain.size() < 2) {
+ throw new KeyAttestationException("Attestation certificate chain contained "
+ + rawChain.size() + " entries. At least two are required.");
+ }
+ final ByteArrayOutputStream concatenatedRawChain = new ByteArrayOutputStream();
+ try {
+ for (final byte[] cert : rawChain) {
+ concatenatedRawChain.write(cert);
+ }
+ return CertificateFactory.getInstance("X.509").generateCertificates(
+ new ByteArrayInputStream(concatenatedRawChain.toByteArray()))
+ .toArray(new X509Certificate[0]);
+ } catch (Exception e) {
+ throw new KeyAttestationException("Unable to construct certificate chain", e);
+ }
+ }
+
+ /**
* Performs attestation of the device's identifiers. This method returns a certificate chain
* whose first element contains the requested device identifiers in an extension. The device's
* manufacturer, model, brand, device and product are always also included in the attestation.
@@ -173,22 +200,18 @@ public abstract class AttestationUtils {
KeyStore.getKeyStoreException(errorCode));
}
- // Extract certificate chain.
- final Collection<byte[]> rawChain = outChain.getCertificates();
- if (rawChain.size() < 2) {
- throw new DeviceIdAttestationException("Attestation certificate chain contained "
- + rawChain.size() + " entries. At least two are required.");
- }
- final ByteArrayOutputStream concatenatedRawChain = new ByteArrayOutputStream();
try {
- for (final byte[] cert : rawChain) {
- concatenatedRawChain.write(cert);
- }
- return CertificateFactory.getInstance("X.509").generateCertificates(
- new ByteArrayInputStream(concatenatedRawChain.toByteArray()))
- .toArray(new X509Certificate[0]);
- } catch (Exception e) {
- throw new DeviceIdAttestationException("Unable to construct certificate chain", e);
+ return parseCertificateChain(outChain);
+ } catch (KeyAttestationException e) {
+ throw new DeviceIdAttestationException(e.getMessage(), e);
}
}
+
+ /**
+ * Returns true if the attestation chain provided is a valid key attestation chain.
+ * @hide
+ */
+ public static boolean isChainValid(KeymasterCertificateChain chain) {
+ return chain != null && chain.getCertificates().size() >= 2;
+ }
}
diff --git a/android/security/keystore/KeyAttestationException.java b/android/security/keystore/KeyAttestationException.java
new file mode 100644
index 00000000..6cf5fb2f
--- /dev/null
+++ b/android/security/keystore/KeyAttestationException.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+/**
+ * Thrown when {@link AttestationUtils} is unable to attest the given key or handle
+ * the resulting attestation record.
+ *
+ * @hide
+ */
+public class KeyAttestationException extends Exception {
+ /**
+ * Constructs a new {@code KeyAttestationException} with the current stack trace and the
+ * specified detail message.
+ *
+ * @param detailMessage the detail message for this exception.
+ */
+ public KeyAttestationException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ /**
+ * Constructs a new {@code KeyAttestationException} with the current stack trace, the
+ * specified detail message and the specified cause.
+ *
+ * @param message the detail message for this exception.
+ * @param cause the cause of this exception, may be {@code null}.
+ */
+ public KeyAttestationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/android/security/keystore/KeyGenParameterSpec.java b/android/security/keystore/KeyGenParameterSpec.java
index ed40b77b..1238d877 100644
--- a/android/security/keystore/KeyGenParameterSpec.java
+++ b/android/security/keystore/KeyGenParameterSpec.java
@@ -195,7 +195,7 @@ import javax.security.auth.x500.X500Principal;
* <pre> {@code
* KeyGenerator keyGenerator = KeyGenerator.getInstance(
* KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
- * keyGenerator.initialize(
+ * keyGenerator.init(
* new KeyGenParameterSpec.Builder("key2",
* KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
* .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
@@ -219,7 +219,7 @@ import javax.security.auth.x500.X500Principal;
* <pre> {@code
* KeyGenerator keyGenerator = KeyGenerator.getInstance(
* KeyProperties.KEY_ALGORITHM_HMAC_SHA256, "AndroidKeyStore");
- * keyGenerator.initialize(
+ * keyGenerator.init(
* new KeyGenParameterSpec.Builder("key2", KeyProperties.PURPOSE_SIGN).build());
* SecretKey key = keyGenerator.generateKey();
* Mac mac = Mac.getInstance("HmacSHA256");
@@ -680,6 +680,40 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec {
}
/**
+ * A Builder constructor taking in an already-built KeyGenParameterSpec, useful for
+ * changing values of the KeyGenParameterSpec quickly.
+ * @hide Should be used internally only.
+ */
+ public Builder(@NonNull KeyGenParameterSpec sourceSpec) {
+ this(sourceSpec.getKeystoreAlias(), sourceSpec.getPurposes());
+ mUid = sourceSpec.getUid();
+ mKeySize = sourceSpec.getKeySize();
+ mSpec = sourceSpec.getAlgorithmParameterSpec();
+ mCertificateSubject = sourceSpec.getCertificateSubject();
+ mCertificateSerialNumber = sourceSpec.getCertificateSerialNumber();
+ mCertificateNotBefore = sourceSpec.getCertificateNotBefore();
+ mCertificateNotAfter = sourceSpec.getCertificateNotAfter();
+ mKeyValidityStart = sourceSpec.getKeyValidityStart();
+ mKeyValidityForOriginationEnd = sourceSpec.getKeyValidityForOriginationEnd();
+ mKeyValidityForConsumptionEnd = sourceSpec.getKeyValidityForConsumptionEnd();
+ mPurposes = sourceSpec.getPurposes();
+ if (sourceSpec.isDigestsSpecified()) {
+ mDigests = sourceSpec.getDigests();
+ }
+ mEncryptionPaddings = sourceSpec.getEncryptionPaddings();
+ mSignaturePaddings = sourceSpec.getSignaturePaddings();
+ mBlockModes = sourceSpec.getBlockModes();
+ mRandomizedEncryptionRequired = sourceSpec.isRandomizedEncryptionRequired();
+ mUserAuthenticationRequired = sourceSpec.isUserAuthenticationRequired();
+ mUserAuthenticationValidityDurationSeconds =
+ sourceSpec.getUserAuthenticationValidityDurationSeconds();
+ mAttestationChallenge = sourceSpec.getAttestationChallenge();
+ mUniqueIdIncluded = sourceSpec.isUniqueIdIncluded();
+ mUserAuthenticationValidWhileOnBody = sourceSpec.isUserAuthenticationValidWhileOnBody();
+ mInvalidatedByBiometricEnrollment = sourceSpec.isInvalidatedByBiometricEnrollment();
+ }
+
+ /**
* Sets the UID which will own the key.
*
* @param uid UID or {@code -1} for the UID of the current process.
diff --git a/android/security/keystore/KeyProperties.java b/android/security/keystore/KeyProperties.java
index d6b1cf1d..a250d1f0 100644
--- a/android/security/keystore/KeyProperties.java
+++ b/android/security/keystore/KeyProperties.java
@@ -39,13 +39,12 @@ public abstract class KeyProperties {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true,
- value = {
- PURPOSE_ENCRYPT,
- PURPOSE_DECRYPT,
- PURPOSE_SIGN,
- PURPOSE_VERIFY,
- })
+ @IntDef(flag = true, prefix = { "PURPOSE_" }, value = {
+ PURPOSE_ENCRYPT,
+ PURPOSE_DECRYPT,
+ PURPOSE_SIGN,
+ PURPOSE_VERIFY,
+ })
public @interface PurposeEnum {}
/**
@@ -126,7 +125,7 @@ public abstract class KeyProperties {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @StringDef({
+ @StringDef(prefix = { "KEY_" }, value = {
KEY_ALGORITHM_RSA,
KEY_ALGORITHM_EC,
KEY_ALGORITHM_AES,
@@ -267,7 +266,7 @@ public abstract class KeyProperties {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @StringDef({
+ @StringDef(prefix = { "BLOCK_MODE_" }, value = {
BLOCK_MODE_ECB,
BLOCK_MODE_CBC,
BLOCK_MODE_CTR,
@@ -354,7 +353,7 @@ public abstract class KeyProperties {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @StringDef({
+ @StringDef(prefix = { "ENCRYPTION_PADDING_" }, value = {
ENCRYPTION_PADDING_NONE,
ENCRYPTION_PADDING_PKCS7,
ENCRYPTION_PADDING_RSA_PKCS1,
@@ -437,7 +436,7 @@ public abstract class KeyProperties {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @StringDef({
+ @StringDef(prefix = { "SIGNATURE_PADDING_" }, value = {
SIGNATURE_PADDING_RSA_PKCS1,
SIGNATURE_PADDING_RSA_PSS,
})
@@ -497,7 +496,7 @@ public abstract class KeyProperties {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @StringDef({
+ @StringDef(prefix = { "DIGEST_" }, value = {
DIGEST_NONE,
DIGEST_MD5,
DIGEST_SHA1,
@@ -647,11 +646,12 @@ public abstract class KeyProperties {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({
- ORIGIN_GENERATED,
- ORIGIN_IMPORTED,
- ORIGIN_UNKNOWN,
- })
+ @IntDef(prefix = { "ORIGIN_" }, value = {
+ ORIGIN_GENERATED,
+ ORIGIN_IMPORTED,
+ ORIGIN_UNKNOWN,
+ })
+
public @interface OriginEnum {}
/** Key was generated inside AndroidKeyStore. */
diff --git a/android/security/keystore/ParcelableKeyGenParameterSpec.java b/android/security/keystore/ParcelableKeyGenParameterSpec.java
new file mode 100644
index 00000000..7cb8e375
--- /dev/null
+++ b/android/security/keystore/ParcelableKeyGenParameterSpec.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import java.math.BigInteger;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.RSAKeyGenParameterSpec;
+import java.util.Date;
+
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * A parcelable version of KeyGenParameterSpec
+ * @hide only used for communicating with the DPMS.
+ */
+public final class ParcelableKeyGenParameterSpec implements Parcelable {
+ private static final int ALGORITHM_PARAMETER_SPEC_NONE = 1;
+ private static final int ALGORITHM_PARAMETER_SPEC_RSA = 2;
+ private static final int ALGORITHM_PARAMETER_SPEC_EC = 3;
+
+ private final KeyGenParameterSpec mSpec;
+
+ public ParcelableKeyGenParameterSpec(
+ KeyGenParameterSpec spec) {
+ mSpec = spec;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ private static void writeOptionalDate(Parcel out, Date date) {
+ if (date != null) {
+ out.writeBoolean(true);
+ out.writeLong(date.getTime());
+ } else {
+ out.writeBoolean(false);
+ }
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mSpec.getKeystoreAlias());
+ out.writeInt(mSpec.getPurposes());
+ out.writeInt(mSpec.getUid());
+ out.writeInt(mSpec.getKeySize());
+
+ // Only needs to support RSAKeyGenParameterSpec and ECGenParameterSpec.
+ AlgorithmParameterSpec algoSpec = mSpec.getAlgorithmParameterSpec();
+ if (algoSpec == null) {
+ out.writeInt(ALGORITHM_PARAMETER_SPEC_NONE);
+ } else if (algoSpec instanceof RSAKeyGenParameterSpec) {
+ RSAKeyGenParameterSpec rsaSpec = (RSAKeyGenParameterSpec) algoSpec;
+ out.writeInt(ALGORITHM_PARAMETER_SPEC_RSA);
+ out.writeInt(rsaSpec.getKeysize());
+ out.writeByteArray(rsaSpec.getPublicExponent().toByteArray());
+ } else if (algoSpec instanceof ECGenParameterSpec) {
+ ECGenParameterSpec ecSpec = (ECGenParameterSpec) algoSpec;
+ out.writeInt(ALGORITHM_PARAMETER_SPEC_EC);
+ out.writeString(ecSpec.getName());
+ } else {
+ throw new IllegalArgumentException(
+ String.format("Unknown algorithm parameter spec: %s", algoSpec.getClass()));
+ }
+ out.writeByteArray(mSpec.getCertificateSubject().getEncoded());
+ out.writeByteArray(mSpec.getCertificateSerialNumber().toByteArray());
+ out.writeLong(mSpec.getCertificateNotBefore().getTime());
+ out.writeLong(mSpec.getCertificateNotAfter().getTime());
+ writeOptionalDate(out, mSpec.getKeyValidityStart());
+ writeOptionalDate(out, mSpec.getKeyValidityForOriginationEnd());
+ writeOptionalDate(out, mSpec.getKeyValidityForConsumptionEnd());
+ if (mSpec.isDigestsSpecified()) {
+ out.writeStringArray(mSpec.getDigests());
+ } else {
+ out.writeStringArray(null);
+ }
+ out.writeStringArray(mSpec.getEncryptionPaddings());
+ out.writeStringArray(mSpec.getSignaturePaddings());
+ out.writeStringArray(mSpec.getBlockModes());
+ out.writeBoolean(mSpec.isRandomizedEncryptionRequired());
+ out.writeBoolean(mSpec.isUserAuthenticationRequired());
+ out.writeInt(mSpec.getUserAuthenticationValidityDurationSeconds());
+ out.writeByteArray(mSpec.getAttestationChallenge());
+ out.writeBoolean(mSpec.isUniqueIdIncluded());
+ out.writeBoolean(mSpec.isUserAuthenticationValidWhileOnBody());
+ out.writeBoolean(mSpec.isInvalidatedByBiometricEnrollment());
+ }
+
+ private static Date readDateOrNull(Parcel in) {
+ boolean hasDate = in.readBoolean();
+ if (hasDate) {
+ return new Date(in.readLong());
+ } else {
+ return null;
+ }
+ }
+
+ private ParcelableKeyGenParameterSpec(Parcel in) {
+ String keystoreAlias = in.readString();
+ int purposes = in.readInt();
+ KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
+ keystoreAlias, purposes);
+ builder.setUid(in.readInt());
+ // KeySize is -1 by default, if the KeyGenParameterSpec previously parcelled had the default
+ // value, do not set it as this will cause setKeySize to throw.
+ int keySize = in.readInt();
+ if (keySize >= 0) {
+ builder.setKeySize(keySize);
+ }
+
+ int keySpecType = in.readInt();
+ AlgorithmParameterSpec algorithmSpec = null;
+ if (keySpecType == ALGORITHM_PARAMETER_SPEC_NONE) {
+ algorithmSpec = null;
+ } else if (keySpecType == ALGORITHM_PARAMETER_SPEC_RSA) {
+ int rsaKeySize = in.readInt();
+ BigInteger publicExponent = new BigInteger(in.createByteArray());
+ algorithmSpec = new RSAKeyGenParameterSpec(rsaKeySize, publicExponent);
+ } else if (keySpecType == ALGORITHM_PARAMETER_SPEC_EC) {
+ String stdName = in.readString();
+ algorithmSpec = new ECGenParameterSpec(stdName);
+ } else {
+ throw new IllegalArgumentException(
+ String.format("Unknown algorithm parameter spec: %d", keySpecType));
+ }
+ if (algorithmSpec != null) {
+ builder.setAlgorithmParameterSpec(algorithmSpec);
+ }
+ builder.setCertificateSubject(new X500Principal(in.createByteArray()));
+ builder.setCertificateSerialNumber(new BigInteger(in.createByteArray()));
+ builder.setCertificateNotBefore(new Date(in.readLong()));
+ builder.setCertificateNotAfter(new Date(in.readLong()));
+ builder.setKeyValidityStart(readDateOrNull(in));
+ builder.setKeyValidityForOriginationEnd(readDateOrNull(in));
+ builder.setKeyValidityForConsumptionEnd(readDateOrNull(in));
+ String[] digests = in.createStringArray();
+ if (digests != null) {
+ builder.setDigests(digests);
+ }
+ builder.setEncryptionPaddings(in.createStringArray());
+ builder.setSignaturePaddings(in.createStringArray());
+ builder.setBlockModes(in.createStringArray());
+ builder.setRandomizedEncryptionRequired(in.readBoolean());
+ builder.setUserAuthenticationRequired(in.readBoolean());
+ builder.setUserAuthenticationValidityDurationSeconds(in.readInt());
+ builder.setAttestationChallenge(in.createByteArray());
+ builder.setUniqueIdIncluded(in.readBoolean());
+ builder.setUserAuthenticationValidWhileOnBody(in.readBoolean());
+ builder.setInvalidatedByBiometricEnrollment(in.readBoolean());
+ mSpec = builder.build();
+ }
+
+ public static final Creator<ParcelableKeyGenParameterSpec> CREATOR = new Creator<ParcelableKeyGenParameterSpec>() {
+ @Override
+ public ParcelableKeyGenParameterSpec createFromParcel(Parcel in) {
+ return new ParcelableKeyGenParameterSpec(in);
+ }
+
+ @Override
+ public ParcelableKeyGenParameterSpec[] newArray(int size) {
+ return new ParcelableKeyGenParameterSpec[size];
+ }
+ };
+
+ public KeyGenParameterSpec getSpec() {
+ return mSpec;
+ }
+}
diff --git a/android/security/recoverablekeystore/KeyDerivationParameters.java b/android/security/recoverablekeystore/KeyDerivationParameters.java
new file mode 100644
index 00000000..978e60ee
--- /dev/null
+++ b/android/security/recoverablekeystore/KeyDerivationParameters.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.recoverablekeystore;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Collection of parameters which define a key derivation function.
+ * Supports
+ *
+ * <ul>
+ * <li>SHA256
+ * <li>Argon2id
+ * </ul>
+ * @hide
+ */
+public final class KeyDerivationParameters implements Parcelable {
+ private final int mAlgorithm;
+ private byte[] mSalt;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ALGORITHM_SHA256, ALGORITHM_ARGON2ID})
+ public @interface KeyDerivationAlgorithm {
+ }
+
+ /**
+ * Salted SHA256
+ */
+ public static final int ALGORITHM_SHA256 = 1;
+
+ /**
+ * Argon2ID
+ */
+ // TODO: add Argon2ID support.
+ public static final int ALGORITHM_ARGON2ID = 2;
+
+ /**
+ * Creates instance of the class to to derive key using salted SHA256 hash.
+ */
+ public static KeyDerivationParameters createSHA256Parameters(@NonNull byte[] salt) {
+ return new KeyDerivationParameters(ALGORITHM_SHA256, salt);
+ }
+
+ private KeyDerivationParameters(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) {
+ mAlgorithm = algorithm;
+ mSalt = Preconditions.checkNotNull(salt);
+ }
+
+ /**
+ * Gets algorithm.
+ */
+ public @KeyDerivationAlgorithm int getAlgorithm() {
+ return mAlgorithm;
+ }
+
+ /**
+ * Gets salt.
+ */
+ public @NonNull byte[] getSalt() {
+ return mSalt;
+ }
+
+ public static final Parcelable.Creator<KeyDerivationParameters> CREATOR =
+ new Parcelable.Creator<KeyDerivationParameters>() {
+ public KeyDerivationParameters createFromParcel(Parcel in) {
+ return new KeyDerivationParameters(in);
+ }
+
+ public KeyDerivationParameters[] newArray(int length) {
+ return new KeyDerivationParameters[length];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mAlgorithm);
+ out.writeByteArray(mSalt);
+ }
+
+ protected KeyDerivationParameters(Parcel in) {
+ mAlgorithm = in.readInt();
+ mSalt = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/recoverablekeystore/KeyEntryRecoveryData.java b/android/security/recoverablekeystore/KeyEntryRecoveryData.java
new file mode 100644
index 00000000..80f5aa71
--- /dev/null
+++ b/android/security/recoverablekeystore/KeyEntryRecoveryData.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.recoverablekeystore;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+
+/**
+ * Helper class with data necessary recover a single application key, given a recovery key.
+ *
+ * <ul>
+ * <li>Alias - Keystore alias of the key.
+ * <li>Encrypted key material.
+ * </ul>
+ *
+ * Note that Application info is not included. Recovery Agent can only make its own keys
+ * recoverable.
+ *
+ * @hide
+ */
+public final class KeyEntryRecoveryData implements Parcelable {
+ private final byte[] mAlias;
+ // The only supported format is AES-256 symmetric key.
+ private final byte[] mEncryptedKeyMaterial;
+
+ public KeyEntryRecoveryData(@NonNull byte[] alias, @NonNull byte[] encryptedKeyMaterial) {
+ mAlias = Preconditions.checkNotNull(alias);
+ mEncryptedKeyMaterial = Preconditions.checkNotNull(encryptedKeyMaterial);
+ }
+
+ /**
+ * Application-specific alias of the key.
+ * @see java.security.KeyStore.aliases
+ */
+ public @NonNull byte[] getAlias() {
+ return mAlias;
+ }
+
+ /**
+ * Encrypted key material encrypted by recovery key.
+ */
+ public @NonNull byte[] getEncryptedKeyMaterial() {
+ return mEncryptedKeyMaterial;
+ }
+
+ public static final Parcelable.Creator<KeyEntryRecoveryData> CREATOR =
+ new Parcelable.Creator<KeyEntryRecoveryData>() {
+ public KeyEntryRecoveryData createFromParcel(Parcel in) {
+ return new KeyEntryRecoveryData(in);
+ }
+
+ public KeyEntryRecoveryData[] newArray(int length) {
+ return new KeyEntryRecoveryData[length];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeByteArray(mAlias);
+ out.writeByteArray(mEncryptedKeyMaterial);
+ }
+
+ protected KeyEntryRecoveryData(Parcel in) {
+ mAlias = in.createByteArray();
+ mEncryptedKeyMaterial = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/recoverablekeystore/KeyStoreRecoveryData.java b/android/security/recoverablekeystore/KeyStoreRecoveryData.java
new file mode 100644
index 00000000..087f7a25
--- /dev/null
+++ b/android/security/recoverablekeystore/KeyStoreRecoveryData.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.recoverablekeystore;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * Helper class which returns data necessary to recover keys.
+ * Contains
+ *
+ * <ul>
+ * <li>Snapshot version.
+ * <li>Recovery metadata with UI and key derivation parameters.
+ * <li>List of application keys encrypted by recovery key.
+ * <li>Encrypted recovery key.
+ * </ul>
+ *
+ * @hide
+ */
+public final class KeyStoreRecoveryData implements Parcelable {
+ private final int mSnapshotVersion;
+ private final List<KeyStoreRecoveryMetadata> mRecoveryMetadata;
+ private final List<KeyEntryRecoveryData> mApplicationKeyBlobs;
+ private final byte[] mEncryptedRecoveryKeyBlob;
+
+ public KeyStoreRecoveryData(int snapshotVersion, @NonNull List<KeyStoreRecoveryMetadata>
+ recoveryMetadata, @NonNull List<KeyEntryRecoveryData> applicationKeyBlobs,
+ @NonNull byte[] encryptedRecoveryKeyBlob) {
+ mSnapshotVersion = snapshotVersion;
+ mRecoveryMetadata = Preconditions.checkNotNull(recoveryMetadata);
+ mApplicationKeyBlobs = Preconditions.checkNotNull(applicationKeyBlobs);
+ mEncryptedRecoveryKeyBlob = Preconditions.checkNotNull(encryptedRecoveryKeyBlob);
+ }
+
+ /**
+ * Snapshot version for given account. It is incremented when user secret or list of application
+ * keys changes.
+ */
+ public int getSnapshotVersion() {
+ return mSnapshotVersion;
+ }
+
+ /**
+ * UI and key derivation parameters. Note that combination of secrets may be used.
+ */
+ public @NonNull List<KeyStoreRecoveryMetadata> getRecoveryMetadata() {
+ return mRecoveryMetadata;
+ }
+
+ /**
+ * List of application keys, with key material encrypted by
+ * the recovery key ({@link #getEncryptedRecoveryKeyBlob}).
+ */
+ public @NonNull List<KeyEntryRecoveryData> getApplicationKeyBlobs() {
+ return mApplicationKeyBlobs;
+ }
+
+ /**
+ * Recovery key blob, encrypted by user secret and recovery service public key.
+ */
+ public @NonNull byte[] getEncryptedRecoveryKeyBlob() {
+ return mEncryptedRecoveryKeyBlob;
+ }
+
+ public static final Parcelable.Creator<KeyStoreRecoveryData> CREATOR =
+ new Parcelable.Creator<KeyStoreRecoveryData>() {
+ public KeyStoreRecoveryData createFromParcel(Parcel in) {
+ return new KeyStoreRecoveryData(in);
+ }
+
+ public KeyStoreRecoveryData[] newArray(int length) {
+ return new KeyStoreRecoveryData[length];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mSnapshotVersion);
+ out.writeTypedList(mRecoveryMetadata);
+ out.writeByteArray(mEncryptedRecoveryKeyBlob);
+ out.writeTypedList(mApplicationKeyBlobs);
+ }
+
+ protected KeyStoreRecoveryData(Parcel in) {
+ mSnapshotVersion = in.readInt();
+ mRecoveryMetadata = in.createTypedArrayList(KeyStoreRecoveryMetadata.CREATOR);
+ mEncryptedRecoveryKeyBlob = in.createByteArray();
+ mApplicationKeyBlobs = in.createTypedArrayList(KeyEntryRecoveryData.CREATOR);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/recoverablekeystore/KeyStoreRecoveryMetadata.java b/android/security/recoverablekeystore/KeyStoreRecoveryMetadata.java
new file mode 100644
index 00000000..43f9c805
--- /dev/null
+++ b/android/security/recoverablekeystore/KeyStoreRecoveryMetadata.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.recoverablekeystore;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * Helper class with data necessary to recover Keystore on a new device.
+ * It defines UI shown to the user and a way to derive a cryptographic key from user output.
+ *
+ * @hide
+ */
+public final class KeyStoreRecoveryMetadata implements Parcelable {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_LOCKSCREEN, TYPE_CUSTOM_PASSWORD})
+ public @interface UserSecretType {
+ }
+
+ /**
+ * Lockscreen secret is required to recover KeyStore.
+ */
+ public static final int TYPE_LOCKSCREEN = 1;
+
+ /**
+ * Custom passphrase, unrelated to lock screen, is required to recover KeyStore.
+ */
+ public static final int TYPE_CUSTOM_PASSWORD = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_PIN, TYPE_PASSWORD, TYPE_PATTERN})
+ public @interface LockScreenUiFormat {
+ }
+
+ /**
+ * Pin with digits only.
+ */
+ public static final int TYPE_PIN = 1;
+
+ /**
+ * Password. String with latin-1 characters only.
+ */
+ public static final int TYPE_PASSWORD = 2;
+
+ /**
+ * Pattern with 3 by 3 grid.
+ */
+ public static final int TYPE_PATTERN = 3;
+
+ @UserSecretType
+ private final int mUserSecretType;
+
+ @LockScreenUiFormat
+ private final int mLockScreenUiFormat;
+
+ /**
+ * Parameters of key derivation function, including algorithm, difficulty, salt.
+ */
+ private KeyDerivationParameters mKeyDerivationParameters;
+ private byte[] mSecret; // Derived from user secret. The field must have limited visibility.
+
+ /**
+ * @param secret Constructor creates a reference to the secret. Caller must use
+ * @link {#clearSecret} to overwrite its value in memory.
+ */
+ public KeyStoreRecoveryMetadata(@UserSecretType int userSecretType,
+ @LockScreenUiFormat int lockScreenUiFormat,
+ @NonNull KeyDerivationParameters keyDerivationParameters, @NonNull byte[] secret) {
+ mUserSecretType = userSecretType;
+ mLockScreenUiFormat = lockScreenUiFormat;
+ mKeyDerivationParameters = Preconditions.checkNotNull(keyDerivationParameters);
+ mSecret = Preconditions.checkNotNull(secret);
+ }
+
+ /**
+ * Specifies UX shown to user during recovery.
+ *
+ * @see KeyStore.TYPE_PIN
+ * @see KeyStore.TYPE_PASSWORD
+ * @see KeyStore.TYPE_PATTERN
+ */
+ public @LockScreenUiFormat int getLockScreenUiFormat() {
+ return mLockScreenUiFormat;
+ }
+
+ /**
+ * Specifies function used to derive symmetric key from user input
+ * Format is defined in separate util class.
+ */
+ public @NonNull KeyDerivationParameters getKeyDerivationParameters() {
+ return mKeyDerivationParameters;
+ }
+
+ /**
+ * Secret string derived from user input.
+ */
+ public @NonNull byte[] getSecret() {
+ return mSecret;
+ }
+
+ /**
+ * @see KeyStore.TYPE_LOCKSCREEN
+ * @see KeyStore.TYPE_CUSTOM_PASSWORD
+ */
+ public @UserSecretType int getUserSecretType() {
+ return mUserSecretType;
+ }
+
+ /**
+ * Removes secret from memory than object is no longer used.
+ * Since finalizer call is not reliable, please use @link {#clearSecret} directly.
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ clearSecret();
+ super.finalize();
+ }
+
+ /**
+ * Fills mSecret with zeroes.
+ */
+ public void clearSecret() {
+ Arrays.fill(mSecret, (byte) 0);
+ }
+
+ public static final Parcelable.Creator<KeyStoreRecoveryMetadata> CREATOR =
+ new Parcelable.Creator<KeyStoreRecoveryMetadata>() {
+ public KeyStoreRecoveryMetadata createFromParcel(Parcel in) {
+ return new KeyStoreRecoveryMetadata(in);
+ }
+
+ public KeyStoreRecoveryMetadata[] newArray(int length) {
+ return new KeyStoreRecoveryMetadata[length];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mUserSecretType);
+ out.writeInt(mLockScreenUiFormat);
+ out.writeTypedObject(mKeyDerivationParameters, flags);
+ out.writeByteArray(mSecret);
+ }
+
+ protected KeyStoreRecoveryMetadata(Parcel in) {
+ mUserSecretType = in.readInt();
+ mLockScreenUiFormat = in.readInt();
+ mKeyDerivationParameters = in.readTypedObject(KeyDerivationParameters.CREATOR);
+ mSecret = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/recoverablekeystore/RecoverableKeyStoreLoader.java b/android/security/recoverablekeystore/RecoverableKeyStoreLoader.java
new file mode 100644
index 00000000..72a138a6
--- /dev/null
+++ b/android/security/recoverablekeystore/RecoverableKeyStoreLoader.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.recoverablekeystore;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
+import android.os.UserHandle;
+import android.security.KeyStore;
+import android.util.AndroidException;
+
+import com.android.internal.widget.ILockSettings;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A wrapper around KeyStore which lets key be exported to trusted hardware on server side and
+ * recovered later.
+ *
+ * @hide
+ */
+public class RecoverableKeyStoreLoader {
+
+ public static final String PERMISSION_RECOVER_KEYSTORE = "android.permission.RECOVER_KEYSTORE";
+
+ public static final int NO_ERROR = KeyStore.NO_ERROR;
+ public static final int SYSTEM_ERROR = KeyStore.SYSTEM_ERROR;
+ public static final int UNINITIALIZED_RECOVERY_PUBLIC_KEY = 20;
+ public static final int NO_SNAPSHOT_PENDING_ERROR = 21;
+
+ /**
+ * Rate limit is enforced to prevent using too many trusted remote devices, since each device
+ * can have its own number of user secret guesses allowed.
+ *
+ * @hide
+ */
+ public static final int RATE_LIMIT_EXCEEDED = 21;
+
+ /** Key has been successfully synced. */
+ public static final int RECOVERY_STATUS_SYNCED = 0;
+ /** Waiting for recovery agent to sync the key. */
+ public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1;
+ /** Recovery account is not available. */
+ public static final int RECOVERY_STATUS_MISSING_ACCOUNT = 2;
+ /** Key cannot be synced. */
+ public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3;
+
+ private final ILockSettings mBinder;
+
+ private RecoverableKeyStoreLoader(ILockSettings binder) {
+ mBinder = binder;
+ }
+
+ /** @hide */
+ public static RecoverableKeyStoreLoader getInstance() {
+ ILockSettings lockSettings =
+ ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings"));
+ return new RecoverableKeyStoreLoader(lockSettings);
+ }
+
+ /**
+ * Exceptions returned by {@link RecoverableKeyStoreLoader}.
+ *
+ * @hide
+ */
+ public static class RecoverableKeyStoreLoaderException extends AndroidException {
+ private int mErrorCode;
+
+ /**
+ * Creates new {@link #RecoverableKeyStoreLoaderException} instance from the error code.
+ *
+ * @param errorCode
+ * @hide
+ */
+ public static RecoverableKeyStoreLoaderException fromErrorCode(int errorCode) {
+ return new RecoverableKeyStoreLoaderException(
+ errorCode, getMessageFromErrorCode(errorCode));
+ }
+
+ /**
+ * Creates new {@link #RecoverableKeyStoreLoaderException} from {@link
+ * ServiceSpecificException}.
+ *
+ * @param e exception thrown on service side.
+ * @hide
+ */
+ static RecoverableKeyStoreLoaderException fromServiceSpecificException(
+ ServiceSpecificException e) throws RecoverableKeyStoreLoaderException {
+ throw RecoverableKeyStoreLoaderException.fromErrorCode(e.errorCode);
+ }
+
+ private RecoverableKeyStoreLoaderException(int errorCode, String message) {
+ super(message);
+ }
+
+ /** Returns errorCode. */
+ public int getErrorCode() {
+ return mErrorCode;
+ }
+
+ /** @hide */
+ private static String getMessageFromErrorCode(int errorCode) {
+ switch (errorCode) {
+ case NO_ERROR:
+ return "OK";
+ case SYSTEM_ERROR:
+ return "System error";
+ case UNINITIALIZED_RECOVERY_PUBLIC_KEY:
+ return "Recovery service is not initialized";
+ case RATE_LIMIT_EXCEEDED:
+ return "Rate limit exceeded";
+ default:
+ return String.valueOf("Unknown error code " + errorCode);
+ }
+ }
+ }
+
+ /**
+ * Initializes key recovery service for the calling application. RecoverableKeyStoreLoader
+ * randomly chooses one of the keys from the list and keeps it to use for future key export
+ * operations. Collection of all keys in the list must be signed by the provided {@code
+ * rootCertificateAlias}, which must also be present in the list of root certificates
+ * preinstalled on the device. The random selection allows RecoverableKeyStoreLoader to select
+ * which of a set of remote recovery service devices will be used.
+ *
+ * <p>In addition, RecoverableKeyStoreLoader enforces a delay of three months between
+ * consecutive initialization attempts, to limit the ability of an attacker to often switch
+ * remote recovery devices and significantly increase number of recovery attempts.
+ *
+ * @param rootCertificateAlias alias of a root certificate preinstalled on the device
+ * @param signedPublicKeyList binary blob a list of X509 certificates and signature
+ * @throws RecoverableKeyStoreLoaderException if signature is invalid, or key rotation was rate
+ * limited.
+ * @hide
+ */
+ public void initRecoveryService(
+ @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList)
+ throws RecoverableKeyStoreLoaderException {
+ try {
+ mBinder.initRecoveryService(
+ rootCertificateAlias, signedPublicKeyList, UserHandle.getCallingUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns data necessary to store all recoverable keys for given account. Key material is
+ * encrypted with user secret and recovery public key.
+ *
+ * @param account specific to Recovery agent.
+ * @return Data necessary to recover keystore.
+ * @hide
+ */
+ public @NonNull KeyStoreRecoveryData getRecoveryData(@NonNull byte[] account)
+ throws RecoverableKeyStoreLoaderException {
+ try {
+ KeyStoreRecoveryData recoveryData =
+ mBinder.getRecoveryData(account, UserHandle.getCallingUserId());
+ return recoveryData;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Sets a listener which notifies recovery agent that new recovery snapshot is available. {@link
+ * #getRecoveryData} can be used to get the snapshot. Note that every recovery agent can have at
+ * most one registered listener at any time.
+ *
+ * @param intent triggered when new snapshot is available. Unregisters listener if the value is
+ * {@code null}.
+ * @hide
+ */
+ public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent)
+ throws RecoverableKeyStoreLoaderException {
+ try {
+ mBinder.setSnapshotCreatedPendingIntent(intent, UserHandle.getCallingUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns a map from recovery agent accounts to corresponding KeyStore recovery snapshot
+ * version. Version zero is used, if no snapshots were created for the account.
+ *
+ * @return Map from recovery agent accounts to snapshot versions.
+ * @see KeyStoreRecoveryData#getSnapshotVersion
+ * @hide
+ */
+ public @NonNull Map<byte[], Integer> getRecoverySnapshotVersions()
+ throws RecoverableKeyStoreLoaderException {
+ try {
+ // IPC doesn't support generic Maps.
+ @SuppressWarnings("unchecked")
+ Map<byte[], Integer> result =
+ (Map<byte[], Integer>)
+ mBinder.getRecoverySnapshotVersions(UserHandle.getCallingUserId());
+ return result;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Server parameters used to generate new recovery key blobs. This value will be included in
+ * {@code KeyStoreRecoveryData.getEncryptedRecoveryKeyBlob()}. The same value must be included
+ * in vaultParams {@link #startRecoverySession}
+ *
+ * @param serverParameters included in recovery key blob.
+ * @see #getRecoveryData
+ * @throws RecoverableKeyStoreLoaderException If parameters rotation is rate limited.
+ * @hide
+ */
+ public void setServerParameters(long serverParameters)
+ throws RecoverableKeyStoreLoaderException {
+ try {
+ mBinder.setServerParameters(serverParameters, UserHandle.getCallingUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Updates recovery status for given keys. It is used to notify keystore that key was
+ * successfully stored on the server or there were an error. Application can check this value
+ * using {@code getRecoveyStatus}.
+ *
+ * @param packageName Application whose recoverable keys' statuses are to be updated.
+ * @param aliases List of application-specific key aliases. If the array is empty, updates the
+ * status for all existing recoverable keys.
+ * @param status Status specific to recovery agent.
+ */
+ public void setRecoveryStatus(
+ @NonNull String packageName, @Nullable String[] aliases, int status)
+ throws NameNotFoundException, RecoverableKeyStoreLoaderException {
+ try {
+ mBinder.setRecoveryStatus(packageName, aliases, status, UserHandle.getCallingUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns a {@code Map} from Application's KeyStore key aliases to their recovery status.
+ * Negative status values are reserved for recovery agent specific codes. List of common codes:
+ *
+ * <ul>
+ * <li>{@link #RECOVERY_STATUS_SYNCED}
+ * <li>{@link #RECOVERY_STATUS_SYNC_IN_PROGRESS}
+ * <li>{@link #RECOVERY_STATUS_MISSING_ACCOUNT}
+ * <li>{@link #RECOVERY_STATUS_PERMANENT_FAILURE}
+ * </ul>
+ *
+ * @param packageName Application whose recoverable keys' statuses are to be retrieved. if
+ * {@code null} caller's package will be used.
+ * @return {@code Map} from KeyStore alias to recovery status.
+ * @see #setRecoveryStatus
+ * @hide
+ */
+ public Map<String, Integer> getRecoveryStatus(@Nullable String packageName)
+ throws RecoverableKeyStoreLoaderException {
+ try {
+ // IPC doesn't support generic Maps.
+ @SuppressWarnings("unchecked")
+ Map<String, Integer> result =
+ (Map<String, Integer>)
+ mBinder.getRecoveryStatus(packageName, UserHandle.getCallingUserId());
+ return result;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Specifies a set of secret types used for end-to-end keystore encryption. Knowing all of them
+ * is necessary to recover data.
+ *
+ * @param secretTypes {@link KeyStoreRecoveryMetadata#TYPE_LOCKSCREEN} or {@link
+ * KeyStoreRecoveryMetadata#TYPE_CUSTOM_PASSWORD}
+ */
+ public void setRecoverySecretTypes(
+ @NonNull @KeyStoreRecoveryMetadata.UserSecretType int[] secretTypes)
+ throws RecoverableKeyStoreLoaderException {
+ try {
+ mBinder.setRecoverySecretTypes(secretTypes, UserHandle.getCallingUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Defines a set of secret types used for end-to-end keystore encryption. Knowing all of them is
+ * necessary to generate KeyStoreRecoveryData.
+ *
+ * @return list of recovery secret types
+ * @see KeyStoreRecoveryData
+ */
+ public @NonNull @KeyStoreRecoveryMetadata.UserSecretType int[] getRecoverySecretTypes()
+ throws RecoverableKeyStoreLoaderException {
+ try {
+ return mBinder.getRecoverySecretTypes(UserHandle.getCallingUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns a list of recovery secret types, necessary to create a pending recovery snapshot.
+ * When user enters a secret of a pending type {@link #recoverySecretAvailable} should be
+ * called.
+ *
+ * @return list of recovery secret types
+ */
+ public @NonNull @KeyStoreRecoveryMetadata.UserSecretType int[] getPendingRecoverySecretTypes()
+ throws RecoverableKeyStoreLoaderException {
+ try {
+ return mBinder.getPendingRecoverySecretTypes(UserHandle.getCallingUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Method notifies KeyStore that a user-generated secret is available. This method generates a
+ * symmetric session key which a trusted remote device can use to return a recovery key. Caller
+ * should use {@link KeyStoreRecoveryMetadata#clearSecret} to override the secret value in
+ * memory.
+ *
+ * @param recoverySecret user generated secret together with parameters necessary to regenerate
+ * it on a new device.
+ */
+ public void recoverySecretAvailable(@NonNull KeyStoreRecoveryMetadata recoverySecret)
+ throws RecoverableKeyStoreLoaderException {
+ try {
+ mBinder.recoverySecretAvailable(recoverySecret, UserHandle.getCallingUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Initializes recovery session and returns a blob with proof of recovery secrets possession.
+ * The method generates symmetric key for a session, which trusted remote device can use to
+ * return recovery key.
+ *
+ * @param sessionId ID for recovery session.
+ * @param verifierPublicKey Certificate with Public key used to create the recovery blob on the
+ * source device. Keystore will verify the certificate using root of trust.
+ * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
+ * Used to limit number of guesses.
+ * @param vaultChallenge Data passed from server for this recovery session and used to prevent
+ * replay attacks
+ * @param secrets Secrets provided by user, the method only uses type and secret fields.
+ * @return Binary blob with recovery claim. It is encrypted with verifierPublicKey and contains
+ * a proof of user secrets, session symmetric key and parameters necessary to identify the
+ * counter with the number of failed recovery attempts.
+ */
+ public @NonNull byte[] startRecoverySession(
+ @NonNull String sessionId,
+ @NonNull byte[] verifierPublicKey,
+ @NonNull byte[] vaultParams,
+ @NonNull byte[] vaultChallenge,
+ @NonNull List<KeyStoreRecoveryMetadata> secrets)
+ throws RecoverableKeyStoreLoaderException {
+ try {
+ byte[] recoveryClaim =
+ mBinder.startRecoverySession(
+ sessionId,
+ verifierPublicKey,
+ vaultParams,
+ vaultChallenge,
+ secrets,
+ UserHandle.getCallingUserId());
+ return recoveryClaim;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Imports keys.
+ *
+ * @param sessionId Id for recovery session, same as in
+ * {@link #startRecoverySession(String, byte[], byte[], byte[], List)} on}.
+ * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
+ * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
+ * and session. KeyStore only uses package names from the application info in {@link
+ * KeyEntryRecoveryData}. Caller is responsibility to perform certificates check.
+ * @return Map from alias to raw key material.
+ */
+ public Map<String, byte[]> recoverKeys(
+ @NonNull String sessionId,
+ @NonNull byte[] recoveryKeyBlob,
+ @NonNull List<KeyEntryRecoveryData> applicationKeys)
+ throws RecoverableKeyStoreLoaderException {
+ try {
+ return (Map<String, byte[]>) mBinder.recoverKeys(
+ sessionId, recoveryKeyBlob, applicationKeys, UserHandle.getCallingUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Generates a key called {@code alias} and loads it into the recoverable key store. Returns the
+ * raw material of the key.
+ *
+ * @throws RecoverableKeyStoreLoaderException if an error occurred generating and storing the
+ * key.
+ */
+ public byte[] generateAndStoreKey(String alias) throws RecoverableKeyStoreLoaderException {
+ try {
+ return mBinder.generateAndStoreKey(alias);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw RecoverableKeyStoreLoaderException.fromServiceSpecificException(e);
+ }
+ }
+}
diff --git a/android/service/autofill/AutofillService.java b/android/service/autofill/AutofillService.java
index cd362c71..917efa8b 100644
--- a/android/service/autofill/AutofillService.java
+++ b/android/service/autofill/AutofillService.java
@@ -438,22 +438,61 @@ import com.android.internal.os.SomeArgs;
* AutofillValue password = passwordNode.getAutofillValue().getTextValue().toString();
*
* save(username, password);
- * </pre>
- *
+ * </pre>
*
* <a name="Privacy"></a>
* <h3>Privacy</h3>
*
* <p>The {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} method is called
* without the user content. The Android system strips some properties of the
- * {@link android.app.assist.AssistStructure.ViewNode view nodes} passed to these calls, but not all
+ * {@link android.app.assist.AssistStructure.ViewNode view nodes} passed to this call, but not all
* of them. For example, the data provided in the {@link android.view.ViewStructure.HtmlInfo}
* objects set by {@link android.webkit.WebView} is never stripped out.
*
* <p>Because this data could contain PII (Personally Identifiable Information, such as username or
* email address), the service should only use it locally (i.e., in the app's process) for
* heuristics purposes, but it should not be sent to external servers.
+ *
+ * <a name="FieldClassification"></a>
+ * <h3>Metrics and field classification</h3
+ *
+ * <p>The service can call {@link #getFillEventHistory()} to get metrics representing the user
+ * actions, and then use these metrics to improve its heuristics.
+ *
+ * <p>Prior to Android {@link android.os.Build.VERSION_CODES#P}, the metrics covered just the
+ * scenarios where the service knew how to autofill an activity, but Android
+ * {@link android.os.Build.VERSION_CODES#P} introduced a new mechanism called field classification,
+ * which allows the service to dinamically classify the meaning of fields based on the existing user
+ * data known by the service.
+ *
+ * <p>Typically, field classification can be used to detect fields that can be autofilled with
+ * user data that is not associated with a specific app&mdash;such as email and physical
+ * address. Once the service identifies that a such field was manually filled by the user, the
+ * service could use this signal to improve its heuristics, either locally (i.e., in the same
+ * device) or globally (i.e., by crowdsourcing the results back to the service's server so it can
+ * be used by other users).
+ *
+ * <p>The field classification workflow involves 4 steps:
+ *
+ * <ol>
+ * <li>Set the user data through {@link AutofillManager#setUserData(UserData)}. This data is
+ * cached until the system restarts (or the service is disabled), so it doesn't need to be set for
+ * all requests.
+ * <li>Identify which fields should be analysed by calling
+ * {@link FillResponse.Builder#setFieldClassificationIds(AutofillId...)}.
+ * <li>Verify the results through {@link FillEventHistory.Event#getFieldsClassification()}.
+ * <li>Use the results to dynamically create {@link Dataset} or {@link SaveInfo} objects in future
+ * requests.
+ * </ol>
+ *
+ * <p>The field classification is an expensive operation and should be used carefully, otherwise it
+ * can reach its rate limit and get blocked by the Android System. Ideally, it should be used just
+ * in cases where the service could not determine how an activity can be autofilled, but it has a
+ * strong suspicious that it could. For example, if an activity has four or more fields and one of
+ * them is a list, chances are that these are address fields (like address, city, state, and
+ * zip code).
*/
+// TODO(b/70407264): add code snippets above???
public abstract class AutofillService extends Service {
private static final String TAG = "AutofillService";
diff --git a/android/service/autofill/Dataset.java b/android/service/autofill/Dataset.java
index cb20e71b..266bcda7 100644
--- a/android/service/autofill/Dataset.java
+++ b/android/service/autofill/Dataset.java
@@ -303,6 +303,10 @@ public final class Dataset implements Parcelable {
* in the constructor in a dataset that is meant to be shown to the user, the autofill UI
* for this field will not be displayed.
*
+ * <p><b>Note:</b> On Android {@link android.os.Build.VERSION_CODES#P} and
+ * higher, datasets that require authentication can be also be filtered by passing a
+ * {@link AutofillValue#forText(CharSequence) text value} as the {@code value} parameter.
+ *
* @param id id returned by {@link
* android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
* @param value value to be autofilled. Pass {@code null} if you do not have the value
@@ -320,6 +324,10 @@ public final class Dataset implements Parcelable {
* Sets the value of a field, using a custom {@link RemoteViews presentation} to
* visualize it.
*
+ * <p><b>Note:</b> On Android {@link android.os.Build.VERSION_CODES#P} and
+ * higher, datasets that require authentication can be also be filtered by passing a
+ * {@link AutofillValue#forText(CharSequence) text value} as the {@code value} parameter.
+ *
* @param id id returned by {@link
* android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
* @param value the value to be autofilled. Pass {@code null} if you do not have the value
@@ -340,12 +348,16 @@ public final class Dataset implements Parcelable {
/**
* Sets the value of a field using an <a href="#Filtering">explicit filter</a>.
*
- * <p>This method is typically used when the dataset is authenticated and the service
+ * <p>This method is typically used when the dataset requires authentication and the service
* does not know its value but wants to hide the dataset after the user enters a minimum
* number of characters. For example, if the dataset represents a credit card number and the
* service does not want to show the "Tap to authenticate" message until the user tapped
* 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}.
*
+ * <p><b>Note:</b> If the dataset requires authentication but the service knows its text
+ * value it's easier to filter by calling {@link #setValue(AutofillId, AutofillValue)} and
+ * use the value to filter.
+ *
* @param id id returned by {@link
* android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
* @param value the value to be autofilled. Pass {@code null} if you do not have the value
@@ -371,12 +383,16 @@ public final class Dataset implements Parcelable {
* Sets the value of a field, using a custom {@link RemoteViews presentation} to
* visualize it and a <a href="#Filtering">explicit filter</a>.
*
- * <p>This method is typically used when the dataset is authenticated and the service
+ * <p>This method is typically used when the dataset requires authentication and the service
* does not know its value but wants to hide the dataset after the user enters a minimum
* number of characters. For example, if the dataset represents a credit card number and the
* service does not want to show the "Tap to authenticate" message until the user tapped
* 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}.
*
+ * <p><b>Note:</b> If the dataset requires authentication but the service knows its text
+ * value it's easier to filter by calling
+ * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} and using the value to filter.
+ *
* @param id id returned by {@link
* android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
* @param value the value to be autofilled. Pass {@code null} if you do not have the value
@@ -405,6 +421,7 @@ public final class Dataset implements Parcelable {
if (existingIdx >= 0) {
mFieldValues.set(existingIdx, value);
mFieldPresentations.set(existingIdx, presentation);
+ mFieldFilters.set(existingIdx, filter);
return;
}
} else {
diff --git a/android/service/autofill/EditDistanceScorer.java b/android/service/autofill/EditDistanceScorer.java
new file mode 100644
index 00000000..0706b377
--- /dev/null
+++ b/android/service/autofill/EditDistanceScorer.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.service.autofill;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.autofill.AutofillValue;
+
+/**
+ * Helper used to calculate the classification score between an actual {@link AutofillValue} filled
+ * by the user and the expected value predicted by an autofill service.
+ */
+// TODO(b/70291841): explain algorithm once it's fully implemented
+public final class EditDistanceScorer extends InternalScorer implements Scorer, Parcelable {
+
+ private static final EditDistanceScorer sInstance = new EditDistanceScorer();
+
+ /**
+ * Gets the singleton instance.
+ */
+ public static EditDistanceScorer getInstance() {
+ return sInstance;
+ }
+
+ private EditDistanceScorer() {
+ }
+
+ /** @hide */
+ @Override
+ public float getScore(@NonNull AutofillValue actualValue, @NonNull String userData) {
+ if (actualValue == null || !actualValue.isText() || userData == null) return 0;
+ // TODO(b/70291841): implement edit distance - currently it's returning either 0, 100%, or
+ // partial match when number of chars match
+ final String textValue = actualValue.getTextValue().toString();
+ final int total = textValue.length();
+ if (total != userData.length()) return 0F;
+
+ int matches = 0;
+ for (int i = 0; i < total; i++) {
+ if (Character.toLowerCase(textValue.charAt(i)) == Character
+ .toLowerCase(userData.charAt(i))) {
+ matches++;
+ }
+ }
+
+ return ((float) matches) / total;
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ return "EditDistanceScorer";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ // Do nothing
+ }
+
+ public static final Parcelable.Creator<EditDistanceScorer> CREATOR =
+ new Parcelable.Creator<EditDistanceScorer>() {
+ @Override
+ public EditDistanceScorer createFromParcel(Parcel parcel) {
+ return EditDistanceScorer.getInstance();
+ }
+
+ @Override
+ public EditDistanceScorer[] newArray(int size) {
+ return new EditDistanceScorer[size];
+ }
+ };
+}
diff --git a/android/service/autofill/FieldClassification.java b/android/service/autofill/FieldClassification.java
new file mode 100644
index 00000000..001b2917
--- /dev/null
+++ b/android/service/autofill/FieldClassification.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.view.autofill.Helper;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Represents the <a href="AutofillService.html#FieldClassification">field classification</a>
+ * results for a given field.
+ */
+public final class FieldClassification {
+
+ private final ArrayList<Match> mMatches;
+
+ /** @hide */
+ public FieldClassification(@NonNull ArrayList<Match> matches) {
+ mMatches = Preconditions.checkNotNull(matches);
+ Collections.sort(mMatches, new Comparator<Match>() {
+ @Override
+ public int compare(Match o1, Match o2) {
+ if (o1.mScore > o2.mScore) return -1;
+ if (o1.mScore < o2.mScore) return 1;
+ return 0;
+ }}
+ );
+ }
+
+ /**
+ * Gets the {@link Match matches} with the highest {@link Match#getScore() scores} (sorted in
+ * descending order).
+ *
+ * <p><b>Note:</b> There's no guarantee of how many matches will be returned. In fact,
+ * the Android System might return just the top match to minimize the impact of field
+ * classification in the device's health.
+ */
+ @NonNull
+ public List<Match> getMatches() {
+ return mMatches;
+ }
+
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "FieldClassification: " + mMatches;
+ }
+
+ private void writeToParcel(Parcel parcel) {
+ parcel.writeInt(mMatches.size());
+ for (int i = 0; i < mMatches.size(); i++) {
+ mMatches.get(i).writeToParcel(parcel);
+ }
+ }
+
+ private static FieldClassification readFromParcel(Parcel parcel) {
+ final int size = parcel.readInt();
+ final ArrayList<Match> matches = new ArrayList<>();
+ for (int i = 0; i < size; i++) {
+ matches.add(i, Match.readFromParcel(parcel));
+ }
+
+ return new FieldClassification(matches);
+ }
+
+ static FieldClassification[] readArrayFromParcel(Parcel parcel) {
+ final int length = parcel.readInt();
+ final FieldClassification[] fcs = new FieldClassification[length];
+ for (int i = 0; i < length; i++) {
+ fcs[i] = readFromParcel(parcel);
+ }
+ return fcs;
+ }
+
+ static void writeArrayToParcel(@NonNull Parcel parcel, @NonNull FieldClassification[] fcs) {
+ parcel.writeInt(fcs.length);
+ for (int i = 0; i < fcs.length; i++) {
+ fcs[i].writeToParcel(parcel);
+ }
+ }
+
+ /**
+ * Represents the score of a {@link UserData} entry for the field.
+ *
+ * <p>The score is defined by {@link #getScore()} and the entry is identified by
+ * {@link #getRemoteId()}.
+ */
+ public static final class Match {
+
+ private final String mRemoteId;
+ private final float mScore;
+
+ /** @hide */
+ public Match(String remoteId, float score) {
+ mRemoteId = Preconditions.checkNotNull(remoteId);
+ mScore = score;
+ }
+
+ /**
+ * Gets the remote id of the {@link UserData} entry.
+ */
+ @NonNull
+ public String getRemoteId() {
+ return mRemoteId;
+ }
+
+ /**
+ * Gets a classification score for the value of this field compared to the value of the
+ * {@link UserData} entry.
+ *
+ * <p>The score is based in a comparison of the field value and the user data entry, and it
+ * ranges from {@code 0.0F} to {@code 1.0F}:
+ * <ul>
+ * <li>{@code 1.0F} represents a full match ({@code 100%}).
+ * <li>{@code 0.0F} represents a full mismatch ({@code 0%}).
+ * <li>Any other value is a partial match.
+ * </ul>
+ *
+ * <p>How the score is calculated depends on the algorithm used by the {@link Scorer}
+ * implementation.
+ */
+ public float getScore() {
+ return mScore;
+ }
+
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ final StringBuilder string = new StringBuilder("Match: remoteId=");
+ Helper.appendRedacted(string, mRemoteId);
+ return string.append(", score=").append(mScore).toString();
+ }
+
+ private void writeToParcel(@NonNull Parcel parcel) {
+ parcel.writeString(mRemoteId);
+ parcel.writeFloat(mScore);
+ }
+
+ private static Match readFromParcel(@NonNull Parcel parcel) {
+ return new Match(parcel.readString(), parcel.readFloat());
+ }
+ }
+}
diff --git a/android/service/autofill/FieldsDetection.java b/android/service/autofill/FieldsDetection.java
deleted file mode 100644
index 550ecf68..00000000
--- a/android/service/autofill/FieldsDetection.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.service.autofill;
-
-import android.annotation.TestApi;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.view.autofill.AutofillId;
-
-/**
- * Class by service to improve autofillable fields detection by tracking the meaning of fields
- * manually edited by the user (when they match values provided by the service).
- *
- * TODO(b/67867469):
- * - proper javadoc
- * - unhide / remove testApi
- * - add FieldsDetection management so service can set it just once and reference it in further
- * calls to improve performance (and also API to refresh it)
- * - rename to FieldsDetectionInfo or FieldClassification? (same for CTS tests)
- * - add FieldsDetectionUnitTest once API is well-defined
- * @hide
- */
-@TestApi
-public final class FieldsDetection implements Parcelable {
-
- private final AutofillId mFieldId;
- private final String mRemoteId;
- private final String mValue;
-
- /**
- * Creates a field detection for just one field / value pair.
- *
- * @param fieldId autofill id of the field in the screen.
- * @param remoteId id used by the service to identify the field later.
- * @param value field value known to the service.
- *
- * TODO(b/67867469):
- * - proper javadoc
- * - change signature to allow more fields / values / match methods
- * - might also need to use a builder, where the constructor is the id for the fieldsdetector
- * - might need id for values as well
- * - add @NonNull / check it / add unit tests
- * - make 'value' input more generic so it can accept distance-based match and other matches
- * - throw exception if field value is less than X characters (somewhere between 7-10)
- * - make sure to limit total number of fields to around 10 or so
- * - use AutofillValue instead of String (so it can compare dates, for example)
- */
- public FieldsDetection(AutofillId fieldId, String remoteId, String value) {
- mFieldId = fieldId;
- mRemoteId = remoteId;
- mValue = value;
- }
-
- /** @hide */
- public AutofillId getFieldId() {
- return mFieldId;
- }
-
- /** @hide */
- public String getRemoteId() {
- return mRemoteId;
- }
-
- /** @hide */
- public String getValue() {
- return mValue;
- }
-
- /////////////////////////////////////
- // Object "contract" methods. //
- /////////////////////////////////////
- @Override
- public String toString() {
- // Cannot disclose remoteId or value because they could contain PII
- return new StringBuilder("FieldsDetection: [field=").append(mFieldId)
- .append(", remoteId_length=").append(mRemoteId.length())
- .append(", value_length=").append(mValue.length())
- .append("]").toString();
- }
-
- /////////////////////////////////////
- // Parcelable "contract" methods. //
- /////////////////////////////////////
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel parcel, int flags) {
- parcel.writeParcelable(mFieldId, flags);
- parcel.writeString(mRemoteId);
- parcel.writeString(mValue);
- }
-
- public static final Parcelable.Creator<FieldsDetection> CREATOR =
- new Parcelable.Creator<FieldsDetection>() {
- @Override
- public FieldsDetection createFromParcel(Parcel parcel) {
- // TODO(b/67867469): remove comment below if it does not use a builder at the end
- // Always go through the builder to ensure the data ingested by
- // the system obeys the contract of the builder to avoid attacks
- // using specially crafted parcels.
- return new FieldsDetection(parcel.readParcelable(null), parcel.readString(),
- parcel.readString());
- }
-
- @Override
- public FieldsDetection[] newArray(int size) {
- return new FieldsDetection[size];
- }
- };
-}
diff --git a/android/service/autofill/FillEventHistory.java b/android/service/autofill/FillEventHistory.java
index 736d9ef4..df624464 100644
--- a/android/service/autofill/FillEventHistory.java
+++ b/android/service/autofill/FillEventHistory.java
@@ -16,16 +16,18 @@
package android.service.autofill;
+import static android.view.autofill.Helper.sVerbose;
+
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.TestApi;
import android.content.IntentSender;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.Log;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
@@ -35,6 +37,7 @@ import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -57,10 +60,7 @@ import java.util.Set;
* the history will clear out after some pre-defined time).
*/
public final class FillEventHistory implements Parcelable {
- /**
- * Not in parcel. The UID of the {@link AutofillService} that created the {@link FillResponse}.
- */
- private final int mServiceUid;
+ private static final String TAG = "FillEventHistory";
/**
* Not in parcel. The ID of the autofill session that created the {@link FillResponse}.
@@ -70,17 +70,6 @@ public final class FillEventHistory implements Parcelable {
@Nullable private final Bundle mClientState;
@Nullable List<Event> mEvents;
- /**
- * Gets the UID of the {@link AutofillService} that created the {@link FillResponse}.
- *
- * @return The UID of the {@link AutofillService}
- *
- * @hide
- */
- public int getServiceUid() {
- return mServiceUid;
- }
-
/** @hide */
public int getSessionId() {
return mSessionId;
@@ -123,9 +112,8 @@ public final class FillEventHistory implements Parcelable {
/**
* @hide
*/
- public FillEventHistory(int serviceUid, int sessionId, @Nullable Bundle clientState) {
+ public FillEventHistory(int sessionId, @Nullable Bundle clientState) {
mClientState = clientState;
- mServiceUid = serviceUid;
mSessionId = sessionId;
}
@@ -140,34 +128,36 @@ public final class FillEventHistory implements Parcelable {
}
@Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeBundle(mClientState);
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeBundle(mClientState);
if (mEvents == null) {
- dest.writeInt(0);
+ parcel.writeInt(0);
} else {
- dest.writeInt(mEvents.size());
+ parcel.writeInt(mEvents.size());
int numEvents = mEvents.size();
for (int i = 0; i < numEvents; i++) {
Event event = mEvents.get(i);
- dest.writeInt(event.mEventType);
- dest.writeString(event.mDatasetId);
- dest.writeBundle(event.mClientState);
- dest.writeStringList(event.mSelectedDatasetIds);
- dest.writeArraySet(event.mIgnoredDatasetIds);
- dest.writeTypedList(event.mChangedFieldIds);
- dest.writeStringList(event.mChangedDatasetIds);
-
- dest.writeTypedList(event.mManuallyFilledFieldIds);
+ parcel.writeInt(event.mEventType);
+ parcel.writeString(event.mDatasetId);
+ parcel.writeBundle(event.mClientState);
+ parcel.writeStringList(event.mSelectedDatasetIds);
+ parcel.writeArraySet(event.mIgnoredDatasetIds);
+ parcel.writeTypedList(event.mChangedFieldIds);
+ parcel.writeStringList(event.mChangedDatasetIds);
+
+ parcel.writeTypedList(event.mManuallyFilledFieldIds);
if (event.mManuallyFilledFieldIds != null) {
final int size = event.mManuallyFilledFieldIds.size();
for (int j = 0; j < size; j++) {
- dest.writeStringList(event.mManuallyFilledDatasetIds.get(j));
+ parcel.writeStringList(event.mManuallyFilledDatasetIds.get(j));
}
}
- dest.writeString(event.mDetectedRemoteId);
- if (event.mDetectedRemoteId != null) {
- dest.writeInt(event.mDetectedFieldScore);
+ final AutofillId[] detectedFields = event.mDetectedFieldIds;
+ parcel.writeParcelableArray(detectedFields, flags);
+ if (detectedFields != null) {
+ FieldClassification.writeArrayToParcel(parcel,
+ event.mDetectedFieldClassifications);
}
}
}
@@ -217,6 +207,7 @@ public final class FillEventHistory implements Parcelable {
* ({@link #getIgnoredDatasetIds()}).
* <li>Which fields in the selected datasets were changed by the user after the dataset
* was selected ({@link #getChangedFields()}.
+ * <li>Which fields match the {@link UserData} set by the service.
* </ul>
*
* <p><b>Note: </b>This event is only generated when:
@@ -231,16 +222,16 @@ public final class FillEventHistory implements Parcelable {
* <p>See {@link android.view.autofill.AutofillManager} for more information about autofill
* contexts.
*/
- // TODO(b/67867469): update with field detection behavior
public static final int TYPE_CONTEXT_COMMITTED = 4;
/** @hide */
- @IntDef(
- value = {TYPE_DATASET_SELECTED,
- TYPE_DATASET_AUTHENTICATION_SELECTED,
- TYPE_AUTHENTICATION_SELECTED,
- TYPE_SAVE_SHOWN,
- TYPE_CONTEXT_COMMITTED})
+ @IntDef(prefix = { "TYPE_" }, value = {
+ TYPE_DATASET_SELECTED,
+ TYPE_DATASET_AUTHENTICATION_SELECTED,
+ TYPE_AUTHENTICATION_SELECTED,
+ TYPE_SAVE_SHOWN,
+ TYPE_CONTEXT_COMMITTED
+ })
@Retention(RetentionPolicy.SOURCE)
@interface EventIds{}
@@ -259,8 +250,8 @@ public final class FillEventHistory implements Parcelable {
@Nullable private final ArrayList<AutofillId> mManuallyFilledFieldIds;
@Nullable private final ArrayList<ArrayList<String>> mManuallyFilledDatasetIds;
- @Nullable private final String mDetectedRemoteId;
- private final int mDetectedFieldScore;
+ @Nullable private final AutofillId[] mDetectedFieldIds;
+ @Nullable private final FieldClassification[] mDetectedFieldClassifications;
/**
* Returns the type of the event.
@@ -364,35 +355,27 @@ public final class FillEventHistory implements Parcelable {
}
/**
- * Gets the results of the last {@link FieldsDetection} request.
- *
- * @return map of edit-distance match ({@code 0} means full match,
- * {@code 1} means 1 character different, etc...) by remote id (as set in the
- * {@link FieldsDetection} constructor), or {@code null} if none of the user-input values
- * matched the requested detection.
+ * Gets the <a href="AutofillService.html#FieldClassification">field classification</a>
+ * results.
*
* <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}, when the
- * service requested {@link FillResponse.Builder#setFieldsDetection(FieldsDetection) fields
- * detection}.
- *
- * TODO(b/67867469):
- * - improve javadoc
- * - refine score meaning (for example, should 1 be different of -1?)
- * - mention when it's set
- * - unhide
- * - unhide / remove testApi
- * - add @NonNull / check it / add unit tests
- *
- * @hide
+ * service requested {@link FillResponse.Builder#setFieldClassificationIds(AutofillId...)
+ * field classification}.
*/
- @TestApi
- @NonNull public Map<String, Integer> getDetectedFields() {
- if (mDetectedRemoteId == null || mDetectedFieldScore == -1) {
+ @NonNull public Map<AutofillId, FieldClassification> getFieldsClassification() {
+ if (mDetectedFieldIds == null) {
return Collections.emptyMap();
}
-
- final ArrayMap<String, Integer> map = new ArrayMap<>(1);
- map.put(mDetectedRemoteId, mDetectedFieldScore);
+ final int size = mDetectedFieldIds.length;
+ final ArrayMap<AutofillId, FieldClassification> map = new ArrayMap<>(size);
+ for (int i = 0; i < size; i++) {
+ final AutofillId id = mDetectedFieldIds[i];
+ final FieldClassification fc = mDetectedFieldClassifications[i];
+ if (sVerbose) {
+ Log.v(TAG, "getFieldsClassification[" + i + "]: id=" + id + ", fc=" + fc);
+ }
+ map.put(id, fc);
+ }
return map;
}
@@ -472,6 +455,8 @@ public final class FillEventHistory implements Parcelable {
* and belonged to datasets.
* @param manuallyFilledDatasetIds The ids of datasets that had values matching the
* respective entry on {@code manuallyFilledFieldIds}.
+ * @param detectedFieldClassifications the field classification matches.
+ *
* @throws IllegalArgumentException If the length of {@code changedFieldIds} and
* {@code changedDatasetIds} doesn't match.
* @throws IllegalArgumentException If the length of {@code manuallyFilledFieldIds} and
@@ -479,7 +464,6 @@ public final class FillEventHistory implements Parcelable {
*
* @hide
*/
- // TODO(b/67867469): document detection field parameters once stable
public Event(int eventType, @Nullable String datasetId, @Nullable Bundle clientState,
@Nullable List<String> selectedDatasetIds,
@Nullable ArraySet<String> ignoredDatasetIds,
@@ -487,7 +471,8 @@ public final class FillEventHistory implements Parcelable {
@Nullable ArrayList<String> changedDatasetIds,
@Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
@Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds,
- @Nullable String detectedRemoteId, int detectedFieldScore) {
+ @Nullable AutofillId[] detectedFieldIds,
+ @Nullable FieldClassification[] detectedFieldClassifications) {
mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_CONTEXT_COMMITTED,
"eventType");
mDatasetId = datasetId;
@@ -510,8 +495,9 @@ public final class FillEventHistory implements Parcelable {
}
mManuallyFilledFieldIds = manuallyFilledFieldIds;
mManuallyFilledDatasetIds = manuallyFilledDatasetIds;
- mDetectedRemoteId = detectedRemoteId;
- mDetectedFieldScore = detectedFieldScore;
+
+ mDetectedFieldIds = detectedFieldIds;
+ mDetectedFieldClassifications = detectedFieldClassifications;
}
@Override
@@ -524,8 +510,9 @@ public final class FillEventHistory implements Parcelable {
+ ", changedDatasetsIds=" + mChangedDatasetIds
+ ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds
+ ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds
- + ", detectedRemoteId=" + mDetectedRemoteId
- + ", detectedFieldScore=" + mDetectedFieldScore
+ + ", detectedFieldIds=" + Arrays.toString(mDetectedFieldIds)
+ + ", detectedFieldClassifications ="
+ + Arrays.toString(mDetectedFieldClassifications)
+ "]";
}
}
@@ -534,7 +521,7 @@ public final class FillEventHistory implements Parcelable {
new Parcelable.Creator<FillEventHistory>() {
@Override
public FillEventHistory createFromParcel(Parcel parcel) {
- FillEventHistory selection = new FillEventHistory(0, 0, parcel.readBundle());
+ FillEventHistory selection = new FillEventHistory(0, parcel.readBundle());
final int numEvents = parcel.readInt();
for (int i = 0; i < numEvents; i++) {
@@ -561,15 +548,18 @@ public final class FillEventHistory implements Parcelable {
} else {
manuallyFilledDatasetIds = null;
}
- final String detectedRemoteId = parcel.readString();
- final int detectedFieldScore = detectedRemoteId == null ? -1
- : parcel.readInt();
+ final AutofillId[] detectedFieldIds = parcel.readParcelableArray(null,
+ AutofillId.class);
+ final FieldClassification[] detectedFieldClassifications =
+ (detectedFieldIds != null)
+ ? FieldClassification.readArrayFromParcel(parcel)
+ : null;
selection.addEvent(new Event(eventType, datasetId, clientState,
selectedDatasetIds, ignoredDatasets,
changedFieldIds, changedDatasetIds,
manuallyFilledFieldIds, manuallyFilledDatasetIds,
- detectedRemoteId, detectedFieldScore));
+ detectedFieldIds, detectedFieldClassifications));
}
return selection;
}
diff --git a/android/service/autofill/FillRequest.java b/android/service/autofill/FillRequest.java
index 3a842240..33619ac6 100644
--- a/android/service/autofill/FillRequest.java
+++ b/android/service/autofill/FillRequest.java
@@ -69,9 +69,9 @@ public final class FillRequest implements Parcelable {
public static final int INVALID_REQUEST_ID = Integer.MIN_VALUE;
/** @hide */
- @IntDef(
- flag = true,
- value = {FLAG_MANUAL_REQUEST})
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
+ FLAG_MANUAL_REQUEST
+ })
@Retention(RetentionPolicy.SOURCE)
@interface RequestFlags{}
@@ -127,12 +127,15 @@ public final class FillRequest implements Parcelable {
}
/**
- * Gets the extra client state returned from the last {@link
- * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)
- * fill request}, so the service can use it for state management.
+ * Gets the latest client state bundle set by the service in a
+ * {@link FillResponse.Builder#setClientState(Bundle) fill response}.
*
- * <p>Once a {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)
- * save request} is made, the client state is cleared.
+ * <p><b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, only client state
+ * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} were considered. On
+ * Android {@link android.os.Build.VERSION_CODES#P} and higher, bundles set in the result of
+ * an authenticated request through the
+ * {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE} extra are
+ * also considered (and take precedence when set).
*
* @return The client state.
*/
diff --git a/android/service/autofill/FillResponse.java b/android/service/autofill/FillResponse.java
index 84a0974d..3a4b6bb8 100644
--- a/android/service/autofill/FillResponse.java
+++ b/android/service/autofill/FillResponse.java
@@ -41,7 +41,7 @@ import java.util.Arrays;
import java.util.List;
/**
- * Response for a {@link
+ * Response for an {@link
* AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}.
*
* <p>See the main {@link AutofillService} documentation for more details and examples.
@@ -49,19 +49,21 @@ import java.util.List;
public final class FillResponse implements Parcelable {
/**
- * Must be set in the last response to generate
- * {@link FillEventHistory.Event#TYPE_CONTEXT_COMMITTED} events.
+ * Flag used to generate {@link FillEventHistory.Event events} of type
+ * {@link FillEventHistory.Event#TYPE_CONTEXT_COMMITTED}&mdash;if this flag is not passed to
+ * {@link Builder#setFlags(int)}, these events are not generated.
*/
public static final int FLAG_TRACK_CONTEXT_COMMITED = 0x1;
/**
- * Used in conjunction to {@link FillResponse.Builder#disableAutofill(long)} to disable autofill
- * only for the activiy associated with the {@link FillResponse}, instead of the whole app.
+ * Flag used to change the behavior of {@link FillResponse.Builder#disableAutofill(long)}&mdash;
+ * when this flag is passed to {@link Builder#setFlags(int)}, autofill is disabled only for the
+ * activiy that generated the {@link FillRequest}, not the whole app.
*/
public static final int FLAG_DISABLE_ACTIVITY_ONLY = 0x2;
/** @hide */
- @IntDef(flag = true, value = {
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
FLAG_TRACK_CONTEXT_COMMITED,
FLAG_DISABLE_ACTIVITY_ONLY
})
@@ -72,11 +74,13 @@ public final class FillResponse implements Parcelable {
private final @Nullable SaveInfo mSaveInfo;
private final @Nullable Bundle mClientState;
private final @Nullable RemoteViews mPresentation;
+ private final @Nullable RemoteViews mHeader;
+ private final @Nullable RemoteViews mFooter;
private final @Nullable IntentSender mAuthentication;
private final @Nullable AutofillId[] mAuthenticationIds;
private final @Nullable AutofillId[] mIgnoredIds;
private final long mDisableDuration;
- private final @Nullable FieldsDetection mFieldsDetection;
+ private final @Nullable AutofillId[] mFieldClassificationIds;
private final int mFlags;
private int mRequestId;
@@ -85,11 +89,13 @@ public final class FillResponse implements Parcelable {
mSaveInfo = builder.mSaveInfo;
mClientState = builder.mClientState;
mPresentation = builder.mPresentation;
+ mHeader = builder.mHeader;
+ mFooter = builder.mFooter;
mAuthentication = builder.mAuthentication;
mAuthenticationIds = builder.mAuthenticationIds;
mIgnoredIds = builder.mIgnoredIds;
mDisableDuration = builder.mDisableDuration;
- mFieldsDetection = builder.mFieldsDetection;
+ mFieldClassificationIds = builder.mFieldClassificationIds;
mFlags = builder.mFlags;
mRequestId = INVALID_REQUEST_ID;
}
@@ -115,6 +121,16 @@ public final class FillResponse implements Parcelable {
}
/** @hide */
+ public @Nullable RemoteViews getHeader() {
+ return mHeader;
+ }
+
+ /** @hide */
+ public @Nullable RemoteViews getFooter() {
+ return mFooter;
+ }
+
+ /** @hide */
public @Nullable IntentSender getAuthentication() {
return mAuthentication;
}
@@ -135,11 +151,12 @@ public final class FillResponse implements Parcelable {
}
/** @hide */
- public @Nullable FieldsDetection getFieldsDetection() {
- return mFieldsDetection;
+ public @Nullable AutofillId[] getFieldClassificationIds() {
+ return mFieldClassificationIds;
}
/** @hide */
+ @TestApi
public int getFlags() {
return mFlags;
}
@@ -171,11 +188,13 @@ public final class FillResponse implements Parcelable {
private SaveInfo mSaveInfo;
private Bundle mClientState;
private RemoteViews mPresentation;
+ private RemoteViews mHeader;
+ private RemoteViews mFooter;
private IntentSender mAuthentication;
private AutofillId[] mAuthenticationIds;
private AutofillId[] mIgnoredIds;
private long mDisableDuration;
- private FieldsDetection mFieldsDetection;
+ private AutofillId[] mFieldClassificationIds;
private int mFlags;
private boolean mDestroyed;
@@ -226,16 +245,24 @@ public final class FillResponse implements Parcelable {
* @param ids id of Views that when focused will display the authentication UI.
*
* @return This builder.
+
* @throws IllegalArgumentException if {@code ids} is {@code null} or empty, or if
* both {@code authentication} and {@code presentation} are {@code null}, or if
* both {@code authentication} and {@code presentation} are non-{@code null}
*
+ * @throws IllegalStateException if a {@link #setHeader(RemoteViews) header} or a
+ * {@link #setFooter(RemoteViews) footer} are already set for this builder.
+ *
* @see android.app.PendingIntent#getIntentSender()
*/
public @NonNull Builder setAuthentication(@NonNull AutofillId[] ids,
@Nullable IntentSender authentication, @Nullable RemoteViews presentation) {
throwIfDestroyed();
throwIfDisableAutofillCalled();
+ if (mHeader != null || mFooter != null) {
+ throw new IllegalStateException("Already called #setHeader() or #setFooter()");
+ }
+
if (ids == null || ids.length == 0) {
throw new IllegalArgumentException("ids cannot be null or empry");
}
@@ -305,19 +332,16 @@ public final class FillResponse implements Parcelable {
}
/**
- * Sets a {@link Bundle state} that will be passed to subsequent APIs that
- * manipulate this response. For example, they are passed to subsequent
- * calls to {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal,
- * FillCallback)} and {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)}.
- * You can use this to store intermediate state that is persistent across multiple
- * fill requests and the subsequent save request.
+ * Sets a bundle with state that is passed to subsequent APIs that manipulate this response.
+ *
+ * <p>You can use this bundle to store intermediate state that is passed to subsequent calls
+ * to {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal,
+ * FillCallback)} and {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)}, and
+ * you can also retrieve it by calling {@link FillEventHistory.Event#getClientState()}.
*
* <p>If this method is called on multiple {@link FillResponse} objects for the same
* screen, just the latest bundle is passed back to the service.
*
- * <p>Once a {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)
- * save request} is made the client state is cleared.
- *
* @param clientState The custom client state.
* @return This builder.
*/
@@ -329,21 +353,26 @@ public final class FillResponse implements Parcelable {
}
/**
- * TODO(b/67867469):
- * - javadoc it
- * - javadoc how to check results
- * - unhide
- * - unhide / remove testApi
- * - throw exception (and document) if response has datasets or saveinfo
- * - throw exception (and document) if id on fieldsDetection is ignored
- *
- * @hide
+ * Sets which fields are used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>
+ *
+ * <p><b>Note:</b> This method automatically adds the
+ * {@link FillResponse#FLAG_TRACK_CONTEXT_COMMITED} to the {@link #setFlags(int) flags}.
+
+ * @throws IllegalArgumentException is length of {@code ids} args is more than
+ * {@link UserData#getMaxFieldClassificationIdsSize()}.
+ * @throws IllegalStateException if {@link #build()} or {@link #disableAutofill(long)} was
+ * already called.
+ * @throws NullPointerException if {@code ids} or any element on it is {@code null}.
*/
- @TestApi
- public Builder setFieldsDetection(@NonNull FieldsDetection fieldsDetection) {
+ public Builder setFieldClassificationIds(@NonNull AutofillId... ids) {
throwIfDestroyed();
throwIfDisableAutofillCalled();
- mFieldsDetection = Preconditions.checkNotNull(fieldsDetection);
+ Preconditions.checkArrayElementsNotNull(ids, "ids");
+ Preconditions.checkArgumentInRange(ids.length, 1,
+ UserData.getMaxFieldClassificationIdsSize(), "ids length");
+ mFieldClassificationIds = ids;
+ mFlags |= FLAG_TRACK_CONTEXT_COMMITED;
return this;
}
@@ -391,8 +420,8 @@ public final class FillResponse implements Parcelable {
* @throws IllegalArgumentException if {@code duration} is not a positive number.
* @throws IllegalStateException if either {@link #addDataset(Dataset)},
* {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)},
- * {@link #setSaveInfo(SaveInfo)}, or {@link #setClientState(Bundle)}
- * was already called.
+ * {@link #setSaveInfo(SaveInfo)}, {@link #setClientState(Bundle)}, or
+ * {@link #setFieldClassificationIds(AutofillId...)} was already called.
*/
public Builder disableAutofill(long duration) {
throwIfDestroyed();
@@ -400,7 +429,7 @@ public final class FillResponse implements Parcelable {
throw new IllegalArgumentException("duration must be greater than 0");
}
if (mAuthentication != null || mDatasets != null || mSaveInfo != null
- || mFieldsDetection != null || mClientState != null) {
+ || mFieldClassificationIds != null || mClientState != null) {
throw new IllegalStateException("disableAutofill() must be the only method called");
}
@@ -409,6 +438,62 @@ public final class FillResponse implements Parcelable {
}
/**
+ * Sets a header to be shown as the first element in the list of datasets.
+ *
+ * <p>When this method is called, you must also {@link #addDataset(Dataset) add a dataset},
+ * otherwise {@link #build()} throws an {@link IllegalStateException}. Similarly, this
+ * method should only be used on {@link FillResponse FillResponses} that do not require
+ * authentication (as the header could have been set directly in the main presentation in
+ * these cases).
+ *
+ * @param header a presentation to represent the header. This presentation is not clickable
+ * &mdash;calling
+ * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent)} on it would
+ * have no effect.
+ *
+ * @return this builder
+ *
+ * @throws IllegalStateException if an
+ * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews) authentication} was
+ * already set for this builder.
+ */
+ // TODO(b/69796626): make it sticky / update javadoc
+ public Builder setHeader(@NonNull RemoteViews header) {
+ throwIfDestroyed();
+ throwIfAuthenticationCalled();
+ mHeader = Preconditions.checkNotNull(header);
+ return this;
+ }
+
+ /**
+ * Sets a footer to be shown as the last element in the list of datasets.
+ *
+ * <p>When this method is called, you must also {@link #addDataset(Dataset) add a dataset},
+ * otherwise {@link #build()} throws an {@link IllegalStateException}. Similarly, this
+ * method should only be used on {@link FillResponse FillResponses} that do not require
+ * authentication (as the footer could have been set directly in the main presentation in
+ * these cases).
+ *
+ * @param footer a presentation to represent the footer. This presentation is not clickable
+ * &mdash;calling
+ * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent)} on it would
+ * have no effect.
+ *
+ * @return this builder
+ *
+ * @throws IllegalStateException if the FillResponse
+ * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)
+ * requires authentication}.
+ */
+ // TODO(b/69796626): make it sticky / update javadoc
+ public Builder setFooter(@NonNull RemoteViews footer) {
+ throwIfDestroyed();
+ throwIfAuthenticationCalled();
+ mFooter = Preconditions.checkNotNull(footer);
+ return this;
+ }
+
+ /**
* Builds a new {@link FillResponse} instance.
*
* @throws IllegalStateException if any of the following conditions occur:
@@ -417,7 +502,10 @@ public final class FillResponse implements Parcelable {
* <li>No call was made to {@link #addDataset(Dataset)},
* {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)},
* {@link #setSaveInfo(SaveInfo)}, {@link #disableAutofill(long)},
- * or {@link #setClientState(Bundle)}.
+ * {@link #setClientState(Bundle)},
+ * or {@link #setFieldClassificationIds(AutofillId...)}.
+ * <li>{@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} is called
+ * without any previous calls to {@link #addDataset(Dataset)}.
* </ol>
*
* @return A built response.
@@ -425,11 +513,16 @@ public final class FillResponse implements Parcelable {
public FillResponse build() {
throwIfDestroyed();
if (mAuthentication == null && mDatasets == null && mSaveInfo == null
- && mDisableDuration == 0 && mFieldsDetection == null && mClientState == null) {
+ && mDisableDuration == 0 && mFieldClassificationIds == null
+ && mClientState == null) {
throw new IllegalStateException("need to provide: at least one DataSet, or a "
+ "SaveInfo, or an authentication with a presentation, "
+ "or a FieldsDetection, or a client state, or disable autofill");
}
+ if (mDatasets == null && (mHeader != null || mFooter != null)) {
+ throw new IllegalStateException(
+ "must add at least 1 dataset when using header or footer");
+ }
mDestroyed = true;
return new FillResponse(this);
}
@@ -445,6 +538,12 @@ public final class FillResponse implements Parcelable {
throw new IllegalStateException("Already called #disableAutofill()");
}
}
+
+ private void throwIfAuthenticationCalled() {
+ if (mAuthentication != null) {
+ throw new IllegalStateException("Already called #setAuthentication()");
+ }
+ }
}
/////////////////////////////////////
@@ -461,12 +560,15 @@ public final class FillResponse implements Parcelable {
.append(", saveInfo=").append(mSaveInfo)
.append(", clientState=").append(mClientState != null)
.append(", hasPresentation=").append(mPresentation != null)
+ .append(", hasHeader=").append(mHeader != null)
+ .append(", hasFooter=").append(mFooter != null)
.append(", hasAuthentication=").append(mAuthentication != null)
.append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds))
.append(", ignoredIds=").append(Arrays.toString(mIgnoredIds))
.append(", disableDuration=").append(mDisableDuration)
.append(", flags=").append(mFlags)
- .append(", fieldDetection=").append(mFieldsDetection)
+ .append(", fieldClassificationIds=")
+ .append(Arrays.toString(mFieldClassificationIds))
.append("]")
.toString();
}
@@ -488,9 +590,11 @@ public final class FillResponse implements Parcelable {
parcel.writeParcelableArray(mAuthenticationIds, flags);
parcel.writeParcelable(mAuthentication, flags);
parcel.writeParcelable(mPresentation, flags);
+ parcel.writeParcelable(mHeader, flags);
+ parcel.writeParcelable(mFooter, flags);
parcel.writeParcelableArray(mIgnoredIds, flags);
parcel.writeLong(mDisableDuration);
- parcel.writeParcelable(mFieldsDetection, flags);
+ parcel.writeParcelableArray(mFieldClassificationIds, flags);
parcel.writeInt(mFlags);
parcel.writeInt(mRequestId);
}
@@ -520,15 +624,24 @@ public final class FillResponse implements Parcelable {
if (authenticationIds != null) {
builder.setAuthentication(authenticationIds, authentication, presentation);
}
+ final RemoteViews header = parcel.readParcelable(null);
+ if (header != null) {
+ builder.setHeader(header);
+ }
+ final RemoteViews footer = parcel.readParcelable(null);
+ if (footer != null) {
+ builder.setFooter(footer);
+ }
builder.setIgnoredIds(parcel.readParcelableArray(null, AutofillId.class));
final long disableDuration = parcel.readLong();
if (disableDuration > 0) {
builder.disableAutofill(disableDuration);
}
- final FieldsDetection fieldsDetection = parcel.readParcelable(null);
- if (fieldsDetection != null) {
- builder.setFieldsDetection(fieldsDetection);
+ final AutofillId[] fieldClassifactionIds =
+ parcel.readParcelableArray(null, AutofillId.class);
+ if (fieldClassifactionIds != null) {
+ builder.setFieldClassificationIds(fieldClassifactionIds);
}
builder.setFlags(parcel.readInt());
diff --git a/android/service/autofill/InternalScorer.java b/android/service/autofill/InternalScorer.java
new file mode 100644
index 00000000..0da5afc2
--- /dev/null
+++ b/android/service/autofill/InternalScorer.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.service.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcelable;
+import android.view.autofill.AutofillValue;
+
+/**
+ * Superclass of all scorer the system understands. As this is not public all
+ * subclasses have to implement {@link Scorer} again.
+ *
+ * @hide
+ */
+@TestApi
+public abstract class InternalScorer implements Scorer, Parcelable {
+
+ /**
+ * Returns the classification score between an actual {@link AutofillValue} filled
+ * by the user and the expected value predicted by an autofill service.
+ *
+ * <p>A full-match is {@code 1.0} (representing 100%), a full mismatch is {@code 0.0} and
+ * partial mathces are something in between, typically using edit-distance algorithms.
+ */
+ public abstract float getScore(@NonNull AutofillValue actualValue, @NonNull String userData);
+}
diff --git a/android/service/autofill/SaveInfo.java b/android/service/autofill/SaveInfo.java
index 0b50f074..a5a6177d 100644
--- a/android/service/autofill/SaveInfo.java
+++ b/android/service/autofill/SaveInfo.java
@@ -198,23 +198,22 @@ public final class SaveInfo implements Parcelable {
public static final int NEGATIVE_BUTTON_STYLE_REJECT = 1;
/** @hide */
- @IntDef(
- value = {
- NEGATIVE_BUTTON_STYLE_CANCEL,
- NEGATIVE_BUTTON_STYLE_REJECT})
+ @IntDef(prefix = { "NEGATIVE_BUTTON_STYLE_" }, value = {
+ NEGATIVE_BUTTON_STYLE_CANCEL,
+ NEGATIVE_BUTTON_STYLE_REJECT
+ })
@Retention(RetentionPolicy.SOURCE)
@interface NegativeButtonStyle{}
/** @hide */
- @IntDef(
- flag = true,
- value = {
- SAVE_DATA_TYPE_GENERIC,
- SAVE_DATA_TYPE_PASSWORD,
- SAVE_DATA_TYPE_ADDRESS,
- SAVE_DATA_TYPE_CREDIT_CARD,
- SAVE_DATA_TYPE_USERNAME,
- SAVE_DATA_TYPE_EMAIL_ADDRESS})
+ @IntDef(flag = true, prefix = { "SAVE_DATA_TYPE_" }, value = {
+ SAVE_DATA_TYPE_GENERIC,
+ SAVE_DATA_TYPE_PASSWORD,
+ SAVE_DATA_TYPE_ADDRESS,
+ SAVE_DATA_TYPE_CREDIT_CARD,
+ SAVE_DATA_TYPE_USERNAME,
+ SAVE_DATA_TYPE_EMAIL_ADDRESS
+ })
@Retention(RetentionPolicy.SOURCE)
@interface SaveDataType{}
@@ -235,9 +234,10 @@ public final class SaveInfo implements Parcelable {
public static final int FLAG_DONT_SAVE_ON_FINISH = 0x2;
/** @hide */
- @IntDef(
- flag = true,
- value = {FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE, FLAG_DONT_SAVE_ON_FINISH})
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
+ FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE,
+ FLAG_DONT_SAVE_ON_FINISH
+ })
@Retention(RetentionPolicy.SOURCE)
@interface SaveInfoFlags{}
diff --git a/android/service/autofill/SaveRequest.java b/android/service/autofill/SaveRequest.java
index f53967bd..4f85e6b9 100644
--- a/android/service/autofill/SaveRequest.java
+++ b/android/service/autofill/SaveRequest.java
@@ -59,10 +59,11 @@ public final class SaveRequest implements Parcelable {
}
/**
- * Gets the latest client state extra returned from the service.
+ * Gets the latest client state bundle set by the service in a
+ * {@link FillResponse.Builder#setClientState(Bundle) fill response}.
*
* <p><b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, only client state
- * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} where considered. On
+ * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} were considered. On
* Android {@link android.os.Build.VERSION_CODES#P} and higher, bundles set in the result of
* an authenticated request through the
* {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE} extra are
diff --git a/android/service/autofill/Scorer.java b/android/service/autofill/Scorer.java
new file mode 100644
index 00000000..c4018558
--- /dev/null
+++ b/android/service/autofill/Scorer.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.service.autofill;
+
+/**
+ * Helper class used to calculate a score.
+ *
+ * <p>Typically used to calculate the
+ * <a href="AutofillService.html#FieldClassification">field classification</a> score between an
+ * actual {@link android.view.autofill.AutofillValue} filled by the user and the expected value
+ * predicted by an autofill service.
+ */
+public interface Scorer {
+
+}
diff --git a/android/service/autofill/UserData.java b/android/service/autofill/UserData.java
new file mode 100644
index 00000000..f0cc360f
--- /dev/null
+++ b/android/service/autofill/UserData.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.service.autofill;
+
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH;
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.content.ContentResolver;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.autofill.Helper;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Defines the user data used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>.
+ */
+public final class UserData implements Parcelable {
+
+ private static final String TAG = "UserData";
+
+ private static final int DEFAULT_MAX_USER_DATA_SIZE = 10;
+ private static final int DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE = 10;
+ private static final int DEFAULT_MIN_VALUE_LENGTH = 5;
+ private static final int DEFAULT_MAX_VALUE_LENGTH = 100;
+
+ private final InternalScorer mScorer;
+ private final String[] mRemoteIds;
+ private final String[] mValues;
+
+ private UserData(Builder builder) {
+ mScorer = builder.mScorer;
+ mRemoteIds = new String[builder.mRemoteIds.size()];
+ builder.mRemoteIds.toArray(mRemoteIds);
+ mValues = new String[builder.mValues.size()];
+ builder.mValues.toArray(mValues);
+ }
+
+ /** @hide */
+ public InternalScorer getScorer() {
+ return mScorer;
+ }
+
+ /** @hide */
+ public String[] getRemoteIds() {
+ return mRemoteIds;
+ }
+
+ /** @hide */
+ public String[] getValues() {
+ return mValues;
+ }
+
+ /** @hide */
+ public void dump(String prefix, PrintWriter pw) {
+ pw.print(prefix); pw.print("Scorer: "); pw.println(mScorer);
+ // Cannot disclose remote ids or values because they could contain PII
+ pw.print(prefix); pw.print("Remote ids size: "); pw.println(mRemoteIds.length);
+ for (int i = 0; i < mRemoteIds.length; i++) {
+ pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": ");
+ pw.println(Helper.getRedacted(mRemoteIds[i]));
+ }
+ pw.print(prefix); pw.print("Values size: "); pw.println(mValues.length);
+ for (int i = 0; i < mValues.length; i++) {
+ pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": ");
+ pw.println(Helper.getRedacted(mValues[i]));
+ }
+ }
+
+ /** @hide */
+ public static void dumpConstraints(String prefix, PrintWriter pw) {
+ pw.print(prefix); pw.print("maxUserDataSize: "); pw.println(getMaxUserDataSize());
+ pw.print(prefix); pw.print("maxFieldClassificationIdsSize: ");
+ pw.println(getMaxFieldClassificationIdsSize());
+ pw.print(prefix); pw.print("minValueLength: "); pw.println(getMinValueLength());
+ pw.print(prefix); pw.print("maxValueLength: "); pw.println(getMaxValueLength());
+ }
+
+ /**
+ * A builder for {@link UserData} objects.
+ */
+ public static final class Builder {
+ private final InternalScorer mScorer;
+ private final ArrayList<String> mRemoteIds;
+ private final ArrayList<String> mValues;
+ private boolean mDestroyed;
+
+ /**
+ * Creates a new builder for the user data used for <a href="#FieldClassification">field
+ * classification</a>.
+ *
+ * @throws IllegalArgumentException if any of the following occurs:
+ * <ol>
+ * <li>{@code remoteId} is empty
+ * <li>{@code value} is empty
+ * <li>the length of {@code value} is lower than {@link UserData#getMinValueLength()}
+ * <li>the length of {@code value} is higher than {@link UserData#getMaxValueLength()}
+ * <li>{@code scorer} is not instance of a class provided by the Android System.
+ * </ol>
+ */
+ public Builder(@NonNull Scorer scorer, @NonNull String remoteId, @NonNull String value) {
+ Preconditions.checkArgument((scorer instanceof InternalScorer),
+ "not provided by Android System: " + scorer);
+ mScorer = (InternalScorer) scorer;
+ checkValidRemoteId(remoteId);
+ checkValidValue(value);
+ final int capacity = getMaxUserDataSize();
+ mRemoteIds = new ArrayList<>(capacity);
+ mValues = new ArrayList<>(capacity);
+ mRemoteIds.add(remoteId);
+ mValues.add(value);
+ }
+
+ /**
+ * Adds a new value for user data.
+ *
+ * @param remoteId unique string used to identify the user data.
+ * @param value value of the user data.
+ *
+ * @throws IllegalStateException if {@link #build()} or
+ * {@link #add(String, String)} with the same {@code remoteId} has already
+ * been called, or if the number of values add (i.e., calls made to this method plus
+ * constructor) is more than {@link UserData#getMaxUserDataSize()}.
+ *
+ * @throws IllegalArgumentException if {@code remoteId} or {@code value} are empty or if the
+ * length of {@code value} is lower than {@link UserData#getMinValueLength()}
+ * or higher than {@link UserData#getMaxValueLength()}.
+ */
+ public Builder add(@NonNull String remoteId, @NonNull String value) {
+ throwIfDestroyed();
+ checkValidRemoteId(remoteId);
+ checkValidValue(value);
+
+ Preconditions.checkState(!mRemoteIds.contains(remoteId),
+ // Don't include remoteId on message because it could contain PII
+ "already has entry with same remoteId");
+ Preconditions.checkState(!mValues.contains(value),
+ // Don't include remoteId on message because it could contain PII
+ "already has entry with same value");
+ Preconditions.checkState(mRemoteIds.size() < getMaxUserDataSize(),
+ "already added " + mRemoteIds.size() + " elements");
+ mRemoteIds.add(remoteId);
+ mValues.add(value);
+
+ return this;
+ }
+
+ private void checkValidRemoteId(@Nullable String remoteId) {
+ Preconditions.checkNotNull(remoteId);
+ Preconditions.checkArgument(!remoteId.isEmpty(), "remoteId cannot be empty");
+ }
+
+ private void checkValidValue(@Nullable String value) {
+ Preconditions.checkNotNull(value);
+ final int length = value.length();
+ Preconditions.checkArgumentInRange(length, getMinValueLength(),
+ getMaxValueLength(), "value length (" + length + ")");
+ }
+
+ /**
+ * Creates a new {@link UserData} instance.
+ *
+ * <p>You should not interact with this builder once this method is called.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return The built dataset.
+ */
+ public UserData build() {
+ throwIfDestroyed();
+ mDestroyed = true;
+ return new UserData(this);
+ }
+
+ private void throwIfDestroyed() {
+ if (mDestroyed) {
+ throw new IllegalStateException("Already called #build()");
+ }
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ final StringBuilder builder = new StringBuilder("UserData: [scorer=").append(mScorer);
+ // Cannot disclose remote ids or values because they could contain PII
+ builder.append(", remoteIds=");
+ Helper.appendRedacted(builder, mRemoteIds);
+ builder.append(", values=");
+ Helper.appendRedacted(builder, mValues);
+ return builder.append("]").toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mScorer, flags);
+ parcel.writeStringArray(mRemoteIds);
+ parcel.writeStringArray(mValues);
+ }
+
+ public static final Parcelable.Creator<UserData> CREATOR =
+ new Parcelable.Creator<UserData>() {
+ @Override
+ public UserData createFromParcel(Parcel parcel) {
+ // Always go through the builder to ensure the data ingested by
+ // the system obeys the contract of the builder to avoid attacks
+ // using specially crafted parcels.
+ final InternalScorer scorer = parcel.readParcelable(null);
+ final String[] remoteIds = parcel.readStringArray();
+ final String[] values = parcel.readStringArray();
+ final Builder builder = new Builder(scorer, remoteIds[0], values[0]);
+ for (int i = 1; i < remoteIds.length; i++) {
+ builder.add(remoteIds[i], values[i]);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public UserData[] newArray(int size) {
+ return new UserData[size];
+ }
+ };
+
+ /**
+ * Gets the maximum number of values that can be added to a {@link UserData}.
+ */
+ public static int getMaxUserDataSize() {
+ return getInt(AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, DEFAULT_MAX_USER_DATA_SIZE);
+ }
+
+ /**
+ * Gets the maximum number of ids that can be passed to {@link
+ * FillResponse.Builder#setFieldClassificationIds(android.view.autofill.AutofillId...)}.
+ */
+ public static int getMaxFieldClassificationIdsSize() {
+ return getInt(AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE,
+ DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE);
+ }
+
+ /**
+ * Gets the minimum length of values passed to the builder's constructor or
+ * or {@link Builder#add(String, String)}.
+ */
+ public static int getMinValueLength() {
+ return getInt(AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, DEFAULT_MIN_VALUE_LENGTH);
+ }
+
+ /**
+ * Gets the maximum length of values passed to the builder's constructor or
+ * or {@link Builder#add(String, String)}.
+ */
+ public static int getMaxValueLength() {
+ return getInt(AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, DEFAULT_MAX_VALUE_LENGTH);
+ }
+
+ private static int getInt(String settings, int defaultValue) {
+ ContentResolver cr = null;
+ final ActivityThread at = ActivityThread.currentActivityThread();
+ if (at != null) {
+ cr = at.getApplication().getContentResolver();
+ }
+
+ if (cr == null) {
+ Log.w(TAG, "Could not read from " + settings + "; hardcoding " + defaultValue);
+ return defaultValue;
+ }
+ return Settings.Secure.getInt(cr, settings, defaultValue);
+ }
+}
diff --git a/android/service/autofill/Validators.java b/android/service/autofill/Validators.java
index 1c838687..0f1ba989 100644
--- a/android/service/autofill/Validators.java
+++ b/android/service/autofill/Validators.java
@@ -33,6 +33,8 @@ public final class Validators {
/**
* Creates a validator that is only valid if all {@code validators} are valid.
*
+ * <p>Used to represent an {@code AND} boolean operation in a chain of validators.
+ *
* @throws IllegalArgumentException if any element of {@code validators} is an instance of a
* class that is not provided by the Android System.
*/
@@ -44,6 +46,8 @@ public final class Validators {
/**
* Creates a validator that is valid if any of the {@code validators} is valid.
*
+ * <p>Used to represent an {@code OR} boolean operation in a chain of validators.
+ *
* @throws IllegalArgumentException if any element of {@code validators} is an instance of a
* class that is not provided by the Android System.
*/
@@ -53,7 +57,9 @@ public final class Validators {
}
/**
- * Creates a validator that is valid only if {@code validator} is not.
+ * Creates a validator that is valid when {@code validator} is not, and vice versa.
+ *
+ * <p>Used to represent a {@code NOT} boolean operation in a chain of validators.
*
* @throws IllegalArgumentException if {@code validator} is an instance of a class that is not
* provided by the Android System.
diff --git a/android/service/carrier/CarrierService.java b/android/service/carrier/CarrierService.java
index 2707f146..b94ccf9e 100644
--- a/android/service/carrier/CarrierService.java
+++ b/android/service/carrier/CarrierService.java
@@ -33,8 +33,8 @@ import com.android.internal.telephony.ITelephonyRegistry;
* To extend this class, you must declare the service in your manifest file to require the
* {@link android.Manifest.permission#BIND_CARRIER_SERVICES} permission and include an intent
* filter with the {@link #CARRIER_SERVICE_INTERFACE}. If the service should have a long-lived
- * binding, set android.service.carrier.LONG_LIVED_BINDING to true in the service's metadata.
- * For example:
+ * binding, set <code>android.service.carrier.LONG_LIVED_BINDING</code> to <code>true</code> in the
+ * service's metadata. For example:
* </p>
*
* <pre>{@code
diff --git a/android/service/euicc/EuiccService.java b/android/service/euicc/EuiccService.java
index df0842f7..fb530074 100644
--- a/android/service/euicc/EuiccService.java
+++ b/android/service/euicc/EuiccService.java
@@ -23,6 +23,7 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.telephony.euicc.DownloadableSubscription;
import android.telephony.euicc.EuiccInfo;
+import android.telephony.euicc.EuiccManager.OtaStatus;
import android.util.ArraySet;
import java.util.concurrent.LinkedBlockingQueue;
@@ -203,6 +204,16 @@ public abstract class EuiccService extends Service {
public abstract String onGetEid(int slotId);
/**
+ * Return the status of OTA update.
+ *
+ * @param slotId ID of the SIM slot to use for the operation. This is currently not populated
+ * but is here to future-proof the APIs.
+ * @return The status of Euicc OTA update.
+ * @see android.telephony.euicc.EuiccManager#getOtaStatus
+ */
+ public abstract @OtaStatus int onGetOtaStatus(int slotId);
+
+ /**
* Populate {@link DownloadableSubscription} metadata for the given downloadable subscription.
*
* @param slotId ID of the SIM slot to use for the operation. This is currently not populated
@@ -385,6 +396,21 @@ public abstract class EuiccService extends Service {
}
@Override
+ public void getOtaStatus(int slotId, IGetOtaStatusCallback callback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ int status = EuiccService.this.onGetOtaStatus(slotId);
+ try {
+ callback.onSuccess(status);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+
+ @Override
public void getDownloadableSubscriptionMetadata(int slotId,
DownloadableSubscription subscription,
boolean forceDeactivateSim,
diff --git a/android/service/notification/Condition.java b/android/service/notification/Condition.java
index 447afe62..2a352adc 100644
--- a/android/service/notification/Condition.java
+++ b/android/service/notification/Condition.java
@@ -39,7 +39,12 @@ public final class Condition implements Parcelable {
public static final String SCHEME = "condition";
/** @hide */
- @IntDef({STATE_FALSE, STATE_TRUE, STATE_TRUE, STATE_ERROR})
+ @IntDef(prefix = { "STATE_" }, value = {
+ STATE_FALSE,
+ STATE_TRUE,
+ STATE_UNKNOWN,
+ STATE_ERROR
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface State {}
diff --git a/android/service/notification/NotificationListenerService.java b/android/service/notification/NotificationListenerService.java
index dac663e7..18d4a1e6 100644
--- a/android/service/notification/NotificationListenerService.java
+++ b/android/service/notification/NotificationListenerService.java
@@ -229,8 +229,11 @@ public abstract class NotificationListenerService extends Service {
/** @hide */
- @IntDef({NOTIFICATION_CHANNEL_OR_GROUP_ADDED, NOTIFICATION_CHANNEL_OR_GROUP_UPDATED,
- NOTIFICATION_CHANNEL_OR_GROUP_DELETED})
+ @IntDef(prefix = { "NOTIFICATION_CHANNEL_OR_GROUP_" }, value = {
+ NOTIFICATION_CHANNEL_OR_GROUP_ADDED,
+ NOTIFICATION_CHANNEL_OR_GROUP_UPDATED,
+ NOTIFICATION_CHANNEL_OR_GROUP_DELETED
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface ChannelOrGroupModificationTypes {}
diff --git a/android/service/notification/ScheduleCalendar.java b/android/service/notification/ScheduleCalendar.java
new file mode 100644
index 00000000..8a7ff4da
--- /dev/null
+++ b/android/service/notification/ScheduleCalendar.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.notification;
+
+import android.service.notification.ZenModeConfig.ScheduleInfo;
+import android.util.ArraySet;
+import android.util.Log;
+
+import java.util.Calendar;
+import java.util.Objects;
+import java.util.TimeZone;
+
+/**
+ * @hide
+ */
+public class ScheduleCalendar {
+ public static final String TAG = "ScheduleCalendar";
+ public static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
+ private final ArraySet<Integer> mDays = new ArraySet<Integer>();
+ private final Calendar mCalendar = Calendar.getInstance();
+
+ private ScheduleInfo mSchedule;
+
+ @Override
+ public String toString() {
+ return "ScheduleCalendar[mDays=" + mDays + ", mSchedule=" + mSchedule + "]";
+ }
+
+ /**
+ * @return true if schedule will exit on alarm, else false
+ */
+ public boolean exitAtAlarm() {
+ return mSchedule.exitAtAlarm;
+ }
+
+ /**
+ * Sets schedule information
+ */
+ public void setSchedule(ScheduleInfo schedule) {
+ if (Objects.equals(mSchedule, schedule)) return;
+ mSchedule = schedule;
+ updateDays();
+ }
+
+ /**
+ * Sets next alarm of the schedule if the saved next alarm has passed or is further
+ * in the future than given nextAlarm
+ * @param now current time in milliseconds
+ * @param nextAlarm time of next alarm in milliseconds
+ */
+ public void maybeSetNextAlarm(long now, long nextAlarm) {
+ if (mSchedule != null && mSchedule.exitAtAlarm) {
+ // alarm canceled
+ if (nextAlarm == 0) {
+ mSchedule.nextAlarm = 0;
+ }
+ // only allow alarms in the future
+ if (nextAlarm > now) {
+ // store earliest alarm
+ if (mSchedule.nextAlarm == 0) {
+ mSchedule.nextAlarm = nextAlarm;
+ } else {
+ mSchedule.nextAlarm = Math.min(mSchedule.nextAlarm, nextAlarm);
+ }
+ } else if (mSchedule.nextAlarm < now) {
+ if (DEBUG) {
+ Log.d(TAG, "All alarms are in the past " + mSchedule.nextAlarm);
+ }
+ mSchedule.nextAlarm = 0;
+ }
+ }
+ }
+
+ /**
+ * Set calendar time zone to tz
+ * @param tz current time zone
+ */
+ public void setTimeZone(TimeZone tz) {
+ mCalendar.setTimeZone(tz);
+ }
+
+ /**
+ * @param now current time in milliseconds
+ * @return next time this rule changes (starts or ends)
+ */
+ public long getNextChangeTime(long now) {
+ if (mSchedule == null) return 0;
+ final long nextStart = getNextTime(now, mSchedule.startHour, mSchedule.startMinute);
+ final long nextEnd = getNextTime(now, mSchedule.endHour, mSchedule.endMinute);
+ long nextScheduleTime = Math.min(nextStart, nextEnd);
+
+ return nextScheduleTime;
+ }
+
+ private long getNextTime(long now, int hr, int min) {
+ final long time = getTime(now, hr, min);
+ return time <= now ? addDays(time, 1) : time;
+ }
+
+ private long getTime(long millis, int hour, int min) {
+ mCalendar.setTimeInMillis(millis);
+ mCalendar.set(Calendar.HOUR_OF_DAY, hour);
+ mCalendar.set(Calendar.MINUTE, min);
+ mCalendar.set(Calendar.SECOND, 0);
+ mCalendar.set(Calendar.MILLISECOND, 0);
+ return mCalendar.getTimeInMillis();
+ }
+
+ /**
+ * @param time milliseconds since Epoch
+ * @return true if time is within the schedule, else false
+ */
+ public boolean isInSchedule(long time) {
+ if (mSchedule == null || mDays.size() == 0) return false;
+ final long start = getTime(time, mSchedule.startHour, mSchedule.startMinute);
+ long end = getTime(time, mSchedule.endHour, mSchedule.endMinute);
+ if (end <= start) {
+ end = addDays(end, 1);
+ }
+ return isInSchedule(-1, time, start, end) || isInSchedule(0, time, start, end);
+ }
+
+ /**
+ * @param time milliseconds since Epoch
+ * @return true if should exit at time for next alarm, else false
+ */
+ public boolean shouldExitForAlarm(long time) {
+ if (mSchedule == null) {
+ return false;
+ }
+ return mSchedule.exitAtAlarm
+ && mSchedule.nextAlarm != 0
+ && time >= mSchedule.nextAlarm;
+ }
+
+ private boolean isInSchedule(int daysOffset, long time, long start, long end) {
+ final int n = Calendar.SATURDAY;
+ final int day = ((getDayOfWeek(time) - 1) + (daysOffset % n) + n) % n + 1;
+ start = addDays(start, daysOffset);
+ end = addDays(end, daysOffset);
+ return mDays.contains(day) && time >= start && time < end;
+ }
+
+ private int getDayOfWeek(long time) {
+ mCalendar.setTimeInMillis(time);
+ return mCalendar.get(Calendar.DAY_OF_WEEK);
+ }
+
+ private void updateDays() {
+ mDays.clear();
+ if (mSchedule != null && mSchedule.days != null) {
+ for (int i = 0; i < mSchedule.days.length; i++) {
+ mDays.add(mSchedule.days[i]);
+ }
+ }
+ }
+
+ private long addDays(long time, int days) {
+ mCalendar.setTimeInMillis(time);
+ mCalendar.add(Calendar.DATE, days);
+ return mCalendar.getTimeInMillis();
+ }
+}
diff --git a/android/service/notification/ZenModeConfig.java b/android/service/notification/ZenModeConfig.java
index 735b8223..f658ae03 100644
--- a/android/service/notification/ZenModeConfig.java
+++ b/android/service/notification/ZenModeConfig.java
@@ -46,8 +46,10 @@ import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
+import java.util.List;
import java.util.Locale;
import java.util.Objects;
+import java.util.TimeZone;
import java.util.UUID;
/**
@@ -64,11 +66,13 @@ public class ZenModeConfig implements Parcelable {
public static final int MAX_SOURCE = SOURCE_STAR;
private static final int DEFAULT_SOURCE = SOURCE_CONTACT;
+ public static final String EVENTS_DEFAULT_RULE_ID = "EVENTS_DEFAULT_RULE";
+ public static final String EVERY_NIGHT_DEFAULT_RULE_ID = "EVERY_NIGHT_DEFAULT_RULE";
+ public static final List<String> DEFAULT_RULE_IDS = Arrays.asList(EVERY_NIGHT_DEFAULT_RULE_ID,
+ EVENTS_DEFAULT_RULE_ID);
+
public static final int[] ALL_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY };
- public static final int[] WEEKNIGHT_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
- Calendar.WEDNESDAY, Calendar.THURSDAY };
- public static final int[] WEEKEND_DAYS = { Calendar.FRIDAY, Calendar.SATURDAY };
public static final int[] MINUTE_BUCKETS = generateMinuteBuckets();
private static final int SECONDS_MS = 1000;
@@ -529,6 +533,13 @@ public class ZenModeConfig implements Parcelable {
rt.creationTime = safeLong(parser, RULE_ATT_CREATION_TIME, 0);
rt.enabler = parser.getAttributeValue(null, RULE_ATT_ENABLER);
rt.condition = readConditionXml(parser);
+
+ // all default rules and user created rules updated to zenMode important interruptions
+ if (rt.zenMode != Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
+ && Condition.isValidId(rt.conditionId, SYSTEM_AUTHORITY)) {
+ Slog.i(TAG, "Updating zenMode of automatic rule " + rt.name);
+ rt.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+ }
return rt;
}
@@ -692,6 +703,20 @@ public class ZenModeConfig implements Parcelable {
suppressedVisualEffects);
}
+ /**
+ * Creates scheduleCalendar from a condition id
+ * @param conditionId
+ * @return ScheduleCalendar with info populated with conditionId
+ */
+ public static ScheduleCalendar toScheduleCalendar(Uri conditionId) {
+ final ScheduleInfo schedule = ZenModeConfig.tryParseScheduleConditionId(conditionId);
+ if (schedule == null || schedule.days == null || schedule.days.length == 0) return null;
+ final ScheduleCalendar sc = new ScheduleCalendar();
+ sc.setSchedule(schedule);
+ sc.setTimeZone(TimeZone.getDefault());
+ return sc;
+ }
+
private static int sourceToPrioritySenders(int source, int def) {
switch (source) {
case SOURCE_ANYONE: return Policy.PRIORITY_SENDERS_ANY;
@@ -793,7 +818,10 @@ public class ZenModeConfig implements Parcelable {
Condition.FLAG_RELEVANT_NOW);
}
- private static CharSequence getFormattedTime(Context context, long time, boolean isSameDay,
+ /**
+ * Creates readable time from time in milliseconds
+ */
+ public static CharSequence getFormattedTime(Context context, long time, boolean isSameDay,
int userHandle) {
String skeleton = (!isSameDay ? "EEE " : "")
+ (DateFormat.is24HourFormat(context, userHandle) ? "Hm" : "hma");
@@ -801,7 +829,10 @@ public class ZenModeConfig implements Parcelable {
return DateFormat.format(pattern, time);
}
- private static boolean isToday(long time) {
+ /**
+ * Determines whether a time in milliseconds is today or not
+ */
+ public static boolean isToday(long time) {
GregorianCalendar now = new GregorianCalendar();
GregorianCalendar endTime = new GregorianCalendar();
endTime.setTimeInMillis(time);
@@ -890,7 +921,17 @@ public class ZenModeConfig implements Parcelable {
}
public static boolean isValidScheduleConditionId(Uri conditionId) {
- return tryParseScheduleConditionId(conditionId) != null;
+ ScheduleInfo info;
+ try {
+ info = tryParseScheduleConditionId(conditionId);
+ } catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
+ return false;
+ }
+
+ if (info == null || info.days == null || info.days.length == 0) {
+ return false;
+ }
+ return true;
}
public static ScheduleInfo tryParseScheduleConditionId(Uri conditionId) {
@@ -1071,7 +1112,10 @@ public class ZenModeConfig implements Parcelable {
return UUID.randomUUID().toString().replace("-", "");
}
- private static String getOwnerCaption(Context context, String owner) {
+ /**
+ * Gets the name of the app associated with owner
+ */
+ public static String getOwnerCaption(Context context, String owner) {
final PackageManager pm = context.getPackageManager();
try {
final ApplicationInfo info = pm.getApplicationInfo(owner, 0);
diff --git a/android/service/persistentdata/PersistentDataBlockManager.java b/android/service/persistentdata/PersistentDataBlockManager.java
index 9332a5be..0bf68b73 100644
--- a/android/service/persistentdata/PersistentDataBlockManager.java
+++ b/android/service/persistentdata/PersistentDataBlockManager.java
@@ -65,10 +65,10 @@ public class PersistentDataBlockManager {
*/
public static final int FLASH_LOCK_LOCKED = 1;
- @IntDef({
- FLASH_LOCK_UNKNOWN,
- FLASH_LOCK_LOCKED,
- FLASH_LOCK_UNLOCKED,
+ @IntDef(prefix = { "FLASH_LOCK_" }, value = {
+ FLASH_LOCK_UNKNOWN,
+ FLASH_LOCK_LOCKED,
+ FLASH_LOCK_UNLOCKED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface FlashLockState {}
diff --git a/android/service/settings/suggestions/Suggestion.java b/android/service/settings/suggestions/Suggestion.java
index cfeb7fce..11e1e674 100644
--- a/android/service/settings/suggestions/Suggestion.java
+++ b/android/service/settings/suggestions/Suggestion.java
@@ -38,7 +38,7 @@ public final class Suggestion implements Parcelable {
/**
* @hide
*/
- @IntDef(flag = true, value = {
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
FLAG_HAS_BUTTON,
})
@Retention(RetentionPolicy.SOURCE)
diff --git a/android/service/trust/TrustAgentService.java b/android/service/trust/TrustAgentService.java
index 5ef934ed..4bade9f9 100644
--- a/android/service/trust/TrustAgentService.java
+++ b/android/service/trust/TrustAgentService.java
@@ -114,11 +114,10 @@ public class TrustAgentService extends Service {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true,
- value = {
- FLAG_GRANT_TRUST_INITIATED_BY_USER,
- FLAG_GRANT_TRUST_DISMISS_KEYGUARD,
- })
+ @IntDef(flag = true, prefix = { "FLAG_GRANT_TRUST_" }, value = {
+ FLAG_GRANT_TRUST_INITIATED_BY_USER,
+ FLAG_GRANT_TRUST_DISMISS_KEYGUARD,
+ })
public @interface GrantTrustFlags {}
@@ -138,11 +137,10 @@ public class TrustAgentService extends Service {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true,
- value = {
- TOKEN_STATE_ACTIVE,
- TOKEN_STATE_INACTIVE,
- })
+ @IntDef(flag = true, prefix = { "TOKEN_STATE_" }, value = {
+ TOKEN_STATE_ACTIVE,
+ TOKEN_STATE_INACTIVE,
+ })
public @interface TokenState {}
private static final int MSG_UNLOCK_ATTEMPT = 1;
diff --git a/android/service/voice/AlwaysOnHotwordDetector.java b/android/service/voice/AlwaysOnHotwordDetector.java
index 9464a875..76d89ef0 100644
--- a/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/android/service/voice/AlwaysOnHotwordDetector.java
@@ -87,11 +87,11 @@ public class AlwaysOnHotwordDetector {
// Keyphrase management actions. Used in getManageIntent() ----//
@Retention(RetentionPolicy.SOURCE)
- @IntDef(value = {
- MANAGE_ACTION_ENROLL,
- MANAGE_ACTION_RE_ENROLL,
- MANAGE_ACTION_UN_ENROLL
- })
+ @IntDef(prefix = { "MANAGE_ACTION_" }, value = {
+ MANAGE_ACTION_ENROLL,
+ MANAGE_ACTION_RE_ENROLL,
+ MANAGE_ACTION_UN_ENROLL
+ })
private @interface ManageActions {}
/**
@@ -116,12 +116,11 @@ public class AlwaysOnHotwordDetector {
//-- Flags for startRecognition ----//
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true,
- value = {
- RECOGNITION_FLAG_NONE,
- RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO,
- RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS
- })
+ @IntDef(flag = true, prefix = { "RECOGNITION_FLAG_" }, value = {
+ RECOGNITION_FLAG_NONE,
+ RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO,
+ RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS
+ })
public @interface RecognitionFlags {}
/**
@@ -150,11 +149,10 @@ public class AlwaysOnHotwordDetector {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true,
- value = {
- RECOGNITION_MODE_VOICE_TRIGGER,
- RECOGNITION_MODE_USER_IDENTIFICATION,
- })
+ @IntDef(flag = true, prefix = { "RECOGNITION_MODE_" }, value = {
+ RECOGNITION_MODE_VOICE_TRIGGER,
+ RECOGNITION_MODE_USER_IDENTIFICATION,
+ })
public @interface RecognitionModes {}
/**
diff --git a/android/service/wallpaper/WallpaperService.java b/android/service/wallpaper/WallpaperService.java
index dd0ae339..595bfb7a 100644
--- a/android/service/wallpaper/WallpaperService.java
+++ b/android/service/wallpaper/WallpaperService.java
@@ -42,6 +42,7 @@ import android.os.SystemClock;
import android.util.Log;
import android.util.MergedConfiguration;
import android.view.Display;
+import android.view.DisplayCutout;
import android.view.Gravity;
import android.view.IWindowSession;
import android.view.InputChannel;
@@ -101,6 +102,7 @@ public abstract class WallpaperService extends Service {
private static final int DO_DETACH = 20;
private static final int DO_SET_DESIRED_SIZE = 30;
private static final int DO_SET_DISPLAY_PADDING = 40;
+ private static final int DO_IN_AMBIENT_MODE = 50;
private static final int MSG_UPDATE_SURFACE = 10000;
private static final int MSG_VISIBILITY_CHANGED = 10010;
@@ -176,6 +178,9 @@ public abstract class WallpaperService extends Service {
final Rect mFinalSystemInsets = new Rect();
final Rect mFinalStableInsets = new Rect();
final Rect mBackdropFrame = new Rect();
+ final DisplayCutout.ParcelableWrapper mDisplayCutout =
+ new DisplayCutout.ParcelableWrapper();
+ DisplayCutout mDispatchedDisplayCutout = DisplayCutout.NO_CUTOUT;
final MergedConfiguration mMergedConfiguration = new MergedConfiguration();
final WindowManager.LayoutParams mLayout
@@ -191,6 +196,7 @@ public abstract class WallpaperService extends Service {
float mPendingYOffsetStep;
boolean mPendingSync;
MotionEvent mPendingMove;
+ boolean mIsInAmbientMode;
// Needed for throttling onComputeColors.
private long mLastColorInvalidation;
@@ -302,7 +308,8 @@ public abstract class WallpaperService extends Service {
public void resized(Rect frame, Rect overscanInsets, Rect contentInsets,
Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw,
MergedConfiguration mergedConfiguration, Rect backDropRect, boolean forceLayout,
- boolean alwaysConsumeNavBar, int displayId) {
+ boolean alwaysConsumeNavBar, int displayId,
+ DisplayCutout.ParcelableWrapper displayCutout) {
Message msg = mCaller.obtainMessageIO(MSG_WINDOW_RESIZED,
reportDraw ? 1 : 0, outsets);
mCaller.sendMessage(msg);
@@ -426,6 +433,15 @@ public abstract class WallpaperService extends Service {
public boolean isPreview() {
return mIWallpaperEngine.mIsPreview;
}
+
+ /**
+ * Returns true if this engine is running in ambient mode -- that is,
+ * it is being shown in low power mode, in always on display.
+ * @hide
+ */
+ public boolean isInAmbientMode() {
+ return mIsInAmbientMode;
+ }
/**
* Control whether this wallpaper will receive raw touch events
@@ -544,6 +560,15 @@ public abstract class WallpaperService extends Service {
}
/**
+ * Called when the device enters or exits ambient mode.
+ *
+ * @param inAmbientMode {@code true} if in ambient mode.
+ * @hide
+ */
+ public void onAmbientModeChanged(boolean inAmbientMode) {
+ }
+
+ /**
* Called when an application has changed the desired virtual size of
* the wallpaper.
*/
@@ -626,6 +651,16 @@ public abstract class WallpaperService extends Service {
return null;
}
+ /**
+ * Sets internal engine state. Only for testing.
+ * @param created {@code true} or {@code false}.
+ * @hide
+ */
+ @VisibleForTesting
+ public void setCreated(boolean created) {
+ mCreated = created;
+ }
+
protected void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) {
out.print(prefix); out.print("mInitializing="); out.print(mInitializing);
out.print(" mDestroyed="); out.println(mDestroyed);
@@ -678,7 +713,8 @@ public abstract class WallpaperService extends Service {
}
Message msg = mCaller.obtainMessageO(MSG_TOUCH_EVENT, event);
mCaller.sendMessage(msg);
- } else {event.recycle();
+ } else {
+ event.recycle();
}
}
@@ -750,7 +786,7 @@ public abstract class WallpaperService extends Service {
mInputChannel = new InputChannel();
if (mSession.addToDisplay(mWindow, mWindow.mSeq, mLayout, View.VISIBLE,
Display.DEFAULT_DISPLAY, mContentInsets, mStableInsets, mOutsets,
- mInputChannel) < 0) {
+ mDisplayCutout, mInputChannel) < 0) {
Log.w(TAG, "Failed to add window while updating wallpaper surface.");
return;
}
@@ -776,7 +812,7 @@ public abstract class WallpaperService extends Service {
mWindow, mWindow.mSeq, mLayout, mWidth, mHeight,
View.VISIBLE, 0, mWinFrame, mOverscanInsets, mContentInsets,
mVisibleInsets, mStableInsets, mOutsets, mBackdropFrame,
- mMergedConfiguration, mSurfaceHolder.mSurface);
+ mDisplayCutout, mMergedConfiguration, mSurfaceHolder.mSurface);
if (DEBUG) Log.v(TAG, "New surface: " + mSurfaceHolder.mSurface
+ ", frame=" + mWinFrame);
@@ -800,6 +836,8 @@ public abstract class WallpaperService extends Service {
mStableInsets.top += padding.top;
mStableInsets.right += padding.right;
mStableInsets.bottom += padding.bottom;
+ mDisplayCutout.set(mDisplayCutout.get().inset(-padding.left, -padding.top,
+ -padding.right, -padding.bottom));
}
if (mCurWidth != w) {
@@ -819,6 +857,7 @@ public abstract class WallpaperService extends Service {
insetsChanged |= !mDispatchedContentInsets.equals(mContentInsets);
insetsChanged |= !mDispatchedStableInsets.equals(mStableInsets);
insetsChanged |= !mDispatchedOutsets.equals(mOutsets);
+ insetsChanged |= !mDispatchedDisplayCutout.equals(mDisplayCutout.get());
mSurfaceHolder.setSurfaceFrameSize(w, h);
mSurfaceHolder.mSurfaceLock.unlock();
@@ -885,12 +924,13 @@ public abstract class WallpaperService extends Service {
mDispatchedContentInsets.set(mContentInsets);
mDispatchedStableInsets.set(mStableInsets);
mDispatchedOutsets.set(mOutsets);
+ mDispatchedDisplayCutout = mDisplayCutout.get();
mFinalSystemInsets.set(mDispatchedOverscanInsets);
mFinalStableInsets.set(mDispatchedStableInsets);
WindowInsets insets = new WindowInsets(mFinalSystemInsets,
null, mFinalStableInsets,
getResources().getConfiguration().isScreenRound(), false,
- null /* displayCutout */);
+ mDispatchedDisplayCutout);
if (DEBUG) {
Log.v(TAG, "dispatching insets=" + insets);
}
@@ -977,6 +1017,26 @@ public abstract class WallpaperService extends Service {
updateSurface(false, false, false);
}
+ /**
+ * Executes life cycle event and updates internal ambient mode state based on
+ * message sent from handler.
+ *
+ * @param inAmbientMode True if in ambient mode.
+ * @hide
+ */
+ @VisibleForTesting
+ public void doAmbientModeChanged(boolean inAmbientMode) {
+ if (!mDestroyed) {
+ if (DEBUG) {
+ Log.v(TAG, "onAmbientModeChanged(" + inAmbientMode + "): " + this);
+ }
+ mIsInAmbientMode = inAmbientMode;
+ if (mCreated) {
+ onAmbientModeChanged(inAmbientMode);
+ }
+ }
+ }
+
void doDesiredSizeChanged(int desiredWidth, int desiredHeight) {
if (!mDestroyed) {
if (DEBUG) Log.v(TAG, "onDesiredSizeChanged("
@@ -1217,6 +1277,12 @@ public abstract class WallpaperService extends Service {
mCaller.sendMessage(msg);
}
+ @Override
+ public void setInAmbientMode(boolean inAmbientDisplay) throws RemoteException {
+ Message msg = mCaller.obtainMessageI(DO_IN_AMBIENT_MODE, inAmbientDisplay ? 1 : 0);
+ mCaller.sendMessage(msg);
+ }
+
public void dispatchPointer(MotionEvent event) {
if (mEngine != null) {
mEngine.dispatchPointer(event);
@@ -1254,6 +1320,7 @@ public abstract class WallpaperService extends Service {
mCaller.sendMessage(msg);
}
+ @Override
public void executeMessage(Message message) {
switch (message.what) {
case DO_ATTACH: {
@@ -1280,6 +1347,11 @@ public abstract class WallpaperService extends Service {
}
case DO_SET_DISPLAY_PADDING: {
mEngine.doDisplayPaddingChanged((Rect) message.obj);
+ return;
+ }
+ case DO_IN_AMBIENT_MODE: {
+ mEngine.doAmbientModeChanged(message.arg1 != 0);
+ return;
}
case MSG_UPDATE_SURFACE:
mEngine.updateSurface(true, false, false);
diff --git a/android/speech/tts/TextToSpeech.java b/android/speech/tts/TextToSpeech.java
index 763ea2ca..01562b32 100644
--- a/android/speech/tts/TextToSpeech.java
+++ b/android/speech/tts/TextToSpeech.java
@@ -80,8 +80,15 @@ public class TextToSpeech {
public static final int STOPPED = -2;
/** @hide */
- @IntDef({ERROR_SYNTHESIS, ERROR_SERVICE, ERROR_OUTPUT, ERROR_NETWORK, ERROR_NETWORK_TIMEOUT,
- ERROR_INVALID_REQUEST, ERROR_NOT_INSTALLED_YET})
+ @IntDef(prefix = { "ERROR_" }, value = {
+ ERROR_SYNTHESIS,
+ ERROR_SERVICE,
+ ERROR_OUTPUT,
+ ERROR_NETWORK,
+ ERROR_NETWORK_TIMEOUT,
+ ERROR_INVALID_REQUEST,
+ ERROR_NOT_INSTALLED_YET
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface Error {}
diff --git a/android/support/LibraryVersions.java b/android/support/LibraryVersions.java
index efa0cbae..6d8d6bf5 100644
--- a/android/support/LibraryVersions.java
+++ b/android/support/LibraryVersions.java
@@ -36,14 +36,24 @@ public class LibraryVersions {
public static final Version ROOM = FLATFOOT_1_0_BATCH;
/**
- * Version code for Lifecycle extensions (live data, view model etc)
+ * Version code for Lifecycle extensions (ProcessLifecycleOwner, Fragment support)
*/
- public static final Version LIFECYCLES_EXT = FLATFOOT_1_0_BATCH;
+ public static final Version LIFECYCLES_EXT = new Version("1.1.0-SNAPSHOT");
+
+ /**
+ * Version code for Lifecycle LiveData
+ */
+ public static final Version LIFECYCLES_LIVEDATA = LIFECYCLES_EXT;
+
+ /**
+ * Version code for Lifecycle ViewModel
+ */
+ public static final Version LIFECYCLES_VIEWMODEL = LIFECYCLES_EXT;
/**
* Version code for RecyclerView & Room paging
*/
- public static final Version PAGING = new Version("1.0.0-alpha3");
+ public static final Version PAGING = new Version("1.0.0-alpha4-1");
private static final Version LIFECYCLES = new Version("1.0.3");
diff --git a/android/support/Version.java b/android/support/Version.java
index 69b7f5e2..36c7728b 100644
--- a/android/support/Version.java
+++ b/android/support/Version.java
@@ -16,6 +16,7 @@
package android.support;
+import java.io.File;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -23,17 +24,28 @@ import java.util.regex.Pattern;
* Utility class which represents a version
*/
public class Version implements Comparable<Version> {
+ private static final Pattern VERSION_FILE_REGEX = Pattern.compile("^(\\d+\\.\\d+\\.\\d+).txt$");
+ private static final Pattern VERSION_REGEX = Pattern
+ .compile("^(\\d+)\\.(\\d+)\\.(\\d+)(-.+)?$");
+
private final int mMajor;
private final int mMinor;
private final int mPatch;
private final String mExtra;
public Version(String versionString) {
- Pattern compile = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)(-.+)?$");
- Matcher matcher = compile.matcher(versionString);
+ this(checkedMatcher(versionString));
+ }
+
+ private static Matcher checkedMatcher(String versionString) {
+ Matcher matcher = VERSION_REGEX.matcher(versionString);
if (!matcher.matches()) {
throw new IllegalArgumentException("Can not parse version: " + versionString);
}
+ return matcher;
+ }
+
+ private Version(Matcher matcher) {
mMajor = Integer.parseInt(matcher.group(1));
mMinor = Integer.parseInt(matcher.group(2));
mPatch = Integer.parseInt(matcher.group(3));
@@ -117,4 +129,29 @@ public class Version implements Comparable<Version> {
result = 31 * result + (mExtra != null ? mExtra.hashCode() : 0);
return result;
}
+
+ /**
+ * @return Version or null, if a name of the given file doesn't match
+ */
+ public static Version from(File file) {
+ if (!file.isFile()) {
+ return null;
+ }
+ Matcher matcher = VERSION_FILE_REGEX.matcher(file.getName());
+ if (!matcher.matches()) {
+ return null;
+ }
+ return new Version(matcher.group(1));
+ }
+
+ /**
+ * @return Version or null, if the given string doesn't match
+ */
+ public static Version from(String versionString) {
+ Matcher matcher = VERSION_REGEX.matcher(versionString);
+ if (!matcher.matches()) {
+ return null;
+ }
+ return new Version(matcher);
+ }
}
diff --git a/android/support/animation/AnimationHandler.java b/android/support/animation/AnimationHandler.java
index 9f63bd15..6c39b23a 100644
--- a/android/support/animation/AnimationHandler.java
+++ b/android/support/animation/AnimationHandler.java
@@ -16,11 +16,11 @@
package android.support.animation;
-import android.annotation.TargetApi;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
+import android.support.annotation.RequiresApi;
import android.support.v4.util.SimpleArrayMap;
import android.view.Choreographer;
@@ -191,7 +191,7 @@ class AnimationHandler {
/**
* Default provider of timing pulse that uses Choreographer for frame callbacks.
*/
- @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
private static class FrameCallbackProvider16 extends AnimationFrameCallbackProvider {
private final Choreographer mChoreographer = Choreographer.getInstance();
diff --git a/android/support/animation/FloatPropertyCompat.java b/android/support/animation/FloatPropertyCompat.java
index cde340c8..ec8d0cac 100644
--- a/android/support/animation/FloatPropertyCompat.java
+++ b/android/support/animation/FloatPropertyCompat.java
@@ -16,7 +16,7 @@
package android.support.animation;
-import android.annotation.TargetApi;
+import android.support.annotation.RequiresApi;
import android.util.FloatProperty;
/**
@@ -51,7 +51,7 @@ public abstract class FloatPropertyCompat<T> {
* @param <T> the class on which the Property is declared
* @return a new {@link FloatPropertyCompat} wrapper for the given {@link FloatProperty} object
*/
- @TargetApi(24)
+ @RequiresApi(24)
public static <T> FloatPropertyCompat<T> createFloatPropertyCompat(
final FloatProperty<T> property) {
return new FloatPropertyCompat<T>(property.getName()) {
diff --git a/android/support/annotation/IntDef.java b/android/support/annotation/IntDef.java
index f621b7f4..99457264 100644
--- a/android/support/annotation/IntDef.java
+++ b/android/support/annotation/IntDef.java
@@ -46,12 +46,14 @@ import java.lang.annotation.Target;
* flag = true,
* value = {NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
* </code></pre>
+ *
+ * @see LongDef
*/
@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface IntDef {
/** Defines the allowed constants for this element */
- long[] value() default {};
+ int[] value() default {};
/** Defines whether the constants can be used as a flag, or just as an enum (the default) */
boolean flag() default false;
diff --git a/android/support/annotation/LongDef.java b/android/support/annotation/LongDef.java
new file mode 100644
index 00000000..3dea338d
--- /dev/null
+++ b/android/support/annotation/LongDef.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.annotation;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Denotes that the annotated long element represents
+ * a logical type and that its value should be one of the explicitly
+ * named constants. If the LongDef#flag() attribute is set to true,
+ * multiple constants can be combined.
+ * <p>
+ * Example:
+ * <pre><code>
+ * &#64;Retention(SOURCE)
+ * &#64;LongDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
+ * public @interface NavigationMode {}
+ * public static final long NAVIGATION_MODE_STANDARD = 0;
+ * public static final long NAVIGATION_MODE_LIST = 1;
+ * public static final long NAVIGATION_MODE_TABS = 2;
+ * ...
+ * public abstract void setNavigationMode(@NavigationMode long mode);
+ * &#64;NavigationMode
+ * public abstract long getNavigationMode();
+ * </code></pre>
+ * For a flag, set the flag attribute:
+ * <pre><code>
+ * &#64;LongDef(
+ * flag = true,
+ * value = {NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
+ * </code></pre>
+ *
+ * @see IntDef
+ */
+@Retention(SOURCE)
+@Target({ANNOTATION_TYPE})
+public @interface LongDef {
+ /** Defines the allowed constants for this element */
+ long[] value() default {};
+
+ /** Defines whether the constants can be used as a flag, or just as an enum (the default) */
+ boolean flag() default false;
+} \ No newline at end of file
diff --git a/android/support/car/drawer/CarDrawerActivity.java b/android/support/car/drawer/CarDrawerActivity.java
deleted file mode 100644
index f46c652b..00000000
--- a/android/support/car/drawer/CarDrawerActivity.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.car.drawer;
-
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.support.annotation.LayoutRes;
-import android.support.annotation.Nullable;
-import android.support.car.R;
-import android.support.v4.widget.DrawerLayout;
-import android.support.v7.app.ActionBarDrawerToggle;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v7.widget.Toolbar;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * Common base Activity for car apps that need to present a Drawer.
- *
- * <p>This Activity manages the overall layout. To use it, sub-classes need to:
- *
- * <ul>
- * <li>Provide the root-items for the Drawer by implementing {@link #getRootAdapter()}.
- * <li>Add their main content using {@link #setMainContent(int)} or {@link #setMainContent(View)}.
- * They can also add fragments to the main-content container by obtaining its id using
- * {@link #getContentContainerId()}
- * </ul>
- *
- * <p>This class will take care of drawer toggling and display.
- *
- * <p>The rootAdapter can implement nested-navigation, in its click-handling, by passing the
- * CarDrawerAdapter for the next level to
- * {@link CarDrawerController#pushAdapter(CarDrawerAdapter)}.
- *
- * <p>Any Activity's based on this class need to set their theme to CarDrawerActivityTheme or a
- * derivative.
- */
-public abstract class CarDrawerActivity extends AppCompatActivity {
- private CarDrawerController mDrawerController;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.car_drawer_activity);
-
- DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);
- ActionBarDrawerToggle drawerToggle = new ActionBarDrawerToggle(
- this /* activity */,
- drawerLayout, /* DrawerLayout object */
- R.string.car_drawer_open,
- R.string.car_drawer_close);
-
- Toolbar toolbar = findViewById(R.id.car_toolbar);
- setSupportActionBar(toolbar);
-
- mDrawerController = new CarDrawerController(toolbar, drawerLayout, drawerToggle);
- mDrawerController.setRootAdapter(getRootAdapter());
-
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
- getSupportActionBar().setHomeButtonEnabled(true);
- }
-
- /**
- * Returns the {@link CarDrawerController} that is responsible for handling events relating
- * to the drawer in this Activity.
- *
- * @return The {@link CarDrawerController} linked to this Activity. This value will be
- * {@code null} if this method is called before {@code onCreate()} has been called.
- */
- @Nullable
- protected CarDrawerController getDrawerController() {
- return mDrawerController;
- }
-
- @Override
- protected void onPostCreate(Bundle savedInstanceState) {
- super.onPostCreate(savedInstanceState);
- mDrawerController.syncState();
- }
-
- /**
- * @return Adapter for root content of the Drawer.
- */
- protected abstract CarDrawerAdapter getRootAdapter();
-
- /**
- * Set main content to display in this Activity. It will be added to R.id.content_frame in
- * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(View)}.
- *
- * @param view View to display as main content.
- */
- public void setMainContent(View view) {
- ViewGroup parent = findViewById(getContentContainerId());
- parent.addView(view);
- }
-
- /**
- * Set main content to display in this Activity. It will be added to R.id.content_frame in
- * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(int)}.
- *
- * @param resourceId Layout to display as main content.
- */
- public void setMainContent(@LayoutRes int resourceId) {
- ViewGroup parent = findViewById(getContentContainerId());
- LayoutInflater inflater = getLayoutInflater();
- inflater.inflate(resourceId, parent, true);
- }
-
- /**
- * Get the id of the main content Container which is a FrameLayout. Subclasses can add their own
- * content/fragments inside here.
- *
- * @return Id of FrameLayout where main content of the subclass Activity can be added.
- */
- protected int getContentContainerId() {
- return R.id.content_frame;
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- mDrawerController.closeDrawer();
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- mDrawerController.onConfigurationChanged(newConfig);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- return mDrawerController.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
- }
-}
diff --git a/android/support/car/drawer/CarDrawerAdapter.java b/android/support/car/drawer/CarDrawerAdapter.java
deleted file mode 100644
index b0fd965d..00000000
--- a/android/support/car/drawer/CarDrawerAdapter.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.car.drawer;
-
-import android.content.Context;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.car.R;
-import android.support.car.widget.PagedListView;
-import android.support.v7.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * Base adapter for displaying items in the car navigation drawer, which uses a
- * {@link PagedListView}.
- *
- * <p>Subclasses must set the title that will be displayed when displaying the contents of the
- * drawer via {@link #setTitle(CharSequence)}. The title can be updated at any point later on. The
- * title of the root adapter will also be the main title showed in the toolbar when the drawer is
- * closed. See {@link CarDrawerController#setRootAdapter(CarDrawerAdapter)} for more information.
- *
- * <p>This class also takes care of implementing the PageListView.ItemCamp contract and subclasses
- * should implement {@link #getActualItemCount()}.
- */
-public abstract class CarDrawerAdapter extends RecyclerView.Adapter<DrawerItemViewHolder>
- implements PagedListView.ItemCap, DrawerItemClickListener {
- private final boolean mShowDisabledListOnEmpty;
- private final Drawable mEmptyListDrawable;
- private int mMaxItems = PagedListView.ItemCap.UNLIMITED;
- private CharSequence mTitle;
- private TitleChangeListener mTitleChangeListener;
-
- /**
- * Interface for a class that will be notified a new title has been set on this adapter.
- */
- interface TitleChangeListener {
- /**
- * Called when {@link #setTitle(CharSequence)} has been called and the title has been
- * changed.
- */
- void onTitleChanged(CharSequence newTitle);
- }
-
- protected CarDrawerAdapter(Context context, boolean showDisabledListOnEmpty) {
- mShowDisabledListOnEmpty = showDisabledListOnEmpty;
-
- mEmptyListDrawable = context.getDrawable(R.drawable.ic_list_view_disable);
- mEmptyListDrawable.setColorFilter(context.getColor(R.color.car_tint),
- PorterDuff.Mode.SRC_IN);
- }
-
- /** Returns the title set via {@link #setTitle(CharSequence)}. */
- CharSequence getTitle() {
- return mTitle;
- }
-
- /** Updates the title to display in the toolbar for this Adapter. */
- public final void setTitle(@NonNull CharSequence title) {
- if (title == null) {
- throw new IllegalArgumentException("setTitle() cannot be passed a null title!");
- }
-
- mTitle = title;
-
- if (mTitleChangeListener != null) {
- mTitleChangeListener.onTitleChanged(mTitle);
- }
- }
-
- /** Sets a listener to be notified whenever the title of this adapter has been changed. */
- void setTitleChangeListener(@Nullable TitleChangeListener listener) {
- mTitleChangeListener = listener;
- }
-
- @Override
- public final void setMaxItems(int maxItems) {
- mMaxItems = maxItems;
- }
-
- @Override
- public final int getItemCount() {
- if (shouldShowDisabledListItem()) {
- return 1;
- }
- return mMaxItems >= 0 ? Math.min(mMaxItems, getActualItemCount()) : getActualItemCount();
- }
-
- /**
- * Returns the absolute number of items that can be displayed in the list.
- *
- * <p>A class should implement this method to supply the number of items to be displayed.
- * Returning 0 from this method will cause an empty list icon to be displayed in the drawer.
- *
- * <p>A class should override this method rather than {@link #getItemCount()} because that
- * method is handling the logic of when to display the empty list icon. It will return 1 when
- * {@link #getActualItemCount()} returns 0.
- *
- * @return The number of items to be displayed in the list.
- */
- protected abstract int getActualItemCount();
-
- @Override
- public final int getItemViewType(int position) {
- if (shouldShowDisabledListItem()) {
- return R.layout.car_drawer_list_item_empty;
- }
-
- return usesSmallLayout(position)
- ? R.layout.car_drawer_list_item_small
- : R.layout.car_drawer_list_item_normal;
- }
-
- /**
- * Used to indicate the layout used for the Drawer item at given position. Subclasses can
- * override this to use normal layout which includes text element below title.
- *
- * <p>A small layout is presented by the layout {@code R.layout.car_drawer_list_item_small}.
- * Otherwise, the layout {@code R.layout.car_drawer_list_item_normal} will be used.
- *
- * @param position Adapter position of item.
- * @return Whether the item at this position will use a small layout (default) or normal layout.
- */
- protected boolean usesSmallLayout(int position) {
- return true;
- }
-
- @Override
- public final DrawerItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
- return new DrawerItemViewHolder(view);
- }
-
- @Override
- public final void onBindViewHolder(DrawerItemViewHolder holder, int position) {
- if (shouldShowDisabledListItem()) {
- holder.getTitle().setText(null);
- holder.getIcon().setImageDrawable(mEmptyListDrawable);
- holder.setItemClickListener(null);
- } else {
- holder.setItemClickListener(this);
- populateViewHolder(holder, position);
- }
- }
-
- /**
- * Whether or not this adapter should be displaying an empty list icon. The icon is shown if it
- * has been configured to show and there are no items to be displayed.
- */
- private boolean shouldShowDisabledListItem() {
- return mShowDisabledListOnEmpty && getActualItemCount() == 0;
- }
-
- /**
- * Subclasses should set all elements in {@code holder} to populate the drawer-item. If some
- * element is not used, it should be nulled out since these ViewHolder/View's are recycled.
- */
- protected abstract void populateViewHolder(DrawerItemViewHolder holder, int position);
-
- /**
- * Called when this adapter has been popped off the stack and is no longer needed. Subclasses
- * can override to do any necessary cleanup.
- */
- public void cleanup() {}
-}
diff --git a/android/support/car/drawer/CarDrawerController.java b/android/support/car/drawer/CarDrawerController.java
deleted file mode 100644
index 7b23714c..00000000
--- a/android/support/car/drawer/CarDrawerController.java
+++ /dev/null
@@ -1,335 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.car.drawer;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.support.annotation.AnimRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.car.R;
-import android.support.car.widget.PagedListView;
-import android.support.v4.widget.DrawerLayout;
-import android.support.v7.app.ActionBarDrawerToggle;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.Toolbar;
-import android.view.Gravity;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.animation.AnimationUtils;
-import android.widget.ProgressBar;
-
-import java.util.Stack;
-
-/**
- * A controller that will handle the set up of the navigation drawer. It will hook up the
- * necessary buttons for up navigation, as well as expose methods to allow for a drill down
- * navigation.
- */
-public class CarDrawerController {
- /** An animation for when a user navigates into a submenu. */
- @AnimRes
- private static final int DRILL_DOWN_ANIM = R.anim.fade_in_trans_right_layout_anim;
-
- /** An animation for when a user navigates up (when the back button is pressed). */
- @AnimRes
- private static final int NAVIGATE_UP_ANIM = R.anim.fade_in_trans_left_layout_anim;
-
- /** The amount that the drawer has been opened before its color should be switched. */
- private static final float COLOR_SWITCH_SLIDE_OFFSET = 0.25f;
-
- /**
- * A representation of the hierarchy of navigation being displayed in the list. The ordering of
- * this stack is the order that the user has visited each level. When the user navigates up,
- * the adapters are popped from this list.
- */
- private final Stack<CarDrawerAdapter> mAdapterStack = new Stack<>();
-
- private final Context mContext;
-
- private final Toolbar mToolbar;
- private final DrawerLayout mDrawerLayout;
- private final ActionBarDrawerToggle mDrawerToggle;
-
- private final PagedListView mDrawerList;
- private final ProgressBar mProgressBar;
- private final View mDrawerContent;
-
- /**
- * Creates a {@link CarDrawerController} that will control the navigation of the drawer given by
- * {@code drawerLayout}.
- *
- * <p>The given {@code drawerLayout} should either have a child View that is inflated from
- * {@code R.layout.car_drawer} or ensure that it three children that have the IDs found in that
- * layout.
- *
- * @param toolbar The {@link Toolbar} that will serve as the action bar for an Activity.
- * @param drawerLayout The top-level container for the window content that shows the
- * interactive drawer.
- * @param drawerToggle The {@link ActionBarDrawerToggle} that bridges the given {@code toolbar}
- * and {@code drawerLayout}.
- */
- public CarDrawerController(Toolbar toolbar,
- DrawerLayout drawerLayout,
- ActionBarDrawerToggle drawerToggle) {
- mToolbar = toolbar;
- mContext = drawerLayout.getContext();
- mDrawerToggle = drawerToggle;
- mDrawerLayout = drawerLayout;
-
- mDrawerContent = drawerLayout.findViewById(R.id.drawer_content);
- mDrawerList = drawerLayout.findViewById(R.id.drawer_list);
- mDrawerList.setMaxPages(PagedListView.ItemCap.UNLIMITED);
- mProgressBar = drawerLayout.findViewById(R.id.drawer_progress);
-
- setupDrawerToggling();
- }
-
- /**
- * Sets the {@link CarDrawerAdapter} that will function as the root adapter. The contents of
- * this root adapter are shown when the drawer is first opened. It is also the top-most level of
- * navigation in the drawer.
- *
- * @param rootAdapter The adapter that will act as the root. If this value is {@code null}, then
- * this method will do nothing.
- */
- public void setRootAdapter(@Nullable CarDrawerAdapter rootAdapter) {
- if (rootAdapter == null) {
- return;
- }
-
- // The root adapter is always the last item in the stack.
- if (mAdapterStack.size() > 0) {
- mAdapterStack.set(0, rootAdapter);
- } else {
- mAdapterStack.push(rootAdapter);
- }
-
- setToolbarTitleFrom(rootAdapter);
- mDrawerList.setAdapter(rootAdapter);
- }
-
- /**
- * Switches to use the given {@link CarDrawerAdapter} as the one to supply the list to display
- * in the navigation drawer. The title will also be updated from the adapter.
- *
- * <p>This switch is treated as a navigation to the next level in the drawer. Navigation away
- * from this level will pop the given adapter off and surface contents of the previous adapter
- * that was set via this method. If no such adapter exists, then the root adapter set by
- * {@link #setRootAdapter(CarDrawerAdapter)} will be used instead.
- *
- * @param adapter Adapter for next level of content in the drawer.
- */
- public final void pushAdapter(CarDrawerAdapter adapter) {
- mAdapterStack.peek().setTitleChangeListener(null);
- mAdapterStack.push(adapter);
- setDisplayAdapter(adapter);
- runLayoutAnimation(DRILL_DOWN_ANIM);
- }
-
- /** Close the drawer. */
- public void closeDrawer() {
- if (mDrawerLayout.isDrawerOpen(Gravity.LEFT)) {
- mDrawerLayout.closeDrawer(Gravity.LEFT);
- }
- }
-
- /** Opens the drawer. */
- public void openDrawer() {
- if (!mDrawerLayout.isDrawerOpen(Gravity.LEFT)) {
- mDrawerLayout.openDrawer(Gravity.LEFT);
- }
- }
-
- /** Sets a listener to be notified of Drawer events. */
- public void addDrawerListener(@NonNull DrawerLayout.DrawerListener listener) {
- mDrawerLayout.addDrawerListener(listener);
- }
-
- /** Removes a listener to be notified of Drawer events. */
- public void removeDrawerListener(@NonNull DrawerLayout.DrawerListener listener) {
- mDrawerLayout.removeDrawerListener(listener);
- }
-
- /**
- * Sets whether the loading progress bar is displayed in the navigation drawer. If {@code true},
- * the progress bar is displayed and the navigation list is hidden and vice versa.
- */
- public void showLoadingProgressBar(boolean show) {
- mDrawerList.setVisibility(show ? View.INVISIBLE : View.VISIBLE);
- mProgressBar.setVisibility(show ? View.VISIBLE : View.GONE);
- }
-
- /** Scroll to given position in the list. */
- public void scrollToPosition(int position) {
- mDrawerList.getRecyclerView().smoothScrollToPosition(position);
- }
-
- /**
- * Retrieves the title from the given {@link CarDrawerAdapter} and set its as the title of this
- * controller's internal Toolbar.
- */
- private void setToolbarTitleFrom(CarDrawerAdapter adapter) {
- if (adapter.getTitle() == null) {
- throw new RuntimeException("CarDrawerAdapter must supply a title via setTitle()");
- }
-
- mToolbar.setTitle(adapter.getTitle());
- adapter.setTitleChangeListener(mToolbar::setTitle);
- }
-
- /**
- * Sets up the necessary listeners for {@link DrawerLayout} so that the navigation drawer
- * hierarchy is properly displayed.
- */
- private void setupDrawerToggling() {
- mDrawerLayout.addDrawerListener(mDrawerToggle);
- mDrawerLayout.addDrawerListener(
- new DrawerLayout.DrawerListener() {
- @Override
- public void onDrawerSlide(View drawerView, float slideOffset) {
- // Correctly set the title and arrow colors as they are different between
- // the open and close states.
- updateTitleAndArrowColor(slideOffset >= COLOR_SWITCH_SLIDE_OFFSET);
- }
-
- @Override
- public void onDrawerClosed(View drawerView) {
- // If drawer is closed, revert stack/drawer to initial root state.
- cleanupStackAndShowRoot();
- scrollToPosition(0);
- }
-
- @Override
- public void onDrawerOpened(View drawerView) {}
-
- @Override
- public void onDrawerStateChanged(int newState) {}
- });
- }
-
- /** Sets the title and arrow color of the drawer depending on if it is open or not. */
- private void updateTitleAndArrowColor(boolean drawerOpen) {
- // When the drawer is open, use car_title, which resolves to appropriate color depending on
- // day-night mode. When drawer is closed, we always use light color.
- int titleColorResId = drawerOpen ? R.color.car_title : R.color.car_title_light;
- int titleColor = mContext.getColor(titleColorResId);
- mToolbar.setTitleTextColor(titleColor);
- mDrawerToggle.getDrawerArrowDrawable().setColor(titleColor);
- }
-
- /**
- * Synchronizes the display of the drawer with its linked {@link DrawerLayout}.
- *
- * <p>This should be called from the associated Activity's
- * {@link android.support.v7.app.AppCompatActivity#onPostCreate(Bundle)} method to synchronize
- * after teh DRawerLayout's instance state has been restored, and any other time when the
- * state may have diverged in such a way that this controller's associated
- * {@link ActionBarDrawerToggle} had not been notified.
- */
- public void syncState() {
- mDrawerToggle.syncState();
-
- // In case we're restarting after a config change (e.g. day, night switch), set colors
- // again. Doing it here so that Drawer state is fully synced and we know if its open or not.
- // NOTE: isDrawerOpen must be passed the second child of the DrawerLayout.
- updateTitleAndArrowColor(mDrawerLayout.isDrawerOpen(mDrawerContent));
- }
-
- /**
- * Notify this controller that device configurations may have changed.
- *
- * <p>This method should be called from the associated Activity's
- * {@code onConfigurationChanged()} method.
- */
- public void onConfigurationChanged(Configuration newConfig) {
- // Pass any configuration change to the drawer toggle.
- mDrawerToggle.onConfigurationChanged(newConfig);
- }
-
- /**
- * An analog to an Activity's {@code onOptionsItemSelected()}. This method should be called
- * when the Activity's method is called and will return {@code true} if the selection has
- * been handled.
- *
- * @return {@code true} if the item processing was handled by this class.
- */
- public boolean onOptionsItemSelected(MenuItem item) {
- // Handle home-click and see if we can navigate up in the drawer.
- if (item != null && item.getItemId() == android.R.id.home && maybeHandleUpClick()) {
- return true;
- }
-
- // DrawerToggle gets next chance to handle up-clicks (and any other clicks).
- return mDrawerToggle.onOptionsItemSelected(item);
- }
-
- /**
- * Sets the given adapter as the one displaying the current contents of the drawer.
- *
- * <p>The drawer's title will also be derived from the given adapter.
- */
- private void setDisplayAdapter(CarDrawerAdapter adapter) {
- setToolbarTitleFrom(adapter);
- // NOTE: We don't use swapAdapter() since different levels in the Drawer may switch between
- // car_drawer_list_item_normal, car_drawer_list_item_small and car_list_empty layouts.
- mDrawerList.getRecyclerView().setAdapter(adapter);
- }
-
- /**
- * Switches to the previous level in the drawer hierarchy if the current list being displayed
- * is not the root adapter. This is analogous to a navigate up.
- *
- * @return {@code true} if a navigate up was possible and executed. {@code false} otherwise.
- */
- private boolean maybeHandleUpClick() {
- // Check if already at the root level.
- if (mAdapterStack.size() <= 1) {
- return false;
- }
-
- CarDrawerAdapter adapter = mAdapterStack.pop();
- adapter.setTitleChangeListener(null);
- adapter.cleanup();
- setDisplayAdapter(mAdapterStack.peek());
- runLayoutAnimation(NAVIGATE_UP_ANIM);
- return true;
- }
-
- /** Clears stack down to root adapter and switches to root adapter. */
- private void cleanupStackAndShowRoot() {
- while (mAdapterStack.size() > 1) {
- CarDrawerAdapter adapter = mAdapterStack.pop();
- adapter.setTitleChangeListener(null);
- adapter.cleanup();
- }
- setDisplayAdapter(mAdapterStack.peek());
- runLayoutAnimation(NAVIGATE_UP_ANIM);
- }
-
- /**
- * Runs the given layout animation on the PagedListView. Running this animation will also
- * refresh the contents of the list.
- */
- private void runLayoutAnimation(@AnimRes int animation) {
- RecyclerView recyclerView = mDrawerList.getRecyclerView();
- recyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(mContext, animation));
- recyclerView.getAdapter().notifyDataSetChanged();
- recyclerView.scheduleLayoutAnimation();
- }
-}
diff --git a/android/support/car/drawer/DrawerItemViewHolder.java b/android/support/car/drawer/DrawerItemViewHolder.java
deleted file mode 100644
index d016b2de..00000000
--- a/android/support/car/drawer/DrawerItemViewHolder.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.car.drawer;
-
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.car.R;
-import android.support.v7.widget.RecyclerView;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-/**
- * Re-usable {@link RecyclerView.ViewHolder} for displaying items in the
- * {@link android.support.car.drawer.CarDrawerAdapter}.
- */
-public class DrawerItemViewHolder extends RecyclerView.ViewHolder {
- private final ImageView mIcon;
- private final TextView mTitle;
- private final TextView mText;
- private final ImageView mEndIcon;
-
- DrawerItemViewHolder(View view) {
- super(view);
- mIcon = view.findViewById(R.id.icon);
- if (mIcon == null) {
- throw new IllegalArgumentException("Icon view cannot be null!");
- }
-
- mTitle = view.findViewById(R.id.title);
- if (mTitle == null) {
- throw new IllegalArgumentException("Title view cannot be null!");
- }
-
- // Next two are optional and may be null.
- mText = view.findViewById(R.id.text);
- mEndIcon = view.findViewById(R.id.end_icon);
- }
-
- /** Returns the view that should be used to display the main icon. */
- @NonNull
- public ImageView getIcon() {
- return mIcon;
- }
-
- /** Returns the view that will display the main title. */
- @NonNull
- public TextView getTitle() {
- return mTitle;
- }
-
- /** Returns the view that is used for text that is smaller than the title text. */
- @Nullable
- public TextView getText() {
- return mText;
- }
-
- /** Returns the icon that is displayed at the end of the view. */
- @Nullable
- public ImageView getEndIcon() {
- return mEndIcon;
- }
-
- /**
- * Sets the listener that will be notified when the view held by this ViewHolder has been
- * clicked. Passing {@code null} will clear any previously set listeners.
- */
- void setItemClickListener(@Nullable DrawerItemClickListener listener) {
- itemView.setOnClickListener(listener != null
- ? v -> listener.onItemClick(getAdapterPosition())
- : null);
- }
-}
diff --git a/android/support/car/utils/ColumnCalculator.java b/android/support/car/utils/ColumnCalculator.java
deleted file mode 100644
index fa5dd432..00000000
--- a/android/support/car/utils/ColumnCalculator.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.car.utils;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.support.car.R;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.WindowManager;
-
-/**
- * Utility class that calculates the size of the columns that will fit on the screen. A column's
- * width is determined by the size of the margins and gutters (space between the columns) that fit
- * on-screen.
- *
- * <p>Refer to the appropriate dimens and integers for the size of the margins and number of
- * columns.
- */
-public class ColumnCalculator {
- private static final String TAG = "ColumnCalculator";
-
- private static ColumnCalculator sInstance;
- private static int sScreenWidth;
-
- private int mNumOfColumns;
- private int mNumOfGutters;
- private int mColumnWidth;
- private int mGutterSize;
-
- /**
- * Gets an instance of the {@link ColumnCalculator}. If this is the first time that this
- * method has been called, then the given {@link Context} will be used to retrieve resources.
- *
- * @param context The current calling Context.
- * @return An instance of {@link ColumnCalculator}.
- */
- public static ColumnCalculator getInstance(Context context) {
- if (sInstance == null) {
- WindowManager windowManager = (WindowManager) context.getSystemService(
- Context.WINDOW_SERVICE);
- DisplayMetrics displayMetrics = new DisplayMetrics();
- windowManager.getDefaultDisplay().getMetrics(displayMetrics);
- sScreenWidth = displayMetrics.widthPixels;
-
- sInstance = new ColumnCalculator(context);
- }
-
- return sInstance;
- }
-
- private ColumnCalculator(Context context) {
- Resources res = context.getResources();
- int marginSize = res.getDimensionPixelSize(R.dimen.car_margin);
- mGutterSize = res.getDimensionPixelSize(R.dimen.car_screen_gutter_size);
- mNumOfColumns = res.getInteger(R.integer.car_screen_num_of_columns);
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, String.format("marginSize: %d; numOfColumns: %d; gutterSize: %d",
- marginSize, mNumOfColumns, mGutterSize));
- }
-
- // The gutters appear between each column. As a result, the number of gutters is one less
- // than the number of columns.
- mNumOfGutters = mNumOfColumns - 1;
-
- // Determine the spacing that is allowed to be filled by the columns by subtracting margins
- // on both size of the screen and the space taken up by the gutters.
- int spaceForColumns = sScreenWidth - (2 * marginSize) - (mNumOfGutters * mGutterSize);
-
- mColumnWidth = spaceForColumns / mNumOfColumns;
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "mColumnWidth: " + mColumnWidth);
- }
- }
-
- /**
- * Returns the total number of columns that fit on the current screen.
- *
- * @return The total number of columns that fit on the screen.
- */
- public int getNumOfColumns() {
- return mNumOfColumns;
- }
-
- /**
- * Returns the size in pixels of each column. The column width is determined by the size of the
- * screen divided by the number of columns, size of gutters and margins.
- *
- * @return The width of a single column in pixels.
- */
- public int getColumnWidth() {
- return mColumnWidth;
- }
-
- /**
- * Returns the total number of gutters that fit on screen. A gutter is the space between each
- * column. This value is always one less than the number of columns.
- *
- * @return The number of gutters on screen.
- */
- public int getNumOfGutters() {
- return mNumOfGutters;
- }
-
- /**
- * Returns the size of each gutter in pixels. A gutter is the space between each column.
- *
- * @return The size of a single gutter in pixels.
- */
- public int getGutterSize() {
- return mGutterSize;
- }
-
- /**
- * Returns the size in pixels for the given number of columns. This value takes into account
- * the size of the gutter between the columns as well. For example, for a column span of four,
- * the size returned is the sum of four columns and three gutters.
- *
- * @return The size in pixels for a given column span.
- */
- public int getSizeForColumnSpan(int columnSpan) {
- int gutterSpan = columnSpan - 1;
- return columnSpan * mColumnWidth + gutterSpan * mGutterSize;
- }
-}
diff --git a/android/support/car/widget/CarItemAnimator.java b/android/support/car/widget/CarItemAnimator.java
deleted file mode 100644
index ef22c484..00000000
--- a/android/support/car/widget/CarItemAnimator.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.car.widget;
-
-import android.support.v7.widget.DefaultItemAnimator;
-import android.support.v7.widget.RecyclerView;
-
-/** {@link DefaultItemAnimator} with a few minor changes where it had undesired behavior. */
-public class CarItemAnimator extends DefaultItemAnimator {
-
- private final PagedLayoutManager mLayoutManager;
-
- public CarItemAnimator(PagedLayoutManager layoutManager) {
- mLayoutManager = layoutManager;
- }
-
- @Override
- public boolean animateChange(RecyclerView.ViewHolder oldHolder,
- RecyclerView.ViewHolder newHolder,
- int fromX,
- int fromY,
- int toX,
- int toY) {
- // The default behavior will cross fade the old view and the new one. However, if we
- // have a card on a colored background, it will make it appear as if a changing card
- // fades in and out.
- float alpha = 0f;
- if (newHolder != null) {
- alpha = newHolder.itemView.getAlpha();
- }
- boolean ret = super.animateChange(oldHolder, newHolder, fromX, fromY, toX, toY);
- if (newHolder != null) {
- newHolder.itemView.setAlpha(alpha);
- }
- return ret;
- }
-
- @Override
- public void onMoveFinished(RecyclerView.ViewHolder item) {
- // The item animator uses translation heavily internally. However, we also use translation
- // to create the paging affect. When an item's move is animated, it will mess up the
- // translation we have set on it so we must re-offset the rows once the animations finish.
-
- // isRunning(ItemAnimationFinishedListener) is the awkward API used to determine when all
- // animations have finished.
- isRunning(mFinishedListener);
- }
-
- private final ItemAnimatorFinishedListener mFinishedListener =
- new ItemAnimatorFinishedListener() {
- @Override
- public void onAnimationsFinished() {
- mLayoutManager.offsetRows();
- }
- };
-}
diff --git a/android/support/car/widget/CarRecyclerView.java b/android/support/car/widget/CarRecyclerView.java
deleted file mode 100644
index bb9cb71a..00000000
--- a/android/support/car/widget/CarRecyclerView.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.car.widget;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.support.annotation.NonNull;
-import android.support.v7.widget.RecyclerView;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * Custom {@link RecyclerView} that helps {@link PagedLayoutManager} properly fling and paginate.
- *
- * <p>It also has the ability to fade children as they scroll off screen that can be set with {@link
- * #setFadeLastItem(boolean)}.
- */
-public class CarRecyclerView extends RecyclerView {
- private boolean mFadeLastItem;
- /**
- * If the user releases the list with a velocity of 0, {@link #fling(int, int)} will not be
- * called. However, we want to make sure that the list still snaps to the next page when this
- * happens.
- */
- private boolean mWasFlingCalledForGesture;
-
- public CarRecyclerView(Context context) {
- this(context, null);
- }
-
- public CarRecyclerView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public CarRecyclerView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- setFocusableInTouchMode(false);
- setFocusable(false);
- }
-
- @Override
- public boolean fling(int velocityX, int velocityY) {
- mWasFlingCalledForGesture = true;
- return ((PagedLayoutManager) getLayoutManager()).settleScrollForFling(this, velocityY);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent e) {
- // We want the parent to handle all touch events. There's a lot going on there,
- // and there is no reason to overwrite that functionality. If we do, bad things will happen.
- final boolean ret = super.onTouchEvent(e);
-
- int action = e.getActionMasked();
- if (action == MotionEvent.ACTION_UP) {
- if (!mWasFlingCalledForGesture) {
- ((PagedLayoutManager) getLayoutManager()).settleScrollForFling(this, 0);
- }
- mWasFlingCalledForGesture = false;
- }
-
- return ret;
- }
-
- @Override
- public boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) {
- if (mFadeLastItem) {
- float onScreen = 1f;
- if ((child.getTop() < getBottom() && child.getBottom() > getBottom())) {
- onScreen = ((float) (getBottom() - child.getTop())) / (float) child.getHeight();
- } else if ((child.getTop() < getTop() && child.getBottom() > getTop())) {
- onScreen = ((float) (child.getBottom() - getTop())) / (float) child.getHeight();
- }
- float alpha = 1 - (1 - onScreen) * (1 - onScreen);
- fadeChild(child, alpha);
- }
-
- return super.drawChild(canvas, child, drawingTime);
- }
-
- public void setFadeLastItem(boolean fadeLastItem) {
- mFadeLastItem = fadeLastItem;
- }
-
- /**
- * Scrolls the contents of this {@link CarRecyclerView} up one page. A page is defined as the
- * number of items that fit completely on the screen.
- */
- public void pageUp() {
- PagedLayoutManager lm = (PagedLayoutManager) getLayoutManager();
- int pageUpPosition = lm.getPageUpPosition();
- if (pageUpPosition == -1) {
- return;
- }
-
- smoothScrollToPosition(pageUpPosition);
- }
-
- /**
- * Scrolls the contents of this {@link CarRecyclerView} down one page. A page is defined as the
- * number of items that fit completely on the screen.
- */
- public void pageDown() {
- PagedLayoutManager lm = (PagedLayoutManager) getLayoutManager();
- int pageDownPosition = lm.getPageDownPosition();
- if (pageDownPosition == -1) {
- return;
- }
-
- smoothScrollToPosition(pageDownPosition);
- }
-
- /**
- * Fades child by alpha. If child is a {@link ViewGroup} then it will recursively fade its
- * children instead.
- */
- private void fadeChild(@NonNull View child, float alpha) {
- if (child instanceof ViewGroup) {
- ViewGroup vg = (ViewGroup) child;
- for (int i = 0; i < vg.getChildCount(); i++) {
- fadeChild(vg.getChildAt(i), alpha);
- }
- } else {
- child.setAlpha(alpha);
- }
- }
-}
diff --git a/android/support/car/widget/ColumnCardView.java b/android/support/car/widget/ColumnCardView.java
deleted file mode 100644
index 06f85536..00000000
--- a/android/support/car/widget/ColumnCardView.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.car.widget;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.support.car.R;
-import android.support.car.utils.ColumnCalculator;
-import android.support.v7.widget.CardView;
-import android.util.AttributeSet;
-import android.util.Log;
-
-/**
- * A {@link CardView} whose width can be specified by the number of columns that it will span.
- *
- * <p>The {@code ColumnCardView} works similarly to a regular {@link CardView}, except that
- * its {@code layout_width} attribute is always ignored. Instead, its width is automatically
- * calculated based on a specified {@code columnSpan} attribute. Alternatively, a user can call
- * {@link #setColumnSpan(int)}. If no column span is given, the {@code ColumnCardView} will have
- * a default span value that it uses.
- *
- * <pre>
- * &lt;android.support.car.widget.ColumnCardView
- * android:layout_width="wrap_content"
- * android:layout_height="wrap_content"
- * app:columnSpan="4" /&gt;
- * </pre>
- *
- * @see ColumnCalculator
- */
-public final class ColumnCardView extends CardView {
- private static final String TAG = "ColumnCardView";
-
- private ColumnCalculator mColumnCalculator;
- private int mColumnSpan;
-
- public ColumnCardView(Context context) {
- super(context);
- init(context, null, 0 /* defStyleAttrs */);
- }
-
- public ColumnCardView(Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context, attrs, 0 /* defStyleAttrs */);
- }
-
- public ColumnCardView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- init(context, attrs, defStyleAttr);
- }
-
- private void init(Context context, AttributeSet attrs, int defStyleAttrs) {
- mColumnCalculator = ColumnCalculator.getInstance(context);
-
- int defaultColumnSpan = getResources().getInteger(
- R.integer.column_card_default_column_span);
-
- TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ColumnCardView,
- defStyleAttrs, 0 /* defStyleRes */);
- mColumnSpan = ta.getInteger(R.styleable.ColumnCardView_columnSpan, defaultColumnSpan);
- ta.recycle();
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Column span: " + mColumnSpan);
- }
- }
-
- @Override
- public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // Override any specified width so that the width is one that is calculated based on
- // column and gutter span.
- int width = mColumnCalculator.getSizeForColumnSpan(mColumnSpan);
- super.onMeasure(
- MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
- heightMeasureSpec);
- }
-
- /**
- * Sets the number of columns that this {@code ColumnCardView} will span. The given span is
- * ignored if it is less than 0 or greater than the number of columns that fit on screen.
- *
- * @param columnSpan The number of columns this {@code ColumnCardView} will span across.
- */
- public void setColumnSpan(int columnSpan) {
- if (columnSpan <= 0 || columnSpan > mColumnCalculator.getNumOfColumns()) {
- return;
- }
-
- mColumnSpan = columnSpan;
- requestLayout();
- }
-
- /**
- * Returns the currently number of columns that this {@code ColumnCardView} spans.
- *
- * @return The number of columns this {@code ColumnCardView} spans across.
- */
- public int getColumnSpan() {
- return mColumnSpan;
- }
-}
diff --git a/android/support/car/widget/DayNightStyle.java b/android/support/car/widget/DayNightStyle.java
deleted file mode 100644
index ff5a1b33..00000000
--- a/android/support/car/widget/DayNightStyle.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.car.widget;
-
-import android.support.annotation.IntDef;
-
-/**
- * Specifies how the system UI should respond to day/night mode events.
- *
- * <p>By default, the Android Auto system UI assumes the app content background is light during the
- * day and dark during the night. The system UI updates the foreground color (such as status bar
- * icon colors) to be dark during day mode and light during night mode. By setting the
- * DayNightStyle, the app can specify how the system should respond to a day/night mode event. For
- * example, if the app has a dark content background for both day and night time, the app can tell
- * the system to use {@link #FORCE_NIGHT} style so the foreground color is locked to light color for
- * both cases.
- *
- * <p>Note: Not all system UI elements can be customized with a DayNightStyle.
- */
-@IntDef({
- DayNightStyle.AUTO,
- DayNightStyle.AUTO_INVERSE,
- DayNightStyle.FORCE_NIGHT,
- DayNightStyle.FORCE_DAY,
-})
-public @interface DayNightStyle {
- /**
- * Sets the foreground color to be automatically changed based on day/night mode, assuming the
- * app content background is light during the day and dark during the night.
- *
- * <p>This is the default behavior.
- */
- int AUTO = 0;
-
- /**
- * Sets the foreground color to be automatically changed based on day/night mode, assuming the
- * app content background is dark during the day and light during the night.
- */
- int AUTO_INVERSE = 1;
-
- /**
- * Sets the foreground color to be locked to the night version, which assumes the app content
- * background is always dark during both day and night.
- */
- int FORCE_NIGHT = 2;
-
- /**
- * Sets the foreground color to be locked to the day version, which assumes the app content
- * background is always light during both day and night.
- */
- int FORCE_DAY = 3;
-}
diff --git a/android/support/car/widget/PagedLayoutManager.java b/android/support/car/widget/PagedLayoutManager.java
deleted file mode 100644
index c4f469a3..00000000
--- a/android/support/car/widget/PagedLayoutManager.java
+++ /dev/null
@@ -1,1687 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.car.widget;
-
-import android.content.Context;
-import android.graphics.PointF;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
-import android.support.annotation.VisibleForTesting;
-import android.support.car.R;
-import android.support.v7.widget.LinearSmoothScroller;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerView.Recycler;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.util.LruCache;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.Animation;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
-import android.view.animation.Transformation;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-
-/**
- * Custom {@link RecyclerView.LayoutManager} that behaves similar to LinearLayoutManager except that
- * it has a few tricks up its sleeve.
- *
- * <ol>
- * <li>In a normal ListView, when views reach the top of the list, they are clipped. In
- * PagedLayoutManager, views have the option of flying off of the top of the screen as the
- * next row settles in to place. This functionality can be enabled or disabled with
- * {@link #setOffsetRows(boolean)}.
- * <li>Standard list physics is disabled. Instead, when the user scrolls, it will settle on the
- * next page.
- * <li>Items can scroll past the bottom edge of the screen. This helps with pagination so that the
- * last page can be properly aligned.
- * </ol>
- *
- * This LayoutManger should be used with {@link CarRecyclerView}.
- */
-public class PagedLayoutManager extends RecyclerView.LayoutManager {
- private static final String TAG = "PagedLayoutManager";
-
- /**
- * Any fling below the threshold will just scroll to the top fully visible row. The units is
- * whatever {@link android.widget.Scroller} would return.
- *
- * <p>A reasonable value is ~200
- *
- * <p>This can be disabled by setting the threshold to -1.
- */
- private static final int FLING_THRESHOLD_TO_PAGINATE = -1;
-
- /**
- * Any fling shorter than this threshold (in px) will just scroll to the top fully visible row.
- *
- * <p>A reasonable value is 15.
- *
- * <p>This can be disabled by setting the distance to -1.
- */
- private static final int DRAG_DISTANCE_TO_PAGINATE = -1;
-
- /**
- * If you scroll really quickly, you can hit the end of the laid out rows before Android has a
- * chance to layout more. To help counter this, we can layout a number of extra rows past
- * wherever the focus is if necessary.
- */
- private static final int NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS = 2;
-
- /**
- * Scroll bar calculation is a bit complicated. This basically defines the granularity we want
- * our scroll bar to move. Set this to 1 means our scrollbar will have really jerky movement.
- * Setting it too big will risk an overflow (although there is no performance impact). Ideally
- * we want to set this higher than the height of our list view. We can't use our list view
- * height directly though because we might run into situations where getHeight() returns 0,
- * for example, when the view is not yet measured.
- */
- private static final int SCROLL_RANGE = 1000;
-
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({BEFORE, AFTER})
- private @interface LayoutDirection {}
-
- private static final int BEFORE = 0;
- private static final int AFTER = 1;
-
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({ROW_OFFSET_MODE_INDIVIDUAL, ROW_OFFSET_MODE_PAGE})
- public @interface RowOffsetMode {}
-
- public static final int ROW_OFFSET_MODE_INDIVIDUAL = 0;
- public static final int ROW_OFFSET_MODE_PAGE = 1;
-
- private final AccelerateInterpolator mDanglingRowInterpolator = new AccelerateInterpolator(2);
- private final Context mContext;
-
- /** Determines whether or not rows will be offset as they slide off screen * */
- private boolean mOffsetRows;
-
- /** Determines whether rows will be offset individually or a page at a time * */
- @RowOffsetMode private int mRowOffsetMode = ROW_OFFSET_MODE_PAGE;
-
- /**
- * The LayoutManager only gets {@link #onScrollStateChanged(int)} updates. This enables the
- * scroll state to be used anywhere.
- */
- private int mScrollState = RecyclerView.SCROLL_STATE_IDLE;
-
- /** Used to inspect the current scroll state to help with the various calculations. */
- private CarSmoothScroller mSmoothScroller;
-
- private PagedListView.OnScrollListener mOnScrollListener;
-
- /** The distance that the list has actually scrolled in the most recent drag gesture. */
- private int mLastDragDistance = 0;
-
- /** {@code True} if the current drag was limited/capped because it was at some boundary. */
- private boolean mReachedLimitOfDrag;
-
- /** The index of the first item on the current page. */
- private int mAnchorPageBreakPosition = 0;
-
- /** The index of the first item on the previous page. */
- private int mUpperPageBreakPosition = -1;
-
- /** The index of the first item on the next page. */
- private int mLowerPageBreakPosition = -1;
-
- /** Used in the bookkeeping of mario style scrolling to prevent extra calculations. */
- private int mLastChildPositionToRequestFocus = -1;
-
- private int mSampleViewHeight = -1;
-
- /** Used for onPageUp and onPageDown */
- private int mViewsPerPage = 1;
-
- private int mCurrentPage = 0;
-
- private static final int MAX_ANIMATIONS_IN_CACHE = 30;
- /**
- * Cache of TranslateAnimation per child view. These are needed since using a single animation
- * for all children doesn't apply the animation effect multiple times. Key = the view the
- * animation will transform.
- */
- private LruCache<View, TranslateAnimation> mFlyOffscreenAnimations;
-
- /** Set the anchor to the following position on the next layout pass. */
- private int mPendingScrollPosition = -1;
-
- public PagedLayoutManager(Context context) {
- mContext = context;
- }
-
- @Override
- public RecyclerView.LayoutParams generateDefaultLayoutParams() {
- return new RecyclerView.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
- }
-
- @Override
- public boolean canScrollVertically() {
- return true;
- }
-
- /**
- * onLayoutChildren is sort of like a "reset" for the layout state. At a high level, it should:
- *
- * <ol>
- * <li>Check the current views to get the current state of affairs
- * <li>Detach all views from the window (a lightweight operation) so that rows not re-added
- * will be removed after onLayoutChildren.
- * <li>Re-add rows as necessary.
- * </ol>
- *
- * @see super#onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)
- */
- @Override
- public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
- /*
- * The anchor view is the first fully visible view on screen at the beginning of
- * onLayoutChildren (or 0 if there is none). This row will be laid out first. After that,
- * layoutNextRow will layout rows above and below it until the boundaries of what should be
- * laid out have been reached. See shouldLayoutNextRow(View, int) for more info.
- */
- int anchorPosition = 0;
- int anchorTop = -1;
- if (mPendingScrollPosition == -1) {
- View anchor = getFirstFullyVisibleChild();
- if (anchor != null) {
- anchorPosition = getPosition(anchor);
- anchorTop = getDecoratedTop(anchor);
- }
- } else {
- anchorPosition = mPendingScrollPosition;
- mPendingScrollPosition = -1;
- mAnchorPageBreakPosition = anchorPosition;
- mUpperPageBreakPosition = -1;
- mLowerPageBreakPosition = -1;
- }
-
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(
- TAG,
- String.format(
- ":: onLayoutChildren anchorPosition:%s, anchorTop:%s,"
- + " mPendingScrollPosition: %s, mAnchorPageBreakPosition:%s,"
- + " mUpperPageBreakPosition:%s, mLowerPageBreakPosition:%s",
- anchorPosition,
- anchorTop,
- mPendingScrollPosition,
- mAnchorPageBreakPosition,
- mUpperPageBreakPosition,
- mLowerPageBreakPosition));
- }
-
- /*
- * Detach all attached view for 2 reasons:
- *
- * 1) So that views are put in the scrap heap. This enables us to call {@link
- * RecyclerView.Recycler#getViewForPosition(int)} which will either return one of these
- * detached views if it is in the scrap heap, one from the recycled pool (will only call
- * onBind in the adapter), or create an entirely new row if needed (will call onCreate
- * and onBind in the adapter).
- * 2) So that views are automatically removed if they are not manually re-added.
- */
- detachAndScrapAttachedViews(recycler);
-
- /*
- * Layout the views recursively.
- *
- * It's possible that this re-layout is triggered because an item gets removed. If the
- * anchor view is at the end of the list, the anchor view position will be bigger than the
- * number of available items. Correct that, and only start the layout if the anchor
- * position is valid.
- */
- anchorPosition = Math.min(anchorPosition, getItemCount() - 1);
- if (anchorPosition >= 0) {
- View anchor = layoutAnchor(recycler, anchorPosition, anchorTop);
- View adjacentRow = anchor;
- while (shouldLayoutNextRow(state, adjacentRow, BEFORE)) {
- adjacentRow = layoutNextRow(recycler, adjacentRow, BEFORE);
- }
- adjacentRow = anchor;
- while (shouldLayoutNextRow(state, adjacentRow, AFTER)) {
- adjacentRow = layoutNextRow(recycler, adjacentRow, AFTER);
- }
- }
-
- updatePageBreakPositions();
- offsetRows();
-
- if (Log.isLoggable(TAG, Log.VERBOSE) && getChildCount() > 1) {
- Log.v(TAG, "Currently showing "
- + getChildCount()
- + " views "
- + getPosition(getChildAt(0))
- + " to "
- + getPosition(getChildAt(getChildCount() - 1))
- + " anchor "
- + anchorPosition);
- }
- // Should be at least 1
- mViewsPerPage =
- Math.max(getLastFullyVisibleChildIndex() + 1 - getFirstFullyVisibleChildIndex(), 1);
- mCurrentPage = getFirstFullyVisibleChildPosition() / mViewsPerPage;
- Log.v(TAG, "viewsPerPage " + mViewsPerPage);
- }
-
- /**
- * scrollVerticallyBy does the work of what should happen when the list scrolls in addition to
- * handling cases where the list hits the end. It should be lighter weight than
- * onLayoutChildren. It doesn't have to detach all views. It only looks at the end of the list
- * and removes views that have gone out of bounds and lays out new ones that scroll in.
- *
- * @param dy The amount that the list is supposed to scroll. > 0 means the list is scrolling
- * down. < 0 means the list is scrolling up.
- * @param recycler The recycler that enables views to be reused or created as they scroll in.
- * @param state Various information about the current state of affairs.
- * @return The amount the list actually scrolled.
- * @see super#scrollVerticallyBy(int, RecyclerView.Recycler, RecyclerView.State)
- */
- @Override
- public int scrollVerticallyBy(
- int dy, @NonNull RecyclerView.Recycler recycler, @NonNull RecyclerView.State state) {
- // If the list is empty, we can prevent the overscroll glow from showing by just
- // telling RecycerView that we scrolled.
- if (getItemCount() == 0) {
- return dy;
- }
-
- // Prevent redundant computations if there is definitely nowhere to scroll to.
- if (getChildCount() <= 1 || dy == 0) {
- mReachedLimitOfDrag = true;
- return 0;
- }
-
- View firstChild = getChildAt(0);
- if (firstChild == null) {
- mReachedLimitOfDrag = true;
- return 0;
- }
- int firstChildPosition = getPosition(firstChild);
- RecyclerView.LayoutParams firstChildParams = getParams(firstChild);
- int firstChildTopWithMargin = getDecoratedTop(firstChild) - firstChildParams.topMargin;
-
- View lastFullyVisibleView = getChildAt(getLastFullyVisibleChildIndex());
- if (lastFullyVisibleView == null) {
- mReachedLimitOfDrag = true;
- return 0;
- }
- boolean isLastViewVisible = getPosition(lastFullyVisibleView) == getItemCount() - 1;
-
- View firstFullyVisibleChild = getFirstFullyVisibleChild();
- if (firstFullyVisibleChild == null) {
- mReachedLimitOfDrag = true;
- return 0;
- }
- int firstFullyVisiblePosition = getPosition(firstFullyVisibleChild);
- RecyclerView.LayoutParams firstFullyVisibleChildParams = getParams(firstFullyVisibleChild);
- int topRemainingSpace =
- getDecoratedTop(firstFullyVisibleChild)
- - firstFullyVisibleChildParams.topMargin
- - getPaddingTop();
-
- if (isLastViewVisible
- && firstFullyVisiblePosition == mAnchorPageBreakPosition
- && dy > topRemainingSpace
- && dy > 0) {
- // Prevent dragging down more than 1 page. As a side effect, this also prevents you
- // from dragging past the bottom because if you are on the second to last page, it
- // prevents you from dragging past the last page.
- dy = topRemainingSpace;
- mReachedLimitOfDrag = true;
- } else if (dy < 0
- && firstChildPosition == 0
- && firstChildTopWithMargin + Math.abs(dy) > getPaddingTop()) {
- // Prevent scrolling past the beginning
- dy = firstChildTopWithMargin - getPaddingTop();
- mReachedLimitOfDrag = true;
- } else {
- mReachedLimitOfDrag = false;
- }
-
- boolean isDragging = mScrollState == RecyclerView.SCROLL_STATE_DRAGGING;
- if (isDragging) {
- mLastDragDistance += dy;
- }
- // We offset by -dy because the views translate in the opposite direction that the
- // list scrolls (think about it.)
- offsetChildrenVertical(-dy);
-
- // The last item in the layout should never scroll above the viewport
- View view = getChildAt(getChildCount() - 1);
- if (view.getTop() < 0) {
- view.setTop(0);
- }
-
- // This is the meat of this function. We remove views on the trailing edge of the scroll
- // and add views at the leading edge as necessary.
- View adjacentRow;
- if (dy > 0) {
- recycleChildrenFromStart(recycler);
- adjacentRow = getChildAt(getChildCount() - 1);
- while (shouldLayoutNextRow(state, adjacentRow, AFTER)) {
- adjacentRow = layoutNextRow(recycler, adjacentRow, AFTER);
- }
- } else {
- recycleChildrenFromEnd(recycler);
- adjacentRow = getChildAt(0);
- while (shouldLayoutNextRow(state, adjacentRow, BEFORE)) {
- adjacentRow = layoutNextRow(recycler, adjacentRow, BEFORE);
- }
- }
- // Now that the correct views are laid out, offset rows as necessary so we can do whatever
- // fancy animation we want such as having the top view fly off the screen as the next one
- // settles in to place.
- updatePageBreakPositions();
- offsetRows();
-
- if (getChildCount() > 1) {
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(
- TAG,
- String.format(
- "Currently showing %d views (%d to %d)",
- getChildCount(),
- getPosition(getChildAt(0)),
- getPosition(getChildAt(getChildCount() - 1))));
- }
- }
- updatePagedState();
- return dy;
- }
-
- private void updatePagedState() {
- int page = getFirstFullyVisibleChildPosition() / mViewsPerPage;
- if (mOnScrollListener != null) {
- if (page > mCurrentPage) {
- mOnScrollListener.onPageDown();
- } else if (page < mCurrentPage) {
- mOnScrollListener.onPageUp();
- }
- }
- mCurrentPage = page;
- }
-
- @Override
- public void scrollToPosition(int position) {
- mPendingScrollPosition = position;
- requestLayout();
- }
-
- @Override
- public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
- int position) {
- /*
- * startSmoothScroll will handle stopping the old one if there is one. We only keep a copy
- * of it to handle the translation of rows as they slide off the screen in
- * offsetRowsWithPageBreak().
- */
- mSmoothScroller = new CarSmoothScroller(mContext, position);
- mSmoothScroller.setTargetPosition(position);
- startSmoothScroll(mSmoothScroller);
- }
-
- /** Miscellaneous bookkeeping. */
- @Override
- public void onScrollStateChanged(int state) {
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, ":: onScrollStateChanged " + state);
- }
- if (state == RecyclerView.SCROLL_STATE_IDLE) {
- // If the focused view is off screen, give focus to one that is.
- // If the first fully visible view is first in the list, focus the first item.
- // Otherwise, focus the second so that you have the first item as scrolling context.
- View focusedChild = getFocusedChild();
- if (focusedChild != null
- && (getDecoratedTop(focusedChild) >= getHeight() - getPaddingBottom()
- || getDecoratedBottom(focusedChild) <= getPaddingTop())) {
- focusedChild.clearFocus();
- requestLayout();
- }
-
- } else if (state == RecyclerView.SCROLL_STATE_DRAGGING) {
- mLastDragDistance = 0;
- }
-
- if (state != RecyclerView.SCROLL_STATE_SETTLING) {
- mSmoothScroller = null;
- }
-
- mScrollState = state;
- updatePageBreakPositions();
- }
-
- @Override
- public void onItemsChanged(RecyclerView recyclerView) {
- super.onItemsChanged(recyclerView);
- // When item changed, our sample view height is no longer accurate, and need to be
- // recomputed.
- mSampleViewHeight = -1;
- }
-
- /**
- * Gives us the opportunity to override the order of the focused views. By default, it will just
- * go from top to bottom. However, if there is no focused views, we take over the logic and
- * start the focused views from the middle of what is visible and move from there until the
- * end of the laid out views in the specified direction.
- */
- @Override
- public boolean onAddFocusables(
- RecyclerView recyclerView, ArrayList<View> views, int direction, int focusableMode) {
- View focusedChild = getFocusedChild();
- if (focusedChild != null) {
- // If there is a view that already has focus, we can just return false and the normal
- // Android addFocusables will work fine.
- return false;
- }
-
- // Now we know that there isn't a focused view. We need to set up focusables such that
- // instead of just focusing the first item that has been laid out, it focuses starting
- // from a visible item.
-
- int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex();
- if (firstFullyVisibleChildIndex == -1) {
- // Somehow there is a focused view but there is no fully visible view. There shouldn't
- // be a way for this to happen but we'd better stop here and return instead of
- // continuing on with -1.
- Log.w(TAG, "There is a focused child but no first fully visible child.");
- return false;
- }
- View firstFullyVisibleChild = getChildAt(firstFullyVisibleChildIndex);
- int firstFullyVisibleChildPosition = getPosition(firstFullyVisibleChild);
-
- int firstFocusableChildIndex = firstFullyVisibleChildIndex;
- if (firstFullyVisibleChildPosition > 0 && firstFocusableChildIndex + 1 < getItemCount()) {
- // We are somewhere in the middle of the list. Instead of starting focus on the first
- // item, start focus on the second item to give some context that we aren't at
- // the beginning.
- firstFocusableChildIndex++;
- }
-
- if (direction == View.FOCUS_FORWARD) {
- // Iterate from the first focusable view to the end.
- for (int i = firstFocusableChildIndex; i < getChildCount(); i++) {
- views.add(getChildAt(i));
- }
- return true;
- } else if (direction == View.FOCUS_BACKWARD) {
- // Iterate from the first focusable view to the beginning.
- for (int i = firstFocusableChildIndex; i >= 0; i--) {
- views.add(getChildAt(i));
- }
- return true;
- } else if (direction == View.FOCUS_DOWN) {
- // Framework calls onAddFocusables with FOCUS_DOWN direction when the focus is first
- // gained. Thereafter, it calls onAddFocusables with FOCUS_FORWARD or FOCUS_BACKWARD.
- // First we try to put the focus back on the last focused item, if it is visible
- int lastFocusedVisibleChildIndex = getLastFocusedChildIndexIfVisible();
- if (lastFocusedVisibleChildIndex != -1) {
- views.add(getChildAt(lastFocusedVisibleChildIndex));
- return true;
- }
- }
- return false;
- }
-
- @Override
- public View onFocusSearchFailed(
- View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state) {
- // This doesn't seem to get called the way focus is handled in gearhead...
- return null;
- }
-
- /**
- * This is the function that decides where to scroll to when a new view is focused. You can get
- * the position of the currently focused child through the child parameter. Once you have that,
- * determine where to smooth scroll to and scroll there.
- *
- * @param parent The RecyclerView hosting this LayoutManager
- * @param state Current state of RecyclerView
- * @param child Direct child of the RecyclerView containing the newly focused view
- * @param focused The newly focused view. This may be the same view as child or it may be null
- * @return {@code true} if the default scroll behavior should be suppressed
- */
- @Override
- public boolean onRequestChildFocus(
- RecyclerView parent, RecyclerView.State state, View child, View focused) {
- if (child == null) {
- Log.w(TAG, "onRequestChildFocus with a null child!");
- return true;
- }
-
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, String.format(":: onRequestChildFocus child: %s, focused: %s", child,
- focused));
- }
-
- return onRequestChildFocusMarioStyle(parent, child);
- }
-
- /**
- * Goal: the scrollbar maintains the same size throughout scrolling and that the scrollbar
- * reaches the bottom of the screen when the last item is fully visible. This is because there
- * are multiple points that could be considered the bottom since the last item can scroll past
- * the bottom edge of the screen.
- *
- * <p>To find the extent, we divide the number of items that can fit on screen by the number of
- * items in total.
- */
- @Override
- public int computeVerticalScrollExtent(RecyclerView.State state) {
- if (getChildCount() <= 1) {
- return 0;
- }
-
- int sampleViewHeight = getSampleViewHeight();
- int availableHeight = getAvailableHeight();
- int sampleViewsThatCanFitOnScreen = availableHeight / sampleViewHeight;
-
- if (state.getItemCount() <= sampleViewsThatCanFitOnScreen) {
- return SCROLL_RANGE;
- } else {
- return SCROLL_RANGE * sampleViewsThatCanFitOnScreen / state.getItemCount();
- }
- }
-
- /**
- * The scrolling offset is calculated by determining what position is at the top of the list.
- * However, instead of using fixed integer positions for each row, the scroll position is
- * factored in and the position is recalculated as a float that takes in to account the
- * current scroll state. This results in a smooth animation for the scrollbar when the user
- * scrolls the list.
- */
- @Override
- public int computeVerticalScrollOffset(RecyclerView.State state) {
- View firstChild = getFirstFullyVisibleChild();
- if (firstChild == null) {
- return 0;
- }
-
- RecyclerView.LayoutParams params = getParams(firstChild);
- int firstChildPosition = getPosition(firstChild);
- float previousChildHieght = (float) (getDecoratedMeasuredHeight(firstChild)
- + params.topMargin + params.bottomMargin);
-
- // Assume the previous view is the same height as the current one.
- float percentOfPreviousViewShowing = (getDecoratedTop(firstChild) - params.topMargin)
- / previousChildHieght;
- // If the previous view is actually larger than the current one then this the percent
- // can be greater than 1.
- percentOfPreviousViewShowing = Math.min(percentOfPreviousViewShowing, 1);
-
- float currentPosition = (float) firstChildPosition - percentOfPreviousViewShowing;
-
- int sampleViewHeight = getSampleViewHeight();
- int availableHeight = getAvailableHeight();
- int numberOfSampleViewsThatCanFitOnScreen = availableHeight / sampleViewHeight;
- int positionWhenLastItemIsVisible =
- state.getItemCount() - numberOfSampleViewsThatCanFitOnScreen;
-
- if (positionWhenLastItemIsVisible <= 0) {
- return 0;
- }
-
- if (currentPosition >= positionWhenLastItemIsVisible) {
- return SCROLL_RANGE;
- }
-
- return (int) (SCROLL_RANGE * currentPosition / positionWhenLastItemIsVisible);
- }
-
- /**
- * The range of the scrollbar can be understood as the granularity of how we want the scrollbar
- * to scroll.
- */
- @Override
- public int computeVerticalScrollRange(RecyclerView.State state) {
- return SCROLL_RANGE;
- }
-
- @Override
- public void onAttachedToWindow(RecyclerView view) {
- super.onAttachedToWindow(view);
- // The purpose of calling this is so that any animation offsets are re-applied. These are
- // cleared in View.onDetachedFromWindow().
- // This fixes b/27672379
- updatePageBreakPositions();
- offsetRows();
- }
-
- @Override
- public void onDetachedFromWindow(RecyclerView recyclerView, Recycler recycler) {
- super.onDetachedFromWindow(recyclerView, recycler);
- }
-
- /**
- * @return The first view that starts on screen. It assumes that it fully fits on the screen
- * though. If the first fully visible child is also taller than the screen then it will
- * still be returned. However, since the LayoutManager snaps to view starts, having a row
- * that tall would lead to a broken experience anyways.
- */
- public int getFirstFullyVisibleChildIndex() {
- for (int i = 0; i < getChildCount(); i++) {
- View child = getChildAt(i);
- RecyclerView.LayoutParams params = getParams(child);
- if (getDecoratedTop(child) - params.topMargin >= getPaddingTop()) {
- return i;
- }
- }
- return -1;
- }
-
- /**
- * @return The position of first visible child in the list. -1 will be returned if there is no
- * child.
- */
- public int getFirstFullyVisibleChildPosition() {
- View child = getFirstFullyVisibleChild();
- if (child == null) {
- return -1;
- }
- return getPosition(child);
- }
-
- /**
- * @return The position of last visible child in the list. -1 will be returned if there is no
- * child.
- */
- public int getLastFullyVisibleChildPosition() {
- View child = getLastFullyVisibleChild();
- if (child == null) {
- return -1;
- }
- return getPosition(child);
- }
-
- /** @return The first View that is completely visible on-screen. */
- public View getFirstFullyVisibleChild() {
- int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex();
- View firstChild = null;
- if (firstFullyVisibleChildIndex != -1) {
- firstChild = getChildAt(firstFullyVisibleChildIndex);
- }
- return firstChild;
- }
-
- /** @return The last View that is completely visible on-screen. */
- public View getLastFullyVisibleChild() {
- int lastFullyVisibleChildIndex = getLastFullyVisibleChildIndex();
- View lastChild = null;
- if (lastFullyVisibleChildIndex != -1) {
- lastChild = getChildAt(lastFullyVisibleChildIndex);
- }
- return lastChild;
- }
-
- /**
- * @return The last view that ends on screen. It assumes that the start is also on screen
- * though. If the last fully visible child is also taller than the screen then it will
- * still be returned. However, since the LayoutManager snaps to view starts, having a row
- * that tall would lead to a broken experience anyways.
- */
- public int getLastFullyVisibleChildIndex() {
- for (int i = getChildCount() - 1; i >= 0; i--) {
- View child = getChildAt(i);
- RecyclerView.LayoutParams params = getParams(child);
- int childBottom = getDecoratedBottom(child) + params.bottomMargin;
- int listBottom = getHeight() - getPaddingBottom();
- if (childBottom <= listBottom) {
- return i;
- }
- }
- return -1;
- }
-
- /**
- * Returns the index of the child in the list that was last focused and is currently visible to
- * the user. If no child is found, returns -1.
- */
- public int getLastFocusedChildIndexIfVisible() {
- if (mLastChildPositionToRequestFocus == -1) {
- return -1;
- }
- for (int i = 0; i < getChildCount(); i++) {
- View child = getChildAt(i);
- if (getPosition(child) == mLastChildPositionToRequestFocus) {
- RecyclerView.LayoutParams params = getParams(child);
- int childBottom = getDecoratedBottom(child) + params.bottomMargin;
- int listBottom = getHeight() - getPaddingBottom();
- if (childBottom <= listBottom) {
- return i;
- }
- break;
- }
- }
- return -1;
- }
-
- /** @return Whether or not the first view is fully visible. */
- public boolean isAtTop() {
- // getFirstFullyVisibleChildIndex() can return -1 which indicates that there are no views
- // and also means that the list is at the top.
- return getFirstFullyVisibleChildIndex() <= 0;
- }
-
- /** @return Whether or not the last view is fully visible. */
- public boolean isAtBottom() {
- int lastFullyVisibleChildIndex = getLastFullyVisibleChildIndex();
- if (lastFullyVisibleChildIndex == -1) {
- return true;
- }
- View lastFullyVisibleChild = getChildAt(lastFullyVisibleChildIndex);
- return getPosition(lastFullyVisibleChild) == getItemCount() - 1;
- }
-
- /**
- * Sets whether or not the rows have an offset animation when it scrolls off-screen. The type
- * of offset is determined by {@link #setRowOffsetMode(int)}.
- *
- * <p>A row being offset means that when they reach the top of the screen, the row is flung off
- * respectively to the rest of the list. This creates a gap between the offset row(s) and the
- * list.
- *
- * @param offsetRows {@code true} if the rows should be offset.
- */
- public void setOffsetRows(boolean offsetRows) {
- mOffsetRows = offsetRows;
- if (offsetRows) {
- // Card animation offsets are only needed when we use the flying off the screen effect
- if (mFlyOffscreenAnimations == null) {
- mFlyOffscreenAnimations = new LruCache<>(MAX_ANIMATIONS_IN_CACHE);
- }
- offsetRows();
- } else {
- int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- setCardFlyingEffectOffset(getChildAt(i), 0);
- }
- mFlyOffscreenAnimations = null;
- }
- }
-
- /**
- * Sets the manner of offsetting the rows when they are scrolled off-screen. The rows are either
- * offset individually or the entire page being scrolled off is offset.
- *
- * @param mode One of {@link #ROW_OFFSET_MODE_INDIVIDUAL} or {@link #ROW_OFFSET_MODE_PAGE}.
- */
- public void setRowOffsetMode(@RowOffsetMode int mode) {
- if (mode == mRowOffsetMode) {
- return;
- }
-
- mRowOffsetMode = mode;
- offsetRows();
- }
-
- /**
- * Sets the listener that will be notified of various scroll events in the list.
- *
- * @param listener The on-scroll listener.
- */
- public void setOnScrollListener(PagedListView.OnScrollListener listener) {
- mOnScrollListener = listener;
- }
-
- /**
- * Finish the pagination taking into account where the gesture started (not where we are now).
- *
- * @return Whether the list was scrolled as a result of the fling.
- */
- public boolean settleScrollForFling(RecyclerView parent, int flingVelocity) {
- if (getChildCount() == 0) {
- return false;
- }
-
- if (mReachedLimitOfDrag) {
- return false;
- }
-
- // If the fling was too slow or too short, settle on the first fully visible row instead.
- if (Math.abs(flingVelocity) <= FLING_THRESHOLD_TO_PAGINATE
- || Math.abs(mLastDragDistance) <= DRAG_DISTANCE_TO_PAGINATE) {
- int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex();
- if (firstFullyVisibleChildIndex != -1) {
- int scrollPosition = getPosition(getChildAt(firstFullyVisibleChildIndex));
- parent.smoothScrollToPosition(scrollPosition);
- return true;
- }
- return false;
- }
-
- // Finish the pagination taking into account where the gesture
- // started (not where we are now).
- boolean isDownGesture = flingVelocity > 0 || (flingVelocity == 0 && mLastDragDistance >= 0);
- boolean isUpGesture = flingVelocity < 0 || (flingVelocity == 0 && mLastDragDistance < 0);
- if (isDownGesture && mLowerPageBreakPosition != -1) {
- // If the last view is fully visible then only settle on the first fully visible view
- // instead of the original page down position. However, don't page down if the last
- // item has come fully into view.
- parent.smoothScrollToPosition(mAnchorPageBreakPosition);
- if (mOnScrollListener != null) {
- mOnScrollListener.onGestureDown();
- }
- return true;
- } else if (isUpGesture && mUpperPageBreakPosition != -1) {
- parent.smoothScrollToPosition(mUpperPageBreakPosition);
- if (mOnScrollListener != null) {
- mOnScrollListener.onGestureUp();
- }
- return true;
- } else {
- Log.e(
- TAG,
- "Error setting scroll for fling! flingVelocity: \t"
- + flingVelocity
- + "\tlastDragDistance: "
- + mLastDragDistance
- + "\tpageUpAtStartOfDrag: "
- + mUpperPageBreakPosition
- + "\tpageDownAtStartOfDrag: "
- + mLowerPageBreakPosition);
- // As a last resort, at the last smooth scroller target position if there is one.
- if (mSmoothScroller != null) {
- parent.smoothScrollToPosition(mSmoothScroller.getTargetPosition());
- return true;
- }
- }
- return false;
- }
-
- /** @return The position that paging up from the current position would settle at. */
- public int getPageUpPosition() {
- return mUpperPageBreakPosition;
- }
-
- /** @return The position that paging down from the current position would settle at. */
- public int getPageDownPosition() {
- return mLowerPageBreakPosition;
- }
-
- @Override
- public Parcelable onSaveInstanceState() {
- SavedState savedState = new SavedState();
- savedState.mFirstChildPosition = getFirstFullyVisibleChildPosition();
- return savedState;
- }
-
- @Override
- public void onRestoreInstanceState(Parcelable state) {
- if (state instanceof SavedState) {
- scrollToPosition(((SavedState) state).mFirstChildPosition);
- }
- }
-
- /** The state that will be saved across configuration changes. */
- static class SavedState implements Parcelable {
- /** The position of the first visible child view in the list. */
- int mFirstChildPosition;
-
- SavedState() {}
-
- private SavedState(Parcel in) {
- mFirstChildPosition = in.readInt();
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(mFirstChildPosition);
- }
-
- public static final Parcelable.Creator<SavedState> CREATOR =
- new Parcelable.Creator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-
- /**
- * Layout the anchor row. The anchor row is the first fully visible row.
- *
- * @param anchorTop The decorated top of the anchor. If it is not known or should be reset to
- * the top, pass -1.
- */
- private View layoutAnchor(RecyclerView.Recycler recycler, int anchorPosition, int anchorTop) {
- View anchor = recycler.getViewForPosition(anchorPosition);
- RecyclerView.LayoutParams params = getParams(anchor);
- measureChildWithMargins(anchor, 0, 0);
- int left = getPaddingLeft() + params.leftMargin;
- int top = (anchorTop == -1) ? params.topMargin : anchorTop;
- int right = left + getDecoratedMeasuredWidth(anchor);
- int bottom = top + getDecoratedMeasuredHeight(anchor);
- layoutDecorated(anchor, left, top, right, bottom);
- addView(anchor);
- return anchor;
- }
-
- /**
- * Lays out the next row in the specified direction next to the specified adjacent row.
- *
- * @param recycler The recycler from which a new view can be created.
- * @param adjacentRow The View of the adjacent row which will be used to position the new one.
- * @param layoutDirection The side of the adjacent row that the new row will be laid out on.
- * @return The new row that was laid out.
- */
- private View layoutNextRow(RecyclerView.Recycler recycler, View adjacentRow,
- @LayoutDirection int layoutDirection) {
- int adjacentRowPosition = getPosition(adjacentRow);
- int newRowPosition = adjacentRowPosition;
- if (layoutDirection == BEFORE) {
- newRowPosition = adjacentRowPosition - 1;
- } else if (layoutDirection == AFTER) {
- newRowPosition = adjacentRowPosition + 1;
- }
-
- // Because we detach all rows in onLayoutChildren, this will often just return a view from
- // the scrap heap.
- View newRow = recycler.getViewForPosition(newRowPosition);
-
- measureChildWithMargins(newRow, 0, 0);
- RecyclerView.LayoutParams newRowParams =
- (RecyclerView.LayoutParams) newRow.getLayoutParams();
- RecyclerView.LayoutParams adjacentRowParams =
- (RecyclerView.LayoutParams) adjacentRow.getLayoutParams();
- int left = getPaddingLeft() + newRowParams.leftMargin;
- int right = left + getDecoratedMeasuredWidth(newRow);
- int top;
- int bottom;
- if (layoutDirection == BEFORE) {
- bottom = adjacentRow.getTop() - adjacentRowParams.topMargin - newRowParams.bottomMargin;
- top = bottom - getDecoratedMeasuredHeight(newRow);
- } else {
- top = getDecoratedBottom(adjacentRow) + adjacentRowParams.bottomMargin
- + newRowParams.topMargin;
- bottom = top + getDecoratedMeasuredHeight(newRow);
- }
- layoutDecorated(newRow, left, top, right, bottom);
-
- if (layoutDirection == BEFORE) {
- addView(newRow, 0);
- } else {
- addView(newRow);
- }
-
- return newRow;
- }
-
- /** @return Whether another row should be laid out in the specified direction. */
- private boolean shouldLayoutNextRow(
- RecyclerView.State state, View adjacentRow, @LayoutDirection int layoutDirection) {
- int adjacentRowPosition = getPosition(adjacentRow);
-
- if (layoutDirection == BEFORE) {
- if (adjacentRowPosition == 0) {
- // We already laid out the first row.
- return false;
- }
- } else if (layoutDirection == AFTER) {
- if (adjacentRowPosition >= state.getItemCount() - 1) {
- // We already laid out the last row.
- return false;
- }
- }
-
- // If we are scrolling layout views until the target position.
- if (mSmoothScroller != null) {
- if (layoutDirection == BEFORE
- && adjacentRowPosition >= mSmoothScroller.getTargetPosition()) {
- return true;
- } else if (layoutDirection == AFTER
- && adjacentRowPosition <= mSmoothScroller.getTargetPosition()) {
- return true;
- }
- }
-
- View focusedRow = getFocusedChild();
- if (focusedRow != null) {
- int focusedRowPosition = getPosition(focusedRow);
- if (layoutDirection == BEFORE && adjacentRowPosition
- >= focusedRowPosition - NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS) {
- return true;
- } else if (layoutDirection == AFTER && adjacentRowPosition
- <= focusedRowPosition + NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS) {
- return true;
- }
- }
-
- RecyclerView.LayoutParams params = getParams(adjacentRow);
- int adjacentRowTop = getDecoratedTop(adjacentRow) - params.topMargin;
- int adjacentRowBottom = getDecoratedBottom(adjacentRow) - params.bottomMargin;
- if (layoutDirection == BEFORE && adjacentRowTop < getPaddingTop() - getHeight()) {
- // View is more than 1 page past the top of the screen and also past where the user has
- // scrolled to. We want to keep one page past the top to make the scroll up calculation
- // easier and scrolling smoother.
- return false;
- } else if (layoutDirection == AFTER
- && adjacentRowBottom > getHeight() - getPaddingBottom()) {
- // View is off of the bottom and also past where the user has scrolled to.
- return false;
- }
-
- return true;
- }
-
- /** Remove and recycle views that are no longer needed. */
- private void recycleChildrenFromStart(RecyclerView.Recycler recycler) {
- // Start laying out children one page before the top of the viewport.
- int childrenStart = getPaddingTop() - getHeight();
-
- int focusedChildPosition = Integer.MAX_VALUE;
- View focusedChild = getFocusedChild();
- if (focusedChild != null) {
- focusedChildPosition = getPosition(focusedChild);
- }
-
- // Count the number of views that should be removed.
- int detachedCount = 0;
- int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
- int childEnd = getDecoratedBottom(child);
- int childPosition = getPosition(child);
-
- if (childEnd >= childrenStart || childPosition >= focusedChildPosition - 1) {
- break;
- }
-
- detachedCount++;
- }
-
- // Remove the number of views counted above. Done by removing the first child n times.
- while (--detachedCount >= 0) {
- final View child = getChildAt(0);
- removeAndRecycleView(child, recycler);
- }
- }
-
- /** Remove and recycle views that are no longer needed. */
- private void recycleChildrenFromEnd(RecyclerView.Recycler recycler) {
- // Layout views until the end of the viewport.
- int childrenEnd = getHeight();
-
- int focusedChildPosition = Integer.MIN_VALUE + 1;
- View focusedChild = getFocusedChild();
- if (focusedChild != null) {
- focusedChildPosition = getPosition(focusedChild);
- }
-
- // Count the number of views that should be removed.
- int firstDetachedPos = 0;
- int detachedCount = 0;
- int childCount = getChildCount();
- for (int i = childCount - 1; i >= 0; i--) {
- final View child = getChildAt(i);
- int childStart = getDecoratedTop(child);
- int childPosition = getPosition(child);
-
- if (childStart <= childrenEnd || childPosition <= focusedChildPosition - 1) {
- break;
- }
-
- firstDetachedPos = i;
- detachedCount++;
- }
-
- while (--detachedCount >= 0) {
- final View child = getChildAt(firstDetachedPos);
- removeAndRecycleView(child, recycler);
- }
- }
-
- /**
- * Offset rows to do fancy animations. If offset rows was not enabled with
- * {@link #setOffsetRows}, this will do nothing.
- *
- * @see #offsetRowsIndividually
- * @see #offsetRowsByPage
- * @see #setOffsetRows
- */
- public void offsetRows() {
- if (!mOffsetRows) {
- return;
- }
-
- if (mRowOffsetMode == ROW_OFFSET_MODE_PAGE) {
- offsetRowsByPage();
- } else if (mRowOffsetMode == ROW_OFFSET_MODE_INDIVIDUAL) {
- offsetRowsIndividually();
- }
- }
-
- /**
- * Offset the single row that is scrolling off the screen such that by the time the next row
- * reaches the top, it will have accelerated completely off of the screen.
- */
- private void offsetRowsIndividually() {
- if (getChildCount() == 0) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, ":: offsetRowsIndividually getChildCount=0");
- }
- return;
- }
-
- // Identify the dangling row. It will be the first row that is at the top of the
- // list or above.
- int danglingChildIndex = -1;
- for (int i = getChildCount() - 1; i >= 0; i--) {
- View child = getChildAt(i);
- if (getDecoratedTop(child) - getParams(child).topMargin <= getPaddingTop()) {
- danglingChildIndex = i;
- break;
- }
- }
-
- mAnchorPageBreakPosition = danglingChildIndex;
-
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, ":: offsetRowsIndividually danglingChildIndex: " + danglingChildIndex);
- }
-
- // Calculate the total amount that the view will need to scroll in order to go completely
- // off screen.
- RecyclerView rv = (RecyclerView) getChildAt(0).getParent();
- int[] locs = new int[2];
- rv.getLocationInWindow(locs);
- int listTopInWindow = locs[1] + rv.getPaddingTop();
- int maxDanglingViewTranslation;
-
- int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- View child = getChildAt(i);
- RecyclerView.LayoutParams params = getParams(child);
-
- maxDanglingViewTranslation = listTopInWindow;
- // If the child has a negative margin, we'll actually need to translate the view a
- // little but further to get it completely off screen.
- if (params.topMargin < 0) {
- maxDanglingViewTranslation -= params.topMargin;
- }
- if (params.bottomMargin < 0) {
- maxDanglingViewTranslation -= params.bottomMargin;
- }
-
- if (i < danglingChildIndex) {
- child.setAlpha(0f);
- } else if (i > danglingChildIndex) {
- child.setAlpha(1f);
- setCardFlyingEffectOffset(child, 0);
- } else {
- int totalScrollDistance =
- getDecoratedMeasuredHeight(child) + params.topMargin + params.bottomMargin;
-
- int distanceLeftInScroll =
- getDecoratedBottom(child) + params.bottomMargin - getPaddingTop();
- float percentageIntoScroll = 1 - distanceLeftInScroll / (float) totalScrollDistance;
- float interpolatedPercentage =
- mDanglingRowInterpolator.getInterpolation(percentageIntoScroll);
-
- child.setAlpha(1f);
- setCardFlyingEffectOffset(child, -(maxDanglingViewTranslation
- * interpolatedPercentage));
- }
- }
- }
-
- /**
- * When the list scrolls, the entire page of rows will offset in one contiguous block. This
- * significantly reduces the amount of extra motion at the top of the screen.
- */
- private void offsetRowsByPage() {
- View anchorView = findViewByPosition(mAnchorPageBreakPosition);
- if (anchorView == null) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, ":: offsetRowsByPage anchorView null");
- }
- return;
- }
- int anchorViewTop = getDecoratedTop(anchorView) - getParams(anchorView).topMargin;
-
- View upperPageBreakView = findViewByPosition(mUpperPageBreakPosition);
- int upperViewTop =
- getDecoratedTop(upperPageBreakView) - getParams(upperPageBreakView).topMargin;
-
- int scrollDistance = upperViewTop - anchorViewTop;
-
- int distanceLeft = anchorViewTop - getPaddingTop();
- float scrollPercentage =
- (Math.abs(scrollDistance) - distanceLeft) / (float) Math.abs(scrollDistance);
-
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, String.format(":: offsetRowsByPage scrollDistance:%s, distanceLeft:%s, "
- + "scrollPercentage:%s",
- scrollDistance, distanceLeft, scrollPercentage));
- }
-
- // Calculate the total amount that the view will need to scroll in order to go completely
- // off screen.
- RecyclerView rv = (RecyclerView) getChildAt(0).getParent();
- int[] locs = new int[2];
- rv.getLocationInWindow(locs);
- int listTopInWindow = locs[1] + rv.getPaddingTop();
-
- int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- View child = getChildAt(i);
- int position = getPosition(child);
- if (position < mUpperPageBreakPosition) {
- child.setAlpha(0f);
- setCardFlyingEffectOffset(child, -listTopInWindow);
- } else if (position < mAnchorPageBreakPosition) {
- // If the child has a negative margin, we need to offset the row by a little bit
- // extra so that it moves completely off screen.
- RecyclerView.LayoutParams params = getParams(child);
- int extraTranslation = 0;
- if (params.topMargin < 0) {
- extraTranslation -= params.topMargin;
- }
- if (params.bottomMargin < 0) {
- extraTranslation -= params.bottomMargin;
- }
- int translation = (int) ((listTopInWindow + extraTranslation)
- * mDanglingRowInterpolator.getInterpolation(scrollPercentage));
- child.setAlpha(1f);
- setCardFlyingEffectOffset(child, -translation);
- } else {
- child.setAlpha(1f);
- setCardFlyingEffectOffset(child, 0);
- }
- }
- }
-
- /**
- * Apply an offset to this view. This offset is applied post-layout so it doesn't affect when
- * views are recycled
- *
- * @param child The view to apply this to
- * @param verticalOffset The offset for this child.
- */
- private void setCardFlyingEffectOffset(View child, float verticalOffset) {
- // Ideally instead of doing all this, we could use View.setTranslationY(). However, the
- // default RecyclerView.ItemAnimator also uses this method which causes layout issues.
- // See: http://b/25977087
- TranslateAnimation anim = mFlyOffscreenAnimations.get(child);
- if (anim == null) {
- anim = new TranslateAnimation();
- anim.setFillEnabled(true);
- anim.setFillAfter(true);
- anim.setDuration(0);
- mFlyOffscreenAnimations.put(child, anim);
- } else if (anim.verticalOffset == verticalOffset) {
- return;
- }
-
- anim.reset();
- anim.verticalOffset = verticalOffset;
- anim.setStartTime(Animation.START_ON_FIRST_FRAME);
- child.setAnimation(anim);
- anim.startNow();
- }
-
- /**
- * Update the page break positions based on the position of the views on screen. This should be
- * called whenever view move or change such as during a scroll or layout.
- */
- private void updatePageBreakPositions() {
- if (getChildCount() == 0) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, ":: updatePageBreakPosition getChildCount: 0");
- }
- return;
- }
-
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, String.format(":: #BEFORE updatePageBreakPositions "
- + "mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s, "
- + "mLowerPageBreakPosition:%s",
- mAnchorPageBreakPosition, mUpperPageBreakPosition,
- mLowerPageBreakPosition));
- }
-
- mAnchorPageBreakPosition = getPosition(getFirstFullyVisibleChild());
-
- if (mAnchorPageBreakPosition == -1) {
- Log.w(TAG, "Unable to update anchor positions. There is no anchor position.");
- return;
- }
-
- View anchorPageBreakView = findViewByPosition(mAnchorPageBreakPosition);
- if (anchorPageBreakView == null) {
- return;
- }
- int topMargin = getParams(anchorPageBreakView).topMargin;
- int anchorTop = getDecoratedTop(anchorPageBreakView) - topMargin;
- View upperPageBreakView = findViewByPosition(mUpperPageBreakPosition);
- int upperPageBreakTop = upperPageBreakView == null
- ? Integer.MIN_VALUE
- : getDecoratedTop(upperPageBreakView) - getParams(upperPageBreakView).topMargin;
-
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, String.format(":: #MID updatePageBreakPositions topMargin:%s, anchorTop:%s"
- + " mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s,"
- + " mLowerPageBreakPosition:%s",
- topMargin,
- anchorTop,
- mAnchorPageBreakPosition,
- mUpperPageBreakPosition,
- mLowerPageBreakPosition));
- }
-
- if (anchorTop < getPaddingTop()) {
- // The anchor has moved above the viewport. We are now on the next page. Shift the page
- // break positions and calculate a new lower one.
- mUpperPageBreakPosition = mAnchorPageBreakPosition;
- mAnchorPageBreakPosition = mLowerPageBreakPosition;
- mLowerPageBreakPosition = calculateNextPageBreakPosition(mAnchorPageBreakPosition);
- } else if (mAnchorPageBreakPosition > 0 && upperPageBreakTop >= getPaddingTop()) {
- // The anchor has moved below the viewport. We are now on the previous page. Shift
- // the page break positions and calculate a new upper one.
- mLowerPageBreakPosition = mAnchorPageBreakPosition;
- mAnchorPageBreakPosition = mUpperPageBreakPosition;
- mUpperPageBreakPosition = calculatePreviousPageBreakPosition(mAnchorPageBreakPosition);
- } else {
- mUpperPageBreakPosition = calculatePreviousPageBreakPosition(mAnchorPageBreakPosition);
- mLowerPageBreakPosition = calculateNextPageBreakPosition(mAnchorPageBreakPosition);
- }
-
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, String.format(":: #AFTER updatePageBreakPositions"
- + " mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s,"
- + " mLowerPageBreakPosition:%s",
- mAnchorPageBreakPosition, mUpperPageBreakPosition,
- mLowerPageBreakPosition));
- }
- }
-
- /**
- * @return The page break position of the page before the anchor page break position. However,
- * if it reaches the end of the laid out children or position 0, it will just return that.
- */
- @VisibleForTesting
- int calculatePreviousPageBreakPosition(int position) {
- if (position == -1) {
- return -1;
- }
- View referenceView = findViewByPosition(position);
- int referenceViewTop = getDecoratedTop(referenceView) - getParams(referenceView).topMargin;
-
- int previousPagePosition = position;
- while (previousPagePosition > 0) {
- previousPagePosition--;
- View child = findViewByPosition(previousPagePosition);
- if (child == null) {
- // View has not been laid out yet.
- return previousPagePosition + 1;
- }
-
- int childTop = getDecoratedTop(child) - getParams(child).topMargin;
- if (childTop < referenceViewTop - getHeight()) {
- return previousPagePosition + 1;
- }
- }
- // Beginning of the list.
- return 0;
- }
-
- /**
- * @return The page break position of the next page after the anchor page break position.
- * However, if it reaches the end of the laid out children or end of the list, it will just
- * return that.
- */
- @VisibleForTesting
- int calculateNextPageBreakPosition(int position) {
- if (position == -1) {
- return -1;
- }
-
- View referenceView = findViewByPosition(position);
- if (referenceView == null) {
- return position;
- }
- int referenceViewTop = getDecoratedTop(referenceView) - getParams(referenceView).topMargin;
-
- int nextPagePosition = position;
-
- // Search for the first child item after the referenceView that didn't fully fit on to the
- // screen. The next page should start from the item before this child, so that users have
- // a visual anchoring point of the page change.
- while (nextPagePosition < getItemCount() - 1) {
- nextPagePosition++;
- View child = findViewByPosition(nextPagePosition);
- if (child == null) {
- // The next view has not been laid out yet.
- return nextPagePosition - 1;
- }
-
- int childTop = getDecoratedTop(child) - getParams(child).topMargin;
- if (childTop > referenceViewTop + getHeight()) {
- // If choosing the previous child causes the view to snap back to the referenceView
- // position, then skip that and go directly to the child. This avoids the case
- // where a tall card in the layout causes the view to constantly snap back to
- // the top when scrolled.
- return nextPagePosition - 1 == position ? nextPagePosition : nextPagePosition - 1;
- }
- }
- // End of the list.
- return nextPagePosition;
- }
-
- /**
- * In this style, the focus will scroll down to the middle of the screen and lock there so that
- * moving in either direction will move the entire list by 1.
- */
- private boolean onRequestChildFocusMarioStyle(RecyclerView parent, View child) {
- int focusedPosition = getPosition(child);
- if (focusedPosition == mLastChildPositionToRequestFocus) {
- return true;
- }
- mLastChildPositionToRequestFocus = focusedPosition;
-
- int availableHeight = getAvailableHeight();
- int focusedChildTop = getDecoratedTop(child);
- int focusedChildBottom = getDecoratedBottom(child);
-
- int childIndex = parent.indexOfChild(child);
- // Iterate through children starting at the focused child to find the child above it to
- // smooth scroll to such that the focused child will be as close to the middle of the screen
- // as possible.
- for (int i = childIndex; i >= 0; i--) {
- View childAtI = getChildAt(i);
- if (childAtI == null) {
- Log.e(TAG, "Child is null at index " + i);
- continue;
- }
- // We haven't found a view that is more than half of the recycler view height above it
- // but we've reached the top so we can't go any further.
- if (i == 0) {
- parent.smoothScrollToPosition(getPosition(childAtI));
- break;
- }
-
- // Because we want to scroll to the first view that is less than half of the screen
- // away from the focused view, we "look ahead" one view. When the look ahead view
- // is more than availableHeight / 2 away, the current child at i is the one we want to
- // scroll to. However, sometimes, that view can be null (ie, if the view is in
- // transition). In that case, just skip that view.
-
- View childBefore = getChildAt(i - 1);
- if (childBefore == null) {
- continue;
- }
- int distanceToChildBeforeFromTop = focusedChildTop - getDecoratedTop(childBefore);
- int distanceToChildBeforeFromBottom = focusedChildBottom - getDecoratedTop(childBefore);
-
- if (distanceToChildBeforeFromTop > availableHeight / 2
- || distanceToChildBeforeFromBottom > availableHeight) {
- parent.smoothScrollToPosition(getPosition(childAtI));
- break;
- }
- }
- return true;
- }
-
- /**
- * We don't actually know the size of every single view, only what is currently laid out. This
- * makes it difficult to do accurate scrollbar calculations. However, lists in the car often
- * consist of views with identical heights. Because of that, we can use a single sample view to
- * do our calculations for. The main exceptions are in the first items of a list (hero card,
- * last call card, etc) so if the first view is at position 0, we pick the next one.
- *
- * @return The decorated measured height of the sample view plus its margins.
- */
- private int getSampleViewHeight() {
- if (mSampleViewHeight != -1) {
- return mSampleViewHeight;
- }
- int sampleViewIndex = getFirstFullyVisibleChildIndex();
- View sampleView = getChildAt(sampleViewIndex);
- if (getPosition(sampleView) == 0 && sampleViewIndex < getChildCount() - 1) {
- sampleView = getChildAt(++sampleViewIndex);
- }
- RecyclerView.LayoutParams params = getParams(sampleView);
- int height = getDecoratedMeasuredHeight(sampleView) + params.topMargin
- + params.bottomMargin;
- if (height == 0) {
- // This can happen if the view isn't measured yet.
- Log.w(
- TAG,
- "The sample view has a height of 0. Returning a dummy value for now "
- + "that won't be cached.");
- height = mContext.getResources().getDimensionPixelSize(R.dimen.car_sample_row_height);
- } else {
- mSampleViewHeight = height;
- }
- return height;
- }
-
- /** @return The height of the RecyclerView excluding padding. */
- private int getAvailableHeight() {
- return getHeight() - getPaddingTop() - getPaddingBottom();
- }
-
- /**
- * @return {@link RecyclerView.LayoutParams} for the given view or null if it isn't a child of
- * {@link RecyclerView}.
- */
- private static RecyclerView.LayoutParams getParams(View view) {
- return (RecyclerView.LayoutParams) view.getLayoutParams();
- }
-
- /**
- * Custom {@link LinearSmoothScroller} that has: a) Custom control over the speed of scrolls. b)
- * Scrolling snaps to start. All of our scrolling logic depends on that. c) Keeps track of some
- * state of the current scroll so that can aid in things like the scrollbar calculations.
- */
- private final class CarSmoothScroller extends LinearSmoothScroller {
- /** This value (150) was hand tuned by UX for what felt right. * */
- private static final float MILLISECONDS_PER_INCH = 150f;
- /** This value (0.45) was hand tuned by UX for what felt right. * */
- private static final float DECELERATION_TIME_DIVISOR = 0.45f;
-
- /** This value (1.8) was hand tuned by UX for what felt right. * */
- private final Interpolator mInterpolator = new DecelerateInterpolator(1.8f);
-
- private final int mTargetPosition;
-
- CarSmoothScroller(Context context, int targetPosition) {
- super(context);
- mTargetPosition = targetPosition;
- }
-
- @Override
- public PointF computeScrollVectorForPosition(int i) {
- if (getChildCount() == 0) {
- return null;
- }
- final int firstChildPos = getPosition(getChildAt(getFirstFullyVisibleChildIndex()));
- final int direction = (mTargetPosition < firstChildPos) ? -1 : 1;
- return new PointF(0, direction);
- }
-
- @Override
- protected int getVerticalSnapPreference() {
- // This is key for most of the scrolling logic that guarantees that scrolling
- // will settle with a view aligned to the top.
- return LinearSmoothScroller.SNAP_TO_START;
- }
-
- @Override
- protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
- int dy = calculateDyToMakeVisible(targetView, SNAP_TO_START);
- if (dy == 0) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Scroll distance is 0");
- }
- return;
- }
-
- final int time = calculateTimeForDeceleration(dy);
- if (time > 0) {
- action.update(0, -dy, time, mInterpolator);
- }
- }
-
- @Override
- protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
- return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
- }
-
- @Override
- protected int calculateTimeForDeceleration(int dx) {
- return (int) Math.ceil(calculateTimeForScrolling(dx) / DECELERATION_TIME_DIVISOR);
- }
-
- @Override
- public int getTargetPosition() {
- return mTargetPosition;
- }
- }
-
- /**
- * Animation that translates a view by the specified amount. Used for card flying off the screen
- * effect.
- */
- private static class TranslateAnimation extends Animation {
- public float verticalOffset;
-
- @Override
- protected void applyTransformation(float interpolatedTime, Transformation t) {
- super.applyTransformation(interpolatedTime, t);
- t.getMatrix().setTranslate(0, verticalOffset);
- }
- }
-}
diff --git a/android/support/car/widget/PagedListView.java b/android/support/car/widget/PagedListView.java
deleted file mode 100644
index 67a6247a..00000000
--- a/android/support/car/widget/PagedListView.java
+++ /dev/null
@@ -1,996 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.car.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Handler;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.IdRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.UiThread;
-import android.support.car.R;
-import android.support.v7.widget.RecyclerView;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.FrameLayout;
-
-/**
- * Custom {@link android.support.v7.widget.RecyclerView} that displays a list of items that
- * resembles a {@link android.widget.ListView} but also has page up and page down arrows on the
- * left side.
- */
-public class PagedListView extends FrameLayout {
- /** Default maximum number of clicks allowed on a list */
- public static final int DEFAULT_MAX_CLICKS = 6;
-
- /**
- * Value to pass to {@link #setMaxPages(int)} to indicate there is no restriction on the
- * maximum number of pages to show.
- */
- public static final int UNLIMITED_PAGES = -1;
-
- /**
- * The amount of time after settling to wait before autoscrolling to the next page when the user
- * holds down a pagination button.
- */
- protected static final int PAGINATION_HOLD_DELAY_MS = 400;
-
- private static final String TAG = "PagedListView";
- private static final int INVALID_RESOURCE_ID = -1;
-
- protected final CarRecyclerView mRecyclerView;
- protected final PagedLayoutManager mLayoutManager;
- protected final Handler mHandler = new Handler();
- private final boolean mScrollBarEnabled;
- private final PagedScrollBarView mScrollBarView;
-
- private int mRowsPerPage = -1;
- protected RecyclerView.Adapter<? extends RecyclerView.ViewHolder> mAdapter;
-
- /** Maximum number of pages to show. */
- private int mMaxPages;
-
- protected OnScrollListener mOnScrollListener;
-
- /** Number of visible rows per page */
- private int mDefaultMaxPages = DEFAULT_MAX_CLICKS;
-
- /** Used to check if there are more items added to the list. */
- private int mLastItemCount = 0;
-
- private boolean mNeedsFocus;
-
- /**
- * Interface for a {@link android.support.v7.widget.RecyclerView.Adapter} to cap the number of
- * items.
- *
- * <p>NOTE: it is still up to the adapter to use maxItems in {@link
- * android.support.v7.widget.RecyclerView.Adapter#getItemCount()}.
- *
- * <p>the recommended way would be with:
- *
- * <pre>{@code
- * {@literal@}Override
- * public int getItemCount() {
- * return Math.min(super.getItemCount(), mMaxItems);
- * }
- * }</pre>
- */
- public interface ItemCap {
- /**
- * A value to pass to {@link #setMaxItems(int)} that indicates there should be no limit.
- */
- int UNLIMITED = -1;
-
- /**
- * Sets the maximum number of items available in the adapter. A value less than '0' means
- * the list should not be capped.
- */
- void setMaxItems(int maxItems);
- }
-
- /**
- * Interface for a {@link android.support.v7.widget.RecyclerView.Adapter} to set the position
- * offset for the adapter to load the data.
- *
- * <p>For example in the adapter, if the positionOffset is 20, then for position 0 it will show
- * the item in position 20 instead, for position 1 it will show the item in position 21 instead
- * and so on.
- */
- public interface ItemPositionOffset {
- /** Sets the position offset for the adapter. */
- void setPositionOffset(int positionOffset);
- }
-
- public PagedListView(Context context, AttributeSet attrs) {
- this(context, attrs, 0 /*defStyleAttrs*/, 0 /*defStyleRes*/);
- }
-
- public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs) {
- this(context, attrs, defStyleAttrs, 0 /*defStyleRes*/);
- }
-
- public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
- this(context, attrs, defStyleAttrs, defStyleRes, 0);
- }
-
- public PagedListView(
- Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes, int layoutId) {
- super(context, attrs, defStyleAttrs, defStyleRes);
- if (layoutId == 0) {
- layoutId = R.layout.car_paged_recycler_view;
- }
- LayoutInflater.from(context).inflate(layoutId, this /*root*/, true /*attachToRoot*/);
-
- TypedArray a = context.obtainStyledAttributes(
- attrs, R.styleable.PagedListView, defStyleAttrs, defStyleRes);
- mRecyclerView = (CarRecyclerView) findViewById(R.id.recycler_view);
- boolean fadeLastItem = a.getBoolean(R.styleable.PagedListView_fadeLastItem, false);
- mRecyclerView.setFadeLastItem(fadeLastItem);
- boolean offsetRows = a.getBoolean(R.styleable.PagedListView_offsetRows, false);
-
- mMaxPages = getDefaultMaxPages();
-
- mLayoutManager = new PagedLayoutManager(context);
- mLayoutManager.setOffsetRows(offsetRows);
- mRecyclerView.setLayoutManager(mLayoutManager);
- mRecyclerView.setOnScrollListener(mRecyclerViewOnScrollListener);
- mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 12);
- mRecyclerView.setItemAnimator(new CarItemAnimator(mLayoutManager));
-
- boolean offsetScrollBar = a.getBoolean(R.styleable.PagedListView_offsetScrollBar, false);
- if (offsetScrollBar) {
- MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams();
- params.setMarginStart(getResources().getDimensionPixelSize(
- R.dimen.car_margin));
- params.setMarginEnd(
- a.getDimensionPixelSize(R.styleable.PagedListView_listEndMargin, 0));
- mRecyclerView.setLayoutParams(params);
- }
-
- if (a.getBoolean(R.styleable.PagedListView_showPagedListViewDivider, true)) {
- int dividerStartMargin = a.getDimensionPixelSize(
- R.styleable.PagedListView_dividerStartMargin, 0);
- int dividerStartId = a.getResourceId(
- R.styleable.PagedListView_alignDividerStartTo, INVALID_RESOURCE_ID);
- int dividerEndId = a.getResourceId(
- R.styleable.PagedListView_alignDividerEndTo, INVALID_RESOURCE_ID);
-
- mRecyclerView.addItemDecoration(new DividerDecoration(context, dividerStartMargin,
- dividerStartId, dividerEndId));
- }
-
- int itemSpacing = a.getDimensionPixelSize(R.styleable.PagedListView_itemSpacing, 0);
- if (itemSpacing > 0) {
- mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing));
- }
-
- // Set this to true so that this view consumes clicks events and views underneath
- // don't receive this click event. Without this it's possible to click places in the
- // view that don't capture the event, and as a result, elements visually hidden consume
- // the event.
- setClickable(true);
-
- // Set focusable false explicitly to handle the behavior change in Android O where
- // clickable view becomes focusable by default.
- setFocusable(false);
-
- mScrollBarEnabled = a.getBoolean(R.styleable.PagedListView_scrollBarEnabled, true);
- mScrollBarView = (PagedScrollBarView) findViewById(R.id.paged_scroll_view);
- mScrollBarView.setPaginationListener(
- new PagedScrollBarView.PaginationListener() {
- @Override
- public void onPaginate(int direction) {
- if (direction == PagedScrollBarView.PaginationListener.PAGE_UP) {
- mRecyclerView.pageUp();
- if (mOnScrollListener != null) {
- mOnScrollListener.onScrollUpButtonClicked();
- }
- } else if (direction == PagedScrollBarView.PaginationListener.PAGE_DOWN) {
- mRecyclerView.pageDown();
- if (mOnScrollListener != null) {
- mOnScrollListener.onScrollDownButtonClicked();
- }
- } else {
- Log.e(TAG, "Unknown pagination direction (" + direction + ")");
- }
- }
- });
-
- Drawable upButtonIcon = a.getDrawable(R.styleable.PagedListView_upButtonIcon);
- if (upButtonIcon != null) {
- setUpButtonIcon(upButtonIcon);
- }
-
- Drawable downButtonIcon = a.getDrawable(R.styleable.PagedListView_downButtonIcon);
- if (downButtonIcon != null) {
- setDownButtonIcon(downButtonIcon);
- }
-
- mScrollBarView.setVisibility(mScrollBarEnabled ? VISIBLE : GONE);
-
- // Modify the layout the Scroll Bar is not visible.
- if (!mScrollBarEnabled) {
- MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams();
- params.setMarginStart(0);
- mRecyclerView.setLayoutParams(params);
- }
-
- setDayNightStyle(DayNightStyle.AUTO);
- a.recycle();
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mHandler.removeCallbacks(mUpdatePaginationRunnable);
- }
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent e) {
- if (e.getAction() == MotionEvent.ACTION_DOWN) {
- // The user has interacted with the list using touch. All movements will now paginate
- // the list.
- mLayoutManager.setRowOffsetMode(PagedLayoutManager.ROW_OFFSET_MODE_PAGE);
- }
- return super.onInterceptTouchEvent(e);
- }
-
- @Override
- public void requestChildFocus(View child, View focused) {
- super.requestChildFocus(child, focused);
- // The user has interacted with the list using the controller. Movements through the list
- // will now be one row at a time.
- mLayoutManager.setRowOffsetMode(PagedLayoutManager.ROW_OFFSET_MODE_INDIVIDUAL);
- }
-
- /**
- * Returns the position of the given View in the list.
- *
- * @param v The View to check for.
- * @return The position or -1 if the given View is {@code null} or not in the list.
- */
- public int positionOf(@Nullable View v) {
- if (v == null || v.getParent() != mRecyclerView) {
- return -1;
- }
- return mLayoutManager.getPosition(v);
- }
-
- @NonNull
- public CarRecyclerView getRecyclerView() {
- return mRecyclerView;
- }
-
- /**
- * Scrolls to the given position in the PagedListView.
- *
- * @param position The position in the list to scroll to.
- */
- public void scrollToPosition(int position) {
- mLayoutManager.scrollToPosition(position);
-
- // Sometimes #scrollToPosition doesn't change the scroll state so we need to make sure
- // the pagination arrows actually get updated. See b/http://b/15801119
- mHandler.post(mUpdatePaginationRunnable);
- }
-
- /** Sets the icon to be used for the up button. */
- public void setUpButtonIcon(Drawable icon) {
- mScrollBarView.setUpButtonIcon(icon);
- }
-
- /** Sets the icon to be used for the down button. */
- public void setDownButtonIcon(Drawable icon) {
- mScrollBarView.setDownButtonIcon(icon);
- }
-
- /**
- * Sets the adapter for the list.
- *
- * <p>The given Adapter can implement {@link ItemCap} if it wishes to control the behavior of
- * a max number of items. Otherwise, methods in the PagedListView to limit the content, such as
- * {@link #setMaxPages(int)}, will do nothing.
- */
- public void setAdapter(
- @NonNull RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter) {
- mAdapter = adapter;
- mRecyclerView.setAdapter(adapter);
- updateMaxItems();
- }
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @NonNull
- public PagedLayoutManager getLayoutManager() {
- return mLayoutManager;
- }
-
- @Nullable
- @SuppressWarnings("unchecked")
- public RecyclerView.Adapter<? extends RecyclerView.ViewHolder> getAdapter() {
- return mRecyclerView.getAdapter();
- }
-
- /**
- * Sets the maximum number of the pages that can be shown in the PagedListView. The size of a
- * page is defined as the number of items that fit completely on the screen at once.
- *
- * <p>Passing {@link #UNLIMITED_PAGES} will remove any restrictions on a maximum number
- * of pages.
- *
- * <p>Note that for any restriction on maximum pages to work, the adapter passed to this
- * PagedListView needs to implement {@link ItemCap}.
- *
- * @param maxPages The maximum number of pages that fit on the screen. Should be positive or
- * {@link #UNLIMITED_PAGES}.
- */
- public void setMaxPages(int maxPages) {
- mMaxPages = Math.max(UNLIMITED_PAGES, maxPages);
- updateMaxItems();
- }
-
- /**
- * Returns the maximum number of pages allowed in the PagedListView. This number is set by
- * {@link #setMaxPages(int)}. If that method has not been called, then this value should match
- * the default value.
- *
- * @return The maximum number of pages to be shown or {@link #UNLIMITED_PAGES} if there is
- * no limit.
- */
- public int getMaxPages() {
- return mMaxPages;
- }
-
- /**
- * Gets the number of rows per page. Default value of mRowsPerPage is -1. If the first child of
- * PagedLayoutManager is null or the height of the first child is 0, it will return 1.
- */
- public int getRowsPerPage() {
- return mRowsPerPage;
- }
-
- /** Resets the maximum number of pages to be shown to be the default. */
- public void resetMaxPages() {
- mMaxPages = getDefaultMaxPages();
- updateMaxItems();
- }
-
- /**
- * @return The position of first visible child in the list. -1 will be returned if there is no
- * child.
- */
- public int getFirstFullyVisibleChildPosition() {
- return mLayoutManager.getFirstFullyVisibleChildPosition();
- }
-
- /**
- * @return The position of last visible child in the list. -1 will be returned if there is no
- * child.
- */
- public int getLastFullyVisibleChildPosition() {
- return mLayoutManager.getLastFullyVisibleChildPosition();
- }
-
- /**
- * Adds an {@link android.support.v7.widget.RecyclerView.ItemDecoration} to this PagedListView.
- *
- * @param decor The decoration to add.
- * @see RecyclerView#addItemDecoration(RecyclerView.ItemDecoration)
- */
- public void addItemDecoration(@NonNull RecyclerView.ItemDecoration decor) {
- mRecyclerView.addItemDecoration(decor);
- }
-
- /**
- * Removes the given {@link android.support.v7.widget.RecyclerView.ItemDecoration} from this
- * PagedListView.
- *
- * <p>The decoration will function the same as the item decoration for a {@link RecyclerView}.
- *
- * @param decor The decoration to remove.
- * @see RecyclerView#removeItemDecoration(RecyclerView.ItemDecoration)
- */
- public void removeItemDecoration(@NonNull RecyclerView.ItemDecoration decor) {
- mRecyclerView.removeItemDecoration(decor);
- }
-
- /**
- * Sets spacing between each item in the list. The spacing will not be added before the first
- * item and after the last.
- *
- * @param itemSpacing the spacing between each item.
- */
- public void setItemSpacing(int itemSpacing) {
- ItemSpacingDecoration existing = null;
- for (int i = 0, count = mRecyclerView.getItemDecorationCount(); i < count; i++) {
- RecyclerView.ItemDecoration itemDecoration = mRecyclerView.getItemDecorationAt(i);
- if (itemDecoration instanceof ItemSpacingDecoration) {
- existing = (ItemSpacingDecoration) itemDecoration;
- break;
- }
- }
-
- if (itemSpacing == 0 && existing != null) {
- mRecyclerView.removeItemDecoration(existing);
- } else if (existing == null) {
- mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing));
- } else {
- existing.setItemSpacing(itemSpacing);
- }
- mRecyclerView.invalidateItemDecorations();
- }
-
- /**
- * Adds an {@link android.support.v7.widget.RecyclerView.OnItemTouchListener} to this
- * PagedListView.
- *
- * <p>The listener will function the same as the listener for a regular {@link RecyclerView}.
- *
- * @param touchListener The touch listener to add.
- * @see RecyclerView#addOnItemTouchListener(RecyclerView.OnItemTouchListener)
- */
- public void addOnItemTouchListener(@NonNull RecyclerView.OnItemTouchListener touchListener) {
- mRecyclerView.addOnItemTouchListener(touchListener);
- }
-
- /**
- * Removes the given {@link android.support.v7.widget.RecyclerView.OnItemTouchListener} from
- * the PagedListView.
- *
- * @param touchListener The touch listener to remove.
- * @see RecyclerView#removeOnItemTouchListener(RecyclerView.OnItemTouchListener)
- */
- public void removeOnItemTouchListener(@NonNull RecyclerView.OnItemTouchListener touchListener) {
- mRecyclerView.removeOnItemTouchListener(touchListener);
- }
- /**
- * Sets how this {@link PagedListView} responds to day/night configuration changes. By
- * default, the PagedListView is darker in the day and lighter at night.
- *
- * @param dayNightStyle A value from {@link DayNightStyle}.
- * @see DayNightStyle
- */
- public void setDayNightStyle(@DayNightStyle int dayNightStyle) {
- // Update the scrollbar
- mScrollBarView.setDayNightStyle(dayNightStyle);
-
- int decorCount = mRecyclerView.getItemDecorationCount();
- for (int i = 0; i < decorCount; i++) {
- RecyclerView.ItemDecoration decor = mRecyclerView.getItemDecorationAt(i);
- if (decor instanceof DividerDecoration) {
- ((DividerDecoration) decor).updateDividerColor();
- }
- }
- }
-
- /**
- * Returns the {@link android.support.v7.widget.RecyclerView.ViewHolder} that corresponds to the
- * last child in the PagedListView that is fully visible.
- *
- * @return The corresponding ViewHolder or {@code null} if none exists.
- */
- @Nullable
- public RecyclerView.ViewHolder getLastViewHolder() {
- View lastFullyVisible = mLayoutManager.getLastFullyVisibleChild();
- if (lastFullyVisible == null) {
- return null;
- }
- int lastFullyVisibleAdapterPosition = mLayoutManager.getPosition(lastFullyVisible);
- RecyclerView.ViewHolder lastViewHolder = getRecyclerView()
- .findViewHolderForAdapterPosition(lastFullyVisibleAdapterPosition + 1);
- // We want to get the very last ViewHolder in the list, even if it's only fully visible
- // If it doesn't exist, return the last fully visible ViewHolder.
- if (lastViewHolder == null) {
- lastViewHolder = getRecyclerView()
- .findViewHolderForAdapterPosition(lastFullyVisibleAdapterPosition);
- }
- return lastViewHolder;
- }
-
- /**
- * Sets the {@link OnScrollListener} that will be notified of scroll events within the
- * PagedListView.
- *
- * @param listener The scroll listener to set.
- */
- public void setOnScrollListener(OnScrollListener listener) {
- mOnScrollListener = listener;
- mLayoutManager.setOnScrollListener(mOnScrollListener);
- }
-
- /** Returns the page the given position is on, starting with page 0. */
- public int getPage(int position) {
- if (mRowsPerPage == -1) {
- return -1;
- }
- if (mRowsPerPage == 0) {
- return 0;
- }
- return position / mRowsPerPage;
- }
-
- /**
- * Sets the default number of pages that this PagedListView is limited to.
- *
- * @param newDefault The default number of pages. Should be positive.
- */
- public void setDefaultMaxPages(int newDefault) {
- if (newDefault < 0) {
- return;
- }
- mDefaultMaxPages = newDefault;
- resetMaxPages();
- }
-
- /** Returns the default number of pages the list should have */
- protected int getDefaultMaxPages() {
- // assume list shown in response to a click, so, reduce number of clicks by one
- return mDefaultMaxPages - 1;
- }
-
- @Override
- public void onLayout(boolean changed, int left, int top, int right, int bottom) {
- // if a late item is added to the top of the layout after the layout is stabilized, causing
- // the former top item to be pushed to the 2nd page, the focus will still be on the former
- // top item. Since our car layout manager tries to scroll the viewport so that the focused
- // item is visible, the view port will be on the 2nd page. That means the newly added item
- // will not be visible, on the first page.
-
- // what we want to do is: if the formerly focused item is the first one in the list, any
- // item added above it will make the focus to move to the new first item.
- // if the focus is not on the formerly first item, then we don't need to do anything. Let
- // the layout manager do the job and scroll the viewport so the currently focused item
- // is visible.
-
- // we need to calculate whether we want to request focus here, before the super call,
- // because after the super call, the first born might be changed.
- View focusedChild = mLayoutManager.getFocusedChild();
- View firstBorn = mLayoutManager.getChildAt(0);
-
- super.onLayout(changed, left, top, right, bottom);
-
- if (mAdapter != null) {
- int itemCount = mAdapter.getItemCount();
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, String.format(
- "onLayout hasFocus: %s, mLastItemCount: %s, itemCount: %s, "
- + "focusedChild: %s, firstBorn: %s, isInTouchMode: %s, "
- + "mNeedsFocus: %s",
- hasFocus(),
- mLastItemCount,
- itemCount,
- focusedChild,
- firstBorn,
- isInTouchMode(),
- mNeedsFocus));
- }
- updateMaxItems();
- // This is a workaround for missing focus because isInTouchMode() is not always
- // returning the right value.
- // This is okay for the Engine release since focus is always showing.
- // However, in Tala and Fender, we want to show focus only when the user uses
- // hardware controllers, so we need to revisit this logic. b/22990605.
- if (mNeedsFocus && itemCount > 0) {
- if (focusedChild == null) {
- requestFocus();
- }
- mNeedsFocus = false;
- }
- if (itemCount > mLastItemCount && focusedChild == firstBorn) {
- requestFocus();
- }
- mLastItemCount = itemCount;
- }
- // We need to update the scroll buttons after layout has happened.
- // Determining if a scrollbar is necessary requires looking at the layout of the child
- // views. Therefore, this determination can only be done after layout has happened.
- // Note: don't animate here to prevent b/26849677
- updatePaginationButtons(false /*animate*/);
- }
-
- /**
- * Returns the View at the given position within the list.
- *
- * @param position A position within the list.
- * @return The View or {@code null} if no View exists at the given position.
- */
- @Nullable
- public View findViewByPosition(int position) {
- return mLayoutManager.findViewByPosition(position);
- }
-
- /**
- * Determines if scrollbar should be visible or not and shows/hides it accordingly. If this is
- * being called as a result of adapter changes, it should be called after the new layout has
- * been calculated because the method of determining scrollbar visibility uses the current
- * layout. If this is called after an adapter change but before the new layout, the visibility
- * determination may not be correct.
- *
- * @param animate {@code true} if the scrollbar should animate to its new position.
- * {@code false} if no animation is used
- */
- protected void updatePaginationButtons(boolean animate) {
- if (!mScrollBarEnabled) {
- // Don't change the visibility of the ScrollBar unless it's enabled.
- return;
- }
-
- if ((mLayoutManager.isAtTop() && mLayoutManager.isAtBottom())
- || mLayoutManager.getItemCount() == 0) {
- mScrollBarView.setVisibility(View.INVISIBLE);
- } else {
- mScrollBarView.setVisibility(View.VISIBLE);
- }
- mScrollBarView.setUpEnabled(shouldEnablePageUpButton());
- mScrollBarView.setDownEnabled(shouldEnablePageDownButton());
-
- mScrollBarView.setParameters(
- mRecyclerView.computeVerticalScrollRange(),
- mRecyclerView.computeVerticalScrollOffset(),
- mRecyclerView.computeVerticalScrollExtent(),
- animate);
- invalidate();
- }
-
- protected boolean shouldEnablePageUpButton() {
- return !mLayoutManager.isAtTop();
- }
-
- protected boolean shouldEnablePageDownButton() {
- return !mLayoutManager.isAtBottom();
- }
-
- @UiThread
- protected void updateMaxItems() {
- if (mAdapter == null) {
- return;
- }
-
- // Ensure mRowsPerPage regardless of if the adapter implements ItemCap.
- updateRowsPerPage();
-
- // If the adapter does not implement ItemCap, then the max items on it cannot be updated.
- if (!(mAdapter instanceof ItemCap)) {
- return;
- }
-
- final int originalCount = mAdapter.getItemCount();
- ((ItemCap) mAdapter).setMaxItems(calculateMaxItemCount());
- final int newCount = mAdapter.getItemCount();
- if (newCount == originalCount) {
- return;
- }
-
- if (newCount < originalCount) {
- mAdapter.notifyItemRangeRemoved(newCount, originalCount - newCount);
- } else {
- mAdapter.notifyItemRangeInserted(originalCount, newCount - originalCount);
- }
- }
-
- protected int calculateMaxItemCount() {
- final View firstChild = mLayoutManager.getChildAt(0);
- if (firstChild == null || firstChild.getHeight() == 0) {
- return -1;
- } else {
- return (mMaxPages < 0) ? -1 : mRowsPerPage * mMaxPages;
- }
- }
-
- /**
- * Updates the rows number per current page, which is used for calculating how many items we
- * want to show.
- */
- protected void updateRowsPerPage() {
- final View firstChild = mLayoutManager.getChildAt(0);
- if (firstChild == null || firstChild.getHeight() == 0) {
- mRowsPerPage = 1;
- } else {
- mRowsPerPage = Math.max(1, (getHeight() - getPaddingTop()) / firstChild.getHeight());
- }
- }
-
- @Override
- protected Parcelable onSaveInstanceState() {
- SavedState savedState = new SavedState(super.onSaveInstanceState());
- savedState.mLayoutManagerState = mLayoutManager.onSaveInstanceState();
- return savedState;
- }
-
- @Override
- protected void onRestoreInstanceState(Parcelable state) {
- SavedState savedState = (SavedState) state;
- mLayoutManager.onRestoreInstanceState(savedState.mLayoutManagerState);
- super.onRestoreInstanceState(savedState.getSuperState());
- }
-
- @Override
- protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
- // There is the possibility of multiple PagedListViews on a page. This means that the ids
- // of the child Views of PagedListView are no longer unique, and onSaveInstanceState()
- // cannot be used. As a result, PagedListViews needs to manually dispatch the instance
- // states. Call dispatchFreezeSelfOnly() so that no child views have onSaveInstanceState()
- // called by the system.
- dispatchFreezeSelfOnly(container);
- }
-
- @Override
- protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
- // Prevent onRestoreInstanceState() from being called on child Views. Instead, PagedListView
- // will manually handle passing the state. See the comment in dispatchSaveInstanceState()
- // for more information.
- dispatchThawSelfOnly(container);
- }
-
- /** The state that will be saved across configuration changes. */
- private static class SavedState extends BaseSavedState {
- /** The state of the {@link #mLayoutManager} of this PagedListView. */
- Parcelable mLayoutManagerState;
-
- SavedState(Parcelable superState) {
- super(superState);
- }
-
- private SavedState(Parcel in) {
- super(in);
- mLayoutManagerState =
- in.readParcelable(PagedLayoutManager.SavedState.class.getClassLoader());
- }
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- super.writeToParcel(out, flags);
- out.writeParcelable(mLayoutManagerState, flags);
- }
-
- public static final ClassLoaderCreator<SavedState> CREATOR =
- new ClassLoaderCreator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel source, ClassLoader loader) {
- return new SavedState(source);
- }
-
- @Override
- public SavedState createFromParcel(Parcel source) {
- return createFromParcel(source, null /* loader */);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-
- private final RecyclerView.OnScrollListener mRecyclerViewOnScrollListener =
- new RecyclerView.OnScrollListener() {
- @Override
- public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
- if (mOnScrollListener != null) {
- mOnScrollListener.onScrolled(recyclerView, dx, dy);
- if (!mLayoutManager.isAtTop() && mLayoutManager.isAtBottom()) {
- mOnScrollListener.onReachBottom();
- } else if (mLayoutManager.isAtTop() || !mLayoutManager.isAtBottom()) {
- mOnScrollListener.onLeaveBottom();
- }
- }
- updatePaginationButtons(false);
- }
-
- @Override
- public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
- if (mOnScrollListener != null) {
- mOnScrollListener.onScrollStateChanged(recyclerView, newState);
- }
- if (newState == RecyclerView.SCROLL_STATE_IDLE) {
- mHandler.postDelayed(mPaginationRunnable, PAGINATION_HOLD_DELAY_MS);
- }
- }
- };
-
- protected final Runnable mPaginationRunnable =
- new Runnable() {
- @Override
- public void run() {
- boolean upPressed = mScrollBarView.isUpPressed();
- boolean downPressed = mScrollBarView.isDownPressed();
- if (upPressed && downPressed) {
- return;
- }
- if (upPressed) {
- mRecyclerView.pageUp();
- } else if (downPressed) {
- mRecyclerView.pageDown();
- }
- }
- };
-
- private final Runnable mUpdatePaginationRunnable =
- new Runnable() {
- @Override
- public void run() {
- updatePaginationButtons(true /*animate*/);
- }
- };
-
- /** Used to listen for {@code PagedListView} scroll events. */
- public abstract static class OnScrollListener {
- /** Called when menu reaches the bottom */
- public void onReachBottom() {}
- /** Called when menu leaves the bottom */
- public void onLeaveBottom() {}
- /** Called when scroll up button is clicked */
- public void onScrollUpButtonClicked() {}
- /** Called when scroll down button is clicked */
- public void onScrollDownButtonClicked() {}
- /** Called when scrolling to the previous page via up gesture */
- public void onGestureUp() {}
- /** Called when scrolling to the next page via down gesture */
- public void onGestureDown() {}
-
- /**
- * Called when RecyclerView.OnScrollListener#onScrolled is called. See
- * RecyclerView.OnScrollListener
- */
- public void onScrolled(RecyclerView recyclerView, int dx, int dy) {}
-
- /** See RecyclerView.OnScrollListener */
- public void onScrollStateChanged(RecyclerView recyclerView, int newState) {}
-
- /** Called when the view scrolls up a page */
- public void onPageUp() {}
-
- /** Called when the view scrolls down a page */
- public void onPageDown() {}
- }
-
- /**
- * A {@link android.support.v7.widget.RecyclerView.ItemDecoration} that will add spacing
- * between each item in the RecyclerView that it is added to.
- */
- private static class ItemSpacingDecoration extends RecyclerView.ItemDecoration {
-
- private int mHalfItemSpacing;
-
- private ItemSpacingDecoration(int itemSpacing) {
- mHalfItemSpacing = itemSpacing / 2;
- }
-
- @Override
- public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
- RecyclerView.State state) {
- super.getItemOffsets(outRect, view, parent, state);
- // Skip top offset for first item and bottom offset for last.
- int position = parent.getChildAdapterPosition(view);
- if (position > 0) {
- outRect.top = mHalfItemSpacing;
- }
- if (position < state.getItemCount() - 1) {
- outRect.bottom = mHalfItemSpacing;
- }
- }
-
- /**
- * @param itemSpacing sets spacing between each item.
- */
- public void setItemSpacing(int itemSpacing) {
- mHalfItemSpacing = itemSpacing / 2;
- }
- }
-
- /**
- * A {@link android.support.v7.widget.RecyclerView.ItemDecoration} that will draw a dividing
- * line between each item in the RecyclerView that it is added to.
- */
- private static class DividerDecoration extends RecyclerView.ItemDecoration {
- private final Context mContext;
- private final Paint mPaint;
- private final int mDividerHeight;
- private final int mDividerStartMargin;
- @IdRes private final int mDividerStartId;
- @IdRes private final int mDividerEndId;
-
- /**
- * @param dividerStartMargin The start offset of the dividing line. This offset will be
- * relative to {@code dividerStartId} if that value is given.
- * @param dividerStartId A child view id whose starting edge will be used as the starting
- * edge of the dividing line. If this value is {@link #INVALID_RESOURCE_ID}, the top
- * container of each child view will be used.
- * @param dividerEndId A child view id whose ending edge will be used as the starting edge
- * of the dividing lin.e If this value is {@link #INVALID_RESOURCE_ID}, then the top
- * container view of each child will be used.
- */
- private DividerDecoration(Context context, int dividerStartMargin,
- @IdRes int dividerStartId, @IdRes int dividerEndId) {
- mContext = context;
- mDividerStartMargin = dividerStartMargin;
- mDividerStartId = dividerStartId;
- mDividerEndId = dividerEndId;
-
- Resources res = context.getResources();
- mPaint = new Paint();
- mPaint.setColor(res.getColor(R.color.car_list_divider));
- mDividerHeight = res.getDimensionPixelSize(R.dimen.car_divider_height);
- }
-
- /** Updates the list divider color which may have changed due to a day night transition. */
- public void updateDividerColor() {
- mPaint.setColor(mContext.getResources().getColor(R.color.car_list_divider));
- }
-
- @Override
- public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
- // Draw a divider line between each item. No need to draw the line for the last item.
- for (int i = 0, childCount = parent.getChildCount(); i < childCount - 1; i++) {
- View container = parent.getChildAt(i);
- View nextContainer = parent.getChildAt(i + 1);
- int spacing = nextContainer.getTop() - container.getBottom();
-
- View startChild =
- mDividerStartId != INVALID_RESOURCE_ID
- ? container.findViewById(mDividerStartId)
- : container;
-
- View endChild =
- mDividerEndId != INVALID_RESOURCE_ID
- ? container.findViewById(mDividerEndId)
- : container;
-
- if (startChild == null || endChild == null) {
- continue;
- }
-
- int left = mDividerStartMargin + startChild.getLeft();
- int right = endChild.getRight();
- int bottom = container.getBottom() + spacing / 2 + mDividerHeight / 2;
- int top = bottom - mDividerHeight;
-
- c.drawRect(left, top, right, bottom, mPaint);
- }
- }
-
- @Override
- public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
- RecyclerView.State state) {
- super.getItemOffsets(outRect, view, parent, state);
- // Skip top offset for first item and bottom offset for last.
- int position = parent.getChildAdapterPosition(view);
- if (position > 0) {
- outRect.top = mDividerHeight / 2;
- }
- if (position < state.getItemCount() - 1) {
- outRect.bottom = mDividerHeight / 2;
- }
- }
- }
-}
diff --git a/android/support/car/widget/PagedScrollBarView.java b/android/support/car/widget/PagedScrollBarView.java
deleted file mode 100644
index 1c46b5d4..00000000
--- a/android/support/car/widget/PagedScrollBarView.java
+++ /dev/null
@@ -1,264 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.car.widget;
-
-import android.content.Context;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
-import android.support.car.R;
-import android.support.v4.content.ContextCompat;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.AccelerateDecelerateInterpolator;
-import android.view.animation.Interpolator;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-
-/** A custom view to provide list scroll behaviour -- up/down buttons and scroll indicator. */
-public class PagedScrollBarView extends FrameLayout
- implements View.OnClickListener, View.OnLongClickListener {
- private static final float BUTTON_DISABLED_ALPHA = 0.2f;
-
- @DayNightStyle private int mDayNightStyle;
-
- /** Listener for when the list should paginate. */
- public interface PaginationListener {
- int PAGE_UP = 0;
- int PAGE_DOWN = 1;
-
- /** Called when the linked view should be paged in the given direction */
- void onPaginate(int direction);
- }
-
- private final ImageView mUpButton;
- private final ImageView mDownButton;
- private final ImageView mScrollThumb;
- /** The "filler" view between the up and down buttons */
- private final View mFiller;
-
- private final Interpolator mPaginationInterpolator = new AccelerateDecelerateInterpolator();
- private final int mMinThumbLength;
- private final int mMaxThumbLength;
- private PaginationListener mPaginationListener;
-
- public PagedScrollBarView(Context context, AttributeSet attrs) {
- this(context, attrs, 0 /*defStyleAttrs*/, 0 /*defStyleRes*/);
- }
-
- public PagedScrollBarView(Context context, AttributeSet attrs, int defStyleAttrs) {
- this(context, attrs, defStyleAttrs, 0 /*defStyleRes*/);
- }
-
- public PagedScrollBarView(
- Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
- super(context, attrs, defStyleAttrs, defStyleRes);
-
- LayoutInflater inflater =
- (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- inflater.inflate(R.layout.car_paged_scrollbar_buttons, this /* root */,
- true /* attachToRoot */);
-
- mUpButton = (ImageView) findViewById(R.id.page_up);
- mUpButton.setOnClickListener(this);
- mUpButton.setOnLongClickListener(this);
- mDownButton = (ImageView) findViewById(R.id.page_down);
- mDownButton.setOnClickListener(this);
- mDownButton.setOnLongClickListener(this);
-
- mScrollThumb = (ImageView) findViewById(R.id.scrollbar_thumb);
- mFiller = findViewById(R.id.filler);
-
- mMinThumbLength = getResources().getDimensionPixelSize(R.dimen.min_thumb_height);
- mMaxThumbLength = getResources().getDimensionPixelSize(R.dimen.max_thumb_height);
- }
-
- @Override
- public void onClick(View v) {
- dispatchPageClick(v);
- }
-
- @Override
- public boolean onLongClick(View v) {
- dispatchPageClick(v);
- return true;
- }
-
- /** Sets the icon to be used for the up button. */
- public void setUpButtonIcon(Drawable icon) {
- mUpButton.setImageDrawable(icon);
- }
-
- /** Sets the icon to be used for the down button. */
- public void setDownButtonIcon(Drawable icon) {
- mDownButton.setImageDrawable(icon);
- }
-
- /**
- * Sets the listener that will be notified when the up and down buttons have been pressed.
- *
- * @param listener The listener to set.
- */
- public void setPaginationListener(PaginationListener listener) {
- mPaginationListener = listener;
- }
-
- /** Returns {@code true} if the "up" button is pressed */
- public boolean isUpPressed() {
- return mUpButton.isPressed();
- }
-
- /** Returns {@code true} if the "down" button is pressed */
- public boolean isDownPressed() {
- return mDownButton.isPressed();
- }
-
- /** Sets the range, offset and extent of the scroll bar. See {@link View}. */
- public void setParameters(int range, int offset, int extent, boolean animate) {
- // This method is where we take the computed parameters from the PagedLayoutManager and
- // render it within the specified constraints ({@link #mMaxThumbLength} and
- // {@link #mMinThumbLength}).
- final int size = mFiller.getHeight() - mFiller.getPaddingTop() - mFiller.getPaddingBottom();
-
- int thumbLength = extent * size / range;
- thumbLength = Math.max(Math.min(thumbLength, mMaxThumbLength), mMinThumbLength);
-
- int thumbOffset = size - thumbLength;
- if (isDownEnabled()) {
- // We need to adjust the offset so that it fits into the possible space inside the
- // filler with regarding to the constraints set by mMaxThumbLength and mMinThumbLength.
- thumbOffset = (size - thumbLength) * offset / range;
- }
-
- // Sets the size of the thumb and request a redraw if needed.
- final ViewGroup.LayoutParams lp = mScrollThumb.getLayoutParams();
- if (lp.height != thumbLength) {
- lp.height = thumbLength;
- mScrollThumb.requestLayout();
- }
-
- moveY(mScrollThumb, thumbOffset, animate);
- }
-
- /**
- * Sets how this {@link PagedScrollBarView} responds to day/night configuration changes. By
- * default, the PagedScrollBarView is darker in the day and lighter at night.
- *
- * @param dayNightStyle A value from {@link DayNightStyle}.
- * @see DayNightStyle
- */
- public void setDayNightStyle(@DayNightStyle int dayNightStyle) {
- mDayNightStyle = dayNightStyle;
- reloadColors();
- }
-
- /**
- * Sets whether or not the up button on the scroll bar is clickable.
- *
- * @param enabled {@code true} if the up button is enabled.
- */
- public void setUpEnabled(boolean enabled) {
- mUpButton.setEnabled(enabled);
- mUpButton.setAlpha(enabled ? 1f : BUTTON_DISABLED_ALPHA);
- }
-
- /**
- * Sets whether or not the down button on the scroll bar is clickable.
- *
- * @param enabled {@code true} if the down button is enabled.
- */
- public void setDownEnabled(boolean enabled) {
- mDownButton.setEnabled(enabled);
- mDownButton.setAlpha(enabled ? 1f : BUTTON_DISABLED_ALPHA);
- }
-
- /**
- * Returns whether or not the down button on the scroll bar is clickable.
- *
- * @return {@code true} if the down button is enabled. {@code false} otherwise.
- */
- public boolean isDownEnabled() {
- return mDownButton.isEnabled();
- }
-
- /** Reload the colors for the current {@link DayNightStyle}. */
- private void reloadColors() {
- int tint;
- int thumbBackground;
- int upDownBackgroundResId;
-
- switch (mDayNightStyle) {
- case DayNightStyle.AUTO:
- tint = ContextCompat.getColor(getContext(), R.color.car_tint);
- thumbBackground = ContextCompat.getColor(getContext(),
- R.color.car_scrollbar_thumb);
- upDownBackgroundResId = R.drawable.car_pagination_background;
- break;
- case DayNightStyle.AUTO_INVERSE:
- tint = ContextCompat.getColor(getContext(), R.color.car_tint_inverse);
- thumbBackground = ContextCompat.getColor(getContext(),
- R.color.car_scrollbar_thumb_inverse);
- upDownBackgroundResId = R.drawable.car_pagination_background_inverse;
- break;
- case DayNightStyle.FORCE_NIGHT:
- tint = ContextCompat.getColor(getContext(), R.color.car_tint_light);
- thumbBackground = ContextCompat.getColor(getContext(),
- R.color.car_scrollbar_thumb_light);
- upDownBackgroundResId = R.drawable.car_pagination_background_night;
- break;
- case DayNightStyle.FORCE_DAY:
- tint = ContextCompat.getColor(getContext(), R.color.car_tint_dark);
- thumbBackground = ContextCompat.getColor(getContext(),
- R.color.car_scrollbar_thumb_dark);
- upDownBackgroundResId = R.drawable.car_pagination_background_day;
- break;
- default:
- throw new IllegalArgumentException("Unknown DayNightStyle: " + mDayNightStyle);
- }
-
- mScrollThumb.setBackgroundColor(thumbBackground);
-
- mUpButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN);
- mUpButton.setBackgroundResource(upDownBackgroundResId);
-
- mDownButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN);
- mDownButton.setBackgroundResource(upDownBackgroundResId);
- }
-
- private void dispatchPageClick(View v) {
- final PaginationListener listener = mPaginationListener;
- if (listener == null) {
- return;
- }
-
- int direction = v.getId() == R.id.page_up
- ? PaginationListener.PAGE_UP
- : PaginationListener.PAGE_DOWN;
- listener.onPaginate(direction);
- }
-
- /** Moves the given view to the specified 'y' position. */
- private void moveY(final View view, float newPosition, boolean animate) {
- final int duration = animate ? 200 : 0;
- view.animate()
- .y(newPosition)
- .setDuration(duration)
- .setInterpolator(mPaginationInterpolator)
- .start();
- }
-}
diff --git a/android/support/checkapi/UpdateApiTask.java b/android/support/checkapi/UpdateApiTask.java
index de2db919..15e91040 100644
--- a/android/support/checkapi/UpdateApiTask.java
+++ b/android/support/checkapi/UpdateApiTask.java
@@ -29,7 +29,6 @@ import java.io.BufferedWriter;
import java.io.File;
import java.nio.charset.Charset;
import java.util.HashSet;
-import java.util.List;
import java.util.Set;
/**
@@ -120,11 +119,6 @@ public class UpdateApiTask extends DefaultTask {
}
if (mWhitelistErrorsFile != null && !mWhitelistErrors.isEmpty()) {
- if (mWhitelistErrorsFile.exists()) {
- List<String> lines =
- Files.readLines(mWhitelistErrorsFile, Charset.defaultCharset());
- mWhitelistErrors.removeAll(lines);
- }
try (BufferedWriter writer = Files.newWriter(
mWhitelistErrorsFile, Charset.defaultCharset())) {
for (String error : mWhitelistErrors) {
diff --git a/android/support/design/widget/CoordinatorLayout.java b/android/support/design/widget/CoordinatorLayout.java
index c45810ef..03cce024 100644
--- a/android/support/design/widget/CoordinatorLayout.java
+++ b/android/support/design/widget/CoordinatorLayout.java
@@ -400,6 +400,7 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
lp.resetTouchBehaviorTracking();
}
+ mBehaviorTouchView = null;
mDisallowInterceptReset = false;
}
diff --git a/android/support/graphics/drawable/VectorDrawableCompat.java b/android/support/graphics/drawable/VectorDrawableCompat.java
index a34fe2b8..943f1aa9 100644
--- a/android/support/graphics/drawable/VectorDrawableCompat.java
+++ b/android/support/graphics/drawable/VectorDrawableCompat.java
@@ -56,8 +56,8 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
+import java.util.ArrayDeque;
import java.util.ArrayList;
-import java.util.Stack;
/**
* For API 24 and above, this class is delegating to the framework's {@link VectorDrawable}.
@@ -730,7 +730,7 @@ public class VectorDrawableCompat extends VectorDrawableCommon {
// Use a stack to help to build the group tree.
// The top of the stack is always the current group.
- final Stack<VGroup> groupStack = new Stack<VGroup>();
+ final ArrayDeque<VGroup> groupStack = new ArrayDeque<>();
groupStack.push(pathRenderer.mRootGroup);
int eventType = parser.getEventType();
@@ -785,14 +785,7 @@ public class VectorDrawableCompat extends VectorDrawableCommon {
}
if (noPathTag) {
- final StringBuffer tag = new StringBuffer();
-
- if (tag.length() > 0) {
- tag.append(" or ");
- }
- tag.append(SHAPE_PATH);
-
- throw new XmlPullParserException("no " + tag + " defined");
+ throw new XmlPullParserException("no " + SHAPE_PATH + " defined");
}
}
diff --git a/android/support/media/ExifInterface.java b/android/support/media/ExifInterface.java
index 72b61cb7..eea69ab1 100644
--- a/android/support/media/ExifInterface.java
+++ b/android/support/media/ExifInterface.java
@@ -4678,9 +4678,7 @@ public class ExifInterface {
private int getMimeType(BufferedInputStream in) throws IOException {
in.mark(SIGNATURE_CHECK_SIZE);
byte[] signatureCheckBytes = new byte[SIGNATURE_CHECK_SIZE];
- if (in.read(signatureCheckBytes) != SIGNATURE_CHECK_SIZE) {
- throw new EOFException();
- }
+ in.read(signatureCheckBytes);
in.reset();
if (isJpegFormat(signatureCheckBytes)) {
return IMAGE_TYPE_JPEG;
@@ -5333,7 +5331,7 @@ public class ExifInterface {
int dataFormat = dataInputStream.readUnsignedShort();
int numberOfComponents = dataInputStream.readInt();
// Next four bytes is for data offset or value.
- long nextEntryOffset = dataInputStream.peek() + 4;
+ long nextEntryOffset = dataInputStream.peek() + 4L;
// Look up a corresponding tag from tag number
ExifTag tag = (ExifTag) sExifTagMapsForReading[ifdType].get(tagNumber);
diff --git a/android/support/media/ExifInterfaceTest.java b/android/support/media/ExifInterfaceTest.java
new file mode 100644
index 00000000..f811d1a7
--- /dev/null
+++ b/android/support/media/ExifInterfaceTest.java
@@ -0,0 +1,898 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.media;
+
+import static android.support.test.InstrumentationRegistry.getContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.location.Location;
+import android.os.Environment;
+import android.support.exifinterface.test.R;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+import android.util.Pair;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test {@link ExifInterface}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExifInterfaceTest {
+ private static final String TAG = ExifInterface.class.getSimpleName();
+ private static final boolean VERBOSE = false; // lots of logging
+ private static final double DIFFERENCE_TOLERANCE = .001;
+
+ private static final String EXIF_BYTE_ORDER_II_JPEG = "image_exif_byte_order_ii.jpg";
+ private static final String EXIF_BYTE_ORDER_MM_JPEG = "image_exif_byte_order_mm.jpg";
+ private static final String LG_G4_ISO_800_DNG = "lg_g4_iso_800.dng";
+ private static final int[] IMAGE_RESOURCES = new int[] {
+ R.raw.image_exif_byte_order_ii, R.raw.image_exif_byte_order_mm, R.raw.lg_g4_iso_800};
+ private static final String[] IMAGE_FILENAMES = new String[] {
+ EXIF_BYTE_ORDER_II_JPEG, EXIF_BYTE_ORDER_MM_JPEG, LG_G4_ISO_800_DNG};
+
+ private static final String TEST_TEMP_FILE_NAME = "testImage";
+ private static final double DELTA = 1e-8;
+ // We translate double to rational in a 1/10000 precision.
+ private static final double RATIONAL_DELTA = 0.0001;
+ private static final int TEST_LAT_LONG_VALUES_ARRAY_LENGTH = 8;
+ private static final int TEST_NUMBER_OF_CORRUPTED_IMAGE_STREAMS = 30;
+ private static final double[] TEST_LATITUDE_VALID_VALUES = new double[]
+ {0, 45, 90, -60, 0.00000001, -89.999999999, 14.2465923626, -68.3434534737};
+ private static final double[] TEST_LONGITUDE_VALID_VALUES = new double[]
+ {0, -45, 90, -120, 180, 0.00000001, -179.99999999999, -58.57834236352};
+ private static final double[] TEST_LATITUDE_INVALID_VALUES = new double[]
+ {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 90.0000000001,
+ 263.34763236326, -1e5, 347.32525, -176.346347754};
+ private static final double[] TEST_LONGITUDE_INVALID_VALUES = new double[]
+ {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 180.0000000001,
+ 263.34763236326, -1e10, 347.325252623, -4000.346323236};
+ private static final double[] TEST_ALTITUDE_VALUES = new double[]
+ {0, -2000, 10000, -355.99999999999, 18.02038};
+ private static final int[][] TEST_ROTATION_STATE_MACHINE = {
+ {ExifInterface.ORIENTATION_UNDEFINED, -90, ExifInterface.ORIENTATION_UNDEFINED},
+ {ExifInterface.ORIENTATION_UNDEFINED, 0, ExifInterface.ORIENTATION_UNDEFINED},
+ {ExifInterface.ORIENTATION_UNDEFINED, 90, ExifInterface.ORIENTATION_UNDEFINED},
+ {ExifInterface.ORIENTATION_UNDEFINED, 180, ExifInterface.ORIENTATION_UNDEFINED},
+ {ExifInterface.ORIENTATION_UNDEFINED, 270, ExifInterface.ORIENTATION_UNDEFINED},
+ {ExifInterface.ORIENTATION_UNDEFINED, 540, ExifInterface.ORIENTATION_UNDEFINED},
+ {ExifInterface.ORIENTATION_NORMAL, -90, ExifInterface.ORIENTATION_ROTATE_270},
+ {ExifInterface.ORIENTATION_NORMAL, 0, ExifInterface.ORIENTATION_NORMAL},
+ {ExifInterface.ORIENTATION_NORMAL, 90, ExifInterface.ORIENTATION_ROTATE_90},
+ {ExifInterface.ORIENTATION_NORMAL, 180, ExifInterface.ORIENTATION_ROTATE_180},
+ {ExifInterface.ORIENTATION_NORMAL, 270, ExifInterface.ORIENTATION_ROTATE_270},
+ {ExifInterface.ORIENTATION_NORMAL, 540, ExifInterface.ORIENTATION_ROTATE_180},
+ {ExifInterface.ORIENTATION_ROTATE_90, -90, ExifInterface.ORIENTATION_NORMAL},
+ {ExifInterface.ORIENTATION_ROTATE_90, 0, ExifInterface.ORIENTATION_ROTATE_90},
+ {ExifInterface.ORIENTATION_ROTATE_90, 90, ExifInterface.ORIENTATION_ROTATE_180},
+ {ExifInterface.ORIENTATION_ROTATE_90, 180 , ExifInterface.ORIENTATION_ROTATE_270},
+ {ExifInterface.ORIENTATION_ROTATE_90, 270, ExifInterface.ORIENTATION_NORMAL},
+ {ExifInterface.ORIENTATION_ROTATE_90, 540, ExifInterface.ORIENTATION_ROTATE_270},
+ {ExifInterface.ORIENTATION_ROTATE_180, -90, ExifInterface.ORIENTATION_ROTATE_90},
+ {ExifInterface.ORIENTATION_ROTATE_180, 0, ExifInterface.ORIENTATION_ROTATE_180},
+ {ExifInterface.ORIENTATION_ROTATE_180, 90, ExifInterface.ORIENTATION_ROTATE_270},
+ {ExifInterface.ORIENTATION_ROTATE_180, 180, ExifInterface.ORIENTATION_NORMAL},
+ {ExifInterface.ORIENTATION_ROTATE_180, 270, ExifInterface.ORIENTATION_ROTATE_90},
+ {ExifInterface.ORIENTATION_ROTATE_180, 540, ExifInterface.ORIENTATION_NORMAL},
+ {ExifInterface.ORIENTATION_ROTATE_270, -90, ExifInterface.ORIENTATION_ROTATE_180},
+ {ExifInterface.ORIENTATION_ROTATE_270, 0, ExifInterface.ORIENTATION_ROTATE_270},
+ {ExifInterface.ORIENTATION_ROTATE_270, 90, ExifInterface.ORIENTATION_NORMAL},
+ {ExifInterface.ORIENTATION_ROTATE_270, 180, ExifInterface.ORIENTATION_ROTATE_90},
+ {ExifInterface.ORIENTATION_ROTATE_270, 270, ExifInterface.ORIENTATION_ROTATE_180},
+ {ExifInterface.ORIENTATION_ROTATE_270, 540, ExifInterface.ORIENTATION_ROTATE_90},
+ {ExifInterface.ORIENTATION_FLIP_VERTICAL, -90, ExifInterface.ORIENTATION_TRANSVERSE},
+ {ExifInterface.ORIENTATION_FLIP_VERTICAL, 0, ExifInterface.ORIENTATION_FLIP_VERTICAL},
+ {ExifInterface.ORIENTATION_FLIP_VERTICAL, 90, ExifInterface.ORIENTATION_TRANSPOSE},
+ {ExifInterface.ORIENTATION_FLIP_VERTICAL, 180,
+ ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
+ {ExifInterface.ORIENTATION_FLIP_VERTICAL, 270, ExifInterface.ORIENTATION_TRANSVERSE},
+ {ExifInterface.ORIENTATION_FLIP_VERTICAL, 540,
+ ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
+ {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, -90, ExifInterface.ORIENTATION_TRANSPOSE},
+ {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, 0,
+ ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
+ {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, 90, ExifInterface.ORIENTATION_TRANSVERSE},
+ {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, 180,
+ ExifInterface.ORIENTATION_FLIP_VERTICAL},
+ {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, 270, ExifInterface.ORIENTATION_TRANSPOSE},
+ {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, 540,
+ ExifInterface.ORIENTATION_FLIP_VERTICAL},
+ {ExifInterface.ORIENTATION_TRANSPOSE, -90, ExifInterface.ORIENTATION_FLIP_VERTICAL},
+ {ExifInterface.ORIENTATION_TRANSPOSE, 0, ExifInterface.ORIENTATION_TRANSPOSE},
+ {ExifInterface.ORIENTATION_TRANSPOSE, 90, ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
+ {ExifInterface.ORIENTATION_TRANSPOSE, 180, ExifInterface.ORIENTATION_TRANSVERSE},
+ {ExifInterface.ORIENTATION_TRANSPOSE, 270, ExifInterface.ORIENTATION_FLIP_VERTICAL},
+ {ExifInterface.ORIENTATION_TRANSPOSE, 540, ExifInterface.ORIENTATION_TRANSVERSE},
+ {ExifInterface.ORIENTATION_TRANSVERSE, -90, ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
+ {ExifInterface.ORIENTATION_TRANSVERSE, 0, ExifInterface.ORIENTATION_TRANSVERSE},
+ {ExifInterface.ORIENTATION_TRANSVERSE, 90, ExifInterface.ORIENTATION_FLIP_VERTICAL},
+ {ExifInterface.ORIENTATION_TRANSVERSE, 180, ExifInterface.ORIENTATION_TRANSPOSE},
+ {ExifInterface.ORIENTATION_TRANSVERSE, 270, ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
+ {ExifInterface.ORIENTATION_TRANSVERSE, 540, ExifInterface.ORIENTATION_TRANSPOSE},
+ };
+ private static final int[][] TEST_FLIP_VERTICALLY_STATE_MACHINE = {
+ {ExifInterface.ORIENTATION_UNDEFINED, ExifInterface.ORIENTATION_UNDEFINED},
+ {ExifInterface.ORIENTATION_NORMAL, ExifInterface.ORIENTATION_FLIP_VERTICAL},
+ {ExifInterface.ORIENTATION_ROTATE_90, ExifInterface.ORIENTATION_TRANSVERSE},
+ {ExifInterface.ORIENTATION_ROTATE_180, ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
+ {ExifInterface.ORIENTATION_ROTATE_270, ExifInterface.ORIENTATION_TRANSPOSE},
+ {ExifInterface.ORIENTATION_FLIP_VERTICAL, ExifInterface.ORIENTATION_NORMAL},
+ {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, ExifInterface.ORIENTATION_ROTATE_180},
+ {ExifInterface.ORIENTATION_TRANSPOSE, ExifInterface.ORIENTATION_ROTATE_270},
+ {ExifInterface.ORIENTATION_TRANSVERSE, ExifInterface.ORIENTATION_ROTATE_90}
+ };
+ private static final int[][] TEST_FLIP_HORIZONTALLY_STATE_MACHINE = {
+ {ExifInterface.ORIENTATION_UNDEFINED, ExifInterface.ORIENTATION_UNDEFINED},
+ {ExifInterface.ORIENTATION_NORMAL, ExifInterface.ORIENTATION_FLIP_HORIZONTAL},
+ {ExifInterface.ORIENTATION_ROTATE_90, ExifInterface.ORIENTATION_TRANSPOSE},
+ {ExifInterface.ORIENTATION_ROTATE_180, ExifInterface.ORIENTATION_FLIP_VERTICAL},
+ {ExifInterface.ORIENTATION_ROTATE_270, ExifInterface.ORIENTATION_TRANSVERSE},
+ {ExifInterface.ORIENTATION_FLIP_VERTICAL, ExifInterface.ORIENTATION_ROTATE_180},
+ {ExifInterface.ORIENTATION_FLIP_HORIZONTAL, ExifInterface.ORIENTATION_NORMAL},
+ {ExifInterface.ORIENTATION_TRANSPOSE, ExifInterface.ORIENTATION_ROTATE_90},
+ {ExifInterface.ORIENTATION_TRANSVERSE, ExifInterface.ORIENTATION_ROTATE_270}
+ };
+ private static final HashMap<Integer, Pair> FLIP_STATE_AND_ROTATION_DEGREES = new HashMap<>();
+ static {
+ FLIP_STATE_AND_ROTATION_DEGREES.put(
+ ExifInterface.ORIENTATION_UNDEFINED, new Pair(false, 0));
+ FLIP_STATE_AND_ROTATION_DEGREES.put(
+ ExifInterface.ORIENTATION_NORMAL, new Pair(false, 0));
+ FLIP_STATE_AND_ROTATION_DEGREES.put(
+ ExifInterface.ORIENTATION_ROTATE_90, new Pair(false, 90));
+ FLIP_STATE_AND_ROTATION_DEGREES.put(
+ ExifInterface.ORIENTATION_ROTATE_180, new Pair(false, 180));
+ FLIP_STATE_AND_ROTATION_DEGREES.put(
+ ExifInterface.ORIENTATION_ROTATE_270, new Pair(false, 270));
+ FLIP_STATE_AND_ROTATION_DEGREES.put(
+ ExifInterface.ORIENTATION_FLIP_HORIZONTAL, new Pair(true, 0));
+ FLIP_STATE_AND_ROTATION_DEGREES.put(
+ ExifInterface.ORIENTATION_TRANSVERSE, new Pair(true, 90));
+ FLIP_STATE_AND_ROTATION_DEGREES.put(
+ ExifInterface.ORIENTATION_FLIP_VERTICAL, new Pair(true, 180));
+ FLIP_STATE_AND_ROTATION_DEGREES.put(
+ ExifInterface.ORIENTATION_TRANSPOSE, new Pair(true, 270));
+ }
+
+ private static final String[] EXIF_TAGS = {
+ ExifInterface.TAG_MAKE,
+ ExifInterface.TAG_MODEL,
+ ExifInterface.TAG_F_NUMBER,
+ ExifInterface.TAG_DATETIME_ORIGINAL,
+ ExifInterface.TAG_EXPOSURE_TIME,
+ ExifInterface.TAG_FLASH,
+ ExifInterface.TAG_FOCAL_LENGTH,
+ ExifInterface.TAG_GPS_ALTITUDE,
+ ExifInterface.TAG_GPS_ALTITUDE_REF,
+ ExifInterface.TAG_GPS_DATESTAMP,
+ ExifInterface.TAG_GPS_LATITUDE,
+ ExifInterface.TAG_GPS_LATITUDE_REF,
+ ExifInterface.TAG_GPS_LONGITUDE,
+ ExifInterface.TAG_GPS_LONGITUDE_REF,
+ ExifInterface.TAG_GPS_PROCESSING_METHOD,
+ ExifInterface.TAG_GPS_TIMESTAMP,
+ ExifInterface.TAG_IMAGE_LENGTH,
+ ExifInterface.TAG_IMAGE_WIDTH,
+ ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
+ ExifInterface.TAG_ORIENTATION,
+ ExifInterface.TAG_WHITE_BALANCE
+ };
+
+ private static class ExpectedValue {
+ // Thumbnail information.
+ public final boolean hasThumbnail;
+ public final int thumbnailWidth;
+ public final int thumbnailHeight;
+
+ // GPS information.
+ public final boolean hasLatLong;
+ public final float latitude;
+ public final float longitude;
+ public final float altitude;
+
+ // Values.
+ public final String make;
+ public final String model;
+ public final float aperture;
+ public final String dateTimeOriginal;
+ public final float exposureTime;
+ public final float flash;
+ public final String focalLength;
+ public final String gpsAltitude;
+ public final String gpsAltitudeRef;
+ public final String gpsDatestamp;
+ public final String gpsLatitude;
+ public final String gpsLatitudeRef;
+ public final String gpsLongitude;
+ public final String gpsLongitudeRef;
+ public final String gpsProcessingMethod;
+ public final String gpsTimestamp;
+ public final int imageLength;
+ public final int imageWidth;
+ public final String iso;
+ public final int orientation;
+ public final int whiteBalance;
+
+ private static String getString(TypedArray typedArray, int index) {
+ String stringValue = typedArray.getString(index);
+ if (stringValue == null || stringValue.equals("")) {
+ return null;
+ }
+ return stringValue.trim();
+ }
+
+ ExpectedValue(TypedArray typedArray) {
+ // Reads thumbnail information.
+ hasThumbnail = typedArray.getBoolean(0, false);
+ thumbnailWidth = typedArray.getInt(1, 0);
+ thumbnailHeight = typedArray.getInt(2, 0);
+
+ // Reads GPS information.
+ hasLatLong = typedArray.getBoolean(3, false);
+ latitude = typedArray.getFloat(4, 0f);
+ longitude = typedArray.getFloat(5, 0f);
+ altitude = typedArray.getFloat(6, 0f);
+
+ // Reads values.
+ make = getString(typedArray, 7);
+ model = getString(typedArray, 8);
+ aperture = typedArray.getFloat(9, 0f);
+ dateTimeOriginal = getString(typedArray, 10);
+ exposureTime = typedArray.getFloat(11, 0f);
+ flash = typedArray.getFloat(12, 0f);
+ focalLength = getString(typedArray, 13);
+ gpsAltitude = getString(typedArray, 14);
+ gpsAltitudeRef = getString(typedArray, 15);
+ gpsDatestamp = getString(typedArray, 16);
+ gpsLatitude = getString(typedArray, 17);
+ gpsLatitudeRef = getString(typedArray, 18);
+ gpsLongitude = getString(typedArray, 19);
+ gpsLongitudeRef = getString(typedArray, 20);
+ gpsProcessingMethod = getString(typedArray, 21);
+ gpsTimestamp = getString(typedArray, 22);
+ imageLength = typedArray.getInt(23, 0);
+ imageWidth = typedArray.getInt(24, 0);
+ iso = getString(typedArray, 25);
+ orientation = typedArray.getInt(26, 0);
+ whiteBalance = typedArray.getInt(27, 0);
+
+ typedArray.recycle();
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+ String outputPath =
+ new File(Environment.getExternalStorageDirectory(), IMAGE_FILENAMES[i])
+ .getAbsolutePath();
+
+ InputStream inputStream = null;
+ FileOutputStream outputStream = null;
+ try {
+ inputStream = getContext().getResources().openRawResource(IMAGE_RESOURCES[i]);
+ outputStream = new FileOutputStream(outputPath);
+ copy(inputStream, outputStream);
+ } finally {
+ closeQuietly(inputStream);
+ closeQuietly(outputStream);
+ }
+ }
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+ String imageFilePath =
+ new File(Environment.getExternalStorageDirectory(), IMAGE_FILENAMES[i])
+ .getAbsolutePath();
+ File imageFile = new File(imageFilePath);
+ if (imageFile.exists()) {
+ imageFile.delete();
+ }
+ }
+ }
+
+ @Test
+ @LargeTest
+ public void testReadExifDataFromExifByteOrderIIJpeg() throws Throwable {
+ testExifInterfaceForJpeg(EXIF_BYTE_ORDER_II_JPEG, R.array.exifbyteorderii_jpg);
+ }
+
+ @Test
+ @LargeTest
+ public void testReadExifDataFromExifByteOrderMMJpeg() throws Throwable {
+ testExifInterfaceForJpeg(EXIF_BYTE_ORDER_MM_JPEG, R.array.exifbyteordermm_jpg);
+ }
+
+ @Test
+ @LargeTest
+ public void testReadExifDataFromLgG4Iso800Dng() throws Throwable {
+ testExifInterfaceForRaw(LG_G4_ISO_800_DNG, R.array.lg_g4_iso_800_dng);
+ }
+
+ @Test
+ @LargeTest
+ public void testDoNotFailOnCorruptedImage() throws Throwable {
+ // ExifInterface shouldn't raise any exceptions except an IOException when unable to open
+ // a file, even with a corrupted image. Generates randomly corrupted image stream for
+ // testing. Uses Epoch date count as random seed so that we can reproduce a broken test.
+ long seed = System.currentTimeMillis() / (86400 * 1000);
+ Log.d(TAG, "testDoNotFailOnCorruptedImage random seed: " + seed);
+ Random random = new Random(seed);
+ byte[] bytes = new byte[8096];
+ ByteBuffer buffer = ByteBuffer.wrap(bytes);
+ for (int i = 0; i < TEST_NUMBER_OF_CORRUPTED_IMAGE_STREAMS; i++) {
+ buffer.clear();
+ random.nextBytes(bytes);
+ if (!randomlyCorrupted(random)) {
+ buffer.put(ExifInterface.JPEG_SIGNATURE);
+ }
+ if (!randomlyCorrupted(random)) {
+ buffer.put(ExifInterface.MARKER_APP1);
+ }
+ buffer.putShort((short) (random.nextInt(100) + 300));
+ if (!randomlyCorrupted(random)) {
+ buffer.put(ExifInterface.IDENTIFIER_EXIF_APP1);
+ }
+ if (!randomlyCorrupted(random)) {
+ buffer.putShort(ExifInterface.BYTE_ALIGN_MM);
+ }
+ if (!randomlyCorrupted(random)) {
+ buffer.put((byte) 0);
+ buffer.put(ExifInterface.START_CODE);
+ }
+ buffer.putInt(8);
+
+ // Primary Tags
+ int numberOfDirectory = random.nextInt(8) + 1;
+ if (!randomlyCorrupted(random)) {
+ buffer.putShort((short) numberOfDirectory);
+ }
+ for (int j = 0; j < numberOfDirectory; j++) {
+ generateRandomExifTag(buffer, ExifInterface.IFD_TYPE_PRIMARY, random);
+ }
+ if (!randomlyCorrupted(random)) {
+ buffer.putInt(buffer.position() - 8);
+ }
+
+ // Thumbnail Tags
+ numberOfDirectory = random.nextInt(8) + 1;
+ if (!randomlyCorrupted(random)) {
+ buffer.putShort((short) numberOfDirectory);
+ }
+ for (int j = 0; j < numberOfDirectory; j++) {
+ generateRandomExifTag(buffer, ExifInterface.IFD_TYPE_THUMBNAIL, random);
+ }
+ if (!randomlyCorrupted(random)) {
+ buffer.putInt(buffer.position() - 8);
+ }
+
+ // Preview Tags
+ numberOfDirectory = random.nextInt(8) + 1;
+ if (!randomlyCorrupted(random)) {
+ buffer.putShort((short) numberOfDirectory);
+ }
+ for (int j = 0; j < numberOfDirectory; j++) {
+ generateRandomExifTag(buffer, ExifInterface.IFD_TYPE_PREVIEW, random);
+ }
+ if (!randomlyCorrupted(random)) {
+ buffer.putInt(buffer.position() - 8);
+ }
+
+ if (!randomlyCorrupted(random)) {
+ buffer.put(ExifInterface.MARKER);
+ }
+ if (!randomlyCorrupted(random)) {
+ buffer.put(ExifInterface.MARKER_EOI);
+ }
+
+ try {
+ new ExifInterface(new ByteArrayInputStream(bytes));
+ // Always success
+ } catch (IOException e) {
+ fail("Should not reach here!");
+ }
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testSetGpsInfo() throws IOException {
+ final String provider = "ExifInterfaceTest";
+ final long timestamp = System.currentTimeMillis();
+ final float speedInMeterPerSec = 36.627533f;
+ Location location = new Location(provider);
+ location.setLatitude(TEST_LATITUDE_VALID_VALUES[TEST_LATITUDE_VALID_VALUES.length - 1]);
+ location.setLongitude(TEST_LONGITUDE_VALID_VALUES[TEST_LONGITUDE_VALID_VALUES.length - 1]);
+ location.setAltitude(TEST_ALTITUDE_VALUES[TEST_ALTITUDE_VALUES.length - 1]);
+ location.setSpeed(speedInMeterPerSec);
+ location.setTime(timestamp);
+ ExifInterface exif = createTestExifInterface();
+ exif.setGpsInfo(location);
+
+ double[] latLong = exif.getLatLong();
+ assertNotNull(latLong);
+ assertEquals(TEST_LATITUDE_VALID_VALUES[TEST_LATITUDE_VALID_VALUES.length - 1],
+ latLong[0], DELTA);
+ assertEquals(TEST_LONGITUDE_VALID_VALUES[TEST_LONGITUDE_VALID_VALUES.length - 1],
+ latLong[1], DELTA);
+ assertEquals(TEST_ALTITUDE_VALUES[TEST_ALTITUDE_VALUES.length - 1], exif.getAltitude(0),
+ RATIONAL_DELTA);
+ assertEquals("K", exif.getAttribute(ExifInterface.TAG_GPS_SPEED_REF));
+ assertEquals(speedInMeterPerSec, exif.getAttributeDouble(ExifInterface.TAG_GPS_SPEED, 0.0)
+ * 1000 / TimeUnit.HOURS.toSeconds(1), RATIONAL_DELTA);
+ assertEquals(provider, exif.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD));
+ // GPS time's precision is secs.
+ assertEquals(TimeUnit.MILLISECONDS.toSeconds(timestamp),
+ TimeUnit.MILLISECONDS.toSeconds(exif.getGpsDateTime()));
+ }
+
+ @Test
+ @SmallTest
+ public void testSetLatLong_withValidValues() throws IOException {
+ for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) {
+ ExifInterface exif = createTestExifInterface();
+ exif.setLatLong(TEST_LATITUDE_VALID_VALUES[i], TEST_LONGITUDE_VALID_VALUES[i]);
+
+ double[] latLong = exif.getLatLong();
+ assertNotNull(latLong);
+ assertEquals(TEST_LATITUDE_VALID_VALUES[i], latLong[0], DELTA);
+ assertEquals(TEST_LONGITUDE_VALID_VALUES[i], latLong[1], DELTA);
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testSetLatLong_withInvalidLatitude() throws IOException {
+ for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) {
+ ExifInterface exif = createTestExifInterface();
+ try {
+ exif.setLatLong(TEST_LATITUDE_INVALID_VALUES[i], TEST_LONGITUDE_VALID_VALUES[i]);
+ fail();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ assertNull(exif.getLatLong());
+ assertLatLongValuesAreNotSet(exif);
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testSetLatLong_withInvalidLongitude() throws IOException {
+ for (int i = 0; i < TEST_LAT_LONG_VALUES_ARRAY_LENGTH; i++) {
+ ExifInterface exif = createTestExifInterface();
+ try {
+ exif.setLatLong(TEST_LATITUDE_VALID_VALUES[i], TEST_LONGITUDE_INVALID_VALUES[i]);
+ fail();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ assertNull(exif.getLatLong());
+ assertLatLongValuesAreNotSet(exif);
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testSetAltitude() throws IOException {
+ for (int i = 0; i < TEST_ALTITUDE_VALUES.length; i++) {
+ ExifInterface exif = createTestExifInterface();
+ exif.setAltitude(TEST_ALTITUDE_VALUES[i]);
+ assertEquals(TEST_ALTITUDE_VALUES[i], exif.getAltitude(Double.NaN), RATIONAL_DELTA);
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testSetDateTime() throws IOException {
+ final String dateTimeValue = "2017:02:02 22:22:22";
+ final String dateTimeOriginalValue = "2017:01:01 11:11:11";
+
+ File imageFile = new File(
+ Environment.getExternalStorageDirectory(), EXIF_BYTE_ORDER_II_JPEG);
+ ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+ exif.setAttribute(ExifInterface.TAG_DATETIME, dateTimeValue);
+ exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateTimeOriginalValue);
+ exif.saveAttributes();
+
+ // Check that the DATETIME value is not overwritten by DATETIME_ORIGINAL's value.
+ exif = new ExifInterface(imageFile.getAbsolutePath());
+ assertEquals(dateTimeValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
+ assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
+
+ // Now remove the DATETIME value.
+ exif.setAttribute(ExifInterface.TAG_DATETIME, null);
+ exif.saveAttributes();
+
+ // When the DATETIME has no value, then it should be set to DATETIME_ORIGINAL's value.
+ exif = new ExifInterface(imageFile.getAbsolutePath());
+ assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
+
+ long currentTimeStamp = System.currentTimeMillis();
+ exif.setDateTime(currentTimeStamp);
+ exif.saveAttributes();
+ exif = new ExifInterface(imageFile.getAbsolutePath());
+ assertEquals(currentTimeStamp, exif.getDateTime());
+ }
+
+ @Test
+ @SmallTest
+ public void testRotation() throws IOException {
+ File imageFile = new File(
+ Environment.getExternalStorageDirectory(), EXIF_BYTE_ORDER_II_JPEG);
+ ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+
+ int num;
+ // Test flip vertically.
+ for (num = 0; num < TEST_FLIP_VERTICALLY_STATE_MACHINE.length; num++) {
+ exif.setAttribute(ExifInterface.TAG_ORIENTATION,
+ Integer.toString(TEST_FLIP_VERTICALLY_STATE_MACHINE[num][0]));
+ exif.flipVertically();
+ exif.saveAttributes();
+ exif = new ExifInterface(imageFile.getAbsolutePath());
+ assertIntTag(exif, ExifInterface.TAG_ORIENTATION,
+ TEST_FLIP_VERTICALLY_STATE_MACHINE[num][1]);
+
+ }
+
+ // Test flip horizontally.
+ for (num = 0; num < TEST_FLIP_VERTICALLY_STATE_MACHINE.length; num++) {
+ exif.setAttribute(ExifInterface.TAG_ORIENTATION,
+ Integer.toString(TEST_FLIP_HORIZONTALLY_STATE_MACHINE[num][0]));
+ exif.flipHorizontally();
+ exif.saveAttributes();
+ exif = new ExifInterface(imageFile.getAbsolutePath());
+ assertIntTag(exif, ExifInterface.TAG_ORIENTATION,
+ TEST_FLIP_HORIZONTALLY_STATE_MACHINE[num][1]);
+
+ }
+
+ // Test rotate by degrees
+ exif.setAttribute(ExifInterface.TAG_ORIENTATION,
+ Integer.toString(ExifInterface.ORIENTATION_NORMAL));
+ try {
+ exif.rotate(108);
+ fail("Rotate with 108 degree should throw IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // Success
+ }
+
+ for (num = 0; num < TEST_ROTATION_STATE_MACHINE.length; num++) {
+ exif.setAttribute(ExifInterface.TAG_ORIENTATION,
+ Integer.toString(TEST_ROTATION_STATE_MACHINE[num][0]));
+ exif.rotate(TEST_ROTATION_STATE_MACHINE[num][1]);
+ exif.saveAttributes();
+ exif = new ExifInterface(imageFile.getAbsolutePath());
+ assertIntTag(exif, ExifInterface.TAG_ORIENTATION, TEST_ROTATION_STATE_MACHINE[num][2]);
+ }
+
+ // Test get flip state and rotation degrees.
+ for (Integer key : FLIP_STATE_AND_ROTATION_DEGREES.keySet()) {
+ exif.setAttribute(ExifInterface.TAG_ORIENTATION, key.toString());
+ exif.saveAttributes();
+ exif = new ExifInterface(imageFile.getAbsolutePath());
+ assertEquals(FLIP_STATE_AND_ROTATION_DEGREES.get(key).first, exif.isFlipped());
+ assertEquals(FLIP_STATE_AND_ROTATION_DEGREES.get(key).second,
+ exif.getRotationDegrees());
+ }
+
+ // Test reset the rotation.
+ exif.setAttribute(ExifInterface.TAG_ORIENTATION,
+ Integer.toString(ExifInterface.ORIENTATION_FLIP_HORIZONTAL));
+ exif.resetOrientation();
+ exif.saveAttributes();
+ exif = new ExifInterface(imageFile.getAbsolutePath());
+ assertIntTag(exif, ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
+
+ }
+
+ @Test
+ @SmallTest
+ public void testInterchangeabilityBetweenTwoIsoSpeedTags() throws IOException {
+ // Tests that two tags TAG_ISO_SPEED_RATINGS and TAG_PHOTOGRAPHIC_SENSITIVITY can be used
+ // interchangeably.
+ final String oldTag = ExifInterface.TAG_ISO_SPEED_RATINGS;
+ final String newTag = ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY;
+ final String isoValue = "50";
+
+ ExifInterface exif = createTestExifInterface();
+ exif.setAttribute(oldTag, isoValue);
+ assertEquals(isoValue, exif.getAttribute(oldTag));
+ assertEquals(isoValue, exif.getAttribute(newTag));
+
+ exif = createTestExifInterface();
+ exif.setAttribute(newTag, isoValue);
+ assertEquals(isoValue, exif.getAttribute(oldTag));
+ assertEquals(isoValue, exif.getAttribute(newTag));
+ }
+
+ private void printExifTagsAndValues(String fileName, ExifInterface exifInterface) {
+ // Prints thumbnail information.
+ if (exifInterface.hasThumbnail()) {
+ byte[] thumbnailBytes = exifInterface.getThumbnailBytes();
+ if (thumbnailBytes != null) {
+ Log.v(TAG, fileName + " Thumbnail size = " + thumbnailBytes.length);
+ Bitmap bitmap = exifInterface.getThumbnailBitmap();
+ if (bitmap == null) {
+ Log.e(TAG, fileName + " Corrupted thumbnail!");
+ } else {
+ Log.v(TAG, fileName + " Thumbnail size: " + bitmap.getWidth() + ", "
+ + bitmap.getHeight());
+ }
+ } else {
+ Log.e(TAG, fileName + " Unexpected result: No thumbnails were found. "
+ + "A thumbnail is expected.");
+ }
+ } else {
+ if (exifInterface.getThumbnailBytes() != null) {
+ Log.e(TAG, fileName + " Unexpected result: A thumbnail was found. "
+ + "No thumbnail is expected.");
+ } else {
+ Log.v(TAG, fileName + " No thumbnail");
+ }
+ }
+
+ // Prints GPS information.
+ Log.v(TAG, fileName + " Altitude = " + exifInterface.getAltitude(.0));
+
+ double[] latLong = exifInterface.getLatLong();
+ if (latLong != null) {
+ Log.v(TAG, fileName + " Latitude = " + latLong[0]);
+ Log.v(TAG, fileName + " Longitude = " + latLong[1]);
+ } else {
+ Log.v(TAG, fileName + " No latlong data");
+ }
+
+ // Prints values.
+ for (String tagKey : EXIF_TAGS) {
+ String tagValue = exifInterface.getAttribute(tagKey);
+ Log.v(TAG, fileName + " Key{" + tagKey + "} = '" + tagValue + "'");
+ }
+ }
+
+ private void assertIntTag(ExifInterface exifInterface, String tag, int expectedValue) {
+ int intValue = exifInterface.getAttributeInt(tag, 0);
+ assertEquals(expectedValue, intValue);
+ }
+
+ private void assertFloatTag(ExifInterface exifInterface, String tag, float expectedValue) {
+ double doubleValue = exifInterface.getAttributeDouble(tag, 0.0);
+ assertEquals(expectedValue, doubleValue, DIFFERENCE_TOLERANCE);
+ }
+
+ private void assertStringTag(ExifInterface exifInterface, String tag, String expectedValue) {
+ String stringValue = exifInterface.getAttribute(tag);
+ if (stringValue != null) {
+ stringValue = stringValue.trim();
+ }
+ stringValue = ("".equals(stringValue)) ? null : stringValue;
+
+ assertEquals(expectedValue, stringValue);
+ }
+
+ private void compareWithExpectedValue(ExifInterface exifInterface,
+ ExpectedValue expectedValue, String verboseTag) {
+ if (VERBOSE) {
+ printExifTagsAndValues(verboseTag, exifInterface);
+ }
+ // Checks a thumbnail image.
+ assertEquals(expectedValue.hasThumbnail, exifInterface.hasThumbnail());
+ if (expectedValue.hasThumbnail) {
+ byte[] thumbnailBytes = exifInterface.getThumbnailBytes();
+ assertNotNull(thumbnailBytes);
+ Bitmap thumbnailBitmap = exifInterface.getThumbnailBitmap();
+ assertNotNull(thumbnailBitmap);
+ assertEquals(expectedValue.thumbnailWidth, thumbnailBitmap.getWidth());
+ assertEquals(expectedValue.thumbnailHeight, thumbnailBitmap.getHeight());
+ } else {
+ assertNull(exifInterface.getThumbnail());
+ }
+
+ // Checks GPS information.
+ double[] latLong = exifInterface.getLatLong();
+ assertEquals(expectedValue.hasLatLong, latLong != null);
+ if (expectedValue.hasLatLong) {
+ assertEquals(expectedValue.latitude, latLong[0], DIFFERENCE_TOLERANCE);
+ assertEquals(expectedValue.longitude, latLong[1], DIFFERENCE_TOLERANCE);
+ }
+ assertEquals(expectedValue.altitude, exifInterface.getAltitude(.0), DIFFERENCE_TOLERANCE);
+
+ // Checks values.
+ assertStringTag(exifInterface, ExifInterface.TAG_MAKE, expectedValue.make);
+ assertStringTag(exifInterface, ExifInterface.TAG_MODEL, expectedValue.model);
+ assertFloatTag(exifInterface, ExifInterface.TAG_F_NUMBER, expectedValue.aperture);
+ assertStringTag(exifInterface, ExifInterface.TAG_DATETIME_ORIGINAL,
+ expectedValue.dateTimeOriginal);
+ assertFloatTag(exifInterface, ExifInterface.TAG_EXPOSURE_TIME, expectedValue.exposureTime);
+ assertFloatTag(exifInterface, ExifInterface.TAG_FLASH, expectedValue.flash);
+ assertStringTag(exifInterface, ExifInterface.TAG_FOCAL_LENGTH, expectedValue.focalLength);
+ assertStringTag(exifInterface, ExifInterface.TAG_GPS_ALTITUDE, expectedValue.gpsAltitude);
+ assertStringTag(exifInterface, ExifInterface.TAG_GPS_ALTITUDE_REF,
+ expectedValue.gpsAltitudeRef);
+ assertStringTag(exifInterface, ExifInterface.TAG_GPS_DATESTAMP, expectedValue.gpsDatestamp);
+ assertStringTag(exifInterface, ExifInterface.TAG_GPS_LATITUDE, expectedValue.gpsLatitude);
+ assertStringTag(exifInterface, ExifInterface.TAG_GPS_LATITUDE_REF,
+ expectedValue.gpsLatitudeRef);
+ assertStringTag(exifInterface, ExifInterface.TAG_GPS_LONGITUDE, expectedValue.gpsLongitude);
+ assertStringTag(exifInterface, ExifInterface.TAG_GPS_LONGITUDE_REF,
+ expectedValue.gpsLongitudeRef);
+ assertStringTag(exifInterface, ExifInterface.TAG_GPS_PROCESSING_METHOD,
+ expectedValue.gpsProcessingMethod);
+ assertStringTag(exifInterface, ExifInterface.TAG_GPS_TIMESTAMP, expectedValue.gpsTimestamp);
+ assertIntTag(exifInterface, ExifInterface.TAG_IMAGE_LENGTH, expectedValue.imageLength);
+ assertIntTag(exifInterface, ExifInterface.TAG_IMAGE_WIDTH, expectedValue.imageWidth);
+ assertStringTag(exifInterface, ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
+ expectedValue.iso);
+ assertIntTag(exifInterface, ExifInterface.TAG_ORIENTATION, expectedValue.orientation);
+ assertIntTag(exifInterface, ExifInterface.TAG_WHITE_BALANCE, expectedValue.whiteBalance);
+ }
+
+ private void testExifInterfaceCommon(String fileName, ExpectedValue expectedValue)
+ throws IOException {
+ File imageFile = new File(Environment.getExternalStorageDirectory(), fileName);
+ String verboseTag = imageFile.getName();
+
+ // Creates via path.
+ ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ assertNotNull(exifInterface);
+ compareWithExpectedValue(exifInterface, expectedValue, verboseTag);
+
+ InputStream in = null;
+ // Creates via InputStream.
+ try {
+ in = new BufferedInputStream(new FileInputStream(imageFile.getAbsolutePath()));
+ exifInterface = new ExifInterface(in);
+ compareWithExpectedValue(exifInterface, expectedValue, verboseTag);
+ } finally {
+ closeQuietly(in);
+ }
+ }
+
+ private void testSaveAttributes_withFileName(String fileName, ExpectedValue expectedValue)
+ throws IOException {
+ File imageFile = new File(Environment.getExternalStorageDirectory(), fileName);
+ String verboseTag = imageFile.getName();
+
+ ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ compareWithExpectedValue(exifInterface, expectedValue, verboseTag);
+
+ // Test for modifying one attribute.
+ String backupValue = exifInterface.getAttribute(ExifInterface.TAG_MAKE);
+ exifInterface.setAttribute(ExifInterface.TAG_MAKE, "abc");
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ assertEquals("abc", exifInterface.getAttribute(ExifInterface.TAG_MAKE));
+ // Restore the backup value.
+ exifInterface.setAttribute(ExifInterface.TAG_MAKE, backupValue);
+ exifInterface.saveAttributes();
+ exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+ compareWithExpectedValue(exifInterface, expectedValue, verboseTag);
+ }
+
+ private void testExifInterfaceForJpeg(String fileName, int typedArrayResourceId)
+ throws IOException {
+ ExpectedValue expectedValue = new ExpectedValue(
+ getContext().getResources().obtainTypedArray(typedArrayResourceId));
+
+ // Test for reading from external data storage.
+ testExifInterfaceCommon(fileName, expectedValue);
+
+ // Test for saving attributes.
+ testSaveAttributes_withFileName(fileName, expectedValue);
+ }
+
+ private void testExifInterfaceForRaw(String fileName, int typedArrayResourceId)
+ throws IOException {
+ ExpectedValue expectedValue = new ExpectedValue(
+ getContext().getResources().obtainTypedArray(typedArrayResourceId));
+
+ // Test for reading from external data storage.
+ testExifInterfaceCommon(fileName, expectedValue);
+
+ // Since ExifInterface does not support for saving attributes for RAW files, do not test
+ // about writing back in here.
+ }
+
+ private void generateRandomExifTag(ByteBuffer buffer, int ifdType, Random random) {
+ ExifInterface.ExifTag[] tagGroup = ExifInterface.EXIF_TAGS[ifdType];
+ ExifInterface.ExifTag tag = tagGroup[random.nextInt(tagGroup.length)];
+ if (!randomlyCorrupted(random)) {
+ buffer.putShort((short) tag.number);
+ }
+ int dataFormat = random.nextInt(ExifInterface.IFD_FORMAT_NAMES.length);
+ if (!randomlyCorrupted(random)) {
+ buffer.putShort((short) dataFormat);
+ }
+ buffer.putInt(1);
+ int dataLength = ExifInterface.IFD_FORMAT_BYTES_PER_FORMAT[dataFormat];
+ if (dataLength > 4) {
+ buffer.putShort((short) random.nextInt(8096 - dataLength));
+ buffer.position(buffer.position() + 2);
+ } else {
+ buffer.position(buffer.position() + 4);
+ }
+ }
+
+ private boolean randomlyCorrupted(Random random) {
+ // Corrupts somewhere in a possibility of 1/500.
+ return random.nextInt(500) == 0;
+ }
+
+ private void closeQuietly(Closeable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (RuntimeException rethrown) {
+ throw rethrown;
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ private int copy(InputStream in, OutputStream out) throws IOException {
+ int total = 0;
+ byte[] buffer = new byte[8192];
+ int c;
+ while ((c = in.read(buffer)) != -1) {
+ total += c;
+ out.write(buffer, 0, c);
+ }
+ return total;
+ }
+
+ private void assertLatLongValuesAreNotSet(ExifInterface exif) {
+ assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE));
+ assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF));
+ assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE));
+ assertNull(exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF));
+ }
+
+ private ExifInterface createTestExifInterface() throws IOException {
+ File image = File.createTempFile(TEST_TEMP_FILE_NAME, ".jpg");
+ image.deleteOnExit();
+ return new ExifInterface(image.getAbsolutePath());
+ }
+}
diff --git a/android/support/media/tv/BasePreviewProgram.java b/android/support/media/tv/BasePreviewProgram.java
index 39c30140..eeaa5ea1 100644
--- a/android/support/media/tv/BasePreviewProgram.java
+++ b/android/support/media/tv/BasePreviewProgram.java
@@ -17,7 +17,6 @@ package android.support.media.tv;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import android.annotation.TargetApi;
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
@@ -40,7 +39,6 @@ import java.util.TimeZone;
*
* @hide
*/
-@TargetApi(26)
public abstract class BasePreviewProgram extends BaseProgram {
/**
* @hide
diff --git a/android/support/media/tv/Channel.java b/android/support/media/tv/Channel.java
index 9b13e422..a24d948f 100644
--- a/android/support/media/tv/Channel.java
+++ b/android/support/media/tv/Channel.java
@@ -17,7 +17,6 @@ package android.support.media.tv;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import android.annotation.TargetApi;
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
@@ -76,7 +75,6 @@ import java.nio.charset.Charset;
* TvContractCompat.buildChannelUri(existingChannel.getId()), null, null);
* </pre>
*/
-@TargetApi(21)
public final class Channel {
/**
* @hide
diff --git a/android/support/media/tv/ChannelLogoUtilsTest.java b/android/support/media/tv/ChannelLogoUtilsTest.java
new file mode 100644
index 00000000..ea315ab3
--- /dev/null
+++ b/android/support/media/tv/ChannelLogoUtilsTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.media.tv;
+
+import static android.support.test.InstrumentationRegistry.getContext;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.support.media.tv.test.R;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.Suppress;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@Suppress // Test is failing b/70905391
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ChannelLogoUtilsTest {
+ private static final String FAKE_INPUT_ID = "ChannelLogoUtils.test";
+
+ private ContentResolver mContentResolver;
+ private Uri mChannelUri;
+ private long mChannelId;
+
+ @Before
+ public void setUp() throws Exception {
+ mContentResolver = getContext().getContentResolver();
+ ContentValues contentValues = new Channel.Builder()
+ .setInputId(FAKE_INPUT_ID)
+ .setType(TvContractCompat.Channels.TYPE_OTHER).build().toContentValues();
+ mChannelUri = mContentResolver.insert(TvContract.Channels.CONTENT_URI, contentValues);
+ mChannelId = ContentUris.parseId(mChannelUri);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mContentResolver.delete(mChannelUri, null, null);
+ }
+
+ @Test
+ public void testStoreChannelLogo_fromBitmap() {
+ assertNull(ChannelLogoUtils.loadChannelLogo(getContext(), mChannelId));
+ Bitmap logo = BitmapFactory.decodeResource(getContext().getResources(),
+ R.drawable.test_icon);
+ assertNotNull(logo);
+ assertTrue(ChannelLogoUtils.storeChannelLogo(getContext(), mChannelId, logo));
+ // Workaround: the file status is not consistent between openInputStream/openOutputStream,
+ // wait 10 secs to make sure that the logo file is written into the disk.
+ SystemClock.sleep(10000);
+ assertNotNull(ChannelLogoUtils.loadChannelLogo(getContext(), mChannelId));
+ }
+
+ @Test
+ public void testStoreChannelLogo_fromResUri() {
+ assertNull(ChannelLogoUtils.loadChannelLogo(getContext(), mChannelId));
+ int resId = R.drawable.test_icon;
+ Resources res = getContext().getResources();
+ Uri logoUri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+ .authority(res.getResourcePackageName(resId))
+ .appendPath(res.getResourceTypeName(resId))
+ .appendPath(res.getResourceEntryName(resId))
+ .build();
+ assertTrue(ChannelLogoUtils.storeChannelLogo(getContext(), mChannelId, logoUri));
+ // Workaround: the file status is not consistent between openInputStream/openOutputStream,
+ // wait 10 secs to make sure that the logo file is written into the disk.
+ SystemClock.sleep(10000);
+ assertNotNull(ChannelLogoUtils.loadChannelLogo(getContext(), mChannelId));
+ }
+}
diff --git a/android/support/media/tv/ChannelTest.java b/android/support/media/tv/ChannelTest.java
new file mode 100644
index 00000000..979a20a4
--- /dev/null
+++ b/android/support/media/tv/ChannelTest.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.media.tv;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Build;
+import android.support.media.tv.TvContractCompat.Channels;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests that channels can be created using the Builder pattern and correctly obtain
+ * values from them
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+public class ChannelTest {
+ @After
+ public void tearDown() {
+ if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
+ return;
+ }
+ ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+ resolver.delete(Channels.CONTENT_URI, null, null);
+ }
+
+ @Test
+ public void testEmptyChannel() {
+ Channel emptyChannel = new Channel.Builder()
+ .build();
+ ContentValues contentValues = emptyChannel.toContentValues(true);
+ compareChannel(emptyChannel, Channel.fromCursor(getChannelCursor(contentValues)), true);
+ }
+
+ @Test
+ public void testSampleChannel() {
+ // Tests cloning and database I/O of a channel with some defined and some undefined
+ // values.
+ Channel sampleChannel = new Channel.Builder()
+ .setDisplayName("Google")
+ .setDisplayNumber("3")
+ .setDescription("This is a sample channel")
+ .setOriginalNetworkId(1)
+ .setAppLinkIntentUri(Uri.parse(new Intent(Intent.ACTION_VIEW).toUri(
+ Intent.URI_INTENT_SCHEME)))
+ .setOriginalNetworkId(0)
+ .build();
+ ContentValues contentValues = sampleChannel.toContentValues(true);
+ compareChannel(sampleChannel, Channel.fromCursor(getChannelCursor(contentValues)), true);
+
+ Channel clonedSampleChannel = new Channel.Builder(sampleChannel).build();
+ compareChannel(sampleChannel, clonedSampleChannel, true);
+ }
+
+ @Test
+ public void testFullyPopulatedChannel() {
+ Channel fullyPopulatedChannel = createFullyPopulatedChannel();
+ ContentValues contentValues = fullyPopulatedChannel.toContentValues(true);
+ compareChannel(fullyPopulatedChannel, Channel.fromCursor(getChannelCursor(contentValues)),
+ true);
+
+ Channel clonedFullyPopulatedChannel = new Channel.Builder(fullyPopulatedChannel).build();
+ compareChannel(fullyPopulatedChannel, clonedFullyPopulatedChannel, true);
+ }
+
+ @Test
+ public void testChannelWithSystemContentProvider() {
+ if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
+ return;
+ }
+ Channel fullyPopulatedChannel = createFullyPopulatedChannel();
+ ContentValues contentValues = fullyPopulatedChannel.toContentValues();
+ ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+ Uri channelUri = resolver.insert(Channels.CONTENT_URI, contentValues);
+ assertNotNull(channelUri);
+
+ Channel channelFromSystemDb = loadChannelFromContentProvider(resolver, channelUri);
+ compareChannel(fullyPopulatedChannel, channelFromSystemDb, false);
+ }
+
+ @Test
+ public void testChannelUpdateWithContentProvider() {
+ if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
+ return;
+ }
+
+ Channel fullyPopulatedChannel = createFullyPopulatedChannel();
+ ContentValues contentValues = fullyPopulatedChannel.toContentValues();
+ ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+ Uri channelUri = resolver.insert(Channels.CONTENT_URI, contentValues);
+ assertNotNull(channelUri);
+
+ Channel channelFromSystemDb = loadChannelFromContentProvider(resolver, channelUri);
+ compareChannel(fullyPopulatedChannel, channelFromSystemDb, false);
+
+ // Update a field from a fully loaded channel.
+ Channel updatedChannel = new Channel.Builder(channelFromSystemDb)
+ .setDescription("new description").build();
+ assertEquals(1, resolver.update(channelUri, updatedChannel.toContentValues(), null, null));
+ channelFromSystemDb = loadChannelFromContentProvider(resolver, channelUri);
+ compareChannel(updatedChannel, channelFromSystemDb, false);
+
+ // Update a field with null from a fully loaded channel.
+ updatedChannel = new Channel.Builder(updatedChannel)
+ .setAppLinkText(null).build();
+ assertEquals(1, resolver.update(
+ channelUri, updatedChannel.toContentValues(), null, null));
+ channelFromSystemDb = loadChannelFromContentProvider(resolver, channelUri);
+ compareChannel(updatedChannel, channelFromSystemDb, false);
+
+ // Update a field without referencing fully channel.
+ ContentValues values = new Channel.Builder().setDisplayName("abc").build()
+ .toContentValues();
+ assertEquals(1, values.size());
+ assertEquals(1, resolver.update(channelUri, values, null, null));
+ channelFromSystemDb = loadChannelFromContentProvider(resolver, channelUri);
+ Channel expectedChannel = new Channel.Builder(channelFromSystemDb)
+ .setDisplayName("abc").build();
+ compareChannel(expectedChannel, channelFromSystemDb, false);
+ }
+
+ @Test
+ public void testChannelEquals() {
+ assertEquals(createFullyPopulatedChannel(), createFullyPopulatedChannel());
+ }
+
+
+ private static Channel loadChannelFromContentProvider(
+ ContentResolver resolver, Uri channelUri) {
+ try (Cursor cursor = resolver.query(channelUri, null, null, null, null)) {
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ cursor.moveToNext();
+ return Channel.fromCursor(cursor);
+ }
+ }
+
+ private static Channel createFullyPopulatedChannel() {
+ return new Channel.Builder()
+ .setAppLinkColor(0x00FF0000)
+ .setAppLinkIconUri(Uri.parse("http://example.com/icon.png"))
+ .setAppLinkIntent(new Intent())
+ .setAppLinkPosterArtUri(Uri.parse("http://example.com/poster.png"))
+ .setAppLinkText("Open an intent")
+ .setDescription("Channel description")
+ .setDisplayName("Display Name")
+ .setDisplayNumber("100")
+ .setInputId("TestInputService")
+ .setNetworkAffiliation("Network Affiliation")
+ .setOriginalNetworkId(2)
+ .setPackageName("android.support.media.tv.test")
+ .setSearchable(false)
+ .setServiceId(3)
+ .setTransportStreamId(4)
+ .setType(TvContractCompat.Channels.TYPE_PREVIEW)
+ .setServiceType(TvContractCompat.Channels.SERVICE_TYPE_AUDIO_VIDEO)
+ .setVideoFormat(TvContractCompat.Channels.VIDEO_FORMAT_240P)
+ .setInternalProviderFlag1(0x4)
+ .setInternalProviderFlag2(0x3)
+ .setInternalProviderFlag3(0x2)
+ .setInternalProviderFlag4(0x1)
+ .setInternalProviderId("Internal Provider")
+ .setTransient(true)
+ .setBrowsable(true)
+ .setLocked(true)
+ .setSystemApproved(true)
+ .build();
+ }
+
+ private static void compareChannel(Channel channelA, Channel channelB,
+ boolean includeIdAndProtectedFields) {
+ assertEquals(channelA.isSearchable(), channelB.isSearchable());
+ assertEquals(channelA.getDescription(), channelB.getDescription());
+ assertEquals(channelA.getDisplayName(), channelB.getDisplayName());
+ assertEquals(channelA.getDisplayNumber(), channelB.getDisplayNumber());
+ assertEquals(channelA.getInputId(), channelB.getInputId());
+ assertEquals(channelA.getNetworkAffiliation(), channelB.getNetworkAffiliation());
+ assertEquals(channelA.getOriginalNetworkId(), channelB.getOriginalNetworkId());
+ assertEquals(channelA.getPackageName(), channelB.getPackageName());
+ assertEquals(channelA.getServiceId(), channelB.getServiceId());
+ assertEquals(channelA.getServiceType(), channelB.getServiceType());
+ assertEquals(channelA.getTransportStreamId(), channelB.getTransportStreamId());
+ assertEquals(channelA.getType(), channelB.getType());
+ assertEquals(channelA.getVideoFormat(), channelB.getVideoFormat());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ assertEquals(channelA.getAppLinkColor(), channelB.getAppLinkColor());
+ assertEquals(channelA.getAppLinkIconUri(), channelB.getAppLinkIconUri());
+ assertEquals(channelA.getAppLinkIntentUri(), channelB.getAppLinkIntentUri());
+ assertEquals(channelA.getAppLinkPosterArtUri(), channelB.getAppLinkPosterArtUri());
+ assertEquals(channelA.getAppLinkText(), channelB.getAppLinkText());
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ assertEquals(channelA.getInternalProviderId(), channelB.getInternalProviderId());
+ assertEquals(channelA.isTransient(), channelB.isTransient());
+ }
+ if (includeIdAndProtectedFields) {
+ // Skip row ID since the one from system DB has the valid ID while the other does not.
+ assertEquals(channelA.getId(), channelB.getId());
+ // When we insert a channel using toContentValues() to the system, we drop some
+ // protected fields since they only can be modified by system apps.
+ assertEquals(channelA.isBrowsable(), channelB.isBrowsable());
+ assertEquals(channelA.isLocked(), channelB.isLocked());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ assertEquals(channelA.isSystemApproved(), channelB.isSystemApproved());
+ }
+ assertEquals(channelA.toContentValues(), channelB.toContentValues());
+ }
+ }
+
+ private static MatrixCursor getChannelCursor(ContentValues contentValues) {
+ String[] cols = Channel.PROJECTION;
+ MatrixCursor cursor = new MatrixCursor(cols);
+ MatrixCursor.RowBuilder builder = cursor.newRow();
+ for (String col : cols) {
+ if (col != null) {
+ builder.add(col, contentValues.get(col));
+ }
+ }
+ cursor.moveToFirst();
+ return cursor;
+ }
+}
diff --git a/android/support/media/tv/PreviewProgram.java b/android/support/media/tv/PreviewProgram.java
index 3df3a744..6d2fbaf4 100644
--- a/android/support/media/tv/PreviewProgram.java
+++ b/android/support/media/tv/PreviewProgram.java
@@ -17,7 +17,6 @@ package android.support.media.tv;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import android.annotation.TargetApi;
import android.content.ContentValues;
import android.database.Cursor;
import android.media.tv.TvContentRating; // For javadoc gen of super class
@@ -74,7 +73,6 @@ import android.support.media.tv.TvContractCompat.Programs.Genres; // For javado
* null, null);
* </pre>
*/
-@TargetApi(26)
public final class PreviewProgram extends BasePreviewProgram {
/**
* @hide
diff --git a/android/support/media/tv/PreviewProgramTest.java b/android/support/media/tv/PreviewProgramTest.java
new file mode 100644
index 00000000..d0baa5f6
--- /dev/null
+++ b/android/support/media/tv/PreviewProgramTest.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.media.tv;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.media.tv.TvContentRating;
+import android.net.Uri;
+import android.support.media.tv.TvContractCompat.Channels;
+import android.support.media.tv.TvContractCompat.PreviewPrograms;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * Tests that preview programs can be created using the Builder pattern and correctly obtain
+ * values from them.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 26)
+public class PreviewProgramTest {
+
+ @After
+ public void tearDown() {
+ if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
+ return;
+ }
+ ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+ resolver.delete(Channels.CONTENT_URI, null, null);
+ }
+
+ @Test
+ public void testEmptyPreviewProgram() {
+ PreviewProgram emptyProgram = new PreviewProgram.Builder().build();
+ ContentValues contentValues = emptyProgram.toContentValues();
+ compareProgram(emptyProgram,
+ PreviewProgram.fromCursor(getProgramCursor(Program.PROJECTION, contentValues)),
+ true);
+ }
+
+ @Test
+ public void testSampleProgram() {
+ PreviewProgram sampleProgram = new PreviewProgram.Builder()
+ .setPackageName("My package")
+ .setTitle("Program Title")
+ .setDescription("This is a sample program")
+ .setEpisodeNumber(5)
+ .setSeasonNumber("The Final Season", 7)
+ .setThumbnailUri(Uri.parse("http://www.example.com/programs/poster.png"))
+ .setChannelId(3)
+ .setWeight(70)
+ .build();
+ ContentValues contentValues = sampleProgram.toContentValues(true);
+ compareProgram(sampleProgram,
+ PreviewProgram.fromCursor(
+ getProgramCursor(PreviewProgram.PROJECTION, contentValues)), true);
+
+ PreviewProgram clonedSampleProgram = new PreviewProgram.Builder(sampleProgram).build();
+ compareProgram(sampleProgram, clonedSampleProgram, true);
+ }
+
+ @Test
+ public void testFullyPopulatedPreviewProgram() {
+ PreviewProgram fullyPopulatedProgram = createFullyPopulatedPreviewProgram(3);
+ ContentValues contentValues = fullyPopulatedProgram.toContentValues(true);
+ compareProgram(fullyPopulatedProgram,
+ PreviewProgram.fromCursor(
+ getProgramCursor(PreviewProgram.PROJECTION, contentValues)), true);
+
+ PreviewProgram clonedFullyPopulatedProgram =
+ new PreviewProgram.Builder(fullyPopulatedProgram).build();
+ compareProgram(fullyPopulatedProgram, clonedFullyPopulatedProgram, true);
+ }
+
+ @Test
+ public void testPreviewProgramWithSystemContentProvider() {
+ if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
+ return;
+ }
+ Channel channel = new Channel.Builder()
+ .setInputId("TestInputService")
+ .setType(TvContractCompat.Channels.TYPE_PREVIEW)
+ .build();
+ ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+ Uri channelUri = resolver.insert(Channels.CONTENT_URI, channel.toContentValues());
+ assertNotNull(channelUri);
+
+ PreviewProgram fullyPopulatedProgram = createFullyPopulatedPreviewProgram(
+ ContentUris.parseId(channelUri));
+ Uri previewProgramUri = resolver.insert(PreviewPrograms.CONTENT_URI,
+ fullyPopulatedProgram.toContentValues());
+
+ PreviewProgram programFromSystemDb =
+ loadPreviewProgramFromContentProvider(resolver, previewProgramUri);
+ compareProgram(fullyPopulatedProgram, programFromSystemDb, false);
+ }
+
+ @Test
+ public void testPreviewProgramUpdateWithContentProvider() {
+ if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
+ return;
+ }
+ Channel channel = new Channel.Builder()
+ .setInputId("TestInputService")
+ .setType(TvContractCompat.Channels.TYPE_PREVIEW)
+ .build();
+ ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+ Uri channelUri = resolver.insert(Channels.CONTENT_URI, channel.toContentValues());
+ assertNotNull(channelUri);
+
+ PreviewProgram fullyPopulatedProgram = createFullyPopulatedPreviewProgram(
+ ContentUris.parseId(channelUri));
+ Uri previewProgramUri = resolver.insert(PreviewPrograms.CONTENT_URI,
+ fullyPopulatedProgram.toContentValues());
+
+ PreviewProgram programFromSystemDb = loadPreviewProgramFromContentProvider(
+ resolver, previewProgramUri);
+ compareProgram(fullyPopulatedProgram, programFromSystemDb, false);
+
+ // Update a field from a fully loaded preview program.
+ PreviewProgram updatedProgram = new PreviewProgram.Builder(programFromSystemDb)
+ .setInteractionCount(programFromSystemDb.getInteractionCount() + 1).build();
+ assertEquals(1, resolver.update(
+ previewProgramUri, updatedProgram.toContentValues(), null, null));
+ programFromSystemDb = loadPreviewProgramFromContentProvider(resolver, previewProgramUri);
+ compareProgram(updatedProgram, programFromSystemDb, false);
+
+ // Update a field with null from a fully loaded preview program.
+ updatedProgram = new PreviewProgram.Builder(updatedProgram)
+ .setLongDescription(null).build();
+ assertEquals(1, resolver.update(
+ previewProgramUri, updatedProgram.toContentValues(), null, null));
+ programFromSystemDb = loadPreviewProgramFromContentProvider(resolver, previewProgramUri);
+ compareProgram(updatedProgram, programFromSystemDb, false);
+
+ // Update a field without referencing fully loaded preview program.
+ ContentValues values = new PreviewProgram.Builder().setInteractionCount(1).build()
+ .toContentValues();
+ assertEquals(1, values.size());
+ assertEquals(1, resolver.update(previewProgramUri, values, null, null));
+ programFromSystemDb = loadPreviewProgramFromContentProvider(resolver, previewProgramUri);
+ PreviewProgram expectedProgram = new PreviewProgram.Builder(programFromSystemDb)
+ .setInteractionCount(1).build();
+ compareProgram(expectedProgram, programFromSystemDb, false);
+ }
+
+ @Test
+ public void testPreviewProgramEquals() {
+ assertEquals(createFullyPopulatedPreviewProgram(1), createFullyPopulatedPreviewProgram(1));
+ }
+
+ private static PreviewProgram loadPreviewProgramFromContentProvider(
+ ContentResolver resolver, Uri previewProgramUri) {
+ try (Cursor cursor = resolver.query(previewProgramUri, null, null, null, null)) {
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ cursor.moveToNext();
+ return PreviewProgram.fromCursor(cursor);
+ }
+ }
+
+ @Test
+ public void testPreviewProgramWithPartialData() {
+ PreviewProgram previewProgram = new PreviewProgram.Builder()
+ .setChannelId(3)
+ .setWeight(100)
+ .setInternalProviderId("ID-4321")
+ .setPreviewVideoUri(Uri.parse("http://example.com/preview-video.mpg"))
+ .setLastPlaybackPositionMillis(0)
+ .setDurationMillis(60 * 1000)
+ .setIntentUri(Uri.parse(new Intent(Intent.ACTION_VIEW).toUri(
+ Intent.URI_INTENT_SCHEME)))
+ .setTransient(false)
+ .setType(PreviewPrograms.TYPE_TV_EPISODE)
+ .setPosterArtAspectRatio(PreviewPrograms.ASPECT_RATIO_3_2)
+ .setThumbnailAspectRatio(PreviewPrograms.ASPECT_RATIO_16_9)
+ .setLogoUri(Uri.parse("http://example.com/program-logo.mpg"))
+ .setAvailability(PreviewPrograms.AVAILABILITY_FREE_WITH_SUBSCRIPTION)
+ .setStartingPrice("9.99 USD")
+ .setOfferPrice("3.99 USD")
+ .setReleaseDate(new Date(Date.UTC(97, 2, 8, 9, 30, 59)))
+ .setLive(false)
+ .setInteractionType(PreviewPrograms.INTERACTION_TYPE_VIEWS)
+ .setInteractionCount(99200)
+ .setAuthor("author_name")
+ .setReviewRatingStyle(PreviewPrograms.REVIEW_RATING_STYLE_PERCENTAGE)
+ .setReviewRating("83.9")
+ .setId(10)
+ .setTitle("Recommended Video 1")
+ .setDescription("You should watch this!")
+ .setPosterArtUri(Uri.parse("http://example.com/poster.png"))
+ .setInternalProviderFlag2(0x0010010084108410L)
+ .build();
+
+ String[] partialProjection = {
+ PreviewPrograms._ID,
+ PreviewPrograms.COLUMN_CHANNEL_ID,
+ PreviewPrograms.COLUMN_TITLE,
+ PreviewPrograms.COLUMN_SHORT_DESCRIPTION,
+ PreviewPrograms.COLUMN_POSTER_ART_URI,
+ PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
+ PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID,
+ PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI,
+ PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS,
+ PreviewPrograms.COLUMN_DURATION_MILLIS,
+ PreviewPrograms.COLUMN_INTENT_URI,
+ PreviewPrograms.COLUMN_WEIGHT,
+ PreviewPrograms.COLUMN_TRANSIENT,
+ PreviewPrograms.COLUMN_TYPE,
+ PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO,
+ PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO,
+ PreviewPrograms.COLUMN_LOGO_URI,
+ PreviewPrograms.COLUMN_AVAILABILITY,
+ PreviewPrograms.COLUMN_STARTING_PRICE,
+ PreviewPrograms.COLUMN_OFFER_PRICE,
+ PreviewPrograms.COLUMN_RELEASE_DATE,
+ PreviewPrograms.COLUMN_ITEM_COUNT,
+ PreviewPrograms.COLUMN_LIVE,
+ PreviewPrograms.COLUMN_INTERACTION_TYPE,
+ PreviewPrograms.COLUMN_INTERACTION_COUNT,
+ PreviewPrograms.COLUMN_AUTHOR,
+ PreviewPrograms.COLUMN_REVIEW_RATING_STYLE,
+ PreviewPrograms.COLUMN_REVIEW_RATING,
+ };
+
+ ContentValues contentValues = previewProgram.toContentValues(true);
+ compareProgram(previewProgram,
+ PreviewProgram.fromCursor(getProgramCursor(partialProjection, contentValues)),
+ true);
+
+ PreviewProgram clonedFullyPopulatedProgram =
+ new PreviewProgram.Builder(previewProgram).build();
+ compareProgram(previewProgram, clonedFullyPopulatedProgram, true);
+ }
+
+ private static PreviewProgram createFullyPopulatedPreviewProgram(long channelId) {
+ return new PreviewProgram.Builder()
+ .setTitle("Google")
+ .setInternalProviderId("ID-4321")
+ .setChannelId(channelId)
+ .setWeight(100)
+ .setPreviewVideoUri(Uri.parse("http://example.com/preview-video.mpg"))
+ .setLastPlaybackPositionMillis(0)
+ .setDurationMillis(60 * 1000)
+ .setIntentUri(Uri.parse(new Intent(Intent.ACTION_VIEW).toUri(
+ Intent.URI_INTENT_SCHEME)))
+ .setTransient(false)
+ .setType(PreviewPrograms.TYPE_MOVIE)
+ .setPosterArtAspectRatio(PreviewPrograms.ASPECT_RATIO_2_3)
+ .setThumbnailAspectRatio(PreviewPrograms.ASPECT_RATIO_16_9)
+ .setLogoUri(Uri.parse("http://example.com/program-logo.mpg"))
+ .setAvailability(PreviewPrograms.AVAILABILITY_AVAILABLE)
+ .setStartingPrice("12.99 USD")
+ .setOfferPrice("4.99 USD")
+ .setReleaseDate("1997")
+ .setItemCount(3)
+ .setLive(false)
+ .setInteractionType(PreviewPrograms.INTERACTION_TYPE_LIKES)
+ .setInteractionCount(10200)
+ .setAuthor("author_name")
+ .setReviewRatingStyle(PreviewPrograms.REVIEW_RATING_STYLE_STARS)
+ .setReviewRating("4.5")
+ .setSearchable(false)
+ .setThumbnailUri(Uri.parse("http://example.com/thumbnail.png"))
+ .setAudioLanguages(new String [] {"eng", "kor"})
+ .setCanonicalGenres(new String[] {TvContractCompat.Programs.Genres.MOVIES})
+ .setContentRatings(new TvContentRating[] {
+ TvContentRating.createRating("com.android.tv", "US_TV", "US_TV_Y7")})
+ .setDescription("This is a sample program")
+ .setEpisodeNumber("Pilot", 0)
+ .setEpisodeTitle("Hello World")
+ .setLongDescription("This is a longer description than the previous description")
+ .setPosterArtUri(Uri.parse("http://example.com/poster.png"))
+ .setSeasonNumber("The Final Season", 7)
+ .setSeasonTitle("The Final Season")
+ .setVideoHeight(1080)
+ .setVideoWidth(1920)
+ .setInternalProviderFlag1(0x4)
+ .setInternalProviderFlag2(0x3)
+ .setInternalProviderFlag3(0x2)
+ .setInternalProviderFlag4(0x1)
+ .setBrowsable(true)
+ .setContentId("CID-8642")
+ .build();
+ }
+
+ private static void compareProgram(PreviewProgram programA, PreviewProgram programB,
+ boolean includeIdAndProtectedFields) {
+ assertTrue(Arrays.equals(programA.getAudioLanguages(), programB.getAudioLanguages()));
+ assertTrue(Arrays.deepEquals(programA.getCanonicalGenres(), programB.getCanonicalGenres()));
+ assertEquals(programA.getChannelId(), programB.getChannelId());
+ assertTrue(Arrays.deepEquals(programA.getContentRatings(), programB.getContentRatings()));
+ assertEquals(programA.getDescription(), programB.getDescription());
+ assertEquals(programA.getEpisodeNumber(), programB.getEpisodeNumber());
+ assertEquals(programA.getEpisodeTitle(), programB.getEpisodeTitle());
+ assertEquals(programA.getLongDescription(), programB.getLongDescription());
+ assertEquals(programA.getPosterArtUri(), programB.getPosterArtUri());
+ assertEquals(programA.getSeasonNumber(), programB.getSeasonNumber());
+ assertEquals(programA.getThumbnailUri(), programB.getThumbnailUri());
+ assertEquals(programA.getTitle(), programB.getTitle());
+ assertEquals(programA.getVideoHeight(), programB.getVideoHeight());
+ assertEquals(programA.getVideoWidth(), programB.getVideoWidth());
+ assertEquals(programA.isSearchable(), programB.isSearchable());
+ assertEquals(programA.getInternalProviderFlag1(), programB.getInternalProviderFlag1());
+ assertEquals(programA.getInternalProviderFlag2(), programB.getInternalProviderFlag2());
+ assertEquals(programA.getInternalProviderFlag3(), programB.getInternalProviderFlag3());
+ assertEquals(programA.getInternalProviderFlag4(), programB.getInternalProviderFlag4());
+ assertTrue(Objects.equals(programA.getSeasonTitle(), programB.getSeasonTitle()));
+ assertEquals(programA.getInternalProviderId(), programB.getInternalProviderId());
+ assertEquals(programA.getPreviewVideoUri(), programB.getPreviewVideoUri());
+ assertEquals(programA.getLastPlaybackPositionMillis(),
+ programB.getLastPlaybackPositionMillis());
+ assertEquals(programA.getDurationMillis(), programB.getDurationMillis());
+ assertEquals(programA.getIntentUri(), programB.getIntentUri());
+ assertEquals(programA.getWeight(), programB.getWeight());
+ assertEquals(programA.isTransient(), programB.isTransient());
+ assertEquals(programA.getType(), programB.getType());
+ assertEquals(programA.getPosterArtAspectRatio(), programB.getPosterArtAspectRatio());
+ assertEquals(programA.getThumbnailAspectRatio(), programB.getThumbnailAspectRatio());
+ assertEquals(programA.getLogoUri(), programB.getLogoUri());
+ assertEquals(programA.getAvailability(), programB.getAvailability());
+ assertEquals(programA.getStartingPrice(), programB.getStartingPrice());
+ assertEquals(programA.getOfferPrice(), programB.getOfferPrice());
+ assertEquals(programA.getReleaseDate(), programB.getReleaseDate());
+ assertEquals(programA.getItemCount(), programB.getItemCount());
+ assertEquals(programA.isLive(), programB.isLive());
+ assertEquals(programA.getInteractionType(), programB.getInteractionType());
+ assertEquals(programA.getInteractionCount(), programB.getInteractionCount());
+ assertEquals(programA.getAuthor(), programB.getAuthor());
+ assertEquals(programA.getReviewRatingStyle(), programB.getReviewRatingStyle());
+ assertEquals(programA.getReviewRating(), programB.getReviewRating());
+ assertEquals(programA.getContentId(), programB.getContentId());
+ if (includeIdAndProtectedFields) {
+ // Skip row ID since the one from system DB has the valid ID while the other does not.
+ assertEquals(programA.getId(), programB.getId());
+ assertEquals(programA.getPackageName(), programB.getPackageName());
+ // When we insert a channel using toContentValues() to the system, we drop some
+ // protected fields since they only can be modified by system apps.
+ assertEquals(programA.isBrowsable(), programB.isBrowsable());
+ assertEquals(programA.toContentValues(), programB.toContentValues());
+ assertEquals(programA, programB);
+ }
+ }
+
+ private static MatrixCursor getProgramCursor(String[] projection, ContentValues contentValues) {
+ MatrixCursor cursor = new MatrixCursor(projection);
+ MatrixCursor.RowBuilder builder = cursor.newRow();
+ for (String col : projection) {
+ if (col != null) {
+ builder.add(col, contentValues.get(col));
+ }
+ }
+ cursor.moveToFirst();
+ return cursor;
+ }
+}
diff --git a/android/support/media/tv/Program.java b/android/support/media/tv/Program.java
index 233f1bab..882916d3 100644
--- a/android/support/media/tv/Program.java
+++ b/android/support/media/tv/Program.java
@@ -17,7 +17,6 @@ package android.support.media.tv;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import android.annotation.TargetApi;
import android.content.ContentValues;
import android.database.Cursor;
import android.media.tv.TvContentRating; // For javadoc gen of super class
@@ -72,7 +71,6 @@ import android.support.media.tv.TvContractCompat.Programs.Genres.Genre;
* null, null);
* </pre>
*/
-@TargetApi(21)
public final class Program extends BaseProgram implements Comparable<Program> {
/**
* @hide
diff --git a/android/support/media/tv/ProgramTest.java b/android/support/media/tv/ProgramTest.java
new file mode 100644
index 00000000..62093832
--- /dev/null
+++ b/android/support/media/tv/ProgramTest.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.media.tv;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.media.tv.TvContentRating;
+import android.net.Uri;
+import android.os.Build;
+import android.support.media.tv.TvContractCompat.Channels;
+import android.support.media.tv.TvContractCompat.Programs;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Tests that programs can be created using the Builder pattern and correctly obtain
+ * values from them.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+public class ProgramTest {
+ @After
+ public void tearDown() {
+ if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
+ return;
+ }
+ ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+ resolver.delete(Channels.CONTENT_URI, null, null);
+ }
+
+ @Test
+ public void testEmptyProgram() {
+ Program emptyProgram = new Program.Builder()
+ .build();
+ ContentValues contentValues = emptyProgram.toContentValues();
+ compareProgram(emptyProgram,
+ Program.fromCursor(getProgramCursor(Program.PROJECTION, contentValues)), true);
+ }
+
+ @Test
+ public void testSampleProgram() {
+ Program sampleProgram = new Program.Builder()
+ .setPackageName("My package")
+ .setTitle("Program Title")
+ .setDescription("This is a sample program")
+ .setEpisodeNumber(5)
+ .setSeasonNumber("The Final Season", 7)
+ .setThumbnailUri(Uri.parse("http://www.example.com/programs/poster.png"))
+ .setChannelId(3)
+ .setStartTimeUtcMillis(0)
+ .setEndTimeUtcMillis(1000)
+ .build();
+ ContentValues contentValues = sampleProgram.toContentValues();
+ compareProgram(sampleProgram,
+ Program.fromCursor(getProgramCursor(Program.PROJECTION, contentValues)), true);
+
+ Program clonedSampleProgram = new Program.Builder(sampleProgram).build();
+ compareProgram(sampleProgram, clonedSampleProgram, true);
+ }
+
+ @Test
+ public void testFullyPopulatedProgram() {
+ Program fullyPopulatedProgram = createFullyPopulatedProgram(3);
+
+ ContentValues contentValues = fullyPopulatedProgram.toContentValues();
+ compareProgram(fullyPopulatedProgram,
+ Program.fromCursor(getProgramCursor(Program.PROJECTION, contentValues)), true);
+
+ Program clonedFullyPopulatedProgram = new Program.Builder(fullyPopulatedProgram).build();
+ compareProgram(fullyPopulatedProgram, clonedFullyPopulatedProgram, true);
+ }
+
+ @Test
+ public void testChannelWithSystemContentProvider() {
+ if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
+ return;
+ }
+ Channel channel = new Channel.Builder()
+ .setInputId("TestInputService")
+ .setType(TvContractCompat.Channels.TYPE_OTHER)
+ .build();
+ ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+ Uri channelUri = resolver.insert(Channels.CONTENT_URI, channel.toContentValues());
+ assertNotNull(channelUri);
+
+ Program fullyPopulatedProgram =
+ createFullyPopulatedProgram(ContentUris.parseId(channelUri));
+ Uri programUri = resolver.insert(Programs.CONTENT_URI,
+ fullyPopulatedProgram.toContentValues());
+
+ Program programFromSystemDb = loadProgramFromContentProvider(resolver, programUri);
+ compareProgram(fullyPopulatedProgram, programFromSystemDb, false);
+ }
+
+ @Test
+ public void testProgramUpdateWithContentProvider() {
+ if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
+ return;
+ }
+ Channel channel = new Channel.Builder()
+ .setInputId("TestInputService")
+ .setType(TvContractCompat.Channels.TYPE_OTHER)
+ .build();
+ ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+ Uri channelUri = resolver.insert(Channels.CONTENT_URI, channel.toContentValues());
+ assertNotNull(channelUri);
+
+ Program fullyPopulatedProgram =
+ createFullyPopulatedProgram(ContentUris.parseId(channelUri));
+ Uri programUri = resolver.insert(Programs.CONTENT_URI,
+ fullyPopulatedProgram.toContentValues());
+
+ Program programFromSystemDb = loadProgramFromContentProvider(resolver, programUri);
+ compareProgram(fullyPopulatedProgram, programFromSystemDb, false);
+
+ // Update a field from a fully loaded program.
+ Program updatedProgram = new Program.Builder(programFromSystemDb)
+ .setDescription("description1").build();
+ assertEquals(1, resolver.update(
+ programUri, updatedProgram.toContentValues(), null, null));
+ programFromSystemDb = loadProgramFromContentProvider(resolver, programUri);
+ compareProgram(updatedProgram, programFromSystemDb, false);
+
+ // Update a field with null from a fully loaded program.
+ updatedProgram = new Program.Builder(updatedProgram)
+ .setLongDescription(null).build();
+ assertEquals(1, resolver.update(
+ programUri, updatedProgram.toContentValues(), null, null));
+ programFromSystemDb = loadProgramFromContentProvider(resolver, programUri);
+ compareProgram(updatedProgram, programFromSystemDb, false);
+
+ // Update a field without referencing fully loaded program.
+ ContentValues values = new Program.Builder().setDescription("description2").build()
+ .toContentValues();
+ assertEquals(1, values.size());
+ assertEquals(1, resolver.update(programUri, values, null, null));
+ programFromSystemDb = loadProgramFromContentProvider(resolver, programUri);
+ Program expectedProgram = new Program.Builder(programFromSystemDb)
+ .setDescription("description2").build();
+ compareProgram(expectedProgram, programFromSystemDb, false);
+ }
+
+ @Test
+ public void testProgramEquals() {
+ assertEquals(createFullyPopulatedProgram(1), createFullyPopulatedProgram(1));
+ }
+
+ private static Program loadProgramFromContentProvider(
+ ContentResolver resolver, Uri programUri) {
+ try (Cursor cursor = resolver.query(programUri, null, null, null, null)) {
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ cursor.moveToNext();
+ return Program.fromCursor(cursor);
+ }
+ }
+
+ private static Program createFullyPopulatedProgram(long channelId) {
+ return new Program.Builder()
+ .setSearchable(false)
+ .setThumbnailUri(Uri.parse("http://example.com/thumbnail.png"))
+ .setAudioLanguages(new String [] {"eng", "kor"})
+ .setCanonicalGenres(new String[] {TvContractCompat.Programs.Genres.MOVIES})
+ .setContentRatings(new TvContentRating[] {
+ TvContentRating.createRating("com.android.tv", "US_TV", "US_TV_Y7")})
+ .setDescription("This is a sample program")
+ .setEpisodeNumber("Pilot", 0)
+ .setEpisodeTitle("Hello World")
+ .setLongDescription("This is a longer description than the previous description")
+ .setPosterArtUri(Uri.parse("http://example.com/poster.png"))
+ .setSeasonNumber("The Final Season", 7)
+ .setSeasonTitle("The Final Season")
+ .setTitle("Google")
+ .setVideoHeight(1080)
+ .setVideoWidth(1920)
+ .setInternalProviderFlag1(0x4)
+ .setInternalProviderFlag2(0x3)
+ .setInternalProviderFlag3(0x2)
+ .setInternalProviderFlag4(0x1)
+ .setReviewRatingStyle(Programs.REVIEW_RATING_STYLE_PERCENTAGE)
+ .setReviewRating("83.9")
+ .setChannelId(channelId)
+ .setStartTimeUtcMillis(0)
+ .setEndTimeUtcMillis(1000)
+ .setBroadcastGenres(new String[] {"Music", "Family"})
+ .setRecordingProhibited(false)
+ .build();
+ }
+
+ private static void compareProgram(Program programA, Program programB,
+ boolean includeIdAndProtectedFields) {
+ assertTrue(Arrays.equals(programA.getAudioLanguages(), programB.getAudioLanguages()));
+ assertTrue(Arrays.deepEquals(programA.getBroadcastGenres(), programB.getBroadcastGenres()));
+ assertTrue(Arrays.deepEquals(programA.getCanonicalGenres(), programB.getCanonicalGenres()));
+ assertEquals(programA.getChannelId(), programB.getChannelId());
+ assertTrue(Arrays.deepEquals(programA.getContentRatings(), programB.getContentRatings()));
+ assertEquals(programA.getDescription(), programB.getDescription());
+ assertEquals(programA.getEndTimeUtcMillis(), programB.getEndTimeUtcMillis());
+ assertEquals(programA.getEpisodeNumber(), programB.getEpisodeNumber());
+ assertEquals(programA.getEpisodeTitle(), programB.getEpisodeTitle());
+ assertEquals(programA.getLongDescription(), programB.getLongDescription());
+ assertEquals(programA.getPosterArtUri(), programB.getPosterArtUri());
+ assertEquals(programA.getSeasonNumber(), programB.getSeasonNumber());
+ assertEquals(programA.getStartTimeUtcMillis(), programB.getStartTimeUtcMillis());
+ assertEquals(programA.getThumbnailUri(), programB.getThumbnailUri());
+ assertEquals(programA.getTitle(), programB.getTitle());
+ assertEquals(programA.getVideoHeight(), programB.getVideoHeight());
+ assertEquals(programA.getVideoWidth(), programB.getVideoWidth());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ assertEquals(programA.isSearchable(), programB.isSearchable());
+ assertEquals(programA.getInternalProviderFlag1(), programB.getInternalProviderFlag1());
+ assertEquals(programA.getInternalProviderFlag2(), programB.getInternalProviderFlag2());
+ assertEquals(programA.getInternalProviderFlag3(), programB.getInternalProviderFlag3());
+ assertEquals(programA.getInternalProviderFlag4(), programB.getInternalProviderFlag4());
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ assertTrue(Objects.equals(programA.getSeasonTitle(), programB.getSeasonTitle()));
+ assertTrue(Objects.equals(programA.isRecordingProhibited(),
+ programB.isRecordingProhibited()));
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ assertEquals(programA.getReviewRatingStyle(), programB.getReviewRatingStyle());
+ assertEquals(programA.getReviewRating(), programB.getReviewRating());
+ }
+ if (includeIdAndProtectedFields) {
+ // Skip row ID since the one from system DB has the valid ID while the other does not.
+ assertEquals(programA.getId(), programB.getId());
+ assertEquals(programA.getPackageName(), programB.getPackageName());
+ assertEquals(programA.toContentValues(), programB.toContentValues());
+ }
+ }
+
+ private static MatrixCursor getProgramCursor(String[] projection, ContentValues contentValues) {
+ MatrixCursor cursor = new MatrixCursor(projection);
+ MatrixCursor.RowBuilder builder = cursor.newRow();
+ for (String row : projection) {
+ if (row != null) {
+ builder.add(row, contentValues.get(row));
+ }
+ }
+ cursor.moveToFirst();
+ return cursor;
+ }
+}
diff --git a/android/support/media/tv/TvContractUtilsTest.java b/android/support/media/tv/TvContractUtilsTest.java
new file mode 100644
index 00000000..bdb739f1
--- /dev/null
+++ b/android/support/media/tv/TvContractUtilsTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.media.tv;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.media.tv.TvContentRating;
+import android.os.Build;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+public class TvContractUtilsTest {
+
+ @Test
+ public void testStringToContentRatings_nullInput() {
+ assertArrayEquals(TvContractUtils.EMPTY, TvContractUtils.stringToContentRatings(null));
+ }
+
+ @Test
+ public void testStringToContentRatings_emptyInput() {
+ assertArrayEquals(TvContractUtils.EMPTY, TvContractUtils.stringToContentRatings(""));
+ }
+
+ @Test
+ public void testStringToContentRatings_singleRating() {
+ TvContentRating[] ratings = new TvContentRating[1];
+ ratings[0] = TvContentRating.createRating(
+ "com.android.tv",
+ "US_TV",
+ "US_TV_PG",
+ "US_TV_D",
+ "US_TV_L",
+ "US_TV_S",
+ "US_TV_V");
+ assertArrayEquals(ratings, TvContractUtils.stringToContentRatings(
+ "com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_L/US_TV_S/US_TV_V"));
+ }
+
+ @Test
+ public void testStringToContentRatings_multipleRatings() {
+ TvContentRating[] ratings = new TvContentRating[3];
+ ratings[0] = TvContentRating.createRating(
+ "com.android.tv",
+ "US_MV",
+ "US_MV_NC17");
+ ratings[1] = TvContentRating.createRating(
+ "com.android.tv",
+ "US_TV",
+ "US_TV_Y7");
+ ratings[2] = TvContentRating.createRating(
+ "com.android.tv",
+ "US_TV",
+ "US_TV_PG",
+ "US_TV_D",
+ "US_TV_L",
+ "US_TV_S",
+ "US_TV_V");
+ assertArrayEquals(ratings, TvContractUtils.stringToContentRatings(
+ "com.android.tv/US_MV/US_MV_NC17,"
+ + "com.android.tv/US_TV/US_TV_Y7,"
+ + "com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_L/US_TV_S/US_TV_V"));
+ }
+
+ @Test
+ public void testStringToContentRatings_allRatingsInvalid() {
+ assertArrayEquals(TvContractUtils.EMPTY, TvContractUtils.stringToContentRatings(
+ "com.android.tv/US_MV," // Invalid
+ + "com.android.tv")); // Invalid
+ }
+
+ @Test
+ public void testStringToContentRatings_someRatingsInvalid() {
+ TvContentRating[] ratings = new TvContentRating[1];
+ ratings[0] = TvContentRating.createRating(
+ "com.android.tv",
+ "US_TV",
+ "US_TV_PG",
+ "US_TV_D",
+ "US_TV_L",
+ "US_TV_S",
+ "US_TV_V");
+ assertArrayEquals(ratings, TvContractUtils.stringToContentRatings(
+ "com.android.tv/US_MV," // Invalid
+ + "com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_L/US_TV_S/US_TV_V," // Valid
+ + "com.android.tv")); // Invalid
+ }
+
+ @Test
+ public void testContentRatingsToString_nullInput() {
+ assertEquals(null, TvContractUtils.contentRatingsToString(null));
+ }
+
+ @Test
+ public void testContentRatingsToString_emptyInput() {
+ assertEquals(null, TvContractUtils.contentRatingsToString(new TvContentRating[0]));
+ }
+
+ @Test
+ public void testContentRatingsToString_singleRating() {
+ TvContentRating[] ratings = new TvContentRating[1];
+ ratings[0] = TvContentRating.createRating(
+ "com.android.tv",
+ "US_TV",
+ "US_TV_PG",
+ "US_TV_D",
+ "US_TV_L",
+ "US_TV_S",
+ "US_TV_V");
+ assertEquals("com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_L/US_TV_S/US_TV_V",
+ TvContractUtils.contentRatingsToString(ratings));
+ }
+
+ @Test
+ public void testContentRatingsToString_multipleRatings() {
+ TvContentRating[] ratings = new TvContentRating[3];
+ ratings[0] = TvContentRating.createRating(
+ "com.android.tv",
+ "US_MV",
+ "US_MV_NC17");
+ ratings[1] = TvContentRating.createRating(
+ "com.android.tv",
+ "US_TV",
+ "US_TV_PG",
+ "US_TV_D",
+ "US_TV_L",
+ "US_TV_S",
+ "US_TV_V");
+ ratings[2] = TvContentRating.createRating(
+ "com.android.tv",
+ "US_TV",
+ "US_TV_Y7");
+ String ratingString = "com.android.tv/US_MV/US_MV_NC17,"
+ + "com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_L/US_TV_S/US_TV_V,"
+ + "com.android.tv/US_TV/US_TV_Y7";
+ assertEquals(ratingString, TvContractUtils.contentRatingsToString(ratings));
+ }
+}
diff --git a/android/support/car/drawer/DrawerItemClickListener.java b/android/support/media/tv/Utils.java
index d707dbd0..a6ff0ad9 100644
--- a/android/support/car/drawer/DrawerItemClickListener.java
+++ b/android/support/media/tv/Utils.java
@@ -14,16 +14,15 @@
* limitations under the License.
*/
-package android.support.car.drawer;
+package android.support.media.tv;
-/**
- * Listener for handling clicks on items/views managed by {@link DrawerItemViewHolder}.
- */
-public interface DrawerItemClickListener {
- /**
- * Callback when item is clicked.
- *
- * @param position Adapter position of the clicked item.
- */
- void onItemClick(int position);
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+public class Utils {
+ private Utils() { }
+
+ public static boolean hasTvInputFramework(Context context) {
+ return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LIVE_TV);
+ }
}
diff --git a/android/support/media/tv/WatchNextProgram.java b/android/support/media/tv/WatchNextProgram.java
index c192745c..61082aac 100644
--- a/android/support/media/tv/WatchNextProgram.java
+++ b/android/support/media/tv/WatchNextProgram.java
@@ -17,7 +17,6 @@ package android.support.media.tv;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import android.annotation.TargetApi;
import android.content.ContentValues;
import android.database.Cursor;
import android.media.tv.TvContentRating; // For javadoc gen of super class
@@ -79,7 +78,6 @@ import java.lang.annotation.RetentionPolicy;
* null, null);
* </pre>
*/
-@TargetApi(26)
public final class WatchNextProgram extends BasePreviewProgram {
/**
* @hide
diff --git a/android/support/media/tv/WatchNextProgramTest.java b/android/support/media/tv/WatchNextProgramTest.java
new file mode 100644
index 00000000..ecce068f
--- /dev/null
+++ b/android/support/media/tv/WatchNextProgramTest.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.media.tv;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.media.tv.TvContentRating;
+import android.net.Uri;
+import android.support.media.tv.TvContractCompat.WatchNextPrograms;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * Tests that watch next programs can be created using the Builder pattern and correctly obtain
+ * values from them.
+ */
+@SmallTest
+@SdkSuppress(minSdkVersion = 26)
+@RunWith(AndroidJUnit4.class)
+public class WatchNextProgramTest {
+
+ @Before
+ public void tearDown() {
+ if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
+ return;
+ }
+ ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+ resolver.delete(WatchNextPrograms.CONTENT_URI, null, null);
+ }
+
+ @Test
+ public void testEmptyPreviewProgram() {
+ WatchNextProgram emptyProgram = new WatchNextProgram.Builder().build();
+ ContentValues contentValues = emptyProgram.toContentValues(true);
+ compareProgram(emptyProgram,
+ WatchNextProgram.fromCursor(getProgramCursor(Program.PROJECTION, contentValues)),
+ true);
+ }
+
+ @Test
+ public void testSampleProgram() {
+ WatchNextProgram sampleProgram = new WatchNextProgram.Builder()
+ .setTitle("Program Title")
+ .setDescription("This is a sample program")
+ .setEpisodeNumber(5)
+ .setSeasonNumber("The Final Season", 7)
+ .setThumbnailUri(Uri.parse("http://www.example.com/programs/poster.png"))
+ .build();
+ ContentValues contentValues = sampleProgram.toContentValues(true);
+ compareProgram(sampleProgram,
+ WatchNextProgram.fromCursor(
+ getProgramCursor(WatchNextProgram.PROJECTION, contentValues)), true);
+
+ WatchNextProgram clonedSampleProgram = new WatchNextProgram.Builder(sampleProgram).build();
+ compareProgram(sampleProgram, clonedSampleProgram, true);
+ }
+
+ @Test
+ public void testFullyPopulatedProgram() {
+ WatchNextProgram fullyPopulatedProgram = createFullyPopulatedWatchNextProgram();
+ ContentValues contentValues = fullyPopulatedProgram.toContentValues(true);
+ compareProgram(fullyPopulatedProgram,
+ WatchNextProgram.fromCursor(
+ getProgramCursor(WatchNextProgram.PROJECTION, contentValues)), true);
+
+ WatchNextProgram clonedFullyPopulatedProgram =
+ new WatchNextProgram.Builder(fullyPopulatedProgram).build();
+ compareProgram(fullyPopulatedProgram, clonedFullyPopulatedProgram, true);
+ }
+
+ @Test
+ public void testChannelWithSystemContentProvider() {
+ if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
+ return;
+ }
+ WatchNextProgram fullyPopulatedProgram = createFullyPopulatedWatchNextProgram();
+ ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+ Uri watchNextProgramUri = resolver.insert(WatchNextPrograms.CONTENT_URI,
+ fullyPopulatedProgram.toContentValues());
+
+ WatchNextProgram programFromSystemDb =
+ loadWatchNextProgramFromContentProvider(resolver, watchNextProgramUri);
+ compareProgram(fullyPopulatedProgram, programFromSystemDb, false);
+ }
+
+ @Test
+ public void testWatchNextProgramUpdateWithContentProvider() {
+ if (!Utils.hasTvInputFramework(InstrumentationRegistry.getContext())) {
+ return;
+ }
+
+ WatchNextProgram fullyPopulatedProgram = createFullyPopulatedWatchNextProgram();
+ ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+ Uri watchNextProgramUri = resolver.insert(WatchNextPrograms.CONTENT_URI,
+ fullyPopulatedProgram.toContentValues());
+
+ WatchNextProgram programFromSystemDb =
+ loadWatchNextProgramFromContentProvider(resolver, watchNextProgramUri);
+ compareProgram(fullyPopulatedProgram, programFromSystemDb, false);
+
+ // Update a field from a fully loaded watch-next program.
+ WatchNextProgram updatedProgram = new WatchNextProgram.Builder(programFromSystemDb)
+ .setInteractionCount(programFromSystemDb.getInteractionCount() + 1).build();
+ assertEquals(1, resolver.update(
+ watchNextProgramUri, updatedProgram.toContentValues(), null, null));
+ programFromSystemDb =
+ loadWatchNextProgramFromContentProvider(resolver, watchNextProgramUri);
+ compareProgram(updatedProgram, programFromSystemDb, false);
+
+ // Update a field with null from a fully loaded watch-next program.
+ updatedProgram = new WatchNextProgram.Builder(updatedProgram)
+ .setPreviewVideoUri(null).build();
+ assertEquals(1, resolver.update(
+ watchNextProgramUri, updatedProgram.toContentValues(), null, null));
+ programFromSystemDb = loadWatchNextProgramFromContentProvider(
+ resolver, watchNextProgramUri);
+ compareProgram(updatedProgram, programFromSystemDb, false);
+
+ // Update a field without referencing fully watch-next program.
+ ContentValues values = new PreviewProgram.Builder().setInteractionCount(1).build()
+ .toContentValues();
+ assertEquals(1, values.size());
+ assertEquals(1, resolver.update(watchNextProgramUri, values, null, null));
+ programFromSystemDb = loadWatchNextProgramFromContentProvider(
+ resolver, watchNextProgramUri);
+ WatchNextProgram expectedProgram = new WatchNextProgram.Builder(programFromSystemDb)
+ .setInteractionCount(1).build();
+ compareProgram(expectedProgram, programFromSystemDb, false);
+ }
+
+ @Test
+ public void testWatchNextProgramEquals() {
+ assertEquals(createFullyPopulatedWatchNextProgram(),
+ createFullyPopulatedWatchNextProgram());
+ }
+
+ private static WatchNextProgram loadWatchNextProgramFromContentProvider(
+ ContentResolver resolver, Uri watchNextProgramUri) {
+ try (Cursor cursor = resolver.query(watchNextProgramUri, null, null, null, null)) {
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ cursor.moveToNext();
+ return WatchNextProgram.fromCursor(cursor);
+ }
+ }
+
+ @Test
+ public void testWatchNextProgramWithPartialData() {
+ WatchNextProgram previewProgram = new WatchNextProgram.Builder()
+ .setInternalProviderId("ID-4321")
+ .setPreviewVideoUri(Uri.parse("http://example.com/preview-video.mpg"))
+ .setLastPlaybackPositionMillis(0)
+ .setDurationMillis(60 * 1000)
+ .setIntentUri(Uri.parse(new Intent(Intent.ACTION_VIEW).toUri(
+ Intent.URI_INTENT_SCHEME)))
+ .setTransient(false)
+ .setType(WatchNextPrograms.TYPE_TV_EPISODE)
+ .setPosterArtAspectRatio(WatchNextPrograms.ASPECT_RATIO_3_2)
+ .setThumbnailAspectRatio(WatchNextPrograms.ASPECT_RATIO_16_9)
+ .setLogoUri(Uri.parse("http://example.com/program-logo.mpg"))
+ .setAvailability(WatchNextPrograms.AVAILABILITY_FREE_WITH_SUBSCRIPTION)
+ .setStartingPrice("9.99 USD")
+ .setOfferPrice("3.99 USD")
+ .setReleaseDate(new Date(97, 2, 8))
+ .setLive(false)
+ .setInteractionType(WatchNextPrograms.INTERACTION_TYPE_VIEWS)
+ .setInteractionCount(99200)
+ .setAuthor("author_name")
+ .setReviewRatingStyle(WatchNextPrograms.REVIEW_RATING_STYLE_PERCENTAGE)
+ .setReviewRating("83.9")
+ .setId(10)
+ .setTitle("Recommended Video 1")
+ .setDescription("You should watch this!")
+ .setPosterArtUri(Uri.parse("http://example.com/poster.png"))
+ .setInternalProviderFlag2(0x0010010084108410L)
+ .build();
+
+ String[] partialProjection = {
+ WatchNextPrograms._ID,
+ WatchNextPrograms.COLUMN_TITLE,
+ WatchNextPrograms.COLUMN_SHORT_DESCRIPTION,
+ WatchNextPrograms.COLUMN_POSTER_ART_URI,
+ WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
+ WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID,
+ WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI,
+ WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS,
+ WatchNextPrograms.COLUMN_DURATION_MILLIS,
+ WatchNextPrograms.COLUMN_INTENT_URI,
+ WatchNextPrograms.COLUMN_TRANSIENT,
+ WatchNextPrograms.COLUMN_TYPE,
+ WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO,
+ WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO,
+ WatchNextPrograms.COLUMN_LOGO_URI,
+ WatchNextPrograms.COLUMN_AVAILABILITY,
+ WatchNextPrograms.COLUMN_STARTING_PRICE,
+ WatchNextPrograms.COLUMN_OFFER_PRICE,
+ WatchNextPrograms.COLUMN_RELEASE_DATE,
+ WatchNextPrograms.COLUMN_ITEM_COUNT,
+ WatchNextPrograms.COLUMN_LIVE,
+ WatchNextPrograms.COLUMN_INTERACTION_TYPE,
+ WatchNextPrograms.COLUMN_INTERACTION_COUNT,
+ WatchNextPrograms.COLUMN_AUTHOR,
+ WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE,
+ WatchNextPrograms.COLUMN_REVIEW_RATING,
+ };
+
+ ContentValues contentValues = previewProgram.toContentValues(true);
+ compareProgram(previewProgram,
+ WatchNextProgram.fromCursor(getProgramCursor(partialProjection, contentValues)),
+ true);
+
+ WatchNextProgram clonedFullyPopulatedProgram =
+ new WatchNextProgram.Builder(previewProgram).build();
+ compareProgram(previewProgram, clonedFullyPopulatedProgram, true);
+ }
+
+ private static WatchNextProgram createFullyPopulatedWatchNextProgram() {
+ return new WatchNextProgram.Builder()
+ .setTitle("Google")
+ .setInternalProviderId("ID-4321")
+ .setPreviewVideoUri(Uri.parse("http://example.com/preview-video.mpg"))
+ .setLastPlaybackPositionMillis(0)
+ .setDurationMillis(60 * 1000)
+ .setIntentUri(Uri.parse(new Intent(Intent.ACTION_VIEW).toUri(
+ Intent.URI_INTENT_SCHEME)))
+ .setTransient(false)
+ .setType(WatchNextPrograms.TYPE_MOVIE)
+ .setWatchNextType(WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE)
+ .setPosterArtAspectRatio(WatchNextPrograms.ASPECT_RATIO_2_3)
+ .setThumbnailAspectRatio(WatchNextPrograms.ASPECT_RATIO_16_9)
+ .setLogoUri(Uri.parse("http://example.com/program-logo.mpg"))
+ .setAvailability(WatchNextPrograms.AVAILABILITY_AVAILABLE)
+ .setStartingPrice("12.99 USD")
+ .setOfferPrice("4.99 USD")
+ .setReleaseDate("1997")
+ .setItemCount(3)
+ .setLive(false)
+ .setInteractionType(WatchNextPrograms.INTERACTION_TYPE_LIKES)
+ .setInteractionCount(10200)
+ .setAuthor("author_name")
+ .setReviewRatingStyle(WatchNextPrograms.REVIEW_RATING_STYLE_STARS)
+ .setReviewRating("4.5")
+ .setSearchable(false)
+ .setThumbnailUri(Uri.parse("http://example.com/thumbnail.png"))
+ .setAudioLanguages(new String [] {"eng", "kor"})
+ .setCanonicalGenres(new String[] {TvContractCompat.Programs.Genres.MOVIES})
+ .setContentRatings(new TvContentRating[] {
+ TvContentRating.createRating("com.android.tv", "US_TV", "US_TV_Y7")})
+ .setDescription("This is a sample program")
+ .setEpisodeNumber("Pilot", 0)
+ .setEpisodeTitle("Hello World")
+ .setLongDescription("This is a longer description than the previous description")
+ .setPosterArtUri(Uri.parse("http://example.com/poster.png"))
+ .setSeasonNumber("The Final Season", 7)
+ .setSeasonTitle("The Final Season")
+ .setVideoHeight(1080)
+ .setVideoWidth(1920)
+ .setInternalProviderFlag1(0x4)
+ .setInternalProviderFlag2(0x3)
+ .setInternalProviderFlag3(0x2)
+ .setInternalProviderFlag4(0x1)
+ .setBrowsable(true)
+ .setContentId("CID-8442")
+ .build();
+ }
+
+ private static void compareProgram(WatchNextProgram programA, WatchNextProgram programB,
+ boolean includeIdAndProtectedFields) {
+ assertTrue(Arrays.equals(programA.getAudioLanguages(), programB.getAudioLanguages()));
+ assertTrue(Arrays.deepEquals(programA.getCanonicalGenres(), programB.getCanonicalGenres()));
+ assertTrue(Arrays.deepEquals(programA.getContentRatings(), programB.getContentRatings()));
+ assertEquals(programA.getDescription(), programB.getDescription());
+ assertEquals(programA.getEpisodeNumber(), programB.getEpisodeNumber());
+ assertEquals(programA.getEpisodeTitle(), programB.getEpisodeTitle());
+ assertEquals(programA.getLongDescription(), programB.getLongDescription());
+ assertEquals(programA.getPosterArtUri(), programB.getPosterArtUri());
+ assertEquals(programA.getSeasonNumber(), programB.getSeasonNumber());
+ assertEquals(programA.getThumbnailUri(), programB.getThumbnailUri());
+ assertEquals(programA.getTitle(), programB.getTitle());
+ assertEquals(programA.getVideoHeight(), programB.getVideoHeight());
+ assertEquals(programA.getVideoWidth(), programB.getVideoWidth());
+ assertEquals(programA.isSearchable(), programB.isSearchable());
+ assertEquals(programA.getInternalProviderFlag1(), programB.getInternalProviderFlag1());
+ assertEquals(programA.getInternalProviderFlag2(), programB.getInternalProviderFlag2());
+ assertEquals(programA.getInternalProviderFlag3(), programB.getInternalProviderFlag3());
+ assertEquals(programA.getInternalProviderFlag4(), programB.getInternalProviderFlag4());
+ assertTrue(Objects.equals(programA.getSeasonTitle(), programB.getSeasonTitle()));
+ assertEquals(programA.getInternalProviderId(), programB.getInternalProviderId());
+ assertEquals(programA.getPreviewVideoUri(), programB.getPreviewVideoUri());
+ assertEquals(programA.getLastPlaybackPositionMillis(),
+ programB.getLastPlaybackPositionMillis());
+ assertEquals(programA.getDurationMillis(), programB.getDurationMillis());
+ assertEquals(programA.getIntentUri(), programB.getIntentUri());
+ assertEquals(programA.isTransient(), programB.isTransient());
+ assertEquals(programA.getType(), programB.getType());
+ assertEquals(programA.getWatchNextType(), programB.getWatchNextType());
+ assertEquals(programA.getPosterArtAspectRatio(), programB.getPosterArtAspectRatio());
+ assertEquals(programA.getThumbnailAspectRatio(), programB.getThumbnailAspectRatio());
+ assertEquals(programA.getLogoUri(), programB.getLogoUri());
+ assertEquals(programA.getAvailability(), programB.getAvailability());
+ assertEquals(programA.getStartingPrice(), programB.getStartingPrice());
+ assertEquals(programA.getOfferPrice(), programB.getOfferPrice());
+ assertEquals(programA.getReleaseDate(), programB.getReleaseDate());
+ assertEquals(programA.getItemCount(), programB.getItemCount());
+ assertEquals(programA.isLive(), programB.isLive());
+ assertEquals(programA.getInteractionType(), programB.getInteractionType());
+ assertEquals(programA.getInteractionCount(), programB.getInteractionCount());
+ assertEquals(programA.getAuthor(), programB.getAuthor());
+ assertEquals(programA.getReviewRatingStyle(), programB.getReviewRatingStyle());
+ assertEquals(programA.getReviewRating(), programB.getReviewRating());
+ assertEquals(programA.getContentId(), programB.getContentId());
+ if (includeIdAndProtectedFields) {
+ // Skip row ID since the one from system DB has the valid ID while the other does not.
+ assertEquals(programA.getId(), programB.getId());
+ // When we insert a channel using toContentValues() to the system, we drop some
+ // protected fields since they only can be modified by system apps.
+ assertEquals(programA.isBrowsable(), programB.isBrowsable());
+ assertEquals(programA.toContentValues(), programB.toContentValues());
+ assertEquals(programA, programB);
+ }
+ }
+
+ private static MatrixCursor getProgramCursor(String[] projection, ContentValues contentValues) {
+ MatrixCursor cursor = new MatrixCursor(projection);
+ MatrixCursor.RowBuilder builder = cursor.newRow();
+ for (String col : projection) {
+ if (col != null) {
+ builder.add(col, contentValues.get(col));
+ }
+ }
+ cursor.moveToFirst();
+ return cursor;
+ }
+}
diff --git a/android/support/mediacompat/testlib/util/PollingCheck.java b/android/support/mediacompat/testlib/util/PollingCheck.java
index 3412da02..47344c03 100644
--- a/android/support/mediacompat/testlib/util/PollingCheck.java
+++ b/android/support/mediacompat/testlib/util/PollingCheck.java
@@ -16,7 +16,7 @@
package android.support.mediacompat.testlib.util;
-import junit.framework.Assert;
+import static org.junit.Assert.fail;
/**
* Utility used for testing that allows to poll for a certain condition to happen within a timeout.
@@ -56,7 +56,7 @@ public abstract class PollingCheck {
try {
Thread.sleep(TIME_SLICE);
} catch (InterruptedException e) {
- Assert.fail("unexpected InterruptedException");
+ fail("unexpected InterruptedException");
}
if (check()) {
@@ -66,7 +66,7 @@ public abstract class PollingCheck {
timeout -= TIME_SLICE;
}
- Assert.fail("unexpected timeout");
+ fail("unexpected timeout");
}
/**
diff --git a/android/support/mediacompat/testlib/util/TestUtil.java b/android/support/mediacompat/testlib/util/TestUtil.java
index d105510c..21fd223e 100644
--- a/android/support/mediacompat/testlib/util/TestUtil.java
+++ b/android/support/mediacompat/testlib/util/TestUtil.java
@@ -16,8 +16,8 @@
package android.support.mediacompat.testlib.util;
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertSame;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
import android.os.Bundle;
diff --git a/android/support/text/emoji/EmojiCompat.java b/android/support/text/emoji/EmojiCompat.java
index 5436aa20..413a9dd4 100644
--- a/android/support/text/emoji/EmojiCompat.java
+++ b/android/support/text/emoji/EmojiCompat.java
@@ -184,6 +184,16 @@ public class EmojiCompat {
private final boolean mReplaceAll;
/**
+ * @see Config#setUseEmojiAsDefaultStyle(boolean)
+ */
+ private final boolean mUseEmojiAsDefaultStyle;
+
+ /**
+ * @see Config#setUseEmojiAsDefaultStyle(boolean, List)
+ */
+ private final int[] mEmojiAsDefaultStyleExceptions;
+
+ /**
* @see Config#setEmojiSpanIndicatorEnabled(boolean)
*/
private final boolean mEmojiSpanIndicatorEnabled;
@@ -201,6 +211,8 @@ public class EmojiCompat {
private EmojiCompat(@NonNull final Config config) {
mInitLock = new ReentrantReadWriteLock();
mReplaceAll = config.mReplaceAll;
+ mUseEmojiAsDefaultStyle = config.mUseEmojiAsDefaultStyle;
+ mEmojiAsDefaultStyleExceptions = config.mEmojiAsDefaultStyleExceptions;
mEmojiSpanIndicatorEnabled = config.mEmojiSpanIndicatorEnabled;
mEmojiSpanIndicatorColor = config.mEmojiSpanIndicatorColor;
mMetadataLoader = config.mMetadataLoader;
@@ -787,6 +799,8 @@ public class EmojiCompat {
public abstract static class Config {
private final MetadataRepoLoader mMetadataLoader;
private boolean mReplaceAll;
+ private boolean mUseEmojiAsDefaultStyle;
+ private int[] mEmojiAsDefaultStyleExceptions;
private Set<InitCallback> mInitCallbacks;
private boolean mEmojiSpanIndicatorEnabled;
private int mEmojiSpanIndicatorColor = Color.GREEN;
@@ -849,6 +863,56 @@ public class EmojiCompat {
}
/**
+ * Determines whether EmojiCompat should use the emoji presentation style for emojis
+ * that have text style as default. By default, the text style would be used, unless these
+ * are followed by the U+FE0F variation selector.
+ * Details about emoji presentation and text presentation styles can be found here:
+ * http://unicode.org/reports/tr51/#Presentation_Style
+ * If useEmojiAsDefaultStyle is true, the emoji presentation style will be used for all
+ * emojis, including potentially unexpected ones (such as digits or other keycap emojis). If
+ * this is not the expected behaviour, method
+ * {@link #setUseEmojiAsDefaultStyle(boolean, List)} can be used to specify the
+ * exception emojis that should be still presented as text style.
+ *
+ * @param useEmojiAsDefaultStyle whether to use the emoji style presentation for all emojis
+ * that would be presented as text style by default
+ */
+ public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle) {
+ return setUseEmojiAsDefaultStyle(useEmojiAsDefaultStyle, null);
+ }
+
+ /**
+ * @see #setUseEmojiAsDefaultStyle(boolean)
+ *
+ * @param emojiAsDefaultStyleExceptions Contains the exception emojis which will be still
+ * presented as text style even if the
+ * useEmojiAsDefaultStyle flag is set to {@code true}.
+ * This list will be ignored if useEmojiAsDefaultStyle
+ * is {@code false}. Note that emojis with default
+ * emoji style presentation will remain emoji style
+ * regardless the value of useEmojiAsDefaultStyle or
+ * whether they are included in the exceptions list or
+ * not. When no exception is wanted, the method
+ * {@link #setUseEmojiAsDefaultStyle(boolean)} should
+ * be used instead.
+ */
+ public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle,
+ @Nullable final List<Integer> emojiAsDefaultStyleExceptions) {
+ mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle;
+ if (mUseEmojiAsDefaultStyle && emojiAsDefaultStyleExceptions != null) {
+ mEmojiAsDefaultStyleExceptions = new int[emojiAsDefaultStyleExceptions.size()];
+ int i = 0;
+ for (Integer exception : emojiAsDefaultStyleExceptions) {
+ mEmojiAsDefaultStyleExceptions[i++] = exception;
+ }
+ Arrays.sort(mEmojiAsDefaultStyleExceptions);
+ } else {
+ mEmojiAsDefaultStyleExceptions = null;
+ }
+ return this;
+ }
+
+ /**
* Determines whether a background will be drawn for the emojis that are found and
* replaced by EmojiCompat. Should be used only for debugging purposes. The indicator color
* can be set using {@link #setEmojiSpanIndicatorColor(int)}.
@@ -1020,7 +1084,9 @@ public class EmojiCompat {
}
mMetadataRepo = metadataRepo;
- mProcessor = new EmojiProcessor(mMetadataRepo, new SpanFactory());
+ mProcessor = new EmojiProcessor(mMetadataRepo, new SpanFactory(),
+ mEmojiCompat.mUseEmojiAsDefaultStyle,
+ mEmojiCompat.mEmojiAsDefaultStyleExceptions);
mEmojiCompat.onMetadataLoadSuccess();
}
diff --git a/android/support/text/emoji/EmojiMetadata.java b/android/support/text/emoji/EmojiMetadata.java
index 488dcf9b..7d495ede 100644
--- a/android/support/text/emoji/EmojiMetadata.java
+++ b/android/support/text/emoji/EmojiMetadata.java
@@ -26,12 +26,13 @@ import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
-import android.support.text.emoji.flatbuffer.MetadataItem;
-import android.support.text.emoji.flatbuffer.MetadataList;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import androidx.text.emoji.flatbuffer.MetadataItem;
+import androidx.text.emoji.flatbuffer.MetadataList;
+
/**
* Information about a single emoji.
*
diff --git a/android/support/text/emoji/EmojiProcessor.java b/android/support/text/emoji/EmojiProcessor.java
index 3feb36d6..f711704e 100644
--- a/android/support/text/emoji/EmojiProcessor.java
+++ b/android/support/text/emoji/EmojiProcessor.java
@@ -22,6 +22,7 @@ import android.support.annotation.AnyThread;
import android.support.annotation.IntDef;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
import android.support.text.emoji.widget.SpannableBuilder;
@@ -40,6 +41,8 @@ import android.view.inputmethod.InputConnection;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.List;
/**
* Processes the CharSequence and adds the emojis.
@@ -90,14 +93,29 @@ final class EmojiProcessor {
*/
private GlyphChecker mGlyphChecker = new GlyphChecker();
+ /**
+ * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean)
+ */
+ private final boolean mUseEmojiAsDefaultStyle;
+
+ /**
+ * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean, List)
+ */
+ private final int[] mEmojiAsDefaultStyleExceptions;
+
EmojiProcessor(@NonNull final MetadataRepo metadataRepo,
- @NonNull final EmojiCompat.SpanFactory spanFactory) {
+ @NonNull final EmojiCompat.SpanFactory spanFactory,
+ final boolean useEmojiAsDefaultStyle,
+ @Nullable final int[] emojiAsDefaultStyleExceptions) {
mSpanFactory = spanFactory;
mMetadataRepo = metadataRepo;
+ mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle;
+ mEmojiAsDefaultStyleExceptions = emojiAsDefaultStyleExceptions;
}
EmojiMetadata getEmojiMetadata(@NonNull final CharSequence charSequence) {
- final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode());
+ final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode(),
+ mUseEmojiAsDefaultStyle, mEmojiAsDefaultStyleExceptions);
final int end = charSequence.length();
int currentOffset = 0;
@@ -189,7 +207,8 @@ final class EmojiProcessor {
}
// add new ones
int addedCount = 0;
- final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode());
+ final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode(),
+ mUseEmojiAsDefaultStyle, mEmojiAsDefaultStyleExceptions);
int currentOffset = start;
int codePoint = Character.codePointAt(charSequence, currentOffset);
@@ -483,9 +502,22 @@ final class EmojiProcessor {
*/
private int mCurrentDepth;
- ProcessorSm(MetadataRepo.Node rootNode) {
+ /**
+ * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean)
+ */
+ private final boolean mUseEmojiAsDefaultStyle;
+
+ /**
+ * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean, List)
+ */
+ private final int[] mEmojiAsDefaultStyleExceptions;
+
+ ProcessorSm(MetadataRepo.Node rootNode, boolean useEmojiAsDefaultStyle,
+ int[] emojiAsDefaultStyleExceptions) {
mRootNode = rootNode;
mCurrentNode = rootNode;
+ mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle;
+ mEmojiAsDefaultStyleExceptions = emojiAsDefaultStyleExceptions;
}
@Action
@@ -505,8 +537,7 @@ final class EmojiProcessor {
action = ACTION_ADVANCE_END;
} else if (mCurrentNode.getData() != null) {
if (mCurrentDepth == 1) {
- if (mCurrentNode.getData().isDefaultEmoji()
- || isEmojiStyle(mLastCodepoint)) {
+ if (shouldUseEmojiPresentationStyleForSingleCodepoint()) {
mFlushNode = mCurrentNode;
action = ACTION_FLUSH;
reset();
@@ -571,9 +602,32 @@ final class EmojiProcessor {
*/
boolean isInFlushableState() {
return mState == STATE_WALKING && mCurrentNode.getData() != null
- && (mCurrentNode.getData().isDefaultEmoji()
- || isEmojiStyle(mLastCodepoint)
- || mCurrentDepth > 1);
+ && (mCurrentDepth > 1 || shouldUseEmojiPresentationStyleForSingleCodepoint());
+ }
+
+ private boolean shouldUseEmojiPresentationStyleForSingleCodepoint() {
+ if (mCurrentNode.getData().isDefaultEmoji()) {
+ // The codepoint is emoji style by default.
+ return true;
+ }
+ if (isEmojiStyle(mLastCodepoint)) {
+ // The codepoint was followed by the emoji style variation selector.
+ return true;
+ }
+ if (mUseEmojiAsDefaultStyle) {
+ // Emoji presentation style for text style default emojis is enabled. We have
+ // to check that the current codepoint is not an exception.
+ if (mEmojiAsDefaultStyleExceptions == null) {
+ return true;
+ }
+ final int codepoint = mCurrentNode.getData().getCodepointAt(0);
+ final int index = Arrays.binarySearch(mEmojiAsDefaultStyleExceptions, codepoint);
+ if (index < 0) {
+ // Index is negative, so the codepoint was not found in the array of exceptions.
+ return true;
+ }
+ }
+ return false;
}
/**
diff --git a/android/support/text/emoji/MetadataListReader.java b/android/support/text/emoji/MetadataListReader.java
index 1008c171..02856cb1 100644
--- a/android/support/text/emoji/MetadataListReader.java
+++ b/android/support/text/emoji/MetadataListReader.java
@@ -22,13 +22,14 @@ import android.support.annotation.AnyThread;
import android.support.annotation.IntRange;
import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
-import android.support.text.emoji.flatbuffer.MetadataList;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import androidx.text.emoji.flatbuffer.MetadataList;
+
/**
* Reads the emoji metadata from a given InputStream or ByteBuffer.
*
diff --git a/android/support/text/emoji/MetadataRepo.java b/android/support/text/emoji/MetadataRepo.java
index e86277e5..f5afec86 100644
--- a/android/support/text/emoji/MetadataRepo.java
+++ b/android/support/text/emoji/MetadataRepo.java
@@ -24,7 +24,6 @@ import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
import android.support.annotation.VisibleForTesting;
-import android.support.text.emoji.flatbuffer.MetadataList;
import android.support.v4.util.Preconditions;
import android.util.SparseArray;
@@ -32,6 +31,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
+import androidx.text.emoji.flatbuffer.MetadataList;
+
/**
* Class to hold the emoji metadata required to process and draw emojis.
*/
diff --git a/android/support/text/emoji/widget/EmojiButton.java b/android/support/text/emoji/widget/EmojiButton.java
index 65afd9c7..752e0523 100644
--- a/android/support/text/emoji/widget/EmojiButton.java
+++ b/android/support/text/emoji/widget/EmojiButton.java
@@ -15,9 +15,9 @@
*/
package android.support.text.emoji.widget;
-import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
+import android.support.annotation.RequiresApi;
import android.text.InputFilter;
import android.util.AttributeSet;
import android.widget.Button;
@@ -50,7 +50,7 @@ public class EmojiButton extends Button {
init();
}
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
diff --git a/android/support/text/emoji/widget/EmojiEditText.java b/android/support/text/emoji/widget/EmojiEditText.java
index 70ca7a66..e1057e8f 100644
--- a/android/support/text/emoji/widget/EmojiEditText.java
+++ b/android/support/text/emoji/widget/EmojiEditText.java
@@ -15,11 +15,11 @@
*/
package android.support.text.emoji.widget;
-import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.IntRange;
import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
import android.support.text.emoji.EmojiCompat;
import android.text.method.KeyListener;
import android.util.AttributeSet;
@@ -57,7 +57,7 @@ public class EmojiEditText extends EditText {
init(attrs, defStyleAttr, 0 /*defStyleRes*/);
}
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs, defStyleAttr, defStyleRes);
diff --git a/android/support/text/emoji/widget/EmojiExtractEditText.java b/android/support/text/emoji/widget/EmojiExtractEditText.java
index 2e4d3caa..1d5e2dd7 100644
--- a/android/support/text/emoji/widget/EmojiExtractEditText.java
+++ b/android/support/text/emoji/widget/EmojiExtractEditText.java
@@ -18,12 +18,12 @@ package android.support.text.emoji.widget;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import android.annotation.TargetApi;
import android.content.Context;
import android.inputmethodservice.ExtractEditText;
import android.os.Build;
import android.support.annotation.IntRange;
import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
import android.support.text.emoji.EmojiCompat;
import android.support.text.emoji.EmojiSpan;
@@ -64,7 +64,7 @@ public class EmojiExtractEditText extends ExtractEditText {
init(attrs, defStyleAttr, 0 /*defStyleRes*/);
}
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
diff --git a/android/support/transition/ArcMotionTest.java b/android/support/transition/ArcMotionTest.java
new file mode 100644
index 00000000..75d61173
--- /dev/null
+++ b/android/support/transition/ArcMotionTest.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Path;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ArcMotionTest extends PathMotionTest {
+
+ @Test
+ public void test90Quadrants() {
+ ArcMotion arcMotion = new ArcMotion();
+ arcMotion.setMaximumAngle(90);
+
+ Path expected = arcWithPoint(0, 100, 100, 0, 100, 100);
+ Path path = arcMotion.getPath(0, 100, 100, 0);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(100, 0, 0, -100, 0, 0);
+ path = arcMotion.getPath(100, 0, 0, -100);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(0, -100, -100, 0, 0, 0);
+ path = arcMotion.getPath(0, -100, -100, 0);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(-100, 0, 0, 100, -100, 100);
+ path = arcMotion.getPath(-100, 0, 0, 100);
+ assertPathMatches(expected, path);
+ }
+
+ @Test
+ public void test345Triangles() {
+ // 3-4-5 triangles are easy to calculate the control points
+ ArcMotion arcMotion = new ArcMotion();
+ arcMotion.setMaximumAngle(90);
+ Path expected;
+ Path path;
+
+ expected = arcWithPoint(0, 120, 160, 0, 125, 120);
+ path = arcMotion.getPath(0, 120, 160, 0);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(0, 160, 120, 0, 120, 125);
+ path = arcMotion.getPath(0, 160, 120, 0);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(-120, 0, 0, 160, -120, 125);
+ path = arcMotion.getPath(-120, 0, 0, 160);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(-160, 0, 0, 120, -125, 120);
+ path = arcMotion.getPath(-160, 0, 0, 120);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(0, -120, -160, 0, -35, 0);
+ path = arcMotion.getPath(0, -120, -160, 0);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(0, -160, -120, 0, 0, -35);
+ path = arcMotion.getPath(0, -160, -120, 0);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(120, 0, 0, -160, 0, -35);
+ path = arcMotion.getPath(120, 0, 0, -160);
+ assertPathMatches(expected, path);
+
+ expected = arcWithPoint(160, 0, 0, -120, 35, 0);
+ path = arcMotion.getPath(160, 0, 0, -120);
+ assertPathMatches(expected, path);
+ }
+
+ private static Path arcWithPoint(float startX, float startY, float endX, float endY,
+ float eX, float eY) {
+ float c1x = (eX + startX) / 2;
+ float c1y = (eY + startY) / 2;
+ float c2x = (eX + endX) / 2;
+ float c2y = (eY + endY) / 2;
+ Path path = new Path();
+ path.moveTo(startX, startY);
+ path.cubicTo(c1x, c1y, c2x, c2y, endX, endY);
+ return path;
+ }
+
+ @Test
+ public void testMaximumAngle() {
+ ArcMotion arcMotion = new ArcMotion();
+ arcMotion.setMaximumAngle(45f);
+ assertEquals(45f, arcMotion.getMaximumAngle(), 0.0f);
+
+ float ratio = (float) Math.tan(Math.PI / 8);
+ float ex = 50 + (50 * ratio);
+ float ey = ex;
+
+ Path expected = arcWithPoint(0, 100, 100, 0, ex, ey);
+ Path path = arcMotion.getPath(0, 100, 100, 0);
+ assertPathMatches(expected, path);
+ }
+
+ @Test
+ public void testMinimumHorizontalAngle() {
+ ArcMotion arcMotion = new ArcMotion();
+ arcMotion.setMinimumHorizontalAngle(45);
+ assertEquals(45, arcMotion.getMinimumHorizontalAngle(), 0.0f);
+
+ float ex = 37.5f;
+ float ey = (float) (Math.tan(Math.PI / 4) * 50);
+ Path expected = arcWithPoint(0, 0, 100, 50, ex, ey);
+ Path path = arcMotion.getPath(0, 0, 100, 50);
+ assertPathMatches(expected, path);
+
+ // Pretty much the same, but follows a different path.
+ expected = arcWithPoint(0, 0, 100.001f, 50, ex, ey);
+ path = arcMotion.getPath(0, 0, 100.001f, 50);
+ assertPathMatches(expected, path);
+
+ // Moving in the opposite direction.
+ expected = arcWithPoint(100, 50, 0, 0, ex, ey);
+ path = arcMotion.getPath(100, 50, 0, 0);
+ assertPathMatches(expected, path);
+
+ // With x < y.
+ ex = 0;
+ ey = (float) (Math.tan(Math.PI / 4) * 62.5f);
+ expected = arcWithPoint(0, 0, 50, 100, ex, ey);
+ path = arcMotion.getPath(0, 0, 50, 100);
+ assertPathMatches(expected, path);
+
+ // Pretty much the same, but follows a different path.
+ expected = arcWithPoint(0, 0, 50, 100.001f, ex, ey);
+ path = arcMotion.getPath(0, 0, 50, 100.001f);
+ assertPathMatches(expected, path);
+
+ // Moving in the opposite direction.
+ expected = arcWithPoint(50, 100, 0, 0, ex, ey);
+ path = arcMotion.getPath(50, 100, 0, 0);
+ assertPathMatches(expected, path);
+ }
+
+ @Test
+ public void testMinimumVerticalAngle() {
+ ArcMotion arcMotion = new ArcMotion();
+ arcMotion.setMinimumVerticalAngle(45);
+ assertEquals(45, arcMotion.getMinimumVerticalAngle(), 0.0f);
+
+ float ex = 0;
+ float ey = 62.5f;
+ Path expected = arcWithPoint(0, 0, 50, 100, ex, ey);
+ Path path = arcMotion.getPath(0, 0, 50, 100);
+ assertPathMatches(expected, path);
+
+ // Pretty much the same, but follows a different path.
+ expected = arcWithPoint(0, 0, 50, 100.001f, ex, ey);
+ path = arcMotion.getPath(0, 0, 50, 100.001f);
+ assertPathMatches(expected, path);
+
+ // Moving in opposite direction.
+ expected = arcWithPoint(50, 100, 0, 0, ex, ey);
+ path = arcMotion.getPath(50, 100, 0, 0);
+ assertPathMatches(expected, path);
+
+ // With x > y.
+ ex = (float) (Math.tan(Math.PI / 4) * 37.5f);
+ ey = 50;
+ expected = arcWithPoint(0, 0, 100, 50, ex, ey);
+ path = arcMotion.getPath(0, 0, 100, 50);
+ assertPathMatches(expected, path);
+
+ // Pretty much the same, but follows a different path.
+ expected = arcWithPoint(0, 0, 100.001f, 50, ex, ey);
+ path = arcMotion.getPath(0, 0, 100.001f, 50);
+ assertPathMatches(expected, path);
+
+ // Moving in opposite direction.
+ expected = arcWithPoint(100, 50, 0, 0, ex, ey);
+ path = arcMotion.getPath(100, 50, 0, 0);
+ assertPathMatches(expected, path);
+
+ }
+
+}
diff --git a/android/support/transition/AutoTransitionTest.java b/android/support/transition/AutoTransitionTest.java
new file mode 100644
index 00000000..2c9a77f5
--- /dev/null
+++ b/android/support/transition/AutoTransitionTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2016 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.support.transition;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import android.graphics.Color;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.MediumTest;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@MediumTest
+public class AutoTransitionTest extends BaseTest {
+
+ private LinearLayout mRoot;
+ private View mView0;
+ private View mView1;
+
+ @UiThreadTest
+ @Before
+ public void setUp() {
+ mRoot = (LinearLayout) rule.getActivity().getRoot();
+ mView0 = new View(rule.getActivity());
+ mView0.setBackgroundColor(Color.RED);
+ mRoot.addView(mView0, new LinearLayout.LayoutParams(100, 100));
+ mView1 = new View(rule.getActivity());
+ mView1.setBackgroundColor(Color.BLUE);
+ mRoot.addView(mView1, new LinearLayout.LayoutParams(100, 100));
+ }
+
+ @LargeTest
+ @Test
+ public void testLayoutBetweenFadeAndChangeBounds() throws Throwable {
+ final LayoutCounter counter = new LayoutCounter();
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ assertThat(mView1.getY(), is(100.f));
+ assertThat(mView0.getVisibility(), is(View.VISIBLE));
+ mView1.addOnLayoutChangeListener(counter);
+ }
+ });
+ final SyncTransitionListener listener = new SyncTransitionListener(
+ SyncTransitionListener.EVENT_END);
+ final Transition transition = new AutoTransition();
+ transition.addListener(listener);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, transition);
+ // This makes view0 fade out and causes view1 to move upwards.
+ mView0.setVisibility(View.GONE);
+ }
+ });
+ assertThat("Timed out waiting for the TransitionListener",
+ listener.await(), is(true));
+ assertThat(mView1.getY(), is(0.f));
+ assertThat(mView0.getVisibility(), is(View.GONE));
+ counter.reset();
+ listener.reset();
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, transition);
+ // Revert
+ mView0.setVisibility(View.VISIBLE);
+ }
+ });
+ assertThat("Timed out waiting for the TransitionListener",
+ listener.await(), is(true));
+ assertThat(mView1.getY(), is(100.f));
+ assertThat(mView0.getVisibility(), is(View.VISIBLE));
+ }
+
+ private static class LayoutCounter implements View.OnLayoutChangeListener {
+
+ private int mCalledCount;
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ mCalledCount++;
+ // There should not be more than one layout request to view1.
+ if (mCalledCount > 1) {
+ fail("View layout happened too many times");
+ }
+ }
+
+ void reset() {
+ mCalledCount = 0;
+ }
+
+ }
+
+}
diff --git a/android/support/transition/BaseTest.java b/android/support/transition/BaseTest.java
new file mode 100644
index 00000000..4ffb2f90
--- /dev/null
+++ b/android/support/transition/BaseTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 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.support.transition;
+
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public abstract class BaseTest {
+
+ @Rule
+ public final ActivityTestRule<TransitionActivity> rule;
+
+ BaseTest() {
+ rule = new ActivityTestRule<>(TransitionActivity.class);
+ }
+
+}
diff --git a/android/support/transition/BaseTransitionTest.java b/android/support/transition/BaseTransitionTest.java
new file mode 100644
index 00000000..5d39d943
--- /dev/null
+++ b/android/support/transition/BaseTransitionTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.support.test.InstrumentationRegistry;
+import android.support.transition.test.R;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import org.junit.Before;
+
+import java.util.ArrayList;
+
+public abstract class BaseTransitionTest extends BaseTest {
+
+ ArrayList<View> mTransitionTargets = new ArrayList<>();
+ LinearLayout mRoot;
+ Transition mTransition;
+ Transition.TransitionListener mListener;
+ float mAnimatedValue;
+
+ @Before
+ public void setUp() {
+ InstrumentationRegistry.getInstrumentation().setInTouchMode(false);
+ mRoot = (LinearLayout) rule.getActivity().findViewById(R.id.root);
+ mTransitionTargets.clear();
+ mTransition = createTransition();
+ mListener = mock(Transition.TransitionListener.class);
+ mTransition.addListener(mListener);
+ }
+
+ Transition createTransition() {
+ return new TestTransition();
+ }
+
+ void waitForStart() {
+ verify(mListener, timeout(3000)).onTransitionStart(any(Transition.class));
+ }
+
+ void waitForEnd() {
+ verify(mListener, timeout(3000)).onTransitionEnd(any(Transition.class));
+ }
+
+ Scene loadScene(final int layoutId) throws Throwable {
+ final Scene[] scene = new Scene[1];
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ scene[0] = Scene.getSceneForLayout(mRoot, layoutId, rule.getActivity());
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ return scene[0];
+ }
+
+ void startTransition(final int layoutId) throws Throwable {
+ startTransition(loadScene(layoutId));
+ }
+
+ private void startTransition(final Scene scene) throws Throwable {
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.go(scene, mTransition);
+ }
+ });
+ waitForStart();
+ }
+
+ void enterScene(final int layoutId) throws Throwable {
+ enterScene(loadScene(layoutId));
+ }
+
+ void enterScene(final Scene scene) throws Throwable {
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ scene.enter();
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+
+ void resetListener() {
+ mTransition.removeListener(mListener);
+ mListener = mock(Transition.TransitionListener.class);
+ mTransition.addListener(mListener);
+ }
+
+ void setAnimatedValue(float animatedValue) {
+ mAnimatedValue = animatedValue;
+ }
+
+ public class TestTransition extends Visibility {
+
+ @Override
+ public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
+ TransitionValues endValues) {
+ mTransitionTargets.add(endValues.view);
+ return ObjectAnimator.ofFloat(BaseTransitionTest.this, "animatedValue", 0, 1);
+ }
+
+ @Override
+ public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues,
+ TransitionValues endValues) {
+ mTransitionTargets.add(startValues.view);
+ return ObjectAnimator.ofFloat(BaseTransitionTest.this, "animatedValue", 1, 0);
+ }
+
+ }
+
+}
diff --git a/android/support/transition/ChangeBoundsTest.java b/android/support/transition/ChangeBoundsTest.java
new file mode 100644
index 00000000..186017cf
--- /dev/null
+++ b/android/support/transition/ChangeBoundsTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+import android.support.test.filters.MediumTest;
+import android.support.transition.test.R;
+import android.view.View;
+import android.view.animation.LinearInterpolator;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Test;
+
+@MediumTest
+public class ChangeBoundsTest extends BaseTransitionTest {
+
+ @Override
+ Transition createTransition() {
+ final ChangeBounds changeBounds = new ChangeBounds();
+ changeBounds.setDuration(400);
+ changeBounds.setInterpolator(new LinearInterpolator());
+ return changeBounds;
+ }
+
+ @Test
+ public void testResizeClip() {
+ ChangeBounds changeBounds = (ChangeBounds) mTransition;
+ assertThat(changeBounds.getResizeClip(), is(false));
+ changeBounds.setResizeClip(true);
+ assertThat(changeBounds.getResizeClip(), is(true));
+ }
+
+ @Test
+ public void testBasic() throws Throwable {
+ enterScene(R.layout.scene1);
+ final ViewHolder startHolder = new ViewHolder(rule.getActivity());
+ assertThat(startHolder.red, is(atTop()));
+ assertThat(startHolder.green, is(below(startHolder.red)));
+ startTransition(R.layout.scene6);
+ waitForEnd();
+ final ViewHolder endHolder = new ViewHolder(rule.getActivity());
+ assertThat(endHolder.green, is(atTop()));
+ assertThat(endHolder.red, is(below(endHolder.green)));
+ }
+
+ private static TypeSafeMatcher<View> atTop() {
+ return new TypeSafeMatcher<View>() {
+ @Override
+ protected boolean matchesSafely(View view) {
+ return view.getTop() == 0;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("is placed at the top of its parent");
+ }
+ };
+ }
+
+ private static TypeSafeMatcher<View> below(final View other) {
+ return new TypeSafeMatcher<View>() {
+ @Override
+ protected boolean matchesSafely(View item) {
+ return other.getBottom() == item.getTop();
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("is placed below the specified view");
+ }
+ };
+ }
+
+ private static class ViewHolder {
+
+ public final View red;
+ public final View green;
+
+ ViewHolder(TransitionActivity activity) {
+ red = activity.findViewById(R.id.redSquare);
+ green = activity.findViewById(R.id.greenSquare);
+ }
+ }
+
+}
diff --git a/android/support/transition/ChangeClipBoundsTest.java b/android/support/transition/ChangeClipBoundsTest.java
new file mode 100644
index 00000000..c227bcac
--- /dev/null
+++ b/android/support/transition/ChangeClipBoundsTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+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 android.graphics.Rect;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.transition.test.R;
+import android.support.v4.view.ViewCompat;
+import android.view.View;
+
+import org.junit.Test;
+
+@MediumTest
+public class ChangeClipBoundsTest extends BaseTransitionTest {
+
+ @Override
+ Transition createTransition() {
+ return new ChangeClipBounds();
+ }
+
+ @SdkSuppress(minSdkVersion = 18)
+ @Test
+ public void testChangeClipBounds() throws Throwable {
+ enterScene(R.layout.scene1);
+
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+ final Rect newClip = new Rect(redSquare.getLeft() + 10, redSquare.getTop() + 10,
+ redSquare.getRight() - 10, redSquare.getBottom() - 10);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ assertNull(ViewCompat.getClipBounds(redSquare));
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ ViewCompat.setClipBounds(redSquare, newClip);
+ }
+ });
+ waitForStart();
+ Thread.sleep(150);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Rect midClip = ViewCompat.getClipBounds(redSquare);
+ assertNotNull(midClip);
+ assertTrue(midClip.left > 0 && midClip.left < newClip.left);
+ assertTrue(midClip.top > 0 && midClip.top < newClip.top);
+ assertTrue(midClip.right < redSquare.getRight() && midClip.right > newClip.right);
+ assertTrue(midClip.bottom < redSquare.getBottom()
+ && midClip.bottom > newClip.bottom);
+ }
+ });
+ waitForEnd();
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final Rect endRect = ViewCompat.getClipBounds(redSquare);
+ assertNotNull(endRect);
+ assertEquals(newClip, endRect);
+ }
+ });
+
+ resetListener();
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ ViewCompat.setClipBounds(redSquare, null);
+ }
+ });
+ waitForStart();
+ Thread.sleep(150);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Rect midClip = ViewCompat.getClipBounds(redSquare);
+ assertNotNull(midClip);
+ assertTrue(midClip.left > 0 && midClip.left < newClip.left);
+ assertTrue(midClip.top > 0 && midClip.top < newClip.top);
+ assertTrue(midClip.right < redSquare.getRight() && midClip.right > newClip.right);
+ assertTrue(midClip.bottom < redSquare.getBottom()
+ && midClip.bottom > newClip.bottom);
+ }
+ });
+ waitForEnd();
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ assertNull(ViewCompat.getClipBounds(redSquare));
+ }
+ });
+
+ }
+
+ @Test
+ public void dummy() {
+ // Avoid "No tests found" on older devices
+ }
+
+}
diff --git a/android/support/transition/ChangeImageTransformTest.java b/android/support/transition/ChangeImageTransformTest.java
new file mode 100644
index 00000000..907e01ef
--- /dev/null
+++ b/android/support/transition/ChangeImageTransformTest.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.graphics.Matrix;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.transition.test.R;
+import android.support.v4.app.ActivityCompat;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import org.junit.Test;
+
+@MediumTest
+public class ChangeImageTransformTest extends BaseTransitionTest {
+
+ private ChangeImageTransform mChangeImageTransform;
+ private Matrix mStartMatrix;
+ private Matrix mEndMatrix;
+ private Drawable mImage;
+ private ImageView mImageView;
+
+ @Override
+ Transition createTransition() {
+ mChangeImageTransform = new CaptureMatrix();
+ mChangeImageTransform.setDuration(100);
+ mTransition = mChangeImageTransform;
+ resetListener();
+ return mChangeImageTransform;
+ }
+
+ @Test
+ public void testCenterToFitXY() throws Throwable {
+ transformImage(ImageView.ScaleType.CENTER, ImageView.ScaleType.FIT_XY);
+ verifyMatrixMatches(centerMatrix(), mStartMatrix);
+ verifyMatrixMatches(fitXYMatrix(), mEndMatrix);
+ }
+
+ @Test
+ public void testCenterCropToFitCenter() throws Throwable {
+ transformImage(ImageView.ScaleType.CENTER_CROP, ImageView.ScaleType.FIT_CENTER);
+ verifyMatrixMatches(centerCropMatrix(), mStartMatrix);
+ verifyMatrixMatches(fitCenterMatrix(), mEndMatrix);
+ }
+
+ @Test
+ public void testCenterInsideToFitEnd() throws Throwable {
+ transformImage(ImageView.ScaleType.CENTER_INSIDE, ImageView.ScaleType.FIT_END);
+ // CENTER_INSIDE and CENTER are the same when the image is smaller than the View
+ verifyMatrixMatches(centerMatrix(), mStartMatrix);
+ verifyMatrixMatches(fitEndMatrix(), mEndMatrix);
+ }
+
+ @Test
+ public void testFitStartToCenter() throws Throwable {
+ transformImage(ImageView.ScaleType.FIT_START, ImageView.ScaleType.CENTER);
+ verifyMatrixMatches(fitStartMatrix(), mStartMatrix);
+ verifyMatrixMatches(centerMatrix(), mEndMatrix);
+ }
+
+ private Matrix centerMatrix() {
+ int imageWidth = mImage.getIntrinsicWidth();
+ int imageViewWidth = mImageView.getWidth();
+ float tx = Math.round((imageViewWidth - imageWidth) / 2f);
+
+ int imageHeight = mImage.getIntrinsicHeight();
+ int imageViewHeight = mImageView.getHeight();
+ float ty = Math.round((imageViewHeight - imageHeight) / 2f);
+
+ Matrix matrix = new Matrix();
+ matrix.postTranslate(tx, ty);
+ return matrix;
+ }
+
+ private Matrix fitXYMatrix() {
+ int imageWidth = mImage.getIntrinsicWidth();
+ int imageViewWidth = mImageView.getWidth();
+ float scaleX = ((float) imageViewWidth) / imageWidth;
+
+ int imageHeight = mImage.getIntrinsicHeight();
+ int imageViewHeight = mImageView.getHeight();
+ float scaleY = ((float) imageViewHeight) / imageHeight;
+
+ Matrix matrix = new Matrix();
+ matrix.postScale(scaleX, scaleY);
+ return matrix;
+ }
+
+ private Matrix centerCropMatrix() {
+ int imageWidth = mImage.getIntrinsicWidth();
+ int imageViewWidth = mImageView.getWidth();
+ float scaleX = ((float) imageViewWidth) / imageWidth;
+
+ int imageHeight = mImage.getIntrinsicHeight();
+ int imageViewHeight = mImageView.getHeight();
+ float scaleY = ((float) imageViewHeight) / imageHeight;
+
+ float maxScale = Math.max(scaleX, scaleY);
+
+ float width = imageWidth * maxScale;
+ float height = imageHeight * maxScale;
+ int tx = Math.round((imageViewWidth - width) / 2f);
+ int ty = Math.round((imageViewHeight - height) / 2f);
+
+ Matrix matrix = new Matrix();
+ matrix.postScale(maxScale, maxScale);
+ matrix.postTranslate(tx, ty);
+ return matrix;
+ }
+
+ private Matrix fitCenterMatrix() {
+ int imageWidth = mImage.getIntrinsicWidth();
+ int imageViewWidth = mImageView.getWidth();
+ float scaleX = ((float) imageViewWidth) / imageWidth;
+
+ int imageHeight = mImage.getIntrinsicHeight();
+ int imageViewHeight = mImageView.getHeight();
+ float scaleY = ((float) imageViewHeight) / imageHeight;
+
+ float minScale = Math.min(scaleX, scaleY);
+
+ float width = imageWidth * minScale;
+ float height = imageHeight * minScale;
+ float tx = (imageViewWidth - width) / 2f;
+ float ty = (imageViewHeight - height) / 2f;
+
+ Matrix matrix = new Matrix();
+ matrix.postScale(minScale, minScale);
+ matrix.postTranslate(tx, ty);
+ return matrix;
+ }
+
+ private Matrix fitStartMatrix() {
+ int imageWidth = mImage.getIntrinsicWidth();
+ int imageViewWidth = mImageView.getWidth();
+ float scaleX = ((float) imageViewWidth) / imageWidth;
+
+ int imageHeight = mImage.getIntrinsicHeight();
+ int imageViewHeight = mImageView.getHeight();
+ float scaleY = ((float) imageViewHeight) / imageHeight;
+
+ float minScale = Math.min(scaleX, scaleY);
+
+ Matrix matrix = new Matrix();
+ matrix.postScale(minScale, minScale);
+ return matrix;
+ }
+
+ private Matrix fitEndMatrix() {
+ int imageWidth = mImage.getIntrinsicWidth();
+ int imageViewWidth = mImageView.getWidth();
+ float scaleX = ((float) imageViewWidth) / imageWidth;
+
+ int imageHeight = mImage.getIntrinsicHeight();
+ int imageViewHeight = mImageView.getHeight();
+ float scaleY = ((float) imageViewHeight) / imageHeight;
+
+ float minScale = Math.min(scaleX, scaleY);
+
+ float width = imageWidth * minScale;
+ float height = imageHeight * minScale;
+ float tx = imageViewWidth - width;
+ float ty = imageViewHeight - height;
+
+ Matrix matrix = new Matrix();
+ matrix.postScale(minScale, minScale);
+ matrix.postTranslate(tx, ty);
+ return matrix;
+ }
+
+ private void verifyMatrixMatches(Matrix expected, Matrix matrix) {
+ if (expected == null) {
+ assertNull(matrix);
+ return;
+ }
+ assertNotNull(matrix);
+ float[] expectedValues = new float[9];
+ expected.getValues(expectedValues);
+
+ float[] values = new float[9];
+ matrix.getValues(values);
+
+ for (int i = 0; i < values.length; i++) {
+ final float expectedValue = expectedValues[i];
+ final float value = values[i];
+ assertEquals("Value [" + i + "]", expectedValue, value, 0.01f);
+ }
+ }
+
+ private void transformImage(ImageView.ScaleType startScale, final ImageView.ScaleType endScale)
+ throws Throwable {
+ final ImageView imageView = enterImageViewScene(startScale);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, mChangeImageTransform);
+ imageView.setScaleType(endScale);
+ }
+ });
+ waitForStart();
+ verify(mListener, (startScale == endScale) ? times(1) : never())
+ .onTransitionEnd(any(Transition.class));
+ waitForEnd();
+ }
+
+ private ImageView enterImageViewScene(final ImageView.ScaleType scaleType) throws Throwable {
+ enterScene(R.layout.scene4);
+ final ViewGroup container = (ViewGroup) rule.getActivity().findViewById(R.id.holder);
+ final ImageView[] imageViews = new ImageView[1];
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mImageView = new ImageView(rule.getActivity());
+ mImage = ActivityCompat.getDrawable(rule.getActivity(),
+ android.R.drawable.ic_media_play);
+ mImageView.setImageDrawable(mImage);
+ mImageView.setScaleType(scaleType);
+ imageViews[0] = mImageView;
+ container.addView(mImageView);
+ ViewGroup.LayoutParams layoutParams = mImageView.getLayoutParams();
+ DisplayMetrics metrics = rule.getActivity().getResources().getDisplayMetrics();
+ float size = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, metrics);
+ layoutParams.width = Math.round(size);
+ layoutParams.height = Math.round(size * 2);
+ mImageView.setLayoutParams(layoutParams);
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ return imageViews[0];
+ }
+
+ private class CaptureMatrix extends ChangeImageTransform {
+
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot, TransitionValues startValues,
+ TransitionValues endValues) {
+ Animator animator = super.createAnimator(sceneRoot, startValues, endValues);
+ assertNotNull(animator);
+ animator.addListener(new CaptureMatrixListener((ImageView) endValues.view));
+ return animator;
+ }
+
+ }
+
+ private class CaptureMatrixListener extends AnimatorListenerAdapter {
+
+ private final ImageView mImageView;
+
+ CaptureMatrixListener(ImageView view) {
+ mImageView = view;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mStartMatrix = copyMatrix();
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mEndMatrix = copyMatrix();
+ }
+
+ private Matrix copyMatrix() {
+ Matrix matrix = mImageView.getImageMatrix();
+ if (matrix != null) {
+ matrix = new Matrix(matrix);
+ }
+ return matrix;
+ }
+
+ }
+
+}
diff --git a/android/support/transition/ChangeScrollTest.java b/android/support/transition/ChangeScrollTest.java
new file mode 100644
index 00000000..0f383d3f
--- /dev/null
+++ b/android/support/transition/ChangeScrollTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.hamcrest.CoreMatchers.both;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.lessThan;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import android.support.test.filters.MediumTest;
+import android.support.transition.test.R;
+import android.view.View;
+
+import org.junit.Test;
+
+@MediumTest
+public class ChangeScrollTest extends BaseTransitionTest {
+
+ @Override
+ Transition createTransition() {
+ return new ChangeScroll();
+ }
+
+ @Test
+ public void testChangeScroll() throws Throwable {
+ enterScene(R.layout.scene5);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final View view = rule.getActivity().findViewById(R.id.text);
+ assertEquals(0, view.getScrollX());
+ assertEquals(0, view.getScrollY());
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ view.scrollTo(150, 300);
+ }
+ });
+ waitForStart();
+ Thread.sleep(150);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final View view = rule.getActivity().findViewById(R.id.text);
+ final int scrollX = view.getScrollX();
+ final int scrollY = view.getScrollY();
+ assertThat(scrollX, is(both(greaterThan(0)).and(lessThan(150))));
+ assertThat(scrollY, is(both(greaterThan(0)).and(lessThan(300))));
+ }
+ });
+ waitForEnd();
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final View view = rule.getActivity().findViewById(R.id.text);
+ assertEquals(150, view.getScrollX());
+ assertEquals(300, view.getScrollY());
+ }
+ });
+ }
+
+}
diff --git a/android/support/transition/ChangeTransformTest.java b/android/support/transition/ChangeTransformTest.java
new file mode 100644
index 00000000..3e543aa2
--- /dev/null
+++ b/android/support/transition/ChangeTransformTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.support.test.filters.MediumTest;
+import android.support.transition.test.R;
+import android.view.View;
+
+import org.junit.Test;
+
+@MediumTest
+public class ChangeTransformTest extends BaseTransitionTest {
+
+ @Override
+ Transition createTransition() {
+ return new ChangeTransform();
+ }
+
+ @Test
+ public void testTranslation() throws Throwable {
+ enterScene(R.layout.scene1);
+
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ redSquare.setTranslationX(500);
+ redSquare.setTranslationY(600);
+ }
+ });
+ waitForStart();
+
+ verify(mListener, never()).onTransitionEnd(any(Transition.class)); // still running
+ // There is no way to validate the intermediate matrix because it uses
+ // hidden properties of the View to execute.
+ waitForEnd();
+ assertEquals(500f, redSquare.getTranslationX(), 0.0f);
+ assertEquals(600f, redSquare.getTranslationY(), 0.0f);
+ }
+
+ @Test
+ public void testRotation() throws Throwable {
+ enterScene(R.layout.scene1);
+
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ redSquare.setRotation(45);
+ }
+ });
+ waitForStart();
+
+ verify(mListener, never()).onTransitionEnd(any(Transition.class)); // still running
+ // There is no way to validate the intermediate matrix because it uses
+ // hidden properties of the View to execute.
+ waitForEnd();
+ assertEquals(45f, redSquare.getRotation(), 0.0f);
+ }
+
+ @Test
+ public void testScale() throws Throwable {
+ enterScene(R.layout.scene1);
+
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ redSquare.setScaleX(2f);
+ redSquare.setScaleY(3f);
+ }
+ });
+ waitForStart();
+
+ verify(mListener, never()).onTransitionEnd(any(Transition.class)); // still running
+ // There is no way to validate the intermediate matrix because it uses
+ // hidden properties of the View to execute.
+ waitForEnd();
+ assertEquals(2f, redSquare.getScaleX(), 0.0f);
+ assertEquals(3f, redSquare.getScaleY(), 0.0f);
+ }
+
+ @Test
+ public void testReparent() throws Throwable {
+ final ChangeTransform changeTransform = (ChangeTransform) mTransition;
+ assertEquals(true, changeTransform.getReparent());
+ enterScene(R.layout.scene5);
+ startTransition(R.layout.scene9);
+ verify(mListener, never()).onTransitionEnd(any(Transition.class)); // still running
+ waitForEnd();
+
+ resetListener();
+ changeTransform.setReparent(false);
+ assertEquals(false, changeTransform.getReparent());
+ startTransition(R.layout.scene5);
+ waitForEnd(); // no transition to run because reparent == false
+ }
+
+}
diff --git a/android/support/transition/CheckCalledRunnable.java b/android/support/transition/CheckCalledRunnable.java
new file mode 100644
index 00000000..9eea6080
--- /dev/null
+++ b/android/support/transition/CheckCalledRunnable.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 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.support.transition;
+
+class CheckCalledRunnable implements Runnable {
+
+ private boolean mWasCalled = false;
+
+ @Override
+ public void run() {
+ mWasCalled = true;
+ }
+
+ /**
+ * @return {@code true} if {@link #run()} was called at least once.
+ */
+ boolean wasCalled() {
+ return mWasCalled;
+ }
+
+}
diff --git a/android/support/transition/ExplodeTest.java b/android/support/transition/ExplodeTest.java
new file mode 100644
index 00000000..b4215373
--- /dev/null
+++ b/android/support/transition/ExplodeTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.transition.test.R;
+import android.view.View;
+
+import org.junit.Test;
+
+@MediumTest
+public class ExplodeTest extends BaseTransitionTest {
+
+ @Override
+ Transition createTransition() {
+ return new Explode();
+ }
+
+ @Test
+ public void testExplode() throws Throwable {
+ enterScene(R.layout.scene10);
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+ final View greenSquare = rule.getActivity().findViewById(R.id.greenSquare);
+ final View blueSquare = rule.getActivity().findViewById(R.id.blueSquare);
+ final View yellowSquare = rule.getActivity().findViewById(R.id.yellowSquare);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ redSquare.setVisibility(View.INVISIBLE);
+ greenSquare.setVisibility(View.INVISIBLE);
+ blueSquare.setVisibility(View.INVISIBLE);
+ yellowSquare.setVisibility(View.INVISIBLE);
+ }
+ });
+ waitForStart();
+ verify(mListener, never()).onTransitionEnd(any(Transition.class));
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(View.VISIBLE, greenSquare.getVisibility());
+ assertEquals(View.VISIBLE, blueSquare.getVisibility());
+ assertEquals(View.VISIBLE, yellowSquare.getVisibility());
+ float redStartX = redSquare.getTranslationX();
+ float redStartY = redSquare.getTranslationY();
+
+ SystemClock.sleep(100);
+ verifyTranslation(redSquare, true, true);
+ verifyTranslation(greenSquare, false, true);
+ verifyTranslation(blueSquare, false, false);
+ verifyTranslation(yellowSquare, true, false);
+ assertTrue(redStartX > redSquare.getTranslationX()); // moving left
+ assertTrue(redStartY > redSquare.getTranslationY()); // moving up
+ waitForEnd();
+
+ verifyNoTranslation(redSquare);
+ verifyNoTranslation(greenSquare);
+ verifyNoTranslation(blueSquare);
+ verifyNoTranslation(yellowSquare);
+ assertEquals(View.INVISIBLE, redSquare.getVisibility());
+ assertEquals(View.INVISIBLE, greenSquare.getVisibility());
+ assertEquals(View.INVISIBLE, blueSquare.getVisibility());
+ assertEquals(View.INVISIBLE, yellowSquare.getVisibility());
+ }
+
+ @Test
+ public void testImplode() throws Throwable {
+ enterScene(R.layout.scene10);
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+ final View greenSquare = rule.getActivity().findViewById(R.id.greenSquare);
+ final View blueSquare = rule.getActivity().findViewById(R.id.blueSquare);
+ final View yellowSquare = rule.getActivity().findViewById(R.id.yellowSquare);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ redSquare.setVisibility(View.INVISIBLE);
+ greenSquare.setVisibility(View.INVISIBLE);
+ blueSquare.setVisibility(View.INVISIBLE);
+ yellowSquare.setVisibility(View.INVISIBLE);
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(mRoot, mTransition);
+ redSquare.setVisibility(View.VISIBLE);
+ greenSquare.setVisibility(View.VISIBLE);
+ blueSquare.setVisibility(View.VISIBLE);
+ yellowSquare.setVisibility(View.VISIBLE);
+ }
+ });
+ waitForStart();
+
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(View.VISIBLE, greenSquare.getVisibility());
+ assertEquals(View.VISIBLE, blueSquare.getVisibility());
+ assertEquals(View.VISIBLE, yellowSquare.getVisibility());
+ float redStartX = redSquare.getTranslationX();
+ float redStartY = redSquare.getTranslationY();
+
+ SystemClock.sleep(100);
+ verifyTranslation(redSquare, true, true);
+ verifyTranslation(greenSquare, false, true);
+ verifyTranslation(blueSquare, false, false);
+ verifyTranslation(yellowSquare, true, false);
+ assertTrue(redStartX < redSquare.getTranslationX()); // moving right
+ assertTrue(redStartY < redSquare.getTranslationY()); // moving down
+ waitForEnd();
+
+ verifyNoTranslation(redSquare);
+ verifyNoTranslation(greenSquare);
+ verifyNoTranslation(blueSquare);
+ verifyNoTranslation(yellowSquare);
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(View.VISIBLE, greenSquare.getVisibility());
+ assertEquals(View.VISIBLE, blueSquare.getVisibility());
+ assertEquals(View.VISIBLE, yellowSquare.getVisibility());
+ }
+
+ private void verifyTranslation(View view, boolean goLeft, boolean goUp) {
+ float translationX = view.getTranslationX();
+ float translationY = view.getTranslationY();
+
+ if (goLeft) {
+ assertTrue(translationX < 0);
+ } else {
+ assertTrue(translationX > 0);
+ }
+
+ if (goUp) {
+ assertTrue(translationY < 0);
+ } else {
+ assertTrue(translationY > 0);
+ }
+ }
+
+ private void verifyNoTranslation(View view) {
+ assertEquals(0f, view.getTranslationX(), 0.0f);
+ assertEquals(0f, view.getTranslationY(), 0.0f);
+ }
+
+}
diff --git a/android/support/transition/FadeTest.java b/android/support/transition/FadeTest.java
new file mode 100644
index 00000000..3b171e2c
--- /dev/null
+++ b/android/support/transition/FadeTest.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2016 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.support.transition;
+
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.lessThan;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@MediumTest
+public class FadeTest extends BaseTest {
+
+ private View mView;
+ private ViewGroup mRoot;
+
+ @UiThreadTest
+ @Before
+ public void setUp() {
+ mRoot = rule.getActivity().getRoot();
+ mView = new View(rule.getActivity());
+ mRoot.addView(mView, new ViewGroup.LayoutParams(100, 100));
+ }
+
+ @Test
+ public void testMode() {
+ assertThat(Fade.IN, is(Visibility.MODE_IN));
+ assertThat(Fade.OUT, is(Visibility.MODE_OUT));
+ final Fade fade = new Fade();
+ assertThat(fade.getMode(), is(Visibility.MODE_IN | Visibility.MODE_OUT));
+ fade.setMode(Visibility.MODE_IN);
+ assertThat(fade.getMode(), is(Visibility.MODE_IN));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testDisappear() {
+ final Fade fade = new Fade();
+ final TransitionValues startValues = new TransitionValues();
+ startValues.view = mView;
+ fade.captureStartValues(startValues);
+ mView.setVisibility(View.INVISIBLE);
+ final TransitionValues endValues = new TransitionValues();
+ endValues.view = mView;
+ fade.captureEndValues(endValues);
+ Animator animator = fade.createAnimator(mRoot, startValues, endValues);
+ assertThat(animator, is(notNullValue()));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testAppear() {
+ mView.setVisibility(View.INVISIBLE);
+ final Fade fade = new Fade();
+ final TransitionValues startValues = new TransitionValues();
+ startValues.view = mView;
+ fade.captureStartValues(startValues);
+ mView.setVisibility(View.VISIBLE);
+ final TransitionValues endValues = new TransitionValues();
+ endValues.view = mView;
+ fade.captureEndValues(endValues);
+ Animator animator = fade.createAnimator(mRoot, startValues, endValues);
+ assertThat(animator, is(notNullValue()));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testNoChange() {
+ final Fade fade = new Fade();
+ final TransitionValues startValues = new TransitionValues();
+ startValues.view = mView;
+ fade.captureStartValues(startValues);
+ final TransitionValues endValues = new TransitionValues();
+ endValues.view = mView;
+ fade.captureEndValues(endValues);
+ Animator animator = fade.createAnimator(mRoot, startValues, endValues);
+ // No visibility change; no animation should happen
+ assertThat(animator, is(nullValue()));
+ }
+
+ @Test
+ public void testFadeOutThenIn() throws Throwable {
+ // Fade out
+ final Runnable interrupt = mock(Runnable.class);
+ float[] valuesOut = new float[2];
+ final InterruptibleFade fadeOut = new InterruptibleFade(Fade.MODE_OUT, interrupt,
+ valuesOut);
+ final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class);
+ fadeOut.addListener(listenerOut);
+ changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE);
+ verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class));
+
+ // The view is in the middle of fading out
+ verify(interrupt, timeout(3000)).run();
+
+ // Fade in
+ float[] valuesIn = new float[2];
+ final InterruptibleFade fadeIn = new InterruptibleFade(Fade.MODE_IN, null, valuesIn);
+ final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class);
+ fadeIn.addListener(listenerIn);
+ changeVisibility(fadeIn, mRoot, mView, View.VISIBLE);
+ verify(listenerOut, timeout(3000)).onTransitionPause(any(Transition.class));
+ verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class));
+ assertThat(valuesOut[1], allOf(greaterThan(0f), lessThan(1f)));
+ if (Build.VERSION.SDK_INT >= 19) {
+ // These won't match on API levels 18 and below due to lack of Animator pause.
+ assertEquals(valuesOut[1], valuesIn[0], 0.01f);
+ }
+
+ verify(listenerIn, timeout(3000)).onTransitionEnd(any(Transition.class));
+ assertThat(mView.getVisibility(), is(View.VISIBLE));
+ assertEquals(valuesIn[1], 1.f, 0.01f);
+ }
+
+ @Test
+ public void testFadeInThenOut() throws Throwable {
+ changeVisibility(null, mRoot, mView, View.INVISIBLE);
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ // Fade in
+ final Runnable interrupt = mock(Runnable.class);
+ float[] valuesIn = new float[2];
+ final InterruptibleFade fadeIn = new InterruptibleFade(Fade.MODE_IN, interrupt, valuesIn);
+ final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class);
+ fadeIn.addListener(listenerIn);
+ changeVisibility(fadeIn, mRoot, mView, View.VISIBLE);
+ verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class));
+
+ // The view is in the middle of fading in
+ verify(interrupt, timeout(3000)).run();
+
+ // Fade out
+ float[] valuesOut = new float[2];
+ final InterruptibleFade fadeOut = new InterruptibleFade(Fade.MODE_OUT, null, valuesOut);
+ final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class);
+ fadeOut.addListener(listenerOut);
+ changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE);
+ verify(listenerIn, timeout(3000)).onTransitionPause(any(Transition.class));
+ verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class));
+ assertThat(valuesIn[1], allOf(greaterThan(0f), lessThan(1f)));
+ if (Build.VERSION.SDK_INT >= 19) {
+ // These won't match on API levels 18 and below due to lack of Animator pause.
+ assertEquals(valuesIn[1], valuesOut[0], 0.01f);
+ }
+
+ verify(listenerOut, timeout(3000)).onTransitionEnd(any(Transition.class));
+ assertThat(mView.getVisibility(), is(View.INVISIBLE));
+ }
+
+ @Test
+ public void testFadeWithAlpha() throws Throwable {
+ // Set the view alpha to 0.5
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mView.setAlpha(0.5f);
+ }
+ });
+ // Fade out
+ final Fade fadeOut = new Fade(Fade.OUT);
+ final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class);
+ fadeOut.addListener(listenerOut);
+ changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE);
+ verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class));
+ verify(listenerOut, timeout(3000)).onTransitionEnd(any(Transition.class));
+ // Fade in
+ final Fade fadeIn = new Fade(Fade.IN);
+ final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class);
+ fadeIn.addListener(listenerIn);
+ changeVisibility(fadeIn, mRoot, mView, View.VISIBLE);
+ verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class));
+ verify(listenerIn, timeout(3000)).onTransitionEnd(any(Transition.class));
+ // Confirm that the view still has the original alpha value
+ assertThat(mView.getVisibility(), is(View.VISIBLE));
+ assertEquals(0.5f, mView.getAlpha(), 0.01f);
+ }
+
+ private void changeVisibility(final Fade fade, final ViewGroup container, final View target,
+ final int visibility) throws Throwable {
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (fade != null) {
+ TransitionManager.beginDelayedTransition(container, fade);
+ }
+ target.setVisibility(visibility);
+ }
+ });
+ }
+
+ /**
+ * A special version of {@link Fade} that runs a specified {@link Runnable} soon after the
+ * target starts fading in or out.
+ */
+ private static class InterruptibleFade extends Fade {
+
+ static final float ALPHA_THRESHOLD = 0.2f;
+
+ float mInitialAlpha = -1;
+ Runnable mMiddle;
+ final float[] mAlphaValues;
+
+ InterruptibleFade(int mode, Runnable middle, float[] alphaValues) {
+ super(mode);
+ mMiddle = middle;
+ mAlphaValues = alphaValues;
+ }
+
+ @Nullable
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot,
+ @Nullable final TransitionValues startValues,
+ @Nullable final TransitionValues endValues) {
+ final Animator animator = super.createAnimator(sceneRoot, startValues, endValues);
+ if (animator instanceof ObjectAnimator) {
+ ((ObjectAnimator) animator).addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final float alpha = (float) animation.getAnimatedValue();
+ mAlphaValues[1] = alpha;
+ if (mInitialAlpha < 0) {
+ mInitialAlpha = alpha;
+ mAlphaValues[0] = mInitialAlpha;
+ } else if (Math.abs(alpha - mInitialAlpha) > ALPHA_THRESHOLD) {
+ if (mMiddle != null) {
+ mMiddle.run();
+ mMiddle = null;
+ }
+ }
+ }
+ });
+ }
+ return animator;
+ }
+
+ }
+
+}
diff --git a/android/support/transition/FragmentTransitionTest.java b/android/support/transition/FragmentTransitionTest.java
new file mode 100644
index 00000000..893d4c66
--- /dev/null
+++ b/android/support/transition/FragmentTransitionTest.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.Nullable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.transition.test.R;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.util.Pair;
+import android.support.v4.util.SparseArrayCompat;
+import android.support.v4.view.ViewCompat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+
+@MediumTest
+@RunWith(Parameterized.class)
+public class FragmentTransitionTest extends BaseTest {
+
+ @Parameterized.Parameters
+ public static Object[] data() {
+ return new Boolean[]{
+ false, true
+ };
+ }
+
+ private final boolean mReorderingAllowed;
+
+ public FragmentTransitionTest(boolean reorderingAllowed) {
+ mReorderingAllowed = reorderingAllowed;
+ }
+
+ @Test
+ public void preconditions() {
+ final TransitionFragment fragment1 = TransitionFragment.newInstance(R.layout.scene2);
+ final TransitionFragment fragment2 = TransitionFragment.newInstance(R.layout.scene3);
+ showFragment(fragment1, false, null);
+ assertNull(fragment1.mRed);
+ assertNotNull(fragment1.mGreen);
+ assertNotNull(fragment1.mBlue);
+ showFragment(fragment2, true, new Pair<>(fragment1.mGreen, "green"));
+ assertNotNull(fragment2.mRed);
+ assertNotNull(fragment2.mGreen);
+ assertNotNull(fragment2.mBlue);
+ }
+
+ @Test
+ public void nonSharedTransition() {
+ final TransitionFragment fragment1 = TransitionFragment.newInstance(R.layout.scene2);
+ final TransitionFragment fragment2 = TransitionFragment.newInstance(R.layout.scene3);
+ showFragment(fragment1, false, null);
+ showFragment(fragment2, true, null);
+ verify(fragment1.mListeners.get(TransitionFragment.TRANSITION_EXIT))
+ .onTransitionStart(any(Transition.class));
+ verify(fragment1.mListeners.get(TransitionFragment.TRANSITION_EXIT), timeout(3000))
+ .onTransitionEnd(any(Transition.class));
+ verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_ENTER))
+ .onTransitionStart(any(Transition.class));
+ verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_ENTER), timeout(3000))
+ .onTransitionEnd(any(Transition.class));
+ popBackStack();
+ verify(fragment1.mListeners.get(TransitionFragment.TRANSITION_REENTER))
+ .onTransitionStart(any(Transition.class));
+ verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_RETURN))
+ .onTransitionStart(any(Transition.class));
+ }
+
+ @Test
+ public void sharedTransition() {
+ final TransitionFragment fragment1 = TransitionFragment.newInstance(R.layout.scene2);
+ final TransitionFragment fragment2 = TransitionFragment.newInstance(R.layout.scene3);
+ showFragment(fragment1, false, null);
+ showFragment(fragment2, true, new Pair<>(fragment1.mGreen, "green"));
+ verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_SHARED_ENTER))
+ .onTransitionStart(any(Transition.class));
+ verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_SHARED_ENTER), timeout(3000))
+ .onTransitionEnd(any(Transition.class));
+ popBackStack();
+ verify(fragment2.mListeners.get(TransitionFragment.TRANSITION_SHARED_RETURN))
+ .onTransitionStart(any(Transition.class));
+ }
+
+ private void showFragment(final Fragment fragment, final boolean addToBackStack,
+ final Pair<View, String> sharedElement) {
+ final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ instrumentation.runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ final FragmentTransaction transaction = getFragmentManager().beginTransaction();
+ transaction.replace(R.id.root, fragment);
+ transaction.setReorderingAllowed(mReorderingAllowed);
+ if (sharedElement != null) {
+ transaction.addSharedElement(sharedElement.first, sharedElement.second);
+ }
+ if (addToBackStack) {
+ transaction.addToBackStack(null);
+ transaction.commit();
+ getFragmentManager().executePendingTransactions();
+ } else {
+ transaction.commitNow();
+ }
+ }
+ });
+ instrumentation.waitForIdleSync();
+ }
+
+ private void popBackStack() {
+ final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ instrumentation.runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ getFragmentManager().popBackStackImmediate();
+ }
+ });
+ instrumentation.waitForIdleSync();
+ }
+
+ private FragmentManager getFragmentManager() {
+ return rule.getActivity().getSupportFragmentManager();
+ }
+
+ /**
+ * A {@link Fragment} with all kinds of {@link Transition} with tracking listeners.
+ */
+ public static class TransitionFragment extends Fragment {
+
+ static final int TRANSITION_ENTER = 1;
+ static final int TRANSITION_EXIT = 2;
+ static final int TRANSITION_REENTER = 3;
+ static final int TRANSITION_RETURN = 4;
+ static final int TRANSITION_SHARED_ENTER = 5;
+ static final int TRANSITION_SHARED_RETURN = 6;
+
+ private static final String ARG_LAYOUT_ID = "layout_id";
+
+ View mRed;
+ View mGreen;
+ View mBlue;
+
+ SparseArrayCompat<Transition.TransitionListener> mListeners = new SparseArrayCompat<>();
+
+ public static TransitionFragment newInstance(@LayoutRes int layout) {
+ final Bundle args = new Bundle();
+ args.putInt(ARG_LAYOUT_ID, layout);
+ final TransitionFragment fragment = new TransitionFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ public TransitionFragment() {
+ setEnterTransition(createTransition(TRANSITION_ENTER));
+ setExitTransition(createTransition(TRANSITION_EXIT));
+ setReenterTransition(createTransition(TRANSITION_REENTER));
+ setReturnTransition(createTransition(TRANSITION_RETURN));
+ setSharedElementEnterTransition(createTransition(TRANSITION_SHARED_ENTER));
+ setSharedElementReturnTransition(createTransition(TRANSITION_SHARED_RETURN));
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(getArguments().getInt(ARG_LAYOUT_ID), container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ mRed = view.findViewById(R.id.redSquare);
+ mGreen = view.findViewById(R.id.greenSquare);
+ mBlue = view.findViewById(R.id.blueSquare);
+ if (mRed != null) {
+ ViewCompat.setTransitionName(mRed, "red");
+ }
+ if (mGreen != null) {
+ ViewCompat.setTransitionName(mGreen, "green");
+ }
+ if (mBlue != null) {
+ ViewCompat.setTransitionName(mBlue, "blue");
+ }
+ }
+
+ private Transition createTransition(int type) {
+ final Transition.TransitionListener listener = mock(
+ Transition.TransitionListener.class);
+ final AutoTransition transition = new AutoTransition();
+ transition.addListener(listener);
+ transition.setDuration(10);
+ mListeners.put(type, listener);
+ return transition;
+ }
+
+ }
+
+}
diff --git a/android/support/transition/PathMotionTest.java b/android/support/transition/PathMotionTest.java
new file mode 100644
index 00000000..8bf738e4
--- /dev/null
+++ b/android/support/transition/PathMotionTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+
+public abstract class PathMotionTest {
+
+ public static void assertPathMatches(Path expectedPath, Path path) {
+ PathMeasure expectedMeasure = new PathMeasure(expectedPath, false);
+ PathMeasure pathMeasure = new PathMeasure(path, false);
+
+ boolean expectedNextContour;
+ boolean pathNextContour;
+ int contourIndex = 0;
+ do {
+ float expectedLength = expectedMeasure.getLength();
+ assertEquals("Lengths differ", expectedLength, pathMeasure.getLength(), 0.01f);
+
+ float minLength = Math.min(expectedLength, pathMeasure.getLength());
+
+ float[] pos = new float[2];
+
+ float increment = minLength / 5f;
+ for (float along = 0; along <= minLength; along += increment) {
+ expectedMeasure.getPosTan(along, pos, null);
+ float expectedX = pos[0];
+ float expectedY = pos[1];
+
+ pathMeasure.getPosTan(along, pos, null);
+ assertEquals("Failed at " + increment + " in contour " + contourIndex,
+ expectedX, pos[0], 0.01f);
+ assertEquals("Failed at " + increment + " in contour " + contourIndex,
+ expectedY, pos[1], 0.01f);
+ }
+ expectedNextContour = expectedMeasure.nextContour();
+ pathNextContour = pathMeasure.nextContour();
+ contourIndex++;
+ } while (expectedNextContour && pathNextContour);
+ assertFalse(expectedNextContour);
+ assertFalse(pathNextContour);
+ }
+
+}
diff --git a/android/support/transition/PatternPathMotionTest.java b/android/support/transition/PatternPathMotionTest.java
new file mode 100644
index 00000000..b14ceaa1
--- /dev/null
+++ b/android/support/transition/PatternPathMotionTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.junit.Assert.assertSame;
+
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PatternPathMotionTest extends PathMotionTest {
+
+ @Test
+ public void testStraightPath() {
+ Path pattern = new Path();
+ pattern.moveTo(100, 500);
+ pattern.lineTo(300, 1000);
+
+ PatternPathMotion pathMotion = new PatternPathMotion(pattern);
+ assertPathMatches(pattern, pathMotion.getPatternPath());
+
+ Path expected = new Path();
+ expected.moveTo(0, 0);
+ expected.lineTo(100, 100);
+
+ assertPathMatches(expected, pathMotion.getPath(0, 0, 100, 100));
+ }
+
+ @Test
+ public void testCurve() {
+ RectF oval = new RectF();
+ Path pattern = new Path();
+ oval.set(0, 0, 100, 100);
+ pattern.addArc(oval, 0, 180);
+
+ PatternPathMotion pathMotion = new PatternPathMotion(pattern);
+ assertPathMatches(pattern, pathMotion.getPatternPath());
+
+ Path expected = new Path();
+ oval.set(-50, 0, 50, 100);
+ expected.addArc(oval, -90, 180);
+
+ assertPathMatches(expected, pathMotion.getPath(0, 0, 0, 100));
+ }
+
+ @Test
+ public void testSetPatternPath() {
+ Path pattern = new Path();
+ RectF oval = new RectF(0, 0, 100, 100);
+ pattern.addArc(oval, 0, 180);
+
+ PatternPathMotion patternPathMotion = new PatternPathMotion();
+ patternPathMotion.setPatternPath(pattern);
+ assertSame(pattern, patternPathMotion.getPatternPath());
+ }
+
+}
diff --git a/android/support/transition/PropagationTest.java b/android/support/transition/PropagationTest.java
new file mode 100644
index 00000000..932c8d3d
--- /dev/null
+++ b/android/support/transition/PropagationTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Rect;
+import android.support.annotation.NonNull;
+import android.support.test.filters.MediumTest;
+import android.support.transition.test.R;
+import android.view.View;
+
+import org.junit.Test;
+
+@MediumTest
+public class PropagationTest extends BaseTransitionTest {
+
+ @Test
+ public void testCircularPropagation() throws Throwable {
+ enterScene(R.layout.scene10);
+ CircularPropagation propagation = new CircularPropagation();
+ mTransition.setPropagation(propagation);
+ final TransitionValues redValues = new TransitionValues();
+ redValues.view = mRoot.findViewById(R.id.redSquare);
+ propagation.captureValues(redValues);
+
+ // Only the reported propagation properties are set
+ for (String prop : propagation.getPropagationProperties()) {
+ assertTrue(redValues.values.keySet().contains(prop));
+ }
+ assertEquals(propagation.getPropagationProperties().length, redValues.values.size());
+
+ // check the visibility
+ assertEquals(View.VISIBLE, propagation.getViewVisibility(redValues));
+ assertEquals(View.GONE, propagation.getViewVisibility(null));
+
+ // Check the positions
+ int[] pos = new int[2];
+ redValues.view.getLocationOnScreen(pos);
+ pos[0] += redValues.view.getWidth() / 2;
+ pos[1] += redValues.view.getHeight() / 2;
+ assertEquals(pos[0], propagation.getViewX(redValues));
+ assertEquals(pos[1], propagation.getViewY(redValues));
+
+ mTransition.setEpicenterCallback(new Transition.EpicenterCallback() {
+ @Override
+ public Rect onGetEpicenter(@NonNull Transition transition) {
+ return new Rect(0, 0, redValues.view.getWidth(), redValues.view.getHeight());
+ }
+ });
+
+ long redDelay = getDelay(R.id.redSquare);
+ // red square's delay should be roughly 0 since it is at the epicenter
+ assertEquals(0f, redDelay, 30f);
+
+ // The green square is on the upper-right
+ long greenDelay = getDelay(R.id.greenSquare);
+ assertTrue(greenDelay < redDelay);
+
+ // The blue square is on the lower-right
+ long blueDelay = getDelay(R.id.blueSquare);
+ assertTrue(blueDelay < greenDelay);
+
+ // Test propagation speed
+ propagation.setPropagationSpeed(1000000000f);
+ assertEquals(0, getDelay(R.id.blueSquare));
+ }
+
+ private TransitionValues capturePropagationValues(int viewId) {
+ TransitionValues transitionValues = new TransitionValues();
+ transitionValues.view = mRoot.findViewById(viewId);
+ TransitionPropagation propagation = mTransition.getPropagation();
+ assertNotNull(propagation);
+ propagation.captureValues(transitionValues);
+ return transitionValues;
+ }
+
+ private long getDelay(int viewId) {
+ TransitionValues transitionValues = capturePropagationValues(viewId);
+ TransitionPropagation propagation = mTransition.getPropagation();
+ assertNotNull(propagation);
+ return propagation.getStartDelay(mRoot, mTransition, transitionValues, null);
+ }
+
+}
diff --git a/android/support/transition/SceneTest.java b/android/support/transition/SceneTest.java
new file mode 100644
index 00000000..129a3ebe
--- /dev/null
+++ b/android/support/transition/SceneTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2016 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.support.transition;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.sameInstance;
+
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.support.transition.test.R;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import org.junit.Test;
+
+@MediumTest
+public class SceneTest extends BaseTest {
+
+ @Test
+ public void testGetSceneRoot() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ Scene scene = new Scene(root);
+ assertThat(scene.getSceneRoot(), is(sameInstance(root)));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testSceneWithViewGroup() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ FrameLayout layout = new FrameLayout(activity);
+ Scene scene = new Scene(root, layout);
+ CheckCalledRunnable enterAction = new CheckCalledRunnable();
+ CheckCalledRunnable exitAction = new CheckCalledRunnable();
+ scene.setEnterAction(enterAction);
+ scene.setExitAction(exitAction);
+ scene.enter();
+ assertThat(enterAction.wasCalled(), is(true));
+ assertThat(exitAction.wasCalled(), is(false));
+ assertThat(root.getChildCount(), is(1));
+ assertThat(root.getChildAt(0), is((View) layout));
+ scene.exit();
+ assertThat(exitAction.wasCalled(), is(true));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testSceneWithView() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ View view = new View(activity);
+ Scene scene = new Scene(root, view);
+ CheckCalledRunnable enterAction = new CheckCalledRunnable();
+ CheckCalledRunnable exitAction = new CheckCalledRunnable();
+ scene.setEnterAction(enterAction);
+ scene.setExitAction(exitAction);
+ scene.enter();
+ assertThat(enterAction.wasCalled(), is(true));
+ assertThat(exitAction.wasCalled(), is(false));
+ assertThat(root.getChildCount(), is(1));
+ assertThat(root.getChildAt(0), is(view));
+ scene.exit();
+ assertThat(exitAction.wasCalled(), is(true));
+ }
+
+ @Test
+ public void testEnterAction() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ Scene scene = new Scene(root);
+ CheckCalledRunnable runnable = new CheckCalledRunnable();
+ scene.setEnterAction(runnable);
+ scene.enter();
+ assertThat(runnable.wasCalled(), is(true));
+ }
+
+ @Test
+ public void testExitAction() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ Scene scene = new Scene(root);
+ scene.enter();
+ CheckCalledRunnable runnable = new CheckCalledRunnable();
+ scene.setExitAction(runnable);
+ scene.exit();
+ assertThat(runnable.wasCalled(), is(true));
+ }
+
+ @Test
+ public void testExitAction_withoutEnter() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ Scene scene = new Scene(root);
+ CheckCalledRunnable runnable = new CheckCalledRunnable();
+ scene.setExitAction(runnable);
+ scene.exit();
+ assertThat(runnable.wasCalled(), is(false));
+ }
+
+ @Test
+ public void testGetSceneForLayout_cache() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ Scene scene = Scene.getSceneForLayout(root, R.layout.support_scene0, activity);
+ assertThat("getSceneForLayout should return the same instance for subsequent calls",
+ Scene.getSceneForLayout(root, R.layout.support_scene0, activity),
+ is(sameInstance(scene)));
+ }
+
+}
diff --git a/android/support/transition/SlideBadEdgeTest.java b/android/support/transition/SlideBadEdgeTest.java
new file mode 100644
index 00000000..e43d4f32
--- /dev/null
+++ b/android/support/transition/SlideBadEdgeTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.junit.Assert.fail;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.Gravity;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SlideBadEdgeTest {
+
+ private static final Object[][] sBadGravity = {
+ {Gravity.AXIS_CLIP, "AXIS_CLIP"},
+ {Gravity.AXIS_PULL_AFTER, "AXIS_PULL_AFTER"},
+ {Gravity.AXIS_PULL_BEFORE, "AXIS_PULL_BEFORE"},
+ {Gravity.AXIS_SPECIFIED, "AXIS_SPECIFIED"},
+ {Gravity.AXIS_Y_SHIFT, "AXIS_Y_SHIFT"},
+ {Gravity.AXIS_X_SHIFT, "AXIS_X_SHIFT"},
+ {Gravity.CENTER, "CENTER"},
+ {Gravity.CLIP_VERTICAL, "CLIP_VERTICAL"},
+ {Gravity.CLIP_HORIZONTAL, "CLIP_HORIZONTAL"},
+ {Gravity.CENTER_VERTICAL, "CENTER_VERTICAL"},
+ {Gravity.CENTER_HORIZONTAL, "CENTER_HORIZONTAL"},
+ {Gravity.DISPLAY_CLIP_VERTICAL, "DISPLAY_CLIP_VERTICAL"},
+ {Gravity.DISPLAY_CLIP_HORIZONTAL, "DISPLAY_CLIP_HORIZONTAL"},
+ {Gravity.FILL_VERTICAL, "FILL_VERTICAL"},
+ {Gravity.FILL, "FILL"},
+ {Gravity.FILL_HORIZONTAL, "FILL_HORIZONTAL"},
+ {Gravity.HORIZONTAL_GRAVITY_MASK, "HORIZONTAL_GRAVITY_MASK"},
+ {Gravity.NO_GRAVITY, "NO_GRAVITY"},
+ {Gravity.RELATIVE_LAYOUT_DIRECTION, "RELATIVE_LAYOUT_DIRECTION"},
+ {Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK, "RELATIVE_HORIZONTAL_GRAVITY_MASK"},
+ {Gravity.VERTICAL_GRAVITY_MASK, "VERTICAL_GRAVITY_MASK"},
+ };
+
+ @Test
+ public void testBadSide() {
+ for (int i = 0; i < sBadGravity.length; i++) {
+ int badEdge = (Integer) sBadGravity[i][0];
+ String edgeName = (String) sBadGravity[i][1];
+ try {
+ new Slide(badEdge);
+ fail("Should not be able to set slide edge to " + edgeName);
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ Slide slide = new Slide();
+ slide.setSlideEdge(badEdge);
+ fail("Should not be able to set slide edge to " + edgeName);
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+ }
+
+}
diff --git a/android/support/transition/SlideDefaultEdgeTest.java b/android/support/transition/SlideDefaultEdgeTest.java
new file mode 100644
index 00000000..a0e7eabb
--- /dev/null
+++ b/android/support/transition/SlideDefaultEdgeTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.junit.Assert.assertEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.Gravity;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SlideDefaultEdgeTest {
+
+ @Test
+ public void testDefaultSide() {
+ // default to bottom
+ Slide slide = new Slide();
+ assertEquals(Gravity.BOTTOM, slide.getSlideEdge());
+ }
+
+}
diff --git a/android/support/transition/SlideEdgeTest.java b/android/support/transition/SlideEdgeTest.java
new file mode 100644
index 00000000..af8d0b62
--- /dev/null
+++ b/android/support/transition/SlideEdgeTest.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.MediumTest;
+import android.support.transition.test.R;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.junit.Test;
+
+@MediumTest
+public class SlideEdgeTest extends BaseTransitionTest {
+
+ private static final Object[][] sSlideEdgeArray = {
+ {Gravity.START, "START"},
+ {Gravity.END, "END"},
+ {Gravity.LEFT, "LEFT"},
+ {Gravity.TOP, "TOP"},
+ {Gravity.RIGHT, "RIGHT"},
+ {Gravity.BOTTOM, "BOTTOM"},
+ };
+
+ @Test
+ public void testSetSide() throws Throwable {
+ for (int i = 0; i < sSlideEdgeArray.length; i++) {
+ int slideEdge = (Integer) (sSlideEdgeArray[i][0]);
+ String edgeName = (String) (sSlideEdgeArray[i][1]);
+ Slide slide = new Slide(slideEdge);
+ assertEquals("Edge not set properly in constructor " + edgeName,
+ slideEdge, slide.getSlideEdge());
+
+ slide = new Slide();
+ slide.setSlideEdge(slideEdge);
+ assertEquals("Edge not set properly with setter " + edgeName,
+ slideEdge, slide.getSlideEdge());
+ }
+ }
+
+ @LargeTest
+ @Test
+ public void testSlideOut() throws Throwable {
+ for (int i = 0; i < sSlideEdgeArray.length; i++) {
+ final int slideEdge = (Integer) (sSlideEdgeArray[i][0]);
+ final Slide slide = new Slide(slideEdge);
+ final Transition.TransitionListener listener =
+ mock(Transition.TransitionListener.class);
+ slide.addListener(listener);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ rule.getActivity().setContentView(R.layout.scene1);
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+ final View greenSquare = rule.getActivity().findViewById(R.id.greenSquare);
+ final View hello = rule.getActivity().findViewById(R.id.hello);
+ final ViewGroup sceneRoot = (ViewGroup) rule.getActivity().findViewById(R.id.holder);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(sceneRoot, slide);
+ redSquare.setVisibility(View.INVISIBLE);
+ greenSquare.setVisibility(View.INVISIBLE);
+ hello.setVisibility(View.INVISIBLE);
+ }
+ });
+ verify(listener, timeout(1000)).onTransitionStart(any(Transition.class));
+ verify(listener, never()).onTransitionEnd(any(Transition.class));
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(View.VISIBLE, greenSquare.getVisibility());
+ assertEquals(View.VISIBLE, hello.getVisibility());
+
+ float redStartX = redSquare.getTranslationX();
+ float redStartY = redSquare.getTranslationY();
+
+ Thread.sleep(200);
+ verifyTranslation(slideEdge, redSquare);
+ verifyTranslation(slideEdge, greenSquare);
+ verifyTranslation(slideEdge, hello);
+
+ final float redMidX = redSquare.getTranslationX();
+ final float redMidY = redSquare.getTranslationY();
+
+ switch (slideEdge) {
+ case Gravity.LEFT:
+ case Gravity.START:
+ assertTrue(
+ "isn't sliding out to left. Expecting " + redStartX + " > " + redMidX,
+ redStartX > redMidX);
+ break;
+ case Gravity.RIGHT:
+ case Gravity.END:
+ assertTrue(
+ "isn't sliding out to right. Expecting " + redStartX + " < " + redMidX,
+ redStartX < redMidX);
+ break;
+ case Gravity.TOP:
+ assertTrue("isn't sliding out to top. Expecting " + redStartY + " > " + redMidY,
+ redStartY > redSquare.getTranslationY());
+ break;
+ case Gravity.BOTTOM:
+ assertTrue(
+ "isn't sliding out to bottom. Expecting " + redStartY + " < " + redMidY,
+ redStartY < redSquare.getTranslationY());
+ break;
+ }
+ verify(listener, timeout(1000)).onTransitionEnd(any(Transition.class));
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ verifyNoTranslation(redSquare);
+ verifyNoTranslation(greenSquare);
+ verifyNoTranslation(hello);
+ assertEquals(View.INVISIBLE, redSquare.getVisibility());
+ assertEquals(View.INVISIBLE, greenSquare.getVisibility());
+ assertEquals(View.INVISIBLE, hello.getVisibility());
+ }
+ }
+
+ @LargeTest
+ @Test
+ public void testSlideIn() throws Throwable {
+ for (int i = 0; i < sSlideEdgeArray.length; i++) {
+ final int slideEdge = (Integer) (sSlideEdgeArray[i][0]);
+ final Slide slide = new Slide(slideEdge);
+ final Transition.TransitionListener listener =
+ mock(Transition.TransitionListener.class);
+ slide.addListener(listener);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ rule.getActivity().setContentView(R.layout.scene1);
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ final View redSquare = rule.getActivity().findViewById(R.id.redSquare);
+ final View greenSquare = rule.getActivity().findViewById(R.id.greenSquare);
+ final View hello = rule.getActivity().findViewById(R.id.hello);
+ final ViewGroup sceneRoot = (ViewGroup) rule.getActivity().findViewById(R.id.holder);
+
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ redSquare.setVisibility(View.INVISIBLE);
+ greenSquare.setVisibility(View.INVISIBLE);
+ hello.setVisibility(View.INVISIBLE);
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ // now slide in
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(sceneRoot, slide);
+ redSquare.setVisibility(View.VISIBLE);
+ greenSquare.setVisibility(View.VISIBLE);
+ hello.setVisibility(View.VISIBLE);
+ }
+ });
+ verify(listener, timeout(1000)).onTransitionStart(any(Transition.class));
+
+ verify(listener, never()).onTransitionEnd(any(Transition.class));
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(View.VISIBLE, greenSquare.getVisibility());
+ assertEquals(View.VISIBLE, hello.getVisibility());
+
+ final float redStartX = redSquare.getTranslationX();
+ final float redStartY = redSquare.getTranslationY();
+
+ Thread.sleep(200);
+ verifyTranslation(slideEdge, redSquare);
+ verifyTranslation(slideEdge, greenSquare);
+ verifyTranslation(slideEdge, hello);
+ final float redMidX = redSquare.getTranslationX();
+ final float redMidY = redSquare.getTranslationY();
+
+ switch (slideEdge) {
+ case Gravity.LEFT:
+ case Gravity.START:
+ assertTrue(
+ "isn't sliding in from left. Expecting " + redStartX + " < " + redMidX,
+ redStartX < redMidX);
+ break;
+ case Gravity.RIGHT:
+ case Gravity.END:
+ assertTrue(
+ "isn't sliding in from right. Expecting " + redStartX + " > " + redMidX,
+ redStartX > redMidX);
+ break;
+ case Gravity.TOP:
+ assertTrue(
+ "isn't sliding in from top. Expecting " + redStartY + " < " + redMidY,
+ redStartY < redSquare.getTranslationY());
+ break;
+ case Gravity.BOTTOM:
+ assertTrue("isn't sliding in from bottom. Expecting " + redStartY + " > "
+ + redMidY,
+ redStartY > redSquare.getTranslationY());
+ break;
+ }
+ verify(listener, timeout(1000)).onTransitionEnd(any(Transition.class));
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ verifyNoTranslation(redSquare);
+ verifyNoTranslation(greenSquare);
+ verifyNoTranslation(hello);
+ assertEquals(View.VISIBLE, redSquare.getVisibility());
+ assertEquals(View.VISIBLE, greenSquare.getVisibility());
+ assertEquals(View.VISIBLE, hello.getVisibility());
+ }
+ }
+
+ private void verifyTranslation(int slideEdge, View view) {
+ switch (slideEdge) {
+ case Gravity.LEFT:
+ case Gravity.START:
+ assertTrue(view.getTranslationX() < 0);
+ assertEquals(0f, view.getTranslationY(), 0.01f);
+ break;
+ case Gravity.RIGHT:
+ case Gravity.END:
+ assertTrue(view.getTranslationX() > 0);
+ assertEquals(0f, view.getTranslationY(), 0.01f);
+ break;
+ case Gravity.TOP:
+ assertTrue(view.getTranslationY() < 0);
+ assertEquals(0f, view.getTranslationX(), 0.01f);
+ break;
+ case Gravity.BOTTOM:
+ assertTrue(view.getTranslationY() > 0);
+ assertEquals(0f, view.getTranslationX(), 0.01f);
+ break;
+ }
+ }
+
+ private void verifyNoTranslation(View view) {
+ assertEquals(0f, view.getTranslationX(), 0.01f);
+ assertEquals(0f, view.getTranslationY(), 0.01f);
+ }
+
+}
diff --git a/android/support/transition/SyncRunnable.java b/android/support/transition/SyncRunnable.java
new file mode 100644
index 00000000..2e8a2e14
--- /dev/null
+++ b/android/support/transition/SyncRunnable.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+class SyncRunnable implements Runnable {
+
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+
+ @Override
+ public void run() {
+ mLatch.countDown();
+ }
+
+ boolean await() {
+ try {
+ return mLatch.await(3000, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ return false;
+ }
+
+}
diff --git a/android/support/transition/SyncTransitionListener.java b/android/support/transition/SyncTransitionListener.java
new file mode 100644
index 00000000..4d7e02e8
--- /dev/null
+++ b/android/support/transition/SyncTransitionListener.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2016 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.support.transition;
+
+import android.support.annotation.NonNull;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This {@link Transition.TransitionListener} synchronously waits for the specified callback.
+ */
+class SyncTransitionListener implements Transition.TransitionListener {
+
+ static final int EVENT_START = 1;
+ static final int EVENT_END = 2;
+ static final int EVENT_CANCEL = 3;
+ static final int EVENT_PAUSE = 4;
+ static final int EVENT_RESUME = 5;
+
+ private final int mTargetEvent;
+ private CountDownLatch mLatch = new CountDownLatch(1);
+
+ SyncTransitionListener(int event) {
+ mTargetEvent = event;
+ }
+
+ boolean await() {
+ try {
+ return mLatch.await(3000, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ return false;
+ }
+ }
+
+ void reset() {
+ mLatch = new CountDownLatch(1);
+ }
+
+ @Override
+ public void onTransitionStart(@NonNull Transition transition) {
+ if (mTargetEvent == EVENT_START) {
+ mLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ if (mTargetEvent == EVENT_END) {
+ mLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ if (mTargetEvent == EVENT_CANCEL) {
+ mLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ if (mTargetEvent == EVENT_PAUSE) {
+ mLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ if (mTargetEvent == EVENT_RESUME) {
+ mLatch.countDown();
+ }
+ }
+}
diff --git a/android/support/transition/TransitionActivity.java b/android/support/transition/TransitionActivity.java
new file mode 100644
index 00000000..ecb9355f
--- /dev/null
+++ b/android/support/transition/TransitionActivity.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 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.support.transition;
+
+import android.os.Bundle;
+import android.support.transition.test.R;
+import android.support.v4.app.FragmentActivity;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+public class TransitionActivity extends FragmentActivity {
+
+ private LinearLayout mRoot;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_transition);
+ mRoot = findViewById(R.id.root);
+ }
+
+ ViewGroup getRoot() {
+ return mRoot;
+ }
+
+}
diff --git a/android/support/transition/TransitionInflaterTest.java b/android/support/transition/TransitionInflaterTest.java
new file mode 100644
index 00000000..f9bd23fa
--- /dev/null
+++ b/android/support/transition/TransitionInflaterTest.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.support.annotation.NonNull;
+import android.support.test.filters.MediumTest;
+import android.support.transition.test.R;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.junit.Test;
+
+import java.util.List;
+
+@MediumTest
+public class TransitionInflaterTest extends BaseTest {
+
+ @Test
+ public void testInflationConstructors() throws Throwable {
+ TransitionInflater inflater = TransitionInflater.from(rule.getActivity());
+ Transition transition = inflater.inflateTransition(R.transition.transition_constructors);
+ assertTrue(transition instanceof TransitionSet);
+ TransitionSet set = (TransitionSet) transition;
+ assertEquals(10, set.getTransitionCount());
+ }
+
+ @Test
+ public void testInflation() {
+ TransitionInflater inflater = TransitionInflater.from(rule.getActivity());
+ verifyFadeProperties(inflater.inflateTransition(R.transition.fade));
+ verifyChangeBoundsProperties(inflater.inflateTransition(R.transition.change_bounds));
+ verifySlideProperties(inflater.inflateTransition(R.transition.slide));
+ verifyExplodeProperties(inflater.inflateTransition(R.transition.explode));
+ verifyChangeImageTransformProperties(
+ inflater.inflateTransition(R.transition.change_image_transform));
+ verifyChangeTransformProperties(inflater.inflateTransition(R.transition.change_transform));
+ verifyChangeClipBoundsProperties(
+ inflater.inflateTransition(R.transition.change_clip_bounds));
+ verifyAutoTransitionProperties(inflater.inflateTransition(R.transition.auto_transition));
+ verifyChangeScrollProperties(inflater.inflateTransition(R.transition.change_scroll));
+ verifyTransitionSetProperties(inflater.inflateTransition(R.transition.transition_set));
+ verifyCustomTransitionProperties(
+ inflater.inflateTransition(R.transition.custom_transition));
+ verifyTargetIds(inflater.inflateTransition(R.transition.target_ids));
+ verifyTargetNames(inflater.inflateTransition(R.transition.target_names));
+ verifyTargetClass(inflater.inflateTransition(R.transition.target_classes));
+ verifyArcMotion(inflater.inflateTransition(R.transition.arc_motion));
+ verifyCustomPathMotion(inflater.inflateTransition(R.transition.custom_path_motion));
+ verifyPatternPathMotion(inflater.inflateTransition(R.transition.pattern_path_motion));
+ }
+
+ // TODO: Add test for TransitionManager
+
+ private void verifyFadeProperties(Transition transition) {
+ assertTrue(transition instanceof Fade);
+ Fade fade = (Fade) transition;
+ assertEquals(Fade.OUT, fade.getMode());
+ }
+
+ private void verifyChangeBoundsProperties(Transition transition) {
+ assertTrue(transition instanceof ChangeBounds);
+ ChangeBounds changeBounds = (ChangeBounds) transition;
+ assertTrue(changeBounds.getResizeClip());
+ }
+
+ private void verifySlideProperties(Transition transition) {
+ assertTrue(transition instanceof Slide);
+ Slide slide = (Slide) transition;
+ assertEquals(Gravity.TOP, slide.getSlideEdge());
+ }
+
+ private void verifyExplodeProperties(Transition transition) {
+ assertTrue(transition instanceof Explode);
+ Visibility visibility = (Visibility) transition;
+ assertEquals(Visibility.MODE_IN, visibility.getMode());
+ }
+
+ private void verifyChangeImageTransformProperties(Transition transition) {
+ assertTrue(transition instanceof ChangeImageTransform);
+ }
+
+ private void verifyChangeTransformProperties(Transition transition) {
+ assertTrue(transition instanceof ChangeTransform);
+ ChangeTransform changeTransform = (ChangeTransform) transition;
+ assertFalse(changeTransform.getReparent());
+ assertFalse(changeTransform.getReparentWithOverlay());
+ }
+
+ private void verifyChangeClipBoundsProperties(Transition transition) {
+ assertTrue(transition instanceof ChangeClipBounds);
+ }
+
+ private void verifyAutoTransitionProperties(Transition transition) {
+ assertTrue(transition instanceof AutoTransition);
+ }
+
+ private void verifyChangeScrollProperties(Transition transition) {
+ assertTrue(transition instanceof ChangeScroll);
+ }
+
+ private void verifyTransitionSetProperties(Transition transition) {
+ assertTrue(transition instanceof TransitionSet);
+ TransitionSet set = (TransitionSet) transition;
+ assertEquals(TransitionSet.ORDERING_SEQUENTIAL, set.getOrdering());
+ assertEquals(2, set.getTransitionCount());
+ assertTrue(set.getTransitionAt(0) instanceof ChangeBounds);
+ assertTrue(set.getTransitionAt(1) instanceof Fade);
+ }
+
+ private void verifyCustomTransitionProperties(Transition transition) {
+ assertTrue(transition instanceof CustomTransition);
+ }
+
+ private void verifyTargetIds(Transition transition) {
+ List<Integer> targets = transition.getTargetIds();
+ assertNotNull(targets);
+ assertEquals(2, targets.size());
+ assertEquals(R.id.hello, (int) targets.get(0));
+ assertEquals(R.id.world, (int) targets.get(1));
+ }
+
+ private void verifyTargetNames(Transition transition) {
+ List<String> targets = transition.getTargetNames();
+ assertNotNull(targets);
+ assertEquals(2, targets.size());
+ assertEquals("hello", targets.get(0));
+ assertEquals("world", targets.get(1));
+ }
+
+ private void verifyTargetClass(Transition transition) {
+ List<Class> targets = transition.getTargetTypes();
+ assertNotNull(targets);
+ assertEquals(2, targets.size());
+ assertEquals(TextView.class, targets.get(0));
+ assertEquals(ImageView.class, targets.get(1));
+ }
+
+ private void verifyArcMotion(Transition transition) {
+ assertNotNull(transition);
+ PathMotion motion = transition.getPathMotion();
+ assertNotNull(motion);
+ assertTrue(motion instanceof ArcMotion);
+ ArcMotion arcMotion = (ArcMotion) motion;
+ assertEquals(1f, arcMotion.getMinimumVerticalAngle(), 0.01f);
+ assertEquals(2f, arcMotion.getMinimumHorizontalAngle(), 0.01f);
+ assertEquals(53f, arcMotion.getMaximumAngle(), 0.01f);
+ }
+
+ private void verifyCustomPathMotion(Transition transition) {
+ assertNotNull(transition);
+ PathMotion motion = transition.getPathMotion();
+ assertNotNull(motion);
+ assertTrue(motion instanceof CustomPathMotion);
+ }
+
+ private void verifyPatternPathMotion(Transition transition) {
+ assertNotNull(transition);
+ PathMotion motion = transition.getPathMotion();
+ assertNotNull(motion);
+ assertTrue(motion instanceof PatternPathMotion);
+ PatternPathMotion pattern = (PatternPathMotion) motion;
+ Path path = pattern.getPatternPath();
+ PathMeasure measure = new PathMeasure(path, false);
+ assertEquals(200f, measure.getLength(), 0.1f);
+ }
+
+ public static class CustomTransition extends Transition {
+ public CustomTransition() {
+ fail("Default constructor was not expected");
+ }
+
+ @SuppressWarnings("unused") // This constructor is used in XML
+ public CustomTransition(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ }
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ }
+ }
+
+ public static class CustomPathMotion extends PathMotion {
+ public CustomPathMotion() {
+ fail("default constructor shouldn't be called.");
+ }
+
+ public CustomPathMotion(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public Path getPath(float startX, float startY, float endX, float endY) {
+ return null;
+ }
+ }
+
+ public static class InflationFade extends Fade {
+ public InflationFade(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationChangeBounds extends ChangeBounds {
+ public InflationChangeBounds(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationSlide extends Slide {
+ public InflationSlide(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationTransitionSet extends TransitionSet {
+ public InflationTransitionSet(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationChangeImageTransform extends ChangeImageTransform {
+ public InflationChangeImageTransform(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationChangeTransform extends ChangeTransform {
+ public InflationChangeTransform(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationAutoTransition extends AutoTransition {
+ public InflationAutoTransition(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationChangeClipBounds extends ChangeClipBounds {
+ public InflationChangeClipBounds(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationChangeScroll extends ChangeScroll {
+ public InflationChangeScroll(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+ public static class InflationExplode extends Explode {
+ public InflationExplode(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ }
+
+}
diff --git a/android/support/transition/TransitionManagerTest.java b/android/support/transition/TransitionManagerTest.java
new file mode 100644
index 00000000..dc4f9832
--- /dev/null
+++ b/android/support/transition/TransitionManagerTest.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2016 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.support.transition;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.support.transition.test.R;
+import android.view.ViewGroup;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@MediumTest
+public class TransitionManagerTest extends BaseTest {
+
+ private Scene[] mScenes = new Scene[2];
+
+ @Before
+ public void prepareScenes() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ mScenes[0] = Scene.getSceneForLayout(root, R.layout.support_scene0, activity);
+ mScenes[1] = Scene.getSceneForLayout(root, R.layout.support_scene1, activity);
+ }
+
+ @Test
+ public void testSetup() {
+ assertThat(mScenes[0], is(notNullValue()));
+ assertThat(mScenes[1], is(notNullValue()));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testGo_enterAction() {
+ CheckCalledRunnable runnable = new CheckCalledRunnable();
+ mScenes[0].setEnterAction(runnable);
+ assertThat(runnable.wasCalled(), is(false));
+ TransitionManager.go(mScenes[0]);
+ assertThat(runnable.wasCalled(), is(true));
+ }
+
+ @Test
+ public void testGo_exitAction() throws Throwable {
+ final CheckCalledRunnable enter = new CheckCalledRunnable();
+ final CheckCalledRunnable exit = new CheckCalledRunnable();
+ mScenes[0].setEnterAction(enter);
+ mScenes[0].setExitAction(exit);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ assertThat(enter.wasCalled(), is(false));
+ assertThat(exit.wasCalled(), is(false));
+ TransitionManager.go(mScenes[0]);
+ assertThat(enter.wasCalled(), is(true));
+ assertThat(exit.wasCalled(), is(false));
+ }
+ });
+ // Let the main thread catch up with the scene change
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.go(mScenes[1]);
+ assertThat(exit.wasCalled(), is(true));
+ }
+ });
+ }
+
+ @Test
+ public void testGo_transitionListenerStart() throws Throwable {
+ final SyncTransitionListener listener =
+ new SyncTransitionListener(SyncTransitionListener.EVENT_START);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Transition transition = new AutoTransition();
+ transition.setDuration(0);
+ assertThat(transition.addListener(listener), is(sameInstance(transition)));
+ TransitionManager.go(mScenes[0], transition);
+ }
+ });
+ assertThat("Timed out waiting for the TransitionListener",
+ listener.await(), is(true));
+ }
+
+ @Test
+ public void testGo_transitionListenerEnd() throws Throwable {
+ final SyncTransitionListener listener =
+ new SyncTransitionListener(SyncTransitionListener.EVENT_END);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Transition transition = new AutoTransition();
+ transition.setDuration(0);
+ assertThat(transition.addListener(listener), is(sameInstance(transition)));
+ TransitionManager.go(mScenes[0], transition);
+ }
+ });
+ assertThat("Timed out waiting for the TransitionListener",
+ listener.await(), is(true));
+ }
+
+ @Test
+ public void testGo_nullParameter() throws Throwable {
+ final ViewGroup root = rule.getActivity().getRoot();
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.go(mScenes[0], null);
+ assertThat(Scene.getCurrentScene(root), is(mScenes[0]));
+ TransitionManager.go(mScenes[1], null);
+ assertThat(Scene.getCurrentScene(root), is(mScenes[1]));
+ }
+ });
+ }
+
+ @Test
+ public void testEndTransitions() throws Throwable {
+ final ViewGroup root = rule.getActivity().getRoot();
+ final Transition transition = new AutoTransition();
+ // This transition is very long, but will be forced to end as soon as it starts
+ transition.setDuration(30000);
+ final Transition.TransitionListener listener = mock(Transition.TransitionListener.class);
+ transition.addListener(listener);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.go(mScenes[0], transition);
+ }
+ });
+ verify(listener, timeout(3000)).onTransitionStart(any(Transition.class));
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.endTransitions(root);
+ }
+ });
+ verify(listener, timeout(3000)).onTransitionEnd(any(Transition.class));
+ }
+
+ @Test
+ public void testEndTransitionsBeforeStarted() throws Throwable {
+ final ViewGroup root = rule.getActivity().getRoot();
+ final Transition transition = new AutoTransition();
+ transition.setDuration(0);
+ final Transition.TransitionListener listener = mock(Transition.TransitionListener.class);
+ transition.addListener(listener);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.go(mScenes[0], transition);
+ // This terminates the transition before it starts
+ TransitionManager.endTransitions(root);
+ }
+ });
+ verify(listener, never()).onTransitionStart(any(Transition.class));
+ verify(listener, never()).onTransitionEnd(any(Transition.class));
+ }
+
+}
diff --git a/android/support/transition/TransitionSetTest.java b/android/support/transition/TransitionSetTest.java
new file mode 100644
index 00000000..aec9ecb2
--- /dev/null
+++ b/android/support/transition/TransitionSetTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.transition;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.sameInstance;
+
+import android.support.test.filters.MediumTest;
+import android.support.transition.test.R;
+import android.view.View;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@MediumTest
+public class TransitionSetTest extends BaseTest {
+
+ private final TransitionSet mTransitionSet = new TransitionSet();
+ private final Transition mTransition = new TransitionTest.EmptyTransition();
+
+ @Before
+ public void setUp() {
+ // mTransitionSet has 1 item from the start
+ mTransitionSet.addTransition(mTransition);
+ }
+
+ @Test
+ public void testOrdering() {
+ assertThat(mTransitionSet.getOrdering(), is(TransitionSet.ORDERING_TOGETHER));
+ assertThat(mTransitionSet.setOrdering(TransitionSet.ORDERING_SEQUENTIAL),
+ is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getOrdering(), is(TransitionSet.ORDERING_SEQUENTIAL));
+ }
+
+ @Test
+ public void testAddAndRemoveTransition() {
+ assertThat(mTransitionSet.getTransitionCount(), is(1));
+ assertThat(mTransitionSet.getTransitionAt(0), is(sameInstance(mTransition)));
+ Transition anotherTransition = new TransitionTest.EmptyTransition();
+ assertThat(mTransitionSet.addTransition(anotherTransition),
+ is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTransitionCount(), is(2));
+ assertThat(mTransitionSet.getTransitionAt(0), is(sameInstance(mTransition)));
+ assertThat(mTransitionSet.getTransitionAt(1), is(sameInstance(anotherTransition)));
+ assertThat(mTransitionSet.removeTransition(mTransition),
+ is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTransitionCount(), is(1));
+ }
+
+ @Test
+ public void testSetDuration() {
+ assertThat(mTransitionSet.setDuration(123), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getDuration(), is(123L));
+ assertThat(mTransition.getDuration(), is(123L));
+ }
+
+ @Test
+ public void testTargetId() {
+ assertThat(mTransitionSet.addTarget(R.id.view0), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargetIds(), hasItem(R.id.view0));
+ assertThat(mTransitionSet.getTargetIds(), hasSize(1));
+ assertThat(mTransition.getTargetIds(), hasItem(R.id.view0));
+ assertThat(mTransition.getTargetIds(), hasSize(1));
+ assertThat(mTransitionSet.removeTarget(R.id.view0), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargetIds(), hasSize(0));
+ assertThat(mTransition.getTargetIds(), hasSize(0));
+ }
+
+ @Test
+ public void testTargetView() {
+ final View view = new View(rule.getActivity());
+ assertThat(mTransitionSet.addTarget(view), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargets(), hasItem(view));
+ assertThat(mTransitionSet.getTargets(), hasSize(1));
+ assertThat(mTransition.getTargets(), hasItem(view));
+ assertThat(mTransition.getTargets(), hasSize(1));
+ assertThat(mTransitionSet.removeTarget(view), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargets(), hasSize(0));
+ assertThat(mTransition.getTargets(), hasSize(0));
+ }
+
+ @Test
+ public void testTargetName() {
+ assertThat(mTransitionSet.addTarget("abc"), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargetNames(), hasItem("abc"));
+ assertThat(mTransitionSet.getTargetNames(), hasSize(1));
+ assertThat(mTransition.getTargetNames(), hasItem("abc"));
+ assertThat(mTransition.getTargetNames(), hasSize(1));
+ assertThat(mTransitionSet.removeTarget("abc"), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargetNames(), hasSize(0));
+ assertThat(mTransition.getTargetNames(), hasSize(0));
+ }
+
+ @Test
+ public void testTargetClass() {
+ assertThat(mTransitionSet.addTarget(View.class), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargetTypes(), hasItem(View.class));
+ assertThat(mTransitionSet.getTargetTypes(), hasSize(1));
+ assertThat(mTransition.getTargetTypes(), hasItem(View.class));
+ assertThat(mTransition.getTargetTypes(), hasSize(1));
+ assertThat(mTransitionSet.removeTarget(View.class), is(sameInstance(mTransitionSet)));
+ assertThat(mTransitionSet.getTargetTypes(), hasSize(0));
+ assertThat(mTransition.getTargetTypes(), hasSize(0));
+ }
+
+}
diff --git a/android/support/transition/TransitionTest.java b/android/support/transition/TransitionTest.java
new file mode 100644
index 00000000..72f6dae9
--- /dev/null
+++ b/android/support/transition/TransitionTest.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2016 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.support.transition;
+
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.graphics.Rect;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.support.transition.test.R;
+import android.support.v4.view.ViewCompat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.LinearInterpolator;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+@MediumTest
+public class TransitionTest extends BaseTest {
+
+ private Scene[] mScenes = new Scene[2];
+ private View[] mViews = new View[3];
+
+ @Before
+ public void prepareScenes() {
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ mScenes[0] = Scene.getSceneForLayout(root, R.layout.support_scene0, activity);
+ mScenes[1] = Scene.getSceneForLayout(root, R.layout.support_scene1, activity);
+ }
+
+ @Test
+ public void testName() {
+ Transition transition = new EmptyTransition();
+ assertThat(transition.getName(),
+ is(equalTo("android.support.transition.TransitionTest$EmptyTransition")));
+ }
+
+ @Test
+ public void testDuration() {
+ Transition transition = new EmptyTransition();
+ long duration = 12345;
+ assertThat(transition.setDuration(duration), is(sameInstance(transition)));
+ assertThat(transition.getDuration(), is(duration));
+ }
+
+ @Test
+ public void testInterpolator() {
+ Transition transition = new EmptyTransition();
+ TimeInterpolator interpolator = new LinearInterpolator();
+ assertThat(transition.setInterpolator(interpolator), is(sameInstance(transition)));
+ assertThat(transition.getInterpolator(), is(interpolator));
+ }
+
+ @Test
+ public void testStartDelay() {
+ Transition transition = new EmptyTransition();
+ long startDelay = 12345;
+ assertThat(transition.setStartDelay(startDelay), is(sameInstance(transition)));
+ assertThat(transition.getStartDelay(), is(startDelay));
+ }
+
+ @Test
+ public void testTargetIds() {
+ Transition transition = new EmptyTransition();
+ assertThat(transition.addTarget(R.id.view0), is(sameInstance(transition)));
+ assertThat(transition.addTarget(R.id.view1), is(sameInstance(transition)));
+ List<Integer> targetIds = transition.getTargetIds();
+ assertThat(targetIds.size(), is(2));
+ assertThat(targetIds, hasItem(R.id.view0));
+ assertThat(targetIds, hasItem(R.id.view1));
+ assertThat(transition.removeTarget(R.id.view0), is(sameInstance(transition)));
+ targetIds = transition.getTargetIds();
+ assertThat(targetIds.size(), is(1));
+ assertThat(targetIds, not(hasItem(R.id.view0)));
+ assertThat(targetIds, hasItem(R.id.view1));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testTargetView() {
+ // Set up views
+ TransitionActivity activity = rule.getActivity();
+ ViewGroup root = activity.getRoot();
+ View container = LayoutInflater.from(activity)
+ .inflate(R.layout.support_scene0, root, false);
+ root.addView(container);
+ View view0 = container.findViewById(R.id.view0);
+ View view1 = container.findViewById(R.id.view1);
+ // Test transition targets
+ Transition transition = new EmptyTransition();
+ assertThat(transition.addTarget(view0), is(sameInstance(transition)));
+ assertThat(transition.addTarget(view1), is(sameInstance(transition)));
+ List<View> targets = transition.getTargets();
+ assertThat(targets.size(), is(2));
+ assertThat(targets, hasItem(sameInstance(view0)));
+ assertThat(targets, hasItem(sameInstance(view1)));
+ assertThat(transition.removeTarget(view0), is(sameInstance(transition)));
+ targets = transition.getTargets();
+ assertThat(targets.size(), is(1));
+ assertThat(targets, not(hasItem(sameInstance(view0))));
+ assertThat(targets, hasItem(sameInstance(view1)));
+ }
+
+ @Test
+ public void testTargetName() {
+ Transition transition = new EmptyTransition();
+ assertThat(transition.addTarget("a"), is(sameInstance(transition)));
+ assertThat(transition.addTarget("b"), is(sameInstance(transition)));
+ List<String> targetNames = transition.getTargetNames();
+ assertNotNull(targetNames);
+ assertThat(targetNames.size(), is(2));
+ assertThat(targetNames, hasItem("a"));
+ assertThat(targetNames, hasItem("b"));
+ transition.removeTarget("a");
+ assertThat(targetNames.size(), is(1));
+ assertThat(targetNames, not(hasItem("a")));
+ assertThat(targetNames, hasItem("b"));
+ }
+
+ @Test
+ public void testTargetType() {
+ Transition transition = new EmptyTransition();
+ assertThat(transition.addTarget(Button.class), is(sameInstance(transition)));
+ assertThat(transition.addTarget(ImageView.class), is(sameInstance(transition)));
+ List<Class> targetTypes = transition.getTargetTypes();
+ assertNotNull(targetTypes);
+ assertThat(targetTypes.size(), is(2));
+ assertThat(targetTypes, hasItem(Button.class));
+ assertThat(targetTypes, hasItem(ImageView.class));
+ transition.removeTarget(Button.class);
+ assertThat(targetTypes.size(), is(1));
+ assertThat(targetTypes, not(hasItem(Button.class)));
+ assertThat(targetTypes, hasItem(ImageView.class));
+ }
+
+ @Test
+ public void testExcludeTargetId() throws Throwable {
+ showInitialScene();
+ Transition transition = new EmptyTransition();
+ transition.addTarget(R.id.view0);
+ transition.addTarget(R.id.view1);
+ View view0 = rule.getActivity().findViewById(R.id.view0);
+ View view1 = rule.getActivity().findViewById(R.id.view1);
+ assertThat(transition.isValidTarget(view0), is(true));
+ assertThat(transition.isValidTarget(view1), is(true));
+ transition.excludeTarget(R.id.view0, true);
+ assertThat(transition.isValidTarget(view0), is(false));
+ assertThat(transition.isValidTarget(view1), is(true));
+ }
+
+ @Test
+ public void testExcludeTargetView() throws Throwable {
+ showInitialScene();
+ Transition transition = new EmptyTransition();
+ View view0 = rule.getActivity().findViewById(R.id.view0);
+ View view1 = rule.getActivity().findViewById(R.id.view1);
+ transition.addTarget(view0);
+ transition.addTarget(view1);
+ assertThat(transition.isValidTarget(view0), is(true));
+ assertThat(transition.isValidTarget(view1), is(true));
+ transition.excludeTarget(view0, true);
+ assertThat(transition.isValidTarget(view0), is(false));
+ assertThat(transition.isValidTarget(view1), is(true));
+ }
+
+ @Test
+ public void testExcludeTargetName() throws Throwable {
+ showInitialScene();
+ Transition transition = new EmptyTransition();
+ View view0 = rule.getActivity().findViewById(R.id.view0);
+ View view1 = rule.getActivity().findViewById(R.id.view1);
+ ViewCompat.setTransitionName(view0, "zero");
+ ViewCompat.setTransitionName(view1, "one");
+ transition.addTarget("zero");
+ transition.addTarget("one");
+ assertThat(transition.isValidTarget(view0), is(true));
+ assertThat(transition.isValidTarget(view1), is(true));
+ transition.excludeTarget("zero", true);
+ assertThat(transition.isValidTarget(view0), is(false));
+ assertThat(transition.isValidTarget(view1), is(true));
+ }
+
+ @Test
+ public void testExcludeTargetType() throws Throwable {
+ showInitialScene();
+ Transition transition = new EmptyTransition();
+ FrameLayout container = (FrameLayout) rule.getActivity().findViewById(R.id.container);
+ View view0 = rule.getActivity().findViewById(R.id.view0);
+ transition.addTarget(View.class);
+ assertThat(transition.isValidTarget(container), is(true));
+ assertThat(transition.isValidTarget(view0), is(true));
+ transition.excludeTarget(FrameLayout.class, true);
+ assertThat(transition.isValidTarget(container), is(false));
+ assertThat(transition.isValidTarget(view0), is(true));
+ }
+
+ @Test
+ public void testListener() {
+ Transition transition = new EmptyTransition();
+ Transition.TransitionListener listener = new EmptyTransitionListener();
+ assertThat(transition.addListener(listener), is(sameInstance(transition)));
+ assertThat(transition.removeListener(listener), is(sameInstance(transition)));
+ }
+
+ @Test
+ public void testMatchOrder() throws Throwable {
+ showInitialScene();
+ final Transition transition = new ChangeBounds() {
+ @Nullable
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot,
+ @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) {
+ if (startValues != null && endValues != null) {
+ fail("Match by View ID should be prevented");
+ }
+ return super.createAnimator(sceneRoot, startValues, endValues);
+ }
+ };
+ transition.setDuration(0);
+ // This prevents matches between start and end scenes because they have different set of
+ // View instances. They will be regarded as independent views even though they share the
+ // same View IDs.
+ transition.setMatchOrder(Transition.MATCH_INSTANCE);
+ SyncRunnable enter1 = new SyncRunnable();
+ mScenes[1].setEnterAction(enter1);
+ goToScene(mScenes[1], transition);
+ if (!enter1.await()) {
+ fail("Timed out while waiting for scene change");
+ }
+ }
+
+ @Test
+ public void testExcludedTransitionAnimator() throws Throwable {
+ showInitialScene();
+ final Animator.AnimatorListener animatorListener = mock(Animator.AnimatorListener.class);
+ final DummyTransition transition = new DummyTransition(animatorListener);
+ final SyncTransitionListener transitionListener = new SyncTransitionListener(
+ SyncTransitionListener.EVENT_END);
+ transition.addListener(transitionListener);
+ transition.addTarget(mViews[0]);
+ transition.excludeTarget(mViews[0], true);
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.beginDelayedTransition(rule.getActivity().getRoot(), transition);
+ mViews[0].setTranslationX(3.f);
+ }
+ });
+ if (!transitionListener.await()) {
+ fail("Timed out waiting for the TransitionListener");
+ }
+ verify(animatorListener, never()).onAnimationStart(any(Animator.class));
+ }
+
+ @Test
+ public void testEpicenter() throws Throwable {
+ final Transition transition = new EmptyTransition();
+ final Transition.EpicenterCallback epicenterCallback = new Transition.EpicenterCallback() {
+ private Rect mRect = new Rect();
+
+ @Override
+ public Rect onGetEpicenter(@NonNull Transition t) {
+ assertThat(t, is(sameInstance(transition)));
+ mRect.set(1, 2, 3, 4);
+ return mRect;
+ }
+ };
+ transition.setEpicenterCallback(epicenterCallback);
+ assertThat(transition.getEpicenterCallback(),
+ is(sameInstance(transition.getEpicenterCallback())));
+ Rect rect = transition.getEpicenter();
+ assertNotNull(rect);
+ assertThat(rect.left, is(1));
+ assertThat(rect.top, is(2));
+ assertThat(rect.right, is(3));
+ assertThat(rect.bottom, is(4));
+ }
+
+ @Test
+ public void testSetPropagation() throws Throwable {
+ final Transition transition = new EmptyTransition();
+ assertThat(transition.getPropagation(), is(nullValue()));
+ final TransitionPropagation propagation = new CircularPropagation();
+ transition.setPropagation(propagation);
+ assertThat(propagation, is(sameInstance(propagation)));
+ }
+
+ @Test
+ public void testIsTransitionRequired() throws Throwable {
+ final EmptyTransition transition = new EmptyTransition();
+ assertThat(transition.isTransitionRequired(null, null), is(false));
+ final TransitionValues start = new TransitionValues();
+ final String propname = "android:transition:dummy";
+ start.values.put(propname, 1);
+ final TransitionValues end = new TransitionValues();
+ end.values.put(propname, 1);
+ assertThat(transition.isTransitionRequired(start, end), is(false));
+ end.values.put(propname, 2);
+ assertThat(transition.isTransitionRequired(start, end), is(true));
+ }
+
+ private void showInitialScene() throws Throwable {
+ SyncRunnable enter0 = new SyncRunnable();
+ mScenes[0].setEnterAction(enter0);
+ AutoTransition transition1 = new AutoTransition();
+ transition1.setDuration(0);
+ goToScene(mScenes[0], transition1);
+ if (!enter0.await()) {
+ fail("Timed out while waiting for scene change");
+ }
+ mViews[0] = rule.getActivity().findViewById(R.id.view0);
+ mViews[1] = rule.getActivity().findViewById(R.id.view1);
+ mViews[2] = rule.getActivity().findViewById(R.id.view2);
+ }
+
+ private void goToScene(final Scene scene, final Transition transition) throws Throwable {
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TransitionManager.go(scene, transition);
+ }
+ });
+ }
+
+ public static class EmptyTransition extends Transition {
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ }
+
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot,
+ @Nullable TransitionValues startValues,
+ @Nullable TransitionValues endValues) {
+ return null;
+ }
+
+ }
+
+ public static class EmptyTransitionListener implements Transition.TransitionListener {
+
+ @Override
+ public void onTransitionStart(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ }
+
+ }
+
+ /**
+ * A dummy transition for monitoring use of its animator by the Transition framework.
+ */
+ private static class DummyTransition extends Transition {
+
+ private final Animator.AnimatorListener mListener;
+
+ DummyTransition(Animator.AnimatorListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ transitionValues.values.put("state", 1);
+ }
+
+ @Override
+ public void captureEndValues(@NonNull TransitionValues transitionValues) {
+ transitionValues.values.put("state", 2);
+ }
+
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot, TransitionValues startValues,
+ TransitionValues endValues) {
+ if (startValues == null || endValues == null) {
+ return null;
+ }
+ final ObjectAnimator animator = ObjectAnimator
+ .ofFloat(startValues.view, "translationX", 1.f, 2.f);
+ animator.addListener(mListener);
+ return animator;
+ }
+
+ }
+}
diff --git a/android/support/transition/VisibilityTest.java b/android/support/transition/VisibilityTest.java
new file mode 100644
index 00000000..dcfcbdc3
--- /dev/null
+++ b/android/support/transition/VisibilityTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2016 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.support.transition;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+@MediumTest
+public class VisibilityTest extends BaseTest {
+
+ private View mView;
+ private ViewGroup mRoot;
+
+ @UiThreadTest
+ @Before
+ public void setUp() {
+ mRoot = rule.getActivity().getRoot();
+ mView = new View(rule.getActivity());
+ mRoot.addView(mView, new ViewGroup.LayoutParams(100, 100));
+ }
+
+ @Test
+ public void testMode() {
+ final CustomVisibility visibility = new CustomVisibility();
+ assertThat(visibility.getMode(), is(Visibility.MODE_IN | Visibility.MODE_OUT));
+ visibility.setMode(Visibility.MODE_IN);
+ assertThat(visibility.getMode(), is(Visibility.MODE_IN));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testCustomVisibility() {
+ final CustomVisibility visibility = new CustomVisibility();
+ assertThat(visibility.getName(), is(equalTo(CustomVisibility.class.getName())));
+ assertNotNull(visibility.getTransitionProperties());
+
+ // Capture start values
+ mView.setScaleX(0.5f);
+ final TransitionValues startValues = new TransitionValues();
+ startValues.view = mView;
+ visibility.captureStartValues(startValues);
+ assertThat((float) startValues.values.get(CustomVisibility.PROPNAME_SCALE_X), is(0.5f));
+
+ // Hide the view and capture end values
+ mView.setVisibility(View.GONE);
+ final TransitionValues endValues = new TransitionValues();
+ endValues.view = mView;
+ visibility.captureEndValues(endValues);
+
+ // This should invoke onDisappear, not onAppear
+ ObjectAnimator animator = (ObjectAnimator) visibility
+ .createAnimator(mRoot, startValues, endValues);
+ assertNotNull(animator);
+ assertThat(animator.getPropertyName(), is(equalTo("scaleX")));
+
+ // Jump to the end of the animation
+ animator.end();
+
+ // This value confirms that onDisappear, not onAppear, was called
+ assertThat((float) animator.getAnimatedValue(), is(0.25f));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testCustomVisibility2() {
+ final CustomVisibility2 visibility = new CustomVisibility2();
+ final TransitionValues startValues = new TransitionValues();
+ startValues.view = mView;
+ visibility.captureStartValues(startValues);
+ mView.setVisibility(View.GONE);
+ final TransitionValues endValues = new TransitionValues();
+ endValues.view = mView;
+ visibility.captureEndValues(endValues);
+ ObjectAnimator animator = (ObjectAnimator) visibility
+ .createAnimator(mRoot, startValues, endValues);
+ assertNotNull(animator);
+
+ // Jump to the end of the animation
+ animator.end();
+
+ // This value confirms that onDisappear, not onAppear, was called
+ assertThat((float) animator.getAnimatedValue(), is(0.25f));
+ }
+
+ /**
+ * A custom {@link Visibility} with 5-arg onAppear/Disappear
+ */
+ public static class CustomVisibility extends Visibility {
+
+ static final String PROPNAME_SCALE_X = "customVisibility:scaleX";
+
+ private static String[] sTransitionProperties;
+
+ @Nullable
+ @Override
+ public String[] getTransitionProperties() {
+ if (sTransitionProperties == null) {
+ String[] properties = super.getTransitionProperties();
+ if (properties != null) {
+ sTransitionProperties = Arrays.copyOf(properties, properties.length + 1);
+ } else {
+ sTransitionProperties = new String[1];
+ }
+ sTransitionProperties[sTransitionProperties.length - 1] = PROPNAME_SCALE_X;
+ }
+ return sTransitionProperties;
+ }
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ super.captureStartValues(transitionValues);
+ transitionValues.values.put(PROPNAME_SCALE_X, transitionValues.view.getScaleX());
+ }
+
+ @Override
+ public Animator onAppear(ViewGroup sceneRoot, TransitionValues startValues,
+ int startVisibility, TransitionValues endValues, int endVisibility) {
+ if (startValues == null) {
+ return null;
+ }
+ float startScaleX = (float) startValues.values.get(PROPNAME_SCALE_X);
+ return ObjectAnimator.ofFloat(startValues.view, "scaleX", startScaleX, 0.75f);
+ }
+
+ @Override
+ public Animator onDisappear(ViewGroup sceneRoot, TransitionValues startValues,
+ int startVisibility, TransitionValues endValues, int endVisibility) {
+ if (startValues == null) {
+ return null;
+ }
+ float startScaleX = (float) startValues.values.get(PROPNAME_SCALE_X);
+ return ObjectAnimator.ofFloat(startValues.view, "scaleX", startScaleX, 0.25f);
+ }
+
+ }
+
+ /**
+ * A custom {@link Visibility} with 4-arg onAppear/Disappear
+ */
+ public static class CustomVisibility2 extends Visibility {
+
+ static final String PROPNAME_SCALE_X = "customVisibility:scaleX";
+
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ super.captureStartValues(transitionValues);
+ transitionValues.values.put(PROPNAME_SCALE_X, transitionValues.view.getScaleX());
+ }
+
+ @Override
+ public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
+ TransitionValues endValues) {
+ float startScaleX = startValues == null ? 0.25f :
+ (float) startValues.values.get(PROPNAME_SCALE_X);
+ return ObjectAnimator.ofFloat(view, "scaleX", startScaleX, 0.75f);
+ }
+
+ @Override
+ public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues,
+ TransitionValues endValues) {
+ if (startValues == null) {
+ return null;
+ }
+ float startScaleX = (float) startValues.values.get(PROPNAME_SCALE_X);
+ return ObjectAnimator.ofFloat(view, "scaleX", startScaleX, 0.25f);
+ }
+
+ }
+
+}
diff --git a/android/support/v13/app/ActivityCompat.java b/android/support/v13/app/ActivityCompat.java
index b0c3c30c..7c1546a5 100644
--- a/android/support/v13/app/ActivityCompat.java
+++ b/android/support/v13/app/ActivityCompat.java
@@ -16,32 +16,22 @@
package android.support.v13.app;
-import android.app.Activity;
-import android.support.v13.view.DragAndDropPermissionsCompat;
-import android.view.DragEvent;
-
/**
* Helper for accessing features in {@link android.app.Activity} in a backwards compatible fashion.
+ *
+ * @deprecated Use {@link android.support.v4.app.ActivityCompat
+ * android.support.v4.app.ActivityCompat}.
*/
+@Deprecated
public class ActivityCompat extends android.support.v4.app.ActivityCompat {
-
- /**
- * Create {@link DragAndDropPermissionsCompat} object bound to this activity and controlling
- * the access permissions for content URIs associated with the {@link android.view.DragEvent}.
- * @param dragEvent Drag event to request permission for
- * @return The {@link DragAndDropPermissionsCompat} object used to control access to the content
- * URIs. {@code null} if no content URIs are associated with the event or if permissions could
- * not be granted.
- */
- public static DragAndDropPermissionsCompat requestDragAndDropPermissions(Activity activity,
- DragEvent dragEvent) {
- return DragAndDropPermissionsCompat.request(activity, dragEvent);
- }
-
/**
* This class should not be instantiated, but the constructor must be
* visible for the class to be extended.
+ *
+ * @deprecated Use {@link android.support.v4.app.ActivityCompat
+ * android.support.v4.app.ActivityCompat}.
*/
+ @Deprecated
protected ActivityCompat() {
// Not publicly instantiable, but may be extended.
}
diff --git a/android/support/v13/app/FragmentCompat.java b/android/support/v13/app/FragmentCompat.java
index 31c2343e..e8915fb8 100644
--- a/android/support/v13/app/FragmentCompat.java
+++ b/android/support/v13/app/FragmentCompat.java
@@ -30,8 +30,19 @@ import java.util.Arrays;
/**
* Helper for accessing features in {@link Fragment} in a backwards compatible fashion.
+ *
+ * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework fragment.
*/
+@Deprecated
public class FragmentCompat {
+
+ /**
+ * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework fragment.
+ */
+ @Deprecated
+ public FragmentCompat() {
+ }
+
interface FragmentCompatImpl {
void setUserVisibleHint(Fragment f, boolean deferStart);
void requestPermissions(Fragment fragment, String[] permissions, int requestCode);
@@ -48,7 +59,11 @@ public class FragmentCompat {
* to the compatibility methods in this class will first check whether the delegate can
* handle the method call, and invoke the corresponding method if it can.
* </p>
+ *
+ * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework
+ * {@link Fragment}.
*/
+ @Deprecated
public interface PermissionCompatDelegate {
/**
@@ -66,7 +81,11 @@ public class FragmentCompat {
*
* @return Whether the delegate has handled the permission request.
* @see FragmentCompat#requestPermissions(Fragment, String[], int)
+ *
+ * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework
+ * {@link Fragment}.
*/
+ @Deprecated
boolean requestPermissions(Fragment fragment, String[] permissions, int requestCode);
}
@@ -157,22 +176,34 @@ public class FragmentCompat {
* delegate.
*
* @param delegate The delegate to be set. {@code null} to clear the set delegate.
+ *
+ * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework
+ * {@link Fragment}.
*/
+ @Deprecated
public static void setPermissionCompatDelegate(PermissionCompatDelegate delegate) {
sDelegate = delegate;
}
/**
* @hide
+ *
+ * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework
+ * {@link Fragment}.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Deprecated
public static PermissionCompatDelegate getPermissionCompatDelegate() {
return sDelegate;
}
/**
* This interface is the contract for receiving the results for permission requests.
+ *
+ * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework
+ * {@link Fragment}.
*/
+ @Deprecated
public interface OnRequestPermissionsResultCallback {
/**
@@ -188,7 +219,11 @@ public class FragmentCompat {
* or {@link android.content.pm.PackageManager#PERMISSION_DENIED}. Never null.
*
* @see #requestPermissions(android.app.Fragment, String[], int)
+ *
+ * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework
+ * {@link Fragment}.
*/
+ @Deprecated
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults);
}
@@ -197,7 +232,8 @@ public class FragmentCompat {
* Call {@link Fragment#setMenuVisibility(boolean) Fragment.setMenuVisibility(boolean)}
* if running on an appropriate version of the platform.
*
- * @deprecated Use {@link Fragment#setMenuVisibility(boolean)} directly.
+ * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework
+ * {@link Fragment}.
*/
@Deprecated
public static void setMenuVisibility(Fragment f, boolean visible) {
@@ -207,7 +243,11 @@ public class FragmentCompat {
/**
* Call {@link Fragment#setUserVisibleHint(boolean) setUserVisibleHint(boolean)}
* if running on an appropriate version of the platform.
+ *
+ * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework
+ * {@link Fragment}.
*/
+ @Deprecated
public static void setUserVisibleHint(Fragment f, boolean deferStart) {
IMPL.setUserVisibleHint(f, deferStart);
}
@@ -262,7 +302,11 @@ public class FragmentCompat {
* @see android.support.v4.content.ContextCompat#checkSelfPermission(
* android.content.Context, String)
* @see #shouldShowRequestPermissionRationale(android.app.Fragment, String)
+ *
+ * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework
+ * {@link Fragment}.
*/
+ @Deprecated
public static void requestPermissions(@NonNull Fragment fragment,
@NonNull String[] permissions, int requestCode) {
if (sDelegate != null && sDelegate.requestPermissions(fragment, permissions, requestCode)) {
@@ -293,7 +337,11 @@ public class FragmentCompat {
* @see android.support.v4.content.ContextCompat#checkSelfPermission(
* android.content.Context, String)
* @see #requestPermissions(android.app.Fragment, String[], int)
+ *
+ * @deprecated Use {@link android.support.v4.app.Fragment} instead of the framework
+ * {@link Fragment}.
*/
+ @Deprecated
public static boolean shouldShowRequestPermissionRationale(@NonNull Fragment fragment,
@NonNull String permission) {
return IMPL.shouldShowRequestPermissionRationale(fragment, permission);
diff --git a/android/support/v13/app/FragmentPagerAdapter.java b/android/support/v13/app/FragmentPagerAdapter.java
index e0b788ab..112ed023 100644
--- a/android/support/v13/app/FragmentPagerAdapter.java
+++ b/android/support/v13/app/FragmentPagerAdapter.java
@@ -61,7 +61,10 @@ import android.view.ViewGroup;
*
* {@sample frameworks/support/samples/Support13Demos/src/main/res/layout/fragment_pager_list.xml
* complete}
+ *
+ * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead.
*/
+@Deprecated
public abstract class FragmentPagerAdapter extends PagerAdapter {
private static final String TAG = "FragmentPagerAdapter";
private static final boolean DEBUG = false;
@@ -70,15 +73,26 @@ public abstract class FragmentPagerAdapter extends PagerAdapter {
private FragmentTransaction mCurTransaction = null;
private Fragment mCurrentPrimaryItem = null;
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead.
+ */
+ @Deprecated
public FragmentPagerAdapter(FragmentManager fm) {
mFragmentManager = fm;
}
/**
* Return the Fragment associated with a specified position.
+ *
+ * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead.
*/
+ @Deprecated
public abstract Fragment getItem(int position);
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead.
+ */
+ @Deprecated
@Override
public void startUpdate(ViewGroup container) {
if (container.getId() == View.NO_ID) {
@@ -87,6 +101,10 @@ public abstract class FragmentPagerAdapter extends PagerAdapter {
}
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead.
+ */
+ @Deprecated
@SuppressWarnings("ReferenceEquality")
@Override
public Object instantiateItem(ViewGroup container, int position) {
@@ -116,6 +134,10 @@ public abstract class FragmentPagerAdapter extends PagerAdapter {
return fragment;
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead.
+ */
+ @Deprecated
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (mCurTransaction == null) {
@@ -126,6 +148,10 @@ public abstract class FragmentPagerAdapter extends PagerAdapter {
mCurTransaction.detach((Fragment)object);
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead.
+ */
+ @Deprecated
@SuppressWarnings("ReferenceEquality")
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
@@ -143,6 +169,10 @@ public abstract class FragmentPagerAdapter extends PagerAdapter {
}
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead.
+ */
+ @Deprecated
@Override
public void finishUpdate(ViewGroup container) {
if (mCurTransaction != null) {
@@ -152,16 +182,28 @@ public abstract class FragmentPagerAdapter extends PagerAdapter {
}
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead.
+ */
+ @Deprecated
@Override
public boolean isViewFromObject(View view, Object object) {
return ((Fragment)object).getView() == view;
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead.
+ */
+ @Deprecated
@Override
public Parcelable saveState() {
return null;
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead.
+ */
+ @Deprecated
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
}
@@ -174,7 +216,10 @@ public abstract class FragmentPagerAdapter extends PagerAdapter {
*
* @param position Position within this adapter
* @return Unique identifier for the item at position
+ *
+ * @deprecated Use {@link android.support.v4.app.FragmentPagerAdapter} instead.
*/
+ @Deprecated
public long getItemId(int position) {
return position;
}
diff --git a/android/support/v13/app/FragmentStatePagerAdapter.java b/android/support/v13/app/FragmentStatePagerAdapter.java
index 45a6bf53..76a32245 100644
--- a/android/support/v13/app/FragmentStatePagerAdapter.java
+++ b/android/support/v13/app/FragmentStatePagerAdapter.java
@@ -64,7 +64,10 @@ import java.util.ArrayList;
*
* {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_pager_list.xml
* complete}
+ *
+ * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead.
*/
+@Deprecated
public abstract class FragmentStatePagerAdapter extends PagerAdapter {
private static final String TAG = "FragStatePagerAdapter";
private static final boolean DEBUG = false;
@@ -76,15 +79,26 @@ public abstract class FragmentStatePagerAdapter extends PagerAdapter {
private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
private Fragment mCurrentPrimaryItem = null;
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead.
+ */
+ @Deprecated
public FragmentStatePagerAdapter(FragmentManager fm) {
mFragmentManager = fm;
}
/**
* Return the Fragment associated with a specified position.
+ *
+ * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead.
*/
+ @Deprecated
public abstract Fragment getItem(int position);
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead.
+ */
+ @Deprecated
@Override
public void startUpdate(ViewGroup container) {
if (container.getId() == View.NO_ID) {
@@ -93,6 +107,10 @@ public abstract class FragmentStatePagerAdapter extends PagerAdapter {
}
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead.
+ */
+ @Deprecated
@Override
public Object instantiateItem(ViewGroup container, int position) {
// If we already have this item instantiated, there is nothing
@@ -129,6 +147,10 @@ public abstract class FragmentStatePagerAdapter extends PagerAdapter {
return fragment;
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead.
+ */
+ @Deprecated
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment) object;
@@ -148,6 +170,10 @@ public abstract class FragmentStatePagerAdapter extends PagerAdapter {
mCurTransaction.remove(fragment);
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead.
+ */
+ @Deprecated
@SuppressWarnings("ReferenceEquality")
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
@@ -165,6 +191,10 @@ public abstract class FragmentStatePagerAdapter extends PagerAdapter {
}
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead.
+ */
+ @Deprecated
@Override
public void finishUpdate(ViewGroup container) {
if (mCurTransaction != null) {
@@ -174,11 +204,19 @@ public abstract class FragmentStatePagerAdapter extends PagerAdapter {
}
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead.
+ */
+ @Deprecated
@Override
public boolean isViewFromObject(View view, Object object) {
return ((Fragment)object).getView() == view;
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead.
+ */
+ @Deprecated
@Override
public Parcelable saveState() {
Bundle state = null;
@@ -201,6 +239,10 @@ public abstract class FragmentStatePagerAdapter extends PagerAdapter {
return state;
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentStatePagerAdapter} instead.
+ */
+ @Deprecated
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
if (state != null) {
diff --git a/android/support/v13/app/FragmentTabHost.java b/android/support/v13/app/FragmentTabHost.java
index 2326ccb6..5c34ab57 100644
--- a/android/support/v13/app/FragmentTabHost.java
+++ b/android/support/v13/app/FragmentTabHost.java
@@ -38,7 +38,10 @@ import java.util.ArrayList;
* Version of {@link android.support.v4.app.FragmentTabHost} that can be
* used with the platform {@link android.app.Fragment} APIs. You will not
* normally use this, instead using action bar tabs.
+ *
+ * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead.
*/
+@Deprecated
public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListener {
private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>();
private FrameLayout mRealTabContent;
@@ -117,6 +120,10 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe
};
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead.
+ */
+ @Deprecated
public FragmentTabHost(Context context) {
// Note that we call through to the version that takes an AttributeSet,
// because the simple Context construct can result in a broken object!
@@ -124,6 +131,10 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe
initFragmentTabHost(context, null);
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead.
+ */
+ @Deprecated
public FragmentTabHost(Context context, AttributeSet attrs) {
super(context, attrs);
initFragmentTabHost(context, attrs);
@@ -167,9 +178,7 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe
}
/**
- * @deprecated Don't call the original TabHost setup, you must instead
- * call {@link #setup(Context, FragmentManager)} or
- * {@link #setup(Context, FragmentManager, int)}.
+ * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead.
*/
@Override
@Deprecated
@@ -178,6 +187,10 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe
"Must call setup() that takes a Context and FragmentManager");
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead.
+ */
+ @Deprecated
public void setup(Context context, FragmentManager manager) {
ensureHierarchy(context); // Ensure views required by super.setup()
super.setup();
@@ -186,6 +199,10 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe
ensureContent();
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead.
+ */
+ @Deprecated
public void setup(Context context, FragmentManager manager, int containerId) {
ensureHierarchy(context); // Ensure views required by super.setup()
super.setup();
@@ -212,11 +229,19 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe
}
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead.
+ */
+ @Deprecated
@Override
public void setOnTabChangedListener(OnTabChangeListener l) {
mOnTabChangeListener = l;
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead.
+ */
+ @Deprecated
public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle args) {
tabSpec.setContent(new DummyTabFactory(mContext));
String tag = tabSpec.getTag();
@@ -239,6 +264,10 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe
addTab(tabSpec);
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead.
+ */
+ @Deprecated
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
@@ -278,12 +307,20 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe
}
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead.
+ */
+ @Deprecated
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mAttached = false;
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead.
+ */
+ @Deprecated
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
@@ -292,6 +329,10 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe
return ss;
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead.
+ */
+ @Deprecated
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
@@ -303,6 +344,10 @@ public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListe
setCurrentTabByTag(ss.curTab);
}
+ /**
+ * @deprecated Use {@link android.support.v4.app.FragmentTabHost} instead.
+ */
+ @Deprecated
@Override
public void onTabChanged(String tabId) {
if (mAttached) {
diff --git a/android/support/v17/leanback/app/BrowseFragment.java b/android/support/v17/leanback/app/BrowseFragment.java
index c561ea99..a2439e43 100644
--- a/android/support/v17/leanback/app/BrowseFragment.java
+++ b/android/support/v17/leanback/app/BrowseFragment.java
@@ -1486,6 +1486,9 @@ public class BrowseFragment extends BaseFragment {
if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
return;
}
+ if (mMainFragment == null || mMainFragment.getView() == null) {
+ return;
+ }
startHeadersTransitionInternal(false);
mMainFragment.getView().requestFocus();
}
diff --git a/android/support/v17/leanback/app/BrowseSupportFragment.java b/android/support/v17/leanback/app/BrowseSupportFragment.java
index c28064ca..114e0a7a 100644
--- a/android/support/v17/leanback/app/BrowseSupportFragment.java
+++ b/android/support/v17/leanback/app/BrowseSupportFragment.java
@@ -1463,6 +1463,9 @@ public class BrowseSupportFragment extends BaseSupportFragment {
if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
return;
}
+ if (mMainFragment == null || mMainFragment.getView() == null) {
+ return;
+ }
startHeadersTransitionInternal(false);
mMainFragment.getView().requestFocus();
}
diff --git a/android/support/v17/leanback/widget/GridLayoutManager.java b/android/support/v17/leanback/widget/GridLayoutManager.java
index 9d159eca..613198fd 100644
--- a/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -22,6 +22,7 @@ import static android.support.v7.widget.RecyclerView.VERTICAL;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
+import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@@ -3655,7 +3656,32 @@ final class GridLayoutManager extends RecyclerView.LayoutManager {
public boolean performAccessibilityAction(Recycler recycler, State state, int action,
Bundle args) {
saveContext(recycler, state);
- switch (action) {
+ int translatedAction = action;
+ boolean reverseFlowPrimary = (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0;
+ if (Build.VERSION.SDK_INT >= 23) {
+ if (mOrientation == HORIZONTAL) {
+ if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat
+ .ACTION_SCROLL_LEFT.getId()) {
+ translatedAction = reverseFlowPrimary
+ ? AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD :
+ AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD;
+ } else if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat
+ .ACTION_SCROLL_RIGHT.getId()) {
+ translatedAction = reverseFlowPrimary
+ ? AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD :
+ AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD;
+ }
+ } else { // VERTICAL layout
+ if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP
+ .getId()) {
+ translatedAction = AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD;
+ } else if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat
+ .ACTION_SCROLL_DOWN.getId()) {
+ translatedAction = AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD;
+ }
+ }
+ }
+ switch (translatedAction) {
case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD:
processSelectionMoves(false, -1);
break;
@@ -3726,12 +3752,39 @@ final class GridLayoutManager extends RecyclerView.LayoutManager {
AccessibilityNodeInfoCompat info) {
saveContext(recycler, state);
int count = state.getItemCount();
- if ((mFlag & PF_SCROLL_ENABLED) != 0 && count > 1 && !isItemFullyVisible(0)) {
- info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
+ boolean reverseFlowPrimary = (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0;
+ if (count > 1 && !isItemFullyVisible(0)) {
+ if (Build.VERSION.SDK_INT >= 23) {
+ if (mOrientation == HORIZONTAL) {
+ info.addAction(reverseFlowPrimary
+ ? AccessibilityNodeInfoCompat.AccessibilityActionCompat
+ .ACTION_SCROLL_RIGHT :
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat
+ .ACTION_SCROLL_LEFT);
+ } else {
+ info.addAction(
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP);
+ }
+ } else {
+ info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
+ }
info.setScrollable(true);
}
- if ((mFlag & PF_SCROLL_ENABLED) != 0 && count > 1 && !isItemFullyVisible(count - 1)) {
- info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
+ if (count > 1 && !isItemFullyVisible(count - 1)) {
+ if (Build.VERSION.SDK_INT >= 23) {
+ if (mOrientation == HORIZONTAL) {
+ info.addAction(reverseFlowPrimary
+ ? AccessibilityNodeInfoCompat.AccessibilityActionCompat
+ .ACTION_SCROLL_LEFT :
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat
+ .ACTION_SCROLL_RIGHT);
+ } else {
+ info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat
+ .ACTION_SCROLL_DOWN);
+ }
+ } else {
+ info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
+ }
info.setScrollable(true);
}
final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo =
diff --git a/android/support/v17/leanback/widget/WindowAlignment.java b/android/support/v17/leanback/widget/WindowAlignment.java
index 55fa7589..c6589d49 100644
--- a/android/support/v17/leanback/widget/WindowAlignment.java
+++ b/android/support/v17/leanback/widget/WindowAlignment.java
@@ -390,11 +390,7 @@ class WindowAlignment {
@Override
public String toString() {
- return new StringBuffer().append("horizontal=")
- .append(horizontal.toString())
- .append("; vertical=")
- .append(vertical.toString())
- .toString();
+ return "horizontal=" + horizontal + "; vertical=" + vertical;
}
}
diff --git a/android/support/v4/app/ActivityCompat.java b/android/support/v4/app/ActivityCompat.java
index 5833481a..9d15be1d 100644
--- a/android/support/v4/app/ActivityCompat.java
+++ b/android/support/v4/app/ActivityCompat.java
@@ -34,7 +34,9 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
+import android.support.v13.view.DragAndDropPermissionsCompat;
import android.support.v4.content.ContextCompat;
+import android.view.DragEvent;
import android.view.View;
import java.util.List;
@@ -528,6 +530,19 @@ public class ActivityCompat extends ContextCompat {
return false;
}
+ /**
+ * Create {@link DragAndDropPermissionsCompat} object bound to this activity and controlling
+ * the access permissions for content URIs associated with the {@link android.view.DragEvent}.
+ * @param dragEvent Drag event to request permission for
+ * @return The {@link DragAndDropPermissionsCompat} object used to control access to the content
+ * URIs. {@code null} if no content URIs are associated with the event or if permissions could
+ * not be granted.
+ */
+ public static DragAndDropPermissionsCompat requestDragAndDropPermissions(Activity activity,
+ DragEvent dragEvent) {
+ return DragAndDropPermissionsCompat.request(activity, dragEvent);
+ }
+
@RequiresApi(21)
private static class SharedElementCallback21Impl extends android.app.SharedElementCallback {
diff --git a/android/support/v4/app/Fragment.java b/android/support/v4/app/Fragment.java
index e734a274..5b560cd2 100644
--- a/android/support/v4/app/Fragment.java
+++ b/android/support/v4/app/Fragment.java
@@ -1816,8 +1816,9 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener
* use the same value as set in {@link #setEnterTransition(Object)}.
*
* @param transition The Transition to use to move Views out of the Scene when the Fragment
- * is preparing to close. <code>transition</code> must be an
- * android.transition.Transition.
+ * is preparing to close. <code>transition</code> must be an
+ * {@link android.transition.Transition android.transition.Transition} or
+ * {@link android.support.transition.Transition android.support.transition.Transition}.
*/
public void setReturnTransition(@Nullable Object transition) {
ensureAnimationInfo().mReturnTransition = transition;
@@ -1854,8 +1855,10 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener
* remain unaffected.
*
* @param transition The Transition to use to move Views out of the Scene when the Fragment
- * is being closed not due to popping the back stack. <code>transition</code>
- * must be an android.transition.Transition.
+ * is being closed not due to popping the back stack. <code>transition</code>
+ * must be an
+ * {@link android.transition.Transition android.transition.Transition} or
+ * {@link android.support.transition.Transition android.support.transition.Transition}.
*/
public void setExitTransition(@Nullable Object transition) {
ensureAnimationInfo().mExitTransition = transition;
@@ -1891,8 +1894,10 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener
* transition as {@link #setExitTransition(Object)}.
*
* @param transition The Transition to use to move Views into the scene when reentering from a
- * previously-started Activity. <code>transition</code>
- * must be an android.transition.Transition.
+ * previously-started Activity. <code>transition</code>
+ * must be an
+ * {@link android.transition.Transition android.transition.Transition} or
+ * {@link android.support.transition.Transition android.support.transition.Transition}.
*/
public void setReenterTransition(@Nullable Object transition) {
ensureAnimationInfo().mReenterTransition = transition;
@@ -1925,7 +1930,9 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener
* value will cause transferred shared elements to blink to the final position.
*
* @param transition The Transition to use for shared elements transferred into the content
- * Scene. <code>transition</code> must be an android.transition.Transition.
+ * Scene. <code>transition</code> must be an
+ * {@link android.transition.Transition android.transition.Transition} or
+ * {@link android.support.transition.Transition android.support.transition.Transition}.
*/
public void setSharedElementEnterTransition(@Nullable Object transition) {
ensureAnimationInfo().mSharedElementEnterTransition = transition;
@@ -1958,7 +1965,9 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener
* {@link #setSharedElementEnterTransition(Object)}.
*
* @param transition The Transition to use for shared elements transferred out of the content
- * Scene. <code>transition</code> must be an android.transition.Transition.
+ * Scene. <code>transition</code> must be an
+ * {@link android.transition.Transition android.transition.Transition} or
+ * {@link android.support.transition.Transition android.support.transition.Transition}.
*/
public void setSharedElementReturnTransition(@Nullable Object transition) {
ensureAnimationInfo().mSharedElementReturnTransition = transition;
diff --git a/android/support/v4/app/NotificationCompat.java b/android/support/v4/app/NotificationCompat.java
index 1077b1f0..6f74e18c 100644
--- a/android/support/v4/app/NotificationCompat.java
+++ b/android/support/v4/app/NotificationCompat.java
@@ -32,6 +32,7 @@ import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
+import android.media.AudioAttributes;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
@@ -116,7 +117,6 @@ public class NotificationCompat {
* default stream type is {@link AudioManager#STREAM_NOTIFICATION}.
*/
public static final int STREAM_DEFAULT = -1;
-
/**
* Bit set in the Notification flags field when LEDs should be turned on
* for this notification.
@@ -404,6 +404,12 @@ public class NotificationCompat {
public static final String EXTRA_MESSAGES = "android.messages";
/**
+ * Notification key: whether the {@link NotificationCompat.MessagingStyle} notification
+ * represents a group conversation.
+ */
+ public static final String EXTRA_IS_GROUP_CONVERSATION = "android.isGroupConversation";
+
+ /**
* Keys into the {@link #getExtras} Bundle: the audio contents of this notification.
*
* This is for use when rendering the notification on an audio-focused interface;
@@ -439,6 +445,14 @@ public class NotificationCompat {
public static final int COLOR_DEFAULT = Color.TRANSPARENT;
/** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({AudioManager.STREAM_VOICE_CALL, AudioManager.STREAM_SYSTEM, AudioManager.STREAM_RING,
+ AudioManager.STREAM_MUSIC, AudioManager.STREAM_ALARM, AudioManager.STREAM_NOTIFICATION,
+ AudioManager.STREAM_DTMF, AudioManager.STREAM_ACCESSIBILITY})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StreamType {}
+
+ /** @hide */
@Retention(SOURCE)
@IntDef({VISIBILITY_PUBLIC, VISIBILITY_PRIVATE, VISIBILITY_SECRET})
public @interface NotificationVisibility {}
@@ -957,6 +971,12 @@ public class NotificationCompat {
public Builder setSound(Uri sound) {
mNotification.sound = sound;
mNotification.audioStreamType = Notification.STREAM_DEFAULT;
+ if (Build.VERSION.SDK_INT >= 21) {
+ mNotification.audioAttributes = new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION)
+ .build();
+ }
return this;
}
@@ -971,9 +991,15 @@ public class NotificationCompat {
* @see Notification#STREAM_DEFAULT
* @see AudioManager for the <code>STREAM_</code> constants.
*/
- public Builder setSound(Uri sound, int streamType) {
+ public Builder setSound(Uri sound, @StreamType int streamType) {
mNotification.sound = sound;
mNotification.audioStreamType = streamType;
+ if (Build.VERSION.SDK_INT >= 21) {
+ mNotification.audioAttributes = new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setLegacyStreamType(streamType)
+ .build();
+ }
return this;
}
@@ -2062,8 +2088,9 @@ public class NotificationCompat {
public static final int MAXIMUM_RETAINED_MESSAGES = 25;
CharSequence mUserDisplayName;
- CharSequence mConversationTitle;
+ @Nullable CharSequence mConversationTitle;
List<Message> mMessages = new ArrayList<>();
+ boolean mIsGroupConversation;
MessagingStyle() {
}
@@ -2086,20 +2113,19 @@ public class NotificationCompat {
}
/**
- * Sets the title to be displayed on this conversation. This should only be used for
- * group messaging and left unset for one-on-one conversations.
+ * Sets the title to be displayed on this conversation. May be set to {@code null}.
* @param conversationTitle Title displayed for this conversation.
* @return this object for method chaining.
*/
- public MessagingStyle setConversationTitle(CharSequence conversationTitle) {
+ public MessagingStyle setConversationTitle(@Nullable CharSequence conversationTitle) {
mConversationTitle = conversationTitle;
return this;
}
/**
- * Return the title to be displayed on this conversation. Can be <code>null</code> and
- * should be for one-on-one conversations
+ * Return the title to be displayed on this conversation. Can be {@code null}.
*/
+ @Nullable
public CharSequence getConversationTitle() {
return mConversationTitle;
}
@@ -2148,6 +2174,24 @@ public class NotificationCompat {
}
/**
+ * Sets whether this conversation notification represents a group.
+ * @param isGroupConversation {@code true} if the conversation represents a group,
+ * {@code false} otherwise.
+ * @return this object for method chaining
+ */
+ public MessagingStyle setGroupConversation(boolean isGroupConversation) {
+ mIsGroupConversation = isGroupConversation;
+ return this;
+ }
+
+ /**
+ * Returns {@code true} if this notification represents a group conversation.
+ */
+ public boolean isGroupConversation() {
+ return mIsGroupConversation;
+ }
+
+ /**
* Retrieves a {@link MessagingStyle} from a {@link Notification}, enabling an application
* that has set a {@link MessagingStyle} using {@link NotificationCompat} or
* {@link android.app.Notification.Builder} to send messaging information to another
@@ -2295,6 +2339,7 @@ public class NotificationCompat {
if (!mMessages.isEmpty()) { extras.putParcelableArray(EXTRA_MESSAGES,
Message.getBundleArrayForMessages(mMessages));
}
+ extras.putBoolean(EXTRA_IS_GROUP_CONVERSATION, mIsGroupConversation);
}
/**
@@ -2310,6 +2355,7 @@ public class NotificationCompat {
if (parcelables != null) {
mMessages = Message.getMessagesFromBundleArray(parcelables);
}
+ mIsGroupConversation = extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION);
}
public static final class Message {
diff --git a/android/support/v4/app/NotificationCompatBuilder.java b/android/support/v4/app/NotificationCompatBuilder.java
index 71f4160f..db775a55 100644
--- a/android/support/v4/app/NotificationCompatBuilder.java
+++ b/android/support/v4/app/NotificationCompatBuilder.java
@@ -28,6 +28,7 @@ import android.app.Notification;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.RestrictTo;
+import android.text.TextUtils;
import android.util.SparseArray;
import android.widget.RemoteViews;
@@ -69,7 +70,6 @@ class NotificationCompatBuilder implements NotificationBuilderWithBuilderAccesso
.setSmallIcon(n.icon, n.iconLevel)
.setContent(n.contentView)
.setTicker(n.tickerText, b.mTickerView)
- .setSound(n.sound, n.audioStreamType)
.setVibrate(n.vibrate)
.setLights(n.ledARGB, n.ledOnMS, n.ledOffMS)
.setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0)
@@ -86,6 +86,9 @@ class NotificationCompatBuilder implements NotificationBuilderWithBuilderAccesso
.setLargeIcon(b.mLargeIcon)
.setNumber(b.mNumber)
.setProgress(b.mProgressMax, b.mProgress, b.mProgressIndeterminate);
+ if (Build.VERSION.SDK_INT < 21) {
+ mBuilder.setSound(n.sound, n.audioStreamType);
+ }
if (Build.VERSION.SDK_INT >= 16) {
mBuilder.setSubText(b.mSubText)
.setUsesChronometer(b.mUseChronometer)
@@ -141,7 +144,8 @@ class NotificationCompatBuilder implements NotificationBuilderWithBuilderAccesso
mBuilder.setCategory(b.mCategory)
.setColor(b.mColor)
.setVisibility(b.mVisibility)
- .setPublicVersion(b.mPublicVersion);
+ .setPublicVersion(b.mPublicVersion)
+ .setSound(n.sound, n.audioAttributes);
for (String person: b.mPeople) {
mBuilder.addPerson(person);
@@ -169,6 +173,13 @@ class NotificationCompatBuilder implements NotificationBuilderWithBuilderAccesso
if (b.mColorizedSet) {
mBuilder.setColorized(b.mColorized);
}
+
+ if (!TextUtils.isEmpty(b.mChannelId)) {
+ mBuilder.setSound(null)
+ .setDefaults(0)
+ .setLights(0, 0, 0)
+ .setVibrate(null);
+ }
}
}
diff --git a/android/support/v4/app/NotificationManagerCompat.java b/android/support/v4/app/NotificationManagerCompat.java
index 1a0f1bca..07fcb6c7 100644
--- a/android/support/v4/app/NotificationManagerCompat.java
+++ b/android/support/v4/app/NotificationManagerCompat.java
@@ -43,10 +43,10 @@ import android.util.Log;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -560,7 +560,7 @@ public final class NotificationManagerCompat {
/** The service stub provided by onServiceConnected */
public INotificationSideChannel service;
/** Queue of pending tasks to send to this listener service */
- public LinkedList<Task> taskQueue = new LinkedList<Task>();
+ public ArrayDeque<Task> taskQueue = new ArrayDeque<>();
/** Number of retries attempted while connecting to this listener service */
public int retryCount = 0;
diff --git a/android/support/v4/content/res/FontResourcesParserCompat.java b/android/support/v4/content/res/FontResourcesParserCompat.java
index 8ad07d31..f597e68c 100644
--- a/android/support/v4/content/res/FontResourcesParserCompat.java
+++ b/android/support/v4/content/res/FontResourcesParserCompat.java
@@ -102,13 +102,17 @@ public class FontResourcesParserCompat {
private final @NonNull String mFileName;
private int mWeight;
private boolean mItalic;
+ private String mVariationSettings;
+ private int mTtcIndex;
private int mResourceId;
public FontFileResourceEntry(@NonNull String fileName, int weight, boolean italic,
- int resourceId) {
+ @Nullable String variationSettings, int ttcIndex, int resourceId) {
mFileName = fileName;
mWeight = weight;
mItalic = italic;
+ mVariationSettings = variationSettings;
+ mTtcIndex = ttcIndex;
mResourceId = resourceId;
}
@@ -124,6 +128,14 @@ public class FontResourcesParserCompat {
return mItalic;
}
+ public @Nullable String getVariationSettings() {
+ return mVariationSettings;
+ }
+
+ public int getTtcIndex() {
+ return mTtcIndex;
+ }
+
public int getResourceId() {
return mResourceId;
}
@@ -260,6 +272,15 @@ public class FontResourcesParserCompat {
? R.styleable.FontFamilyFont_fontStyle
: R.styleable.FontFamilyFont_android_fontStyle;
boolean isItalic = ITALIC == array.getInt(styleAttr, 0);
+ final int ttcIndexAttr = array.hasValue(R.styleable.FontFamilyFont_ttcIndex)
+ ? R.styleable.FontFamilyFont_ttcIndex
+ : R.styleable.FontFamilyFont_android_ttcIndex;
+ final int variationSettingsAttr =
+ array.hasValue(R.styleable.FontFamilyFont_fontVariationSettings)
+ ? R.styleable.FontFamilyFont_fontVariationSettings
+ : R.styleable.FontFamilyFont_android_fontVariationSettings;
+ String variationSettings = array.getString(variationSettingsAttr);
+ int ttcIndex = array.getInt(ttcIndexAttr, 0);
final int resourceAttr = array.hasValue(R.styleable.FontFamilyFont_font)
? R.styleable.FontFamilyFont_font
: R.styleable.FontFamilyFont_android_font;
@@ -269,7 +290,8 @@ public class FontResourcesParserCompat {
while (parser.next() != XmlPullParser.END_TAG) {
skip(parser);
}
- return new FontFileResourceEntry(filename, weight, isItalic, resourceId);
+ return new FontFileResourceEntry(filename, weight, isItalic, variationSettings, ttcIndex,
+ resourceId);
}
private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
diff --git a/android/support/v4/graphics/TypefaceCompat.java b/android/support/v4/graphics/TypefaceCompat.java
index 734f1837..b763101a 100644
--- a/android/support/v4/graphics/TypefaceCompat.java
+++ b/android/support/v4/graphics/TypefaceCompat.java
@@ -32,6 +32,7 @@ import android.support.v4.content.res.FontResourcesParserCompat.FamilyResourceEn
import android.support.v4.content.res.FontResourcesParserCompat.FontFamilyFilesResourceEntry;
import android.support.v4.content.res.FontResourcesParserCompat.ProviderResourceEntry;
import android.support.v4.content.res.ResourcesCompat;
+import android.support.v4.os.BuildCompat;
import android.support.v4.provider.FontsContractCompat;
import android.support.v4.provider.FontsContractCompat.FontInfo;
import android.support.v4.util.LruCache;
@@ -45,7 +46,7 @@ public class TypefaceCompat {
private static final TypefaceCompatImpl sTypefaceCompatImpl;
static {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ if (BuildCompat.isAtLeastP()) {
sTypefaceCompatImpl = new TypefaceCompatApi28Impl();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
sTypefaceCompatImpl = new TypefaceCompatApi26Impl();
diff --git a/android/support/v4/graphics/TypefaceCompatApi24Impl.java b/android/support/v4/graphics/TypefaceCompatApi24Impl.java
index 89a6ec40..a8c19888 100644
--- a/android/support/v4/graphics/TypefaceCompatApi24Impl.java
+++ b/android/support/v4/graphics/TypefaceCompatApi24Impl.java
@@ -159,8 +159,7 @@ class TypefaceCompatApi24Impl extends TypefaceCompatBaseImpl {
if (buffer == null) {
return null;
}
- // TODO: support ttc index.
- if (!addFontWeightStyle(family, buffer, 0, e.getWeight(), e.isItalic())) {
+ if (!addFontWeightStyle(family, buffer, e.getTtcIndex(), e.getWeight(), e.isItalic())) {
return null;
}
}
diff --git a/android/support/v4/graphics/TypefaceCompatApi26Impl.java b/android/support/v4/graphics/TypefaceCompatApi26Impl.java
index f23ac0d4..955284e3 100644
--- a/android/support/v4/graphics/TypefaceCompatApi26Impl.java
+++ b/android/support/v4/graphics/TypefaceCompatApi26Impl.java
@@ -61,6 +61,7 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl {
private static final String FREEZE_METHOD = "freeze";
private static final String ABORT_CREATION_METHOD = "abortCreation";
private static final int RESOLVE_BY_FONT_TABLE = -1;
+ private static final String DEFAULT_FAMILY = "sans-serif";
protected final Class mFontFamily;
protected final Constructor mFontFamilyCtor;
@@ -133,11 +134,11 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl {
* boolean isAsset, int ttcIndex, int weight, int isItalic, FontVariationAxis[] axes)
*/
private boolean addFontFromAssetManager(Context context, Object family, String fileName,
- int ttcIndex, int weight, int style) {
+ int ttcIndex, int weight, int style, @Nullable FontVariationAxis[] axes) {
try {
final Boolean result = (Boolean) mAddFontFromAssetManager.invoke(family,
context.getAssets(), fileName, 0 /* cookie */, false /* isAsset */, ttcIndex,
- weight, style, null /* axes */);
+ weight, style, axes);
return result.booleanValue();
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
@@ -206,9 +207,9 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl {
}
Object fontFamily = newFamily();
for (final FontFileResourceEntry fontFile : entry.getEntries()) {
- // TODO: Add ttc and variation font support. (b/37853920)
if (!addFontFromAssetManager(context, fontFamily, fontFile.getFileName(),
- 0 /* ttcIndex */, fontFile.getWeight(), fontFile.isItalic() ? 1 : 0)) {
+ fontFile.getTtcIndex(), fontFile.getWeight(), fontFile.isItalic() ? 1 : 0,
+ FontVariationAxis.fromFontVariationSettings(fontFile.getVariationSettings()))) {
abortCreation(fontFamily);
return null;
}
@@ -233,6 +234,9 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl {
final ContentResolver resolver = context.getContentResolver();
try (ParcelFileDescriptor pfd =
resolver.openFileDescriptor(bestFont.getUri(), "r", cancellationSignal)) {
+ if (pfd == null) {
+ return null;
+ }
return new Typeface.Builder(pfd.getFileDescriptor())
.setWeight(bestFont.getWeight())
.setItalic(bestFont.isItalic())
@@ -282,7 +286,7 @@ public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl {
Object fontFamily = newFamily();
if (!addFontFromAssetManager(context, fontFamily, path,
0 /* ttcIndex */, RESOLVE_BY_FONT_TABLE /* weight */,
- RESOLVE_BY_FONT_TABLE /* italic */)) {
+ RESOLVE_BY_FONT_TABLE /* italic */, null /* axes */)) {
abortCreation(fontFamily);
return null;
}
diff --git a/android/support/v4/graphics/drawable/IconCompat.java b/android/support/v4/graphics/drawable/IconCompat.java
index 359c96b3..dc226c1e 100644
--- a/android/support/v4/graphics/drawable/IconCompat.java
+++ b/android/support/v4/graphics/drawable/IconCompat.java
@@ -220,6 +220,7 @@ public class IconCompat {
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
+ @SuppressWarnings("deprecation")
public void addToShortcutIntent(@NonNull Intent outIntent, @Nullable Drawable badge) {
Bitmap icon;
switch (mType) {
diff --git a/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java b/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java
index 68f94768..6747d11d 100644
--- a/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java
+++ b/android/support/v4/hardware/fingerprint/FingerprintManagerCompat.java
@@ -16,7 +16,6 @@
package android.support.v4.hardware.fingerprint;
-import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.fingerprint.FingerprintManager;
@@ -58,7 +57,6 @@ public final class FingerprintManagerCompat {
*
* @return true if at least one fingerprint is enrolled, false otherwise
*/
- @TargetApi(23)
@RequiresPermission(android.Manifest.permission.USE_FINGERPRINT)
public boolean hasEnrolledFingerprints() {
if (Build.VERSION.SDK_INT >= 23) {
@@ -74,7 +72,6 @@ public final class FingerprintManagerCompat {
*
* @return true if hardware is present and functional, false otherwise.
*/
- @TargetApi(23)
@RequiresPermission(android.Manifest.permission.USE_FINGERPRINT)
public boolean isHardwareDetected() {
if (Build.VERSION.SDK_INT >= 23) {
@@ -99,7 +96,6 @@ public final class FingerprintManagerCompat {
* @param callback an object to receive authentication events
* @param handler an optional handler for events
*/
- @TargetApi(23)
@RequiresPermission(android.Manifest.permission.USE_FINGERPRINT)
public void authenticate(@Nullable CryptoObject crypto, int flags,
@Nullable CancellationSignal cancel, @NonNull AuthenticationCallback callback,
diff --git a/android/support/v4/media/session/MediaControllerCompat.java b/android/support/v4/media/session/MediaControllerCompat.java
index 2509cd49..f24da1e1 100644
--- a/android/support/v4/media/session/MediaControllerCompat.java
+++ b/android/support/v4/media/session/MediaControllerCompat.java
@@ -1919,6 +1919,7 @@ public final class MediaControllerCompat {
}
} else {
synchronized (mPendingCallbacks) {
+ callback.mHasExtraCallback = false;
mPendingCallbacks.add(callback);
}
}
@@ -1931,6 +1932,7 @@ public final class MediaControllerCompat {
try {
ExtraCallback extraCallback = mCallbackMap.remove(callback);
if (extraCallback != null) {
+ callback.mHasExtraCallback = false;
mExtraBinder.unregisterCallbackListener(extraCallback);
}
} catch (RemoteException e) {
diff --git a/android/support/v4/media/session/PlaybackStateCompat.java b/android/support/v4/media/session/PlaybackStateCompat.java
index d7634b00..3b061257 100644
--- a/android/support/v4/media/session/PlaybackStateCompat.java
+++ b/android/support/v4/media/session/PlaybackStateCompat.java
@@ -24,6 +24,7 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.annotation.IntDef;
+import android.support.annotation.LongDef;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.text.TextUtils;
@@ -45,7 +46,7 @@ public final class PlaybackStateCompat implements Parcelable {
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
- @IntDef(flag=true, value={ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND,
+ @LongDef(flag=true, value={ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND,
ACTION_SKIP_TO_PREVIOUS, ACTION_SKIP_TO_NEXT, ACTION_FAST_FORWARD, ACTION_SET_RATING,
ACTION_SEEK_TO, ACTION_PLAY_PAUSE, ACTION_PLAY_FROM_MEDIA_ID, ACTION_PLAY_FROM_SEARCH,
ACTION_SKIP_TO_QUEUE_ITEM, ACTION_PLAY_FROM_URI, ACTION_PREPARE,
@@ -58,7 +59,7 @@ public final class PlaybackStateCompat implements Parcelable {
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
- @IntDef({ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND, ACTION_SKIP_TO_PREVIOUS,
+ @LongDef({ACTION_STOP, ACTION_PAUSE, ACTION_PLAY, ACTION_REWIND, ACTION_SKIP_TO_PREVIOUS,
ACTION_SKIP_TO_NEXT, ACTION_FAST_FORWARD, ACTION_PLAY_PAUSE})
@Retention(RetentionPolicy.SOURCE)
public @interface MediaKeyAction {}
diff --git a/android/support/v4/provider/FontsContractCompat.java b/android/support/v4/provider/FontsContractCompat.java
index 09261869..39acf686 100644
--- a/android/support/v4/provider/FontsContractCompat.java
+++ b/android/support/v4/provider/FontsContractCompat.java
@@ -274,7 +274,10 @@ public class FontsContractCompat {
: new ReplyCallback<TypefaceResult>() {
@Override
public void onReply(final TypefaceResult typeface) {
- if (typeface.mResult == FontFamilyResult.STATUS_OK) {
+ if (typeface == null) {
+ fontCallback.callbackFailAsync(
+ FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND, handler);
+ } else if (typeface.mResult == FontFamilyResult.STATUS_OK) {
fontCallback.callbackSuccessAsync(typeface.mTypeface, handler);
} else {
fontCallback.callbackFailAsync(typeface.mResult, handler);
diff --git a/android/support/v4/view/ViewConfigurationCompat.java b/android/support/v4/view/ViewConfigurationCompat.java
index f14b8060..60d37a9f 100644
--- a/android/support/v4/view/ViewConfigurationCompat.java
+++ b/android/support/v4/view/ViewConfigurationCompat.java
@@ -27,10 +27,7 @@ import java.lang.reflect.Method;
/**
* Helper for accessing features in {@link ViewConfiguration}.
- *
- * @deprecated Use {@link ViewConfiguration} directly.
*/
-@Deprecated
public final class ViewConfigurationCompat {
private static final String TAG = "ViewConfigCompat";
diff --git a/android/support/v4/widget/DrawerLayout.java b/android/support/v4/widget/DrawerLayout.java
index a73e1f10..aa2077d6 100644
--- a/android/support/v4/widget/DrawerLayout.java
+++ b/android/support/v4/widget/DrawerLayout.java
@@ -44,7 +44,6 @@ import android.support.v4.view.AbsSavedState;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.ViewCompat;
-import android.support.v4.view.ViewGroupCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import android.util.AttributeSet;
@@ -331,7 +330,7 @@ public class DrawerLayout extends ViewGroup {
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate());
- ViewGroupCompat.setMotionEventSplittingEnabled(this, false);
+ setMotionEventSplittingEnabled(false);
if (ViewCompat.getFitsSystemWindows(this)) {
if (Build.VERSION.SDK_INT >= 21) {
setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
diff --git a/android/support/v4/widget/NestedScrollView.java b/android/support/v4/widget/NestedScrollView.java
index 73ff0848..6fe19289 100644
--- a/android/support/v4/widget/NestedScrollView.java
+++ b/android/support/v4/widget/NestedScrollView.java
@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
+import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@@ -1820,10 +1821,20 @@ public class NestedScrollView extends FrameLayout implements NestedScrollingPare
final int scrollY = getScrollY();
if (!mEdgeGlowTop.isFinished()) {
final int restoreCount = canvas.save();
- final int width = getWidth() - getPaddingLeft() - getPaddingRight();
-
- canvas.translate(getPaddingLeft(), Math.min(0, scrollY));
- mEdgeGlowTop.setSize(width, getHeight());
+ int width = getWidth();
+ int height = getHeight();
+ int xTranslation = 0;
+ int yTranslation = Math.min(0, scrollY);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {
+ width -= getPaddingLeft() + getPaddingRight();
+ xTranslation += getPaddingLeft();
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) {
+ height -= getPaddingTop() + getPaddingBottom();
+ yTranslation += getPaddingTop();
+ }
+ canvas.translate(xTranslation, yTranslation);
+ mEdgeGlowTop.setSize(width, height);
if (mEdgeGlowTop.draw(canvas)) {
ViewCompat.postInvalidateOnAnimation(this);
}
@@ -1831,11 +1842,19 @@ public class NestedScrollView extends FrameLayout implements NestedScrollingPare
}
if (!mEdgeGlowBottom.isFinished()) {
final int restoreCount = canvas.save();
- final int width = getWidth() - getPaddingLeft() - getPaddingRight();
- final int height = getHeight();
-
- canvas.translate(-width + getPaddingLeft(),
- Math.max(getScrollRange(), scrollY) + height);
+ int width = getWidth();
+ int height = getHeight();
+ int xTranslation = 0;
+ int yTranslation = Math.max(getScrollRange(), scrollY) + height;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {
+ width -= getPaddingLeft() + getPaddingRight();
+ xTranslation += getPaddingLeft();
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) {
+ height -= getPaddingTop() + getPaddingBottom();
+ yTranslation -= getPaddingBottom();
+ }
+ canvas.translate(xTranslation - width, yTranslation);
canvas.rotate(180, width, 0);
mEdgeGlowBottom.setSize(width, height);
if (mEdgeGlowBottom.draw(canvas)) {
diff --git a/android/support/v7/app/AlertController.java b/android/support/v7/app/AlertController.java
index 5ff4537d..01bc4499 100644
--- a/android/support/v7/app/AlertController.java
+++ b/android/support/v7/app/AlertController.java
@@ -65,6 +65,7 @@ class AlertController {
private final Context mContext;
final AppCompatDialog mDialog;
private final Window mWindow;
+ private final int mButtonIconDimen;
private CharSequence mTitle;
private CharSequence mMessage;
@@ -82,14 +83,17 @@ class AlertController {
Button mButtonPositive;
private CharSequence mButtonPositiveText;
Message mButtonPositiveMessage;
+ private Drawable mButtonPositiveIcon;
Button mButtonNegative;
private CharSequence mButtonNegativeText;
Message mButtonNegativeMessage;
+ private Drawable mButtonNegativeIcon;
Button mButtonNeutral;
private CharSequence mButtonNeutralText;
Message mButtonNeutralMessage;
+ private Drawable mButtonNeutralIcon;
NestedScrollView mScrollView;
@@ -192,6 +196,7 @@ class AlertController {
.getResourceId(R.styleable.AlertDialog_singleChoiceItemLayout, 0);
mListItemLayout = a.getResourceId(R.styleable.AlertDialog_listItemLayout, 0);
mShowTitle = a.getBoolean(R.styleable.AlertDialog_showTitle, true);
+ mButtonIconDimen = a.getDimensionPixelSize(R.styleable.AlertDialog_buttonIconDimen, 0);
a.recycle();
@@ -298,8 +303,8 @@ class AlertController {
}
/**
- * Sets a click listener or a message to be sent when the button is clicked.
- * You only need to pass one of {@code listener} or {@code msg}.
+ * Sets an icon, a click listener or a message to be sent when the button is clicked.
+ * You only need to pass one of {@code icon}, {@code listener} or {@code msg}.
*
* @param whichButton Which button, can be one of
* {@link DialogInterface#BUTTON_POSITIVE},
@@ -308,9 +313,11 @@ class AlertController {
* @param text The text to display in positive button.
* @param listener The {@link DialogInterface.OnClickListener} to use.
* @param msg The {@link Message} to be sent when clicked.
+ * @param icon The (@link Drawable) to be used as an icon for the button.
+ *
*/
public void setButton(int whichButton, CharSequence text,
- DialogInterface.OnClickListener listener, Message msg) {
+ DialogInterface.OnClickListener listener, Message msg, Drawable icon) {
if (msg == null && listener != null) {
msg = mHandler.obtainMessage(whichButton, listener);
@@ -321,16 +328,19 @@ class AlertController {
case DialogInterface.BUTTON_POSITIVE:
mButtonPositiveText = text;
mButtonPositiveMessage = msg;
+ mButtonPositiveIcon = icon;
break;
case DialogInterface.BUTTON_NEGATIVE:
mButtonNegativeText = text;
mButtonNegativeMessage = msg;
+ mButtonNegativeIcon = icon;
break;
case DialogInterface.BUTTON_NEUTRAL:
mButtonNeutralText = text;
mButtonNeutralMessage = msg;
+ mButtonNeutralIcon = icon;
break;
default:
@@ -752,35 +762,45 @@ class AlertController {
mButtonPositive = (Button) buttonPanel.findViewById(android.R.id.button1);
mButtonPositive.setOnClickListener(mButtonHandler);
- if (TextUtils.isEmpty(mButtonPositiveText)) {
+ if (TextUtils.isEmpty(mButtonPositiveText) && mButtonPositiveIcon == null) {
mButtonPositive.setVisibility(View.GONE);
} else {
mButtonPositive.setText(mButtonPositiveText);
+ if (mButtonPositiveIcon != null) {
+ mButtonPositiveIcon.setBounds(0, 0, mButtonIconDimen, mButtonIconDimen);
+ mButtonPositive.setCompoundDrawables(mButtonPositiveIcon, null, null, null);
+ }
mButtonPositive.setVisibility(View.VISIBLE);
whichButtons = whichButtons | BIT_BUTTON_POSITIVE;
}
- mButtonNegative = (Button) buttonPanel.findViewById(android.R.id.button2);
+ mButtonNegative = buttonPanel.findViewById(android.R.id.button2);
mButtonNegative.setOnClickListener(mButtonHandler);
- if (TextUtils.isEmpty(mButtonNegativeText)) {
+ if (TextUtils.isEmpty(mButtonNegativeText) && mButtonNegativeIcon == null) {
mButtonNegative.setVisibility(View.GONE);
} else {
mButtonNegative.setText(mButtonNegativeText);
+ if (mButtonNegativeIcon != null) {
+ mButtonNegativeIcon.setBounds(0, 0, mButtonIconDimen, mButtonIconDimen);
+ mButtonNegative.setCompoundDrawables(mButtonNegativeIcon, null, null, null);
+ }
mButtonNegative.setVisibility(View.VISIBLE);
-
whichButtons = whichButtons | BIT_BUTTON_NEGATIVE;
}
mButtonNeutral = (Button) buttonPanel.findViewById(android.R.id.button3);
mButtonNeutral.setOnClickListener(mButtonHandler);
- if (TextUtils.isEmpty(mButtonNeutralText)) {
+ if (TextUtils.isEmpty(mButtonNeutralText) && mButtonNeutralIcon == null) {
mButtonNeutral.setVisibility(View.GONE);
} else {
mButtonNeutral.setText(mButtonNeutralText);
+ if (mButtonPositiveIcon != null) {
+ mButtonPositiveIcon.setBounds(0, 0, mButtonIconDimen, mButtonIconDimen);
+ mButtonPositive.setCompoundDrawables(mButtonPositiveIcon, null, null, null);
+ }
mButtonNeutral.setVisibility(View.VISIBLE);
-
whichButtons = whichButtons | BIT_BUTTON_NEUTRAL;
}
@@ -852,10 +872,13 @@ class AlertController {
public View mCustomTitleView;
public CharSequence mMessage;
public CharSequence mPositiveButtonText;
+ public Drawable mPositiveButtonIcon;
public DialogInterface.OnClickListener mPositiveButtonListener;
public CharSequence mNegativeButtonText;
+ public Drawable mNegativeButtonIcon;
public DialogInterface.OnClickListener mNegativeButtonListener;
public CharSequence mNeutralButtonText;
+ public Drawable mNeutralButtonIcon;
public DialogInterface.OnClickListener mNeutralButtonListener;
public boolean mCancelable;
public DialogInterface.OnCancelListener mOnCancelListener;
@@ -923,17 +946,17 @@ class AlertController {
if (mMessage != null) {
dialog.setMessage(mMessage);
}
- if (mPositiveButtonText != null) {
+ if (mPositiveButtonText != null || mPositiveButtonIcon != null) {
dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
- mPositiveButtonListener, null);
+ mPositiveButtonListener, null, mPositiveButtonIcon);
}
- if (mNegativeButtonText != null) {
+ if (mNegativeButtonText != null || mNegativeButtonIcon != null) {
dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText,
- mNegativeButtonListener, null);
+ mNegativeButtonListener, null, mNegativeButtonIcon);
}
- if (mNeutralButtonText != null) {
+ if (mNeutralButtonText != null || mNeutralButtonIcon != null) {
dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText,
- mNeutralButtonListener, null);
+ mNeutralButtonListener, null, mNeutralButtonIcon);
}
// For a list, the client can either supply an array of items or an
// adapter or a cursor
diff --git a/android/support/v7/app/AlertDialog.java b/android/support/v7/app/AlertDialog.java
index 4b87dcc0..1712f20b 100644
--- a/android/support/v7/app/AlertDialog.java
+++ b/android/support/v7/app/AlertDialog.java
@@ -207,7 +207,7 @@ public class AlertDialog extends AppCompatDialog implements DialogInterface {
* @param msg The {@link Message} to be sent when clicked.
*/
public void setButton(int whichButton, CharSequence text, Message msg) {
- mAlert.setButton(whichButton, text, null, msg);
+ mAlert.setButton(whichButton, text, null, msg, null);
}
/**
@@ -222,7 +222,25 @@ public class AlertDialog extends AppCompatDialog implements DialogInterface {
* @param listener The {@link DialogInterface.OnClickListener} to use.
*/
public void setButton(int whichButton, CharSequence text, OnClickListener listener) {
- mAlert.setButton(whichButton, text, listener, null);
+ mAlert.setButton(whichButton, text, listener, null, null);
+ }
+
+ /**
+ * Sets an icon to be displayed along with the button text and a listener to be invoked when
+ * the positive button of the dialog is pressed. This method has no effect if called after
+ * {@link #show()}.
+ *
+ * @param whichButton Which button to set the listener on, can be one of
+ * {@link DialogInterface#BUTTON_POSITIVE},
+ * {@link DialogInterface#BUTTON_NEGATIVE}, or
+ * {@link DialogInterface#BUTTON_NEUTRAL}
+ * @param text The text to display in positive button.
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ * @param icon The {@link Drawable} to be set as an icon for the button.
+ */
+ public void setButton(int whichButton, CharSequence text, Drawable icon,
+ OnClickListener listener) {
+ mAlert.setButton(whichButton, text, listener, null, icon);
}
/**
@@ -470,6 +488,16 @@ public class AlertDialog extends AppCompatDialog implements DialogInterface {
}
/**
+ * Set an icon to be displayed for the positive button.
+ * @param icon The icon to be displayed
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setPositiveButtonIcon(Drawable icon) {
+ P.mPositiveButtonIcon = icon;
+ return this;
+ }
+
+ /**
* Set a listener to be invoked when the negative button of the dialog is pressed.
* @param textId The resource id of the text to display in the negative button
* @param listener The {@link DialogInterface.OnClickListener} to use.
@@ -496,6 +524,16 @@ public class AlertDialog extends AppCompatDialog implements DialogInterface {
}
/**
+ * Set an icon to be displayed for the negative button.
+ * @param icon The icon to be displayed
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setNegativeButtonIcon(Drawable icon) {
+ P.mNegativeButtonIcon = icon;
+ return this;
+ }
+
+ /**
* Set a listener to be invoked when the neutral button of the dialog is pressed.
* @param textId The resource id of the text to display in the neutral button
* @param listener The {@link DialogInterface.OnClickListener} to use.
@@ -522,6 +560,16 @@ public class AlertDialog extends AppCompatDialog implements DialogInterface {
}
/**
+ * Set an icon to be displayed for the neutral button.
+ * @param icon The icon to be displayed
+ * @return This Builder object to allow for chaining of calls to set methods
+ */
+ public Builder setNeutralButtonIcon(Drawable icon) {
+ P.mNeutralButtonIcon = icon;
+ return this;
+ }
+
+ /**
* Sets whether the dialog is cancelable or not. Default is true.
*
* @return This Builder object to allow for chaining of calls to set methods
diff --git a/android/support/v7/graphics/BucketTests.java b/android/support/v7/graphics/BucketTests.java
new file mode 100644
index 00000000..ca8e5085
--- /dev/null
+++ b/android/support/v7/graphics/BucketTests.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 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 android.support.v7.graphics;
+
+import static android.support.v7.graphics.TestUtils.assertCloseColors;
+import static android.support.v7.graphics.TestUtils.loadSampleBitmap;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+@RunWith(AndroidJUnit4.class)
+public class BucketTests {
+
+ @Test
+ @SmallTest
+ public void testSourceBitmapNotRecycled() {
+ final Bitmap sample = loadSampleBitmap();
+
+ Palette.from(sample).generate();
+ assertFalse(sample.isRecycled());
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ @SmallTest
+ public void testSwatchesUnmodifiable() {
+ Palette p = Palette.from(loadSampleBitmap()).generate();
+ p.getSwatches().remove(0);
+ }
+
+ @Test
+ @SmallTest
+ public void testSwatchesBuilder() {
+ ArrayList<Palette.Swatch> swatches = new ArrayList<>();
+ swatches.add(new Palette.Swatch(Color.BLACK, 40));
+ swatches.add(new Palette.Swatch(Color.GREEN, 60));
+ swatches.add(new Palette.Swatch(Color.BLUE, 10));
+
+ Palette p = Palette.from(swatches);
+
+ assertEquals(swatches, p.getSwatches());
+ }
+
+ @Test
+ @SmallTest
+ public void testRegionWhole() {
+ final Bitmap sample = loadSampleBitmap();
+
+ Palette.Builder b = new Palette.Builder(sample);
+ b.setRegion(0, 0, sample.getWidth(), sample.getHeight());
+ b.generate();
+ }
+
+ @Test
+ @SmallTest
+ public void testRegionUpperLeft() {
+ final Bitmap sample = loadSampleBitmap();
+
+ Palette.Builder b = new Palette.Builder(sample);
+ b.setRegion(0, 0, sample.getWidth() / 2, sample.getHeight() / 2);
+ b.generate();
+ }
+
+ @Test
+ @SmallTest
+ public void testRegionBottomRight() {
+ final Bitmap sample = loadSampleBitmap();
+
+ Palette.Builder b = new Palette.Builder(sample);
+ b.setRegion(sample.getWidth() / 2, sample.getHeight() / 2,
+ sample.getWidth(), sample.getHeight());
+ b.generate();
+ }
+
+ @Test
+ @SmallTest
+ public void testOnePixelTallBitmap() {
+ final Bitmap bitmap = Bitmap.createBitmap(1000, 1, Bitmap.Config.ARGB_8888);
+
+ Palette.Builder b = new Palette.Builder(bitmap);
+ b.generate();
+ }
+
+ @Test
+ @SmallTest
+ public void testOnePixelWideBitmap() {
+ final Bitmap bitmap = Bitmap.createBitmap(1, 1000, Bitmap.Config.ARGB_8888);
+
+ Palette.Builder b = new Palette.Builder(bitmap);
+ b.generate();
+ }
+
+ @Test
+ @SmallTest
+ public void testBlueBitmapReturnsBlueSwatch() {
+ final Bitmap bitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ canvas.drawColor(Color.BLUE);
+
+ final Palette palette = Palette.from(bitmap).generate();
+
+ assertEquals(1, palette.getSwatches().size());
+
+ final Palette.Swatch swatch = palette.getSwatches().get(0);
+ assertCloseColors(Color.BLUE, swatch.getRgb());
+ }
+
+ @Test
+ @SmallTest
+ public void testBlueBitmapWithRegionReturnsBlueSwatch() {
+ final Bitmap bitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ canvas.drawColor(Color.BLUE);
+
+ final Palette palette = Palette.from(bitmap)
+ .setRegion(0, bitmap.getHeight() / 2, bitmap.getWidth(), bitmap.getHeight())
+ .generate();
+
+ assertEquals(1, palette.getSwatches().size());
+
+ final Palette.Swatch swatch = palette.getSwatches().get(0);
+ assertCloseColors(Color.BLUE, swatch.getRgb());
+ }
+
+ @Test
+ @SmallTest
+ public void testDominantSwatch() {
+ final Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+ // First fill the canvas with blue
+ Canvas canvas = new Canvas(bitmap);
+ canvas.drawColor(Color.BLUE);
+
+ final Paint paint = new Paint();
+ // Now we'll draw the top 10px tall rect with green
+ paint.setColor(Color.GREEN);
+ canvas.drawRect(0, 0, 100, 10, paint);
+
+ // Now we'll draw the next 20px tall rect with red
+ paint.setColor(Color.RED);
+ canvas.drawRect(0, 11, 100, 30, paint);
+
+ // Now generate a palette from the bitmap
+ final Palette palette = Palette.from(bitmap).generate();
+
+ // First assert that there are 3 swatches
+ assertEquals(3, palette.getSwatches().size());
+
+ // Now assert that the dominant swatch is blue
+ final Palette.Swatch swatch = palette.getDominantSwatch();
+ assertNotNull(swatch);
+ assertCloseColors(Color.BLUE, swatch.getRgb());
+ }
+
+}
diff --git a/android/support/v7/graphics/ConsistencyTest.java b/android/support/v7/graphics/ConsistencyTest.java
new file mode 100644
index 00000000..d9ac12ee
--- /dev/null
+++ b/android/support/v7/graphics/ConsistencyTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 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 android.support.v7.graphics;
+
+import static android.support.v7.graphics.TestUtils.loadSampleBitmap;
+
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Bitmap;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class ConsistencyTest {
+
+ private static final int NUMBER_TRIALS = 10;
+
+ @Test
+ @MediumTest
+ public void testConsistency() {
+ Palette lastPalette = null;
+ final Bitmap bitmap = loadSampleBitmap();
+
+ for (int i = 0; i < NUMBER_TRIALS; i++) {
+ Palette newPalette = Palette.from(bitmap).generate();
+ if (lastPalette != null) {
+ assetPalettesEqual(lastPalette, newPalette);
+ }
+ lastPalette = newPalette;
+ }
+ }
+
+ private static void assetPalettesEqual(Palette p1, Palette p2) {
+ assertEquals(p1.getVibrantSwatch(), p2.getVibrantSwatch());
+ assertEquals(p1.getLightVibrantSwatch(), p2.getLightVibrantSwatch());
+ assertEquals(p1.getDarkVibrantSwatch(), p2.getDarkVibrantSwatch());
+ assertEquals(p1.getMutedSwatch(), p2.getMutedSwatch());
+ assertEquals(p1.getLightMutedSwatch(), p2.getLightMutedSwatch());
+ assertEquals(p1.getDarkMutedSwatch(), p2.getDarkMutedSwatch());
+ }
+}
diff --git a/android/support/v7/graphics/MaxColorsTest.java b/android/support/v7/graphics/MaxColorsTest.java
new file mode 100644
index 00000000..fbcf6ae1
--- /dev/null
+++ b/android/support/v7/graphics/MaxColorsTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 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 android.support.v7.graphics;
+
+import static android.support.v7.graphics.TestUtils.loadSampleBitmap;
+
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class MaxColorsTest {
+
+ @Test
+ @SmallTest
+ public void testMaxColorCount32() {
+ testMaxColorCount(32);
+ }
+
+ @Test
+ @SmallTest
+ public void testMaxColorCount1() {
+ testMaxColorCount(1);
+ }
+
+ @Test
+ @SmallTest
+ public void testMaxColorCount15() {
+ testMaxColorCount(15);
+ }
+
+ private void testMaxColorCount(int colorCount) {
+ Palette newPalette = Palette.from(loadSampleBitmap())
+ .maximumColorCount(colorCount)
+ .generate();
+ assertTrue(newPalette.getSwatches().size() <= colorCount);
+ }
+}
diff --git a/android/support/v7/graphics/SwatchTests.java b/android/support/v7/graphics/SwatchTests.java
new file mode 100644
index 00000000..efbcda40
--- /dev/null
+++ b/android/support/v7/graphics/SwatchTests.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 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 android.support.v7.graphics;
+
+import static android.support.v4.graphics.ColorUtils.HSLToColor;
+import static android.support.v4.graphics.ColorUtils.calculateContrast;
+import static android.support.v7.graphics.TestUtils.loadSampleBitmap;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Color;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SwatchTests {
+
+ private static final float MIN_CONTRAST_TITLE_TEXT = 3.0f;
+ private static final float MIN_CONTRAST_BODY_TEXT = 4.5f;
+
+ @Test
+ @SmallTest
+ public void testTextColorContrasts() {
+ final Palette p = Palette.from(loadSampleBitmap()).generate();
+
+ for (Palette.Swatch swatch : p.getSwatches()) {
+ testSwatchTextColorContrasts(swatch);
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testHslNotNull() {
+ final Palette p = Palette.from(loadSampleBitmap()).generate();
+
+ for (Palette.Swatch swatch : p.getSwatches()) {
+ assertNotNull(swatch.getHsl());
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testHslIsRgb() {
+ final Palette p = Palette.from(loadSampleBitmap()).generate();
+
+ for (Palette.Swatch swatch : p.getSwatches()) {
+ assertEquals(HSLToColor(swatch.getHsl()), swatch.getRgb());
+ }
+ }
+
+ private void testSwatchTextColorContrasts(Palette.Swatch swatch) {
+ final int bodyTextColor = swatch.getBodyTextColor();
+ assertTrue(calculateContrast(bodyTextColor, swatch.getRgb()) >= MIN_CONTRAST_BODY_TEXT);
+
+ final int titleTextColor = swatch.getTitleTextColor();
+ assertTrue(calculateContrast(titleTextColor, swatch.getRgb()) >= MIN_CONTRAST_TITLE_TEXT);
+ }
+
+ @Test
+ @SmallTest
+ public void testEqualsWhenSame() {
+ Palette.Swatch swatch1 = new Palette.Swatch(Color.WHITE, 50);
+ Palette.Swatch swatch2 = new Palette.Swatch(Color.WHITE, 50);
+ assertEquals(swatch1, swatch2);
+ }
+
+ @Test
+ @SmallTest
+ public void testEqualsWhenColorDifferent() {
+ Palette.Swatch swatch1 = new Palette.Swatch(Color.BLACK, 50);
+ Palette.Swatch swatch2 = new Palette.Swatch(Color.WHITE, 50);
+ assertFalse(swatch1.equals(swatch2));
+ }
+
+ @Test
+ @SmallTest
+ public void testEqualsWhenPopulationDifferent() {
+ Palette.Swatch swatch1 = new Palette.Swatch(Color.BLACK, 50);
+ Palette.Swatch swatch2 = new Palette.Swatch(Color.BLACK, 100);
+ assertFalse(swatch1.equals(swatch2));
+ }
+
+ @Test
+ @SmallTest
+ public void testEqualsWhenDifferent() {
+ Palette.Swatch swatch1 = new Palette.Swatch(Color.BLUE, 50);
+ Palette.Swatch swatch2 = new Palette.Swatch(Color.BLACK, 100);
+ assertFalse(swatch1.equals(swatch2));
+ }
+}
diff --git a/android/support/v7/graphics/TestUtils.java b/android/support/v7/graphics/TestUtils.java
new file mode 100644
index 00000000..8de70c52
--- /dev/null
+++ b/android/support/v7/graphics/TestUtils.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016 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.support.v7.graphics;
+
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.support.test.InstrumentationRegistry;
+import android.support.v7.palette.test.R;
+
+class TestUtils {
+
+ static Bitmap loadSampleBitmap() {
+ return BitmapFactory.decodeResource(
+ InstrumentationRegistry.getContext().getResources(),
+ R.drawable.photo);
+ }
+
+ static void assertCloseColors(int expected, int actual) {
+ assertEquals(Color.red(expected), Color.red(actual), 8);
+ assertEquals(Color.green(expected), Color.green(actual), 8);
+ assertEquals(Color.blue(expected), Color.blue(actual), 8);
+ }
+
+}
diff --git a/android/support/v7/media/MediaRouter.java b/android/support/v7/media/MediaRouter.java
index cf6fc1f1..cc372ec9 100644
--- a/android/support/v7/media/MediaRouter.java
+++ b/android/support/v7/media/MediaRouter.java
@@ -2560,12 +2560,16 @@ public final class MediaRouter {
// TODO: Remove the following logging when no longer needed.
if (sGlobal == null || (mBluetoothRoute != null && route.isDefault())) {
final StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
- StringBuffer sb = new StringBuffer();
+ StringBuilder sb = new StringBuilder();
// callStack[3] is the caller of this method.
for (int i = 3; i < callStack.length; i++) {
StackTraceElement caller = callStack[i];
- sb.append(caller.getClassName() + "." + caller.getMethodName()
- + ":" + caller.getLineNumber()).append(" ");
+ sb.append(caller.getClassName())
+ .append(".")
+ .append(caller.getMethodName())
+ .append(":")
+ .append(caller.getLineNumber())
+ .append(" ");
}
if (sGlobal == null) {
Log.w(TAG, "setSelectedRouteInternal is called while sGlobal is null: pkgName="
diff --git a/android/support/v7/util/SortedListTest.java b/android/support/v7/util/SortedListTest.java
index f8bc496c..e628de11 100644
--- a/android/support/v7/util/SortedListTest.java
+++ b/android/support/v7/util/SortedListTest.java
@@ -16,11 +16,18 @@
package android.support.v7.util;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
import android.support.annotation.Nullable;
import android.support.test.filters.SmallTest;
-import junit.framework.TestCase;
-
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -39,13 +46,13 @@ import java.util.concurrent.atomic.AtomicInteger;
@RunWith(JUnit4.class)
@SmallTest
-public class SortedListTest extends TestCase {
+public class SortedListTest {
SortedList<Item> mList;
- List<Pair> mAdditions = new ArrayList<Pair>();
- List<Pair> mRemovals = new ArrayList<Pair>();
- List<Pair> mMoves = new ArrayList<Pair>();
- List<Pair> mUpdates = new ArrayList<Pair>();
+ List<Pair> mAdditions = new ArrayList<>();
+ List<Pair> mRemovals = new ArrayList<>();
+ List<Pair> mMoves = new ArrayList<>();
+ List<Pair> mUpdates = new ArrayList<>();
private boolean mPayloadChanges = false;
List<PayloadChange> mPayloadUpdates = new ArrayList<>();
Queue<AssertListStateRunnable> mCallbackRunnables;
@@ -69,11 +76,8 @@ public class SortedListTest extends TestCase {
public abstract void onChanged(int position, int count);
}
- @Override
@Before
public void setUp() throws Exception {
- super.setUp();
-
mCallback = new SortedList.Callback<Item>() {
@Override
public int compare(Item o1, Item o2) {
diff --git a/android/support/v7/view/menu/CascadingMenuPopup.java b/android/support/v7/view/menu/CascadingMenuPopup.java
index 564bbfca..834f8544 100644
--- a/android/support/v7/view/menu/CascadingMenuPopup.java
+++ b/android/support/v7/view/menu/CascadingMenuPopup.java
@@ -54,7 +54,6 @@ import android.widget.TextView;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
-import java.util.LinkedList;
import java.util.List;
/**
@@ -85,7 +84,7 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey
final Handler mSubMenuHoverHandler;
/** List of menus that were added before this popup was shown. */
- private final List<MenuBuilder> mPendingMenus = new LinkedList<>();
+ private final List<MenuBuilder> mPendingMenus = new ArrayList<>();
/**
* List of open menus. The first item is the root menu and each
diff --git a/android/support/v7/widget/ActionMenuView.java b/android/support/v7/widget/ActionMenuView.java
index 76e06da6..14723a0c 100644
--- a/android/support/v7/widget/ActionMenuView.java
+++ b/android/support/v7/widget/ActionMenuView.java
@@ -268,10 +268,10 @@ public class ActionMenuView extends LinearLayoutCompat implements MenuBuilder.It
// Mark indices of children that can receive an extra cell.
if (lp.cellsUsed < minCells) {
minCells = lp.cellsUsed;
- minCellsAt = 1 << i;
+ minCellsAt = 1L << i;
minCellsItemCount = 1;
} else if (lp.cellsUsed == minCells) {
- minCellsAt |= 1 << i;
+ minCellsAt |= 1L << i;
minCellsItemCount++;
}
}
diff --git a/android/support/v7/widget/AdapterHelperTest.java b/android/support/v7/widget/AdapterHelperTest.java
index e0dbde50..a76f40e2 100644
--- a/android/support/v7/widget/AdapterHelperTest.java
+++ b/android/support/v7/widget/AdapterHelperTest.java
@@ -18,19 +18,21 @@ package android.support.v7.widget;
import static android.support.v7.widget.RecyclerView.ViewHolder;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+
import android.support.test.filters.SmallTest;
-import android.test.AndroidTestCase;
-import android.util.Log;
import android.view.View;
-import android.widget.TextView;
-
-import junit.framework.AssertionFailedError;
-import junit.framework.TestResult;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.LinkedList;
@@ -41,7 +43,7 @@ import java.util.concurrent.atomic.AtomicInteger;
@RunWith(JUnit4.class)
@SmallTest
-public class AdapterHelperTest extends AndroidTestCase {
+public class AdapterHelperTest {
private static final boolean DEBUG = false;
@@ -49,35 +51,40 @@ public class AdapterHelperTest extends AndroidTestCase {
private static final String TAG = "AHT";
- List<MockViewHolder> mViewHolders;
+ private List<MockViewHolder> mViewHolders;
- AdapterHelper mAdapterHelper;
+ private AdapterHelper mAdapterHelper;
- List<AdapterHelper.UpdateOp> mFirstPassUpdates, mSecondPassUpdates;
+ private List<AdapterHelper.UpdateOp> mFirstPassUpdates, mSecondPassUpdates;
TestAdapter mTestAdapter;
- TestAdapter mPreProcessClone; // we clone adapter pre-process to run operations to see result
+ private TestAdapter mPreProcessClone;
+ // we clone adapter pre-process to run operations to see result
private List<TestAdapter.Item> mPreLayoutItems;
private StringBuilder mLog = new StringBuilder();
- @Override
- public void run(TestResult result) {
- super.run(result);
- if (!result.wasSuccessful()) {
- result.addFailure(this, new AssertionFailedError(mLog.toString()));
+ @Rule
+ public TestWatcher reportErrorLog = new TestWatcher() {
+ @Override
+ protected void failed(Throwable e, Description description) {
+ System.out.println(mLog.toString());
}
- }
+
+ @Override
+ protected void succeeded(Description description) {
+ }
+ };
@Before
public void cleanState() {
mLog.setLength(0);
- mPreLayoutItems = new ArrayList<TestAdapter.Item>();
- mViewHolders = new ArrayList<MockViewHolder>();
- mFirstPassUpdates = new ArrayList<AdapterHelper.UpdateOp>();
- mSecondPassUpdates = new ArrayList<AdapterHelper.UpdateOp>();
+ mPreLayoutItems = new ArrayList<>();
+ mViewHolders = new ArrayList<>();
+ mFirstPassUpdates = new ArrayList<>();
+ mSecondPassUpdates = new ArrayList<>();
mPreProcessClone = null;
mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
@Override
@@ -189,7 +196,7 @@ public class AdapterHelperTest extends AndroidTestCase {
if (mCollectLogs) {
mLog.append(msg).append("\n");
} else {
- Log.d(TAG, msg);
+ System.out.println(TAG + ":" + msg);
}
}
@@ -205,8 +212,7 @@ public class AdapterHelperTest extends AndroidTestCase {
}
private void addViewHolder(int position) {
- MockViewHolder viewHolder = new MockViewHolder(
- new TextView(getContext()));
+ MockViewHolder viewHolder = new MockViewHolder();
viewHolder.mPosition = position;
viewHolder.mItem = mTestAdapter.mItems.get(position);
mViewHolders.add(viewHolder);
@@ -502,7 +508,7 @@ public class AdapterHelperTest extends AndroidTestCase {
}
@Test
- public void testScenario18() throws InterruptedException {
+ public void testScenario18() {
setupBasic(10, 1, 4);
add(2, 11);
rm(16, 1);
@@ -622,7 +628,7 @@ public class AdapterHelperTest extends AndroidTestCase {
}
@Test
- public void testScenerio30() throws InterruptedException {
+ public void testScenerio30() {
mCollectLogs = true;
setupBasic(10, 3, 1);
rm(3, 2);
@@ -631,7 +637,7 @@ public class AdapterHelperTest extends AndroidTestCase {
}
@Test
- public void testScenerio31() throws InterruptedException {
+ public void testScenerio31() {
mCollectLogs = true;
setupBasic(10, 3, 1);
rm(3, 1);
@@ -844,7 +850,7 @@ public class AdapterHelperTest extends AndroidTestCase {
Random random = new Random(System.nanoTime());
for (int i = 0; i < 100; i++) {
try {
- Log.d(TAG, "running random test " + i);
+ log("running random test " + i);
randomTest(random, Math.max(40, 10 + nextInt(random, i)));
} catch (Throwable t) {
throw new Throwable("failure at random test " + i + "\n" + t.getMessage()
@@ -853,7 +859,7 @@ public class AdapterHelperTest extends AndroidTestCase {
}
}
- public void randomTest(Random random, int opCount) {
+ private void randomTest(Random random, int opCount) {
cleanState();
if (DEBUG) {
log("randomTest");
@@ -907,14 +913,14 @@ public class AdapterHelperTest extends AndroidTestCase {
preProcess();
}
- int nextInt(Random random, int n) {
+ private int nextInt(Random random, int n) {
if (n == 0) {
return 0;
}
return random.nextInt(n);
}
- public void assertOps(List<AdapterHelper.UpdateOp> actual,
+ private void assertOps(List<AdapterHelper.UpdateOp> actual,
AdapterHelper.UpdateOp... expected) {
assertEquals(expected.length, actual.size());
for (int i = 0; i < expected.length; i++) {
@@ -922,12 +928,12 @@ public class AdapterHelperTest extends AndroidTestCase {
}
}
- void assertDispatch(int firstPass, int secondPass) {
+ private void assertDispatch(int firstPass, int secondPass) {
assertEquals(firstPass, mFirstPassUpdates.size());
assertEquals(secondPass, mSecondPassUpdates.size());
}
- void preProcess() {
+ private void preProcess() {
for (MockViewHolder vh : mViewHolders) {
final int ind = mTestAdapter.mItems.indexOf(vh.mItem);
assertEquals("actual adapter position should match", ind,
@@ -975,23 +981,20 @@ public class AdapterHelperTest extends AndroidTestCase {
assertEquals(0, a2.mPendingAdded.size());
}
- AdapterHelper.UpdateOp op(int cmd, int start, int count) {
+ private AdapterHelper.UpdateOp op(int cmd, int start, int count) {
return new AdapterHelper.UpdateOp(cmd, start, count, null);
}
- AdapterHelper.UpdateOp op(int cmd, int start, int count, Object payload) {
+ private AdapterHelper.UpdateOp op(int cmd, int start, int count, Object payload) {
return new AdapterHelper.UpdateOp(cmd, start, count, payload);
}
- AdapterHelper.UpdateOp addOp(int start, int count) {
- return op(AdapterHelper.UpdateOp.ADD, start, count);
- }
-
- AdapterHelper.UpdateOp rmOp(int start, int count) {
+ private AdapterHelper.UpdateOp rmOp(int start, int count) {
return op(AdapterHelper.UpdateOp.REMOVE, start, count);
}
- AdapterHelper.UpdateOp upOp(int start, int count, Object payload) {
+ private AdapterHelper.UpdateOp upOp(int start, int count, @SuppressWarnings
+ ("SameParameterValue") Object payload) {
return op(AdapterHelper.UpdateOp.UPDATE, start, count, payload);
}
@@ -1002,7 +1005,7 @@ public class AdapterHelperTest extends AndroidTestCase {
mTestAdapter.add(start, count);
}
- boolean isItemLaidOut(int pos) {
+ private boolean isItemLaidOut(int pos) {
for (ViewHolder viewHolder : mViewHolders) {
if (viewHolder.mOldPosition == pos) {
return true;
@@ -1018,7 +1021,7 @@ public class AdapterHelperTest extends AndroidTestCase {
mTestAdapter.move(from, to);
}
- void rm(int start, int count) {
+ private void rm(int start, int count) {
if (DEBUG) {
log("rm(" + start + "," + count + ");");
}
@@ -1054,9 +1057,9 @@ public class AdapterHelperTest extends AndroidTestCase {
Queue<Item> mPendingAdded;
public TestAdapter(int initialCount, AdapterHelper container) {
- mItems = new ArrayList<Item>();
+ mItems = new ArrayList<>();
mAdapterHelper = container;
- mPendingAdded = new LinkedList<Item>();
+ mPendingAdded = new LinkedList<>();
for (int i = 0; i < initialCount; i++) {
mItems.add(new Item());
}
@@ -1102,15 +1105,13 @@ public class AdapterHelperTest extends AndroidTestCase {
));
}
- protected TestAdapter createCopy() {
+ TestAdapter createCopy() {
TestAdapter adapter = new TestAdapter(0, mAdapterHelper);
- for (Item item : mItems) {
- adapter.mItems.add(item);
- }
+ adapter.mItems.addAll(mItems);
return adapter;
}
- public void applyOps(List<AdapterHelper.UpdateOp> updates,
+ void applyOps(List<AdapterHelper.UpdateOp> updates,
TestAdapter dataSource) {
for (AdapterHelper.UpdateOp op : updates) {
switch (op.cmd) {
@@ -1140,20 +1141,16 @@ public class AdapterHelperTest extends AndroidTestCase {
return mPendingAdded.remove();
}
- public void createFakeItemAt(int fakeAddedItemIndex) {
- Item fakeItem = new Item();
- ((LinkedList<Item>) mPendingAdded).add(fakeAddedItemIndex, fakeItem);
- }
-
public static class Item {
private static AtomicInteger itemCounter = new AtomicInteger();
+ @SuppressWarnings("unused")
private final int id;
private int mVersionCount = 0;
- private ArrayList<Object> mPayloads = new ArrayList<Object>();
+ private ArrayList<Object> mPayloads = new ArrayList<>();
public Item() {
id = itemCounter.incrementAndGet();
@@ -1164,26 +1161,23 @@ public class AdapterHelperTest extends AndroidTestCase {
mVersionCount++;
}
- public void handleUpdate(Object payload) {
+ void handleUpdate(Object payload) {
assertSame(payload, mPayloads.get(0));
mPayloads.remove(0);
mVersionCount--;
}
- public int getUpdateCount() {
+ int getUpdateCount() {
return mVersionCount;
}
}
}
- void waitForDebugger() {
- android.os.Debug.waitForDebugger();
- }
-
static class MockViewHolder extends RecyclerView.ViewHolder {
- public Object mItem;
- public MockViewHolder(View itemView) {
- super(itemView);
+ TestAdapter.Item mItem;
+
+ MockViewHolder() {
+ super(Mockito.mock(View.class));
}
@Override
diff --git a/android/support/v7/widget/AppCompatTextViewAutoSizeHelper.java b/android/support/v7/widget/AppCompatTextViewAutoSizeHelper.java
index e82e4694..6b9d05a7 100644
--- a/android/support/v7/widget/AppCompatTextViewAutoSizeHelper.java
+++ b/android/support/v7/widget/AppCompatTextViewAutoSizeHelper.java
@@ -18,7 +18,6 @@ package android.support.v7.widget;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
@@ -26,6 +25,7 @@ import android.graphics.RectF;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
import android.support.v4.widget.TextViewCompat;
import android.support.v7.appcompat.R;
@@ -45,8 +45,8 @@ import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.Hashtable;
import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
/**
* Utility class which encapsulates the logic for the TextView auto-size text feature added to
@@ -66,7 +66,8 @@ class AppCompatTextViewAutoSizeHelper {
private static final int DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX = 1;
// Cache of TextView methods used via reflection; the key is the method name and the value is
// the method itself or null if it can not be found.
- private static Hashtable<String, Method> sTextViewMethodByNameCache = new Hashtable<>();
+ private static ConcurrentHashMap<String, Method> sTextViewMethodByNameCache =
+ new ConcurrentHashMap<>();
// Use this to specify that any of the auto-size configuration int values have not been set.
static final float UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE = -1f;
// Ported from TextView#VERY_WIDE. Represents a maximum width in pixels the TextView takes when
@@ -701,7 +702,7 @@ class AppCompatTextViewAutoSizeHelper {
return true;
}
- @TargetApi(23)
+ @RequiresApi(23)
private StaticLayout createStaticLayoutForMeasuring(CharSequence text,
Layout.Alignment alignment, int availableWidth, int maxLines) {
// Can use the StaticLayout.Builder (along with TextView params added in or after
@@ -725,7 +726,6 @@ class AppCompatTextViewAutoSizeHelper {
.build();
}
- @TargetApi(14)
private StaticLayout createStaticLayoutForMeasuringPre23(CharSequence text,
Layout.Alignment alignment, int availableWidth) {
// Setup defaults.
diff --git a/android/support/v7/widget/ButtonBarLayout.java b/android/support/v7/widget/ButtonBarLayout.java
index b47a5689..f4bbc6c3 100644
--- a/android/support/v7/widget/ButtonBarLayout.java
+++ b/android/support/v7/widget/ButtonBarLayout.java
@@ -35,9 +35,6 @@ import android.widget.LinearLayout;
*/
@RestrictTo(LIBRARY_GROUP)
public class ButtonBarLayout extends LinearLayout {
- /** Minimum screen height required for button stacking. */
- private static final int ALLOW_STACKING_MIN_HEIGHT_DP = 320;
-
/** Amount of the second button to "peek" above the fold when stacked. */
private static final int PEEK_BUTTON_DP = 16;
@@ -50,11 +47,8 @@ public class ButtonBarLayout extends LinearLayout {
public ButtonBarLayout(Context context, AttributeSet attrs) {
super(context, attrs);
- final boolean allowStackingDefault =
- getResources().getConfiguration().screenHeightDp >= ALLOW_STACKING_MIN_HEIGHT_DP;
final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ButtonBarLayout);
- mAllowStacking = ta.getBoolean(R.styleable.ButtonBarLayout_allowStacking,
- allowStackingDefault);
+ mAllowStacking = ta.getBoolean(R.styleable.ButtonBarLayout_allowStacking, true);
ta.recycle();
}
diff --git a/android/support/v7/widget/CardView.java b/android/support/v7/widget/CardView.java
index 58a04f0a..a45ee989 100644
--- a/android/support/v7/widget/CardView.java
+++ b/android/support/v7/widget/CardView.java
@@ -108,18 +108,57 @@ public class CardView extends FrameLayout {
final Rect mShadowBounds = new Rect();
public CardView(@NonNull Context context) {
- super(context);
- initialize(context, null, 0);
+ this(context, null);
}
public CardView(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- initialize(context, attrs, 0);
+ this(context, attrs, R.attr.cardViewStyle);
}
public CardView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
- initialize(context, attrs, defStyleAttr);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr,
+ R.style.CardView);
+ ColorStateList backgroundColor;
+ if (a.hasValue(R.styleable.CardView_cardBackgroundColor)) {
+ backgroundColor = a.getColorStateList(R.styleable.CardView_cardBackgroundColor);
+ } else {
+ // There isn't one set, so we'll compute one based on the theme
+ final TypedArray aa = getContext().obtainStyledAttributes(COLOR_BACKGROUND_ATTR);
+ final int themeColorBackground = aa.getColor(0, 0);
+ aa.recycle();
+
+ // If the theme colorBackground is light, use our own light color, otherwise dark
+ final float[] hsv = new float[3];
+ Color.colorToHSV(themeColorBackground, hsv);
+ backgroundColor = ColorStateList.valueOf(hsv[2] > 0.5f
+ ? getResources().getColor(R.color.cardview_light_background)
+ : getResources().getColor(R.color.cardview_dark_background));
+ }
+ float radius = a.getDimension(R.styleable.CardView_cardCornerRadius, 0);
+ float elevation = a.getDimension(R.styleable.CardView_cardElevation, 0);
+ float maxElevation = a.getDimension(R.styleable.CardView_cardMaxElevation, 0);
+ mCompatPadding = a.getBoolean(R.styleable.CardView_cardUseCompatPadding, false);
+ mPreventCornerOverlap = a.getBoolean(R.styleable.CardView_cardPreventCornerOverlap, true);
+ int defaultPadding = a.getDimensionPixelSize(R.styleable.CardView_contentPadding, 0);
+ mContentPadding.left = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingLeft,
+ defaultPadding);
+ mContentPadding.top = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingTop,
+ defaultPadding);
+ mContentPadding.right = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingRight,
+ defaultPadding);
+ mContentPadding.bottom = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingBottom,
+ defaultPadding);
+ if (elevation > maxElevation) {
+ maxElevation = elevation;
+ }
+ mUserSetMinWidth = a.getDimensionPixelSize(R.styleable.CardView_android_minWidth, 0);
+ mUserSetMinHeight = a.getDimensionPixelSize(R.styleable.CardView_android_minHeight, 0);
+ a.recycle();
+
+ IMPL.initialize(mCardViewDelegate, context, backgroundColor, radius,
+ elevation, maxElevation);
}
@Override
@@ -220,50 +259,6 @@ public class CardView extends FrameLayout {
}
}
- private void initialize(Context context, AttributeSet attrs, int defStyleAttr) {
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr,
- R.style.CardView);
- ColorStateList backgroundColor;
- if (a.hasValue(R.styleable.CardView_cardBackgroundColor)) {
- backgroundColor = a.getColorStateList(R.styleable.CardView_cardBackgroundColor);
- } else {
- // There isn't one set, so we'll compute one based on the theme
- final TypedArray aa = getContext().obtainStyledAttributes(COLOR_BACKGROUND_ATTR);
- final int themeColorBackground = aa.getColor(0, 0);
- aa.recycle();
-
- // If the theme colorBackground is light, use our own light color, otherwise dark
- final float[] hsv = new float[3];
- Color.colorToHSV(themeColorBackground, hsv);
- backgroundColor = ColorStateList.valueOf(hsv[2] > 0.5f
- ? getResources().getColor(R.color.cardview_light_background)
- : getResources().getColor(R.color.cardview_dark_background));
- }
- float radius = a.getDimension(R.styleable.CardView_cardCornerRadius, 0);
- float elevation = a.getDimension(R.styleable.CardView_cardElevation, 0);
- float maxElevation = a.getDimension(R.styleable.CardView_cardMaxElevation, 0);
- mCompatPadding = a.getBoolean(R.styleable.CardView_cardUseCompatPadding, false);
- mPreventCornerOverlap = a.getBoolean(R.styleable.CardView_cardPreventCornerOverlap, true);
- int defaultPadding = a.getDimensionPixelSize(R.styleable.CardView_contentPadding, 0);
- mContentPadding.left = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingLeft,
- defaultPadding);
- mContentPadding.top = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingTop,
- defaultPadding);
- mContentPadding.right = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingRight,
- defaultPadding);
- mContentPadding.bottom = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingBottom,
- defaultPadding);
- if (elevation > maxElevation) {
- maxElevation = elevation;
- }
- mUserSetMinWidth = a.getDimensionPixelSize(R.styleable.CardView_android_minWidth, 0);
- mUserSetMinHeight = a.getDimensionPixelSize(R.styleable.CardView_android_minHeight, 0);
- a.recycle();
-
- IMPL.initialize(mCardViewDelegate, context, backgroundColor, radius,
- elevation, maxElevation);
- }
-
@Override
public void setMinimumWidth(int minWidth) {
mUserSetMinWidth = minWidth;
diff --git a/android/support/v7/widget/LinearLayoutManager.java b/android/support/v7/widget/LinearLayoutManager.java
index 27df4901..fe4a37a6 100644
--- a/android/support/v7/widget/LinearLayoutManager.java
+++ b/android/support/v7/widget/LinearLayoutManager.java
@@ -48,9 +48,9 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements
static final boolean DEBUG = false;
- public static final int HORIZONTAL = OrientationHelper.HORIZONTAL;
+ public static final int HORIZONTAL = RecyclerView.HORIZONTAL;
- public static final int VERTICAL = OrientationHelper.VERTICAL;
+ public static final int VERTICAL = RecyclerView.VERTICAL;
public static final int INVALID_OFFSET = Integer.MIN_VALUE;
@@ -66,7 +66,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements
* Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}
*/
@RecyclerView.Orientation
- int mOrientation;
+ int mOrientation = RecyclerView.DEFAULT_ORIENTATION;
/**
* Helper class that keeps temporary layout state.
@@ -78,8 +78,6 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements
/**
* Many calculations are made depending on orientation. To keep it clean, this interface
* helps {@link LinearLayoutManager} make those decisions.
- * Based on {@link #mOrientation}, an implementation is lazily created in
- * {@link #ensureLayoutState} method.
*/
OrientationHelper mOrientationHelper;
@@ -154,7 +152,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements
* @param context Current context, will be used to access resources.
*/
public LinearLayoutManager(Context context) {
- this(context, VERTICAL, false);
+ this(context, RecyclerView.DEFAULT_ORIENTATION, false);
}
/**
@@ -335,13 +333,16 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements
if (orientation != HORIZONTAL && orientation != VERTICAL) {
throw new IllegalArgumentException("invalid orientation:" + orientation);
}
+
assertNotInLayoutOrScroll(null);
- if (orientation == mOrientation) {
- return;
+
+ if (orientation != mOrientation || mOrientationHelper == null) {
+ mOrientationHelper =
+ OrientationHelper.createOrientationHelper(this, orientation);
+ mAnchorInfo.mOrientationHelper = mOrientationHelper;
+ mOrientation = orientation;
+ requestLayout();
}
- mOrientation = orientation;
- mOrientationHelper = null;
- requestLayout();
}
/**
@@ -516,7 +517,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements
// TestResizingRelayoutWithAutoMeasure), which happens if we were to call
// updateAnchorInfoForLayout for an anchor that's not the focused view (e.g. a reference
// child which can change between layout passes).
- mAnchorInfo.assignFromViewAndKeepVisibleRect(focused);
+ mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
}
if (DEBUG) {
Log.d(TAG, "Anchor info:" + mAnchorInfo);
@@ -781,7 +782,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements
}
final View focused = getFocusedChild();
if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {
- anchorInfo.assignFromViewAndKeepVisibleRect(focused);
+ anchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
return true;
}
if (mLastStackFromEnd != mStackFromEnd) {
@@ -791,7 +792,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements
? findReferenceChildClosestToEnd(recycler, state)
: findReferenceChildClosestToStart(recycler, state);
if (referenceChild != null) {
- anchorInfo.assignFromView(referenceChild);
+ anchorInfo.assignFromView(referenceChild, getPosition(referenceChild));
// If all visible views are removed in 1 pass, reference child might be out of bounds.
// If that is the case, offset it back to 0 so that we use these pre-layout children.
if (!state.isPreLayout() && supportsPredictiveItemAnimations()) {
@@ -985,9 +986,6 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements
if (mLayoutState == null) {
mLayoutState = createLayoutState();
}
- if (mOrientationHelper == null) {
- mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation);
- }
}
/**
@@ -2370,7 +2368,8 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements
/**
* Simple data class to keep Anchor information
*/
- class AnchorInfo {
+ static class AnchorInfo {
+ OrientationHelper mOrientationHelper;
int mPosition;
int mCoordinate;
boolean mLayoutFromEnd;
@@ -2413,13 +2412,13 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements
&& lp.getViewLayoutPosition() < state.getItemCount();
}
- public void assignFromViewAndKeepVisibleRect(View child) {
+ public void assignFromViewAndKeepVisibleRect(View child, int position) {
final int spaceChange = mOrientationHelper.getTotalSpaceChange();
if (spaceChange >= 0) {
- assignFromView(child);
+ assignFromView(child, position);
return;
}
- mPosition = getPosition(child);
+ mPosition = position;
if (mLayoutFromEnd) {
final int prevLayoutEnd = mOrientationHelper.getEndAfterPadding() - spaceChange;
final int childEnd = mOrientationHelper.getDecoratedEnd(child);
@@ -2460,7 +2459,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements
}
}
- public void assignFromView(View child) {
+ public void assignFromView(View child, int position) {
if (mLayoutFromEnd) {
mCoordinate = mOrientationHelper.getDecoratedEnd(child)
+ mOrientationHelper.getTotalSpaceChange();
@@ -2468,7 +2467,7 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements
mCoordinate = mOrientationHelper.getDecoratedStart(child);
}
- mPosition = getPosition(child);
+ mPosition = position;
}
}
diff --git a/android/support/v7/widget/ListPopupWindow.java b/android/support/v7/widget/ListPopupWindow.java
index edc9781c..b98197c9 100644
--- a/android/support/v7/widget/ListPopupWindow.java
+++ b/android/support/v7/widget/ListPopupWindow.java
@@ -283,7 +283,7 @@ public class ListPopupWindow implements ShowableListMenu {
mAdapter.unregisterDataSetObserver(mObserver);
}
mAdapter = adapter;
- if (mAdapter != null) {
+ if (adapter != null) {
adapter.registerDataSetObserver(mObserver);
}
diff --git a/android/support/v7/widget/OrientationHelper.java b/android/support/v7/widget/OrientationHelper.java
index 5e90f2e7..99bcbaa0 100644
--- a/android/support/v7/widget/OrientationHelper.java
+++ b/android/support/v7/widget/OrientationHelper.java
@@ -48,6 +48,14 @@ public abstract class OrientationHelper {
}
/**
+ * Returns the {@link android.support.v7.widget.RecyclerView.LayoutManager LayoutManager} that
+ * is associated with this OrientationHelper.
+ */
+ public RecyclerView.LayoutManager getLayoutManager() {
+ return mLayoutManager;
+ }
+
+ /**
* Call this method after onLayout method is complete if state is NOT pre-layout.
* This method records information like layout bounds that might be useful in the next layout
* calculations.
@@ -435,4 +443,4 @@ public abstract class OrientationHelper {
}
};
}
-} \ No newline at end of file
+}
diff --git a/android/support/v7/widget/RecyclerView.java b/android/support/v7/widget/RecyclerView.java
index 84c28b10..a2879796 100644
--- a/android/support/v7/widget/RecyclerView.java
+++ b/android/support/v7/widget/RecyclerView.java
@@ -217,6 +217,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
public static final int VERTICAL = LinearLayout.VERTICAL;
+ static final int DEFAULT_ORIENTATION = VERTICAL;
public static final int NO_POSITION = -1;
public static final long NO_ID = -1;
public static final int INVALID_TYPE = -1;
@@ -349,7 +350,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
return;
}
if (mLayoutFrozen) {
- mLayoutRequestEaten = true;
+ mLayoutWasDefered = true;
return; //we'll process updates when ice age ends.
}
consumePendingUpdateOperations();
@@ -371,10 +372,21 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
boolean mEnableFastScroller;
@VisibleForTesting boolean mFirstLayoutComplete;
- // Counting lock to control whether we should ignore requestLayout calls from children or not.
- private int mEatRequestLayout = 0;
+ /**
+ * The current depth of nested calls to {@link #startInterceptRequestLayout()} (number of
+ * calls to {@link #startInterceptRequestLayout()} - number of calls to
+ * {@link #stopInterceptRequestLayout(boolean)} . This is used to signal whether we
+ * should defer layout operations caused by layout requests from children of
+ * {@link RecyclerView}.
+ */
+ private int mInterceptRequestLayoutDepth = 0;
+
+ /**
+ * True if a call to requestLayout was intercepted and prevented from executing like normal and
+ * we plan on continuing with normal execution later.
+ */
+ boolean mLayoutWasDefered;
- boolean mLayoutRequestEaten;
boolean mLayoutFrozen;
private boolean mIgnoreMotionEventTillDown;
@@ -1355,7 +1367,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
* @return true if an animating view is removed
*/
boolean removeAnimatingView(View view) {
- eatRequestLayout();
+ startInterceptRequestLayout();
final boolean removed = mChildHelper.removeViewIfHidden(view);
if (removed) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
@@ -1366,7 +1378,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
}
}
// only clear request eaten flag if we removed the view.
- resumeRequestLayout(!removed);
+ stopInterceptRequestLayout(!removed);
return removed;
}
@@ -1736,10 +1748,10 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
.hasAnyUpdateTypes(AdapterHelper.UpdateOp.ADD | AdapterHelper.UpdateOp.REMOVE
| AdapterHelper.UpdateOp.MOVE)) {
TraceCompat.beginSection(TRACE_HANDLE_ADAPTER_UPDATES_TAG);
- eatRequestLayout();
+ startInterceptRequestLayout();
onEnterLayoutOrScroll();
mAdapterHelper.preProcess();
- if (!mLayoutRequestEaten) {
+ if (!mLayoutWasDefered) {
if (hasUpdatedView()) {
dispatchLayout();
} else {
@@ -1747,7 +1759,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
mAdapterHelper.consumePostponedUpdates();
}
}
- resumeRequestLayout(true);
+ stopInterceptRequestLayout(true);
onExitLayoutOrScroll();
TraceCompat.endSection();
} else if (mAdapterHelper.hasPendingUpdates()) {
@@ -1791,7 +1803,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
consumePendingUpdateOperations();
if (mAdapter != null) {
- eatRequestLayout();
+ startInterceptRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
fillRemainingScrollValues(mState);
@@ -1806,7 +1818,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
TraceCompat.endSection();
repositionShadowingViews();
onExitLayoutOrScroll();
- resumeRequestLayout(false);
+ stopInterceptRequestLayout(false);
}
if (!mItemDecorations.isEmpty()) {
invalidate();
@@ -1983,24 +1995,45 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollRange(mState) : 0;
}
-
- void eatRequestLayout() {
- mEatRequestLayout++;
- if (mEatRequestLayout == 1 && !mLayoutFrozen) {
- mLayoutRequestEaten = false;
+ /**
+ * This method should be called before any code that may trigger a child view to cause a call to
+ * {@link RecyclerView#requestLayout()}. Doing so enables {@link RecyclerView} to avoid
+ * reacting to additional redundant calls to {@link #requestLayout()}.
+ * <p>
+ * A call to this method must always be accompanied by a call to
+ * {@link #stopInterceptRequestLayout(boolean)} that follows the code that may trigger a
+ * child View to cause a call to {@link RecyclerView#requestLayout()}.
+ *
+ * @see #stopInterceptRequestLayout(boolean)
+ */
+ void startInterceptRequestLayout() {
+ mInterceptRequestLayoutDepth++;
+ if (mInterceptRequestLayoutDepth == 1 && !mLayoutFrozen) {
+ mLayoutWasDefered = false;
}
}
- void resumeRequestLayout(boolean performLayoutChildren) {
- if (mEatRequestLayout < 1) {
+ /**
+ * This method should be called after any code that may trigger a child view to cause a call to
+ * {@link RecyclerView#requestLayout()}.
+ * <p>
+ * A call to this method must always be accompanied by a call to
+ * {@link #startInterceptRequestLayout()} that precedes the code that may trigger a child
+ * View to cause a call to {@link RecyclerView#requestLayout()}.
+ *
+ * @see #startInterceptRequestLayout()
+ */
+ void stopInterceptRequestLayout(boolean performLayoutChildren) {
+ if (mInterceptRequestLayoutDepth < 1) {
//noinspection PointlessBooleanExpression
if (DEBUG) {
- throw new IllegalStateException("invalid eat request layout count"
+ throw new IllegalStateException("stopInterceptRequestLayout was called more "
+ + "times than startInterceptRequestLayout."
+ exceptionLabel());
}
- mEatRequestLayout = 1;
+ mInterceptRequestLayoutDepth = 1;
}
- if (!performLayoutChildren) {
+ if (!performLayoutChildren && !mLayoutFrozen) {
// Reset the layout request eaten counter.
// This is necessary since eatRequest calls can be nested in which case the other
// call will override the inner one.
@@ -2009,19 +2042,19 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
// eat layout for dispatchLayout
// a bunch of req layout calls arrive
- mLayoutRequestEaten = false;
+ mLayoutWasDefered = false;
}
- if (mEatRequestLayout == 1) {
+ if (mInterceptRequestLayoutDepth == 1) {
// when layout is frozen we should delay dispatchLayout()
- if (performLayoutChildren && mLayoutRequestEaten && !mLayoutFrozen
+ if (performLayoutChildren && mLayoutWasDefered && !mLayoutFrozen
&& mLayout != null && mAdapter != null) {
dispatchLayout();
}
if (!mLayoutFrozen) {
- mLayoutRequestEaten = false;
+ mLayoutWasDefered = false;
}
}
- mEatRequestLayout--;
+ mInterceptRequestLayoutDepth--;
}
/**
@@ -2051,10 +2084,10 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
assertNotInLayoutOrScroll("Do not setLayoutFrozen in layout or scroll");
if (!frozen) {
mLayoutFrozen = false;
- if (mLayoutRequestEaten && mLayout != null && mAdapter != null) {
+ if (mLayoutWasDefered && mLayout != null && mAdapter != null) {
requestLayout();
}
- mLayoutRequestEaten = false;
+ mLayoutWasDefered = false;
} else {
final long now = SystemClock.uptimeMillis();
MotionEvent cancelEvent = MotionEvent.obtain(now, now,
@@ -2471,9 +2504,9 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
// panic, focused view is not a child anymore, cannot call super.
return null;
}
- eatRequestLayout();
+ startInterceptRequestLayout();
mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState);
- resumeRequestLayout(false);
+ stopInterceptRequestLayout(false);
}
result = ff.findNextFocus(this, focused, direction);
} else {
@@ -2485,9 +2518,9 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
// panic, focused view is not a child anymore, cannot call super.
return null;
}
- eatRequestLayout();
+ startInterceptRequestLayout();
result = mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState);
- resumeRequestLayout(false);
+ stopInterceptRequestLayout(false);
}
}
if (result != null && !result.hasFocusable()) {
@@ -3202,7 +3235,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
- eatRequestLayout();
+ startInterceptRequestLayout();
onEnterLayoutOrScroll();
processAdapterUpdatesAndSetAnimationFlags();
onExitLayoutOrScroll();
@@ -3215,7 +3248,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
mState.mInPreLayout = false;
}
mAdapterUpdateDuringMeasure = false;
- resumeRequestLayout(false);
+ stopInterceptRequestLayout(false);
} else if (mState.mRunPredictiveAnimations) {
// If mAdapterUpdateDuringMeasure is false and mRunPredictiveAnimations is true:
// this means there is already an onMeasure() call performed to handle the pending
@@ -3231,9 +3264,9 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
} else {
mState.mItemCount = 0;
}
- eatRequestLayout();
+ startInterceptRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
- resumeRequestLayout(false);
+ stopInterceptRequestLayout(false);
mState.mInPreLayout = false; // clear
}
}
@@ -3668,7 +3701,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
mState.assertLayoutStep(State.STEP_START);
fillRemainingScrollValues(mState);
mState.mIsMeasuring = false;
- eatRequestLayout();
+ startInterceptRequestLayout();
mViewInfoStore.clear();
onEnterLayoutOrScroll();
processAdapterUpdatesAndSetAnimationFlags();
@@ -3748,7 +3781,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
clearOldPositions();
}
onExitLayoutOrScroll();
- resumeRequestLayout(false);
+ stopInterceptRequestLayout(false);
mState.mLayoutStep = State.STEP_LAYOUT;
}
@@ -3757,7 +3790,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
* This step might be run multiple times if necessary (e.g. measure).
*/
private void dispatchLayoutStep2() {
- eatRequestLayout();
+ startInterceptRequestLayout();
onEnterLayoutOrScroll();
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
@@ -3775,7 +3808,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
- resumeRequestLayout(false);
+ stopInterceptRequestLayout(false);
}
/**
@@ -3784,7 +3817,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
*/
private void dispatchLayoutStep3() {
mState.assertLayoutStep(State.STEP_ANIMATIONS);
- eatRequestLayout();
+ startInterceptRequestLayout();
onEnterLayoutOrScroll();
mState.mLayoutStep = State.STEP_START;
if (mState.mRunSimpleAnimations) {
@@ -3860,7 +3893,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
mLayout.onLayoutCompleted(mState);
onExitLayoutOrScroll();
- resumeRequestLayout(false);
+ stopInterceptRequestLayout(false);
mViewInfoStore.clear();
if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) {
dispatchOnScrolled(0, 0);
@@ -4043,10 +4076,10 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
@Override
public void requestLayout() {
- if (mEatRequestLayout == 0 && !mLayoutFrozen) {
+ if (mInterceptRequestLayoutDepth == 0 && !mLayoutFrozen) {
super.requestLayout();
} else {
- mLayoutRequestEaten = true;
+ mLayoutWasDefered = true;
}
}
@@ -4905,7 +4938,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
}
if (mAdapter != null) {
- eatRequestLayout();
+ startInterceptRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
fillRemainingScrollValues(mState);
@@ -4921,7 +4954,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
repositionShadowingViews();
onExitLayoutOrScroll();
- resumeRequestLayout(false);
+ stopInterceptRequestLayout(false);
if (smoothScroller != null && !smoothScroller.isPendingInitialRun()
&& smoothScroller.isRunning()) {
@@ -6544,7 +6577,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
* @see #getItemViewType(int)
* @see #onBindViewHolder(ViewHolder, int)
*/
- public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);
+ @NonNull
+ public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType);
/**
* Called by RecyclerView to display the data at the specified position. This method should
@@ -6566,7 +6600,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
* item at the given position in the data set.
* @param position The position of the item within the adapter's data set.
*/
- public abstract void onBindViewHolder(VH holder, int position);
+ public abstract void onBindViewHolder(@NonNull VH holder, int position);
/**
* Called by RecyclerView to display the data at the specified position. This method
@@ -6597,7 +6631,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
* @param payloads A non-null list of merged payloads. Can be empty list if requires full
* update.
*/
- public void onBindViewHolder(VH holder, int position, List<Object> payloads) {
+ public void onBindViewHolder(@NonNull VH holder, int position,
+ @NonNull List<Object> payloads) {
onBindViewHolder(holder, position);
}
@@ -6607,7 +6642,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
*
* @see #onCreateViewHolder(ViewGroup, int)
*/
- public final VH createViewHolder(ViewGroup parent, int viewType) {
+ public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
final VH holder = onCreateViewHolder(parent, viewType);
holder.mItemViewType = viewType;
@@ -6622,7 +6657,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
*
* @see #onBindViewHolder(ViewHolder, int)
*/
- public final void bindViewHolder(VH holder, int position) {
+ public final void bindViewHolder(@NonNull VH holder, int position) {
holder.mPosition = position;
if (hasStableIds()) {
holder.mItemId = getItemId(position);
@@ -6719,7 +6754,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
*
* @param holder The ViewHolder for the view being recycled
*/
- public void onViewRecycled(VH holder) {
+ public void onViewRecycled(@NonNull VH holder) {
}
/**
@@ -6756,7 +6791,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
* RecyclerView will check the View's transient state again before giving a final decision.
* Default implementation returns false.
*/
- public boolean onFailedToRecycleView(VH holder) {
+ public boolean onFailedToRecycleView(@NonNull VH holder) {
return false;
}
@@ -6770,7 +6805,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
*
* @param holder Holder of the view being attached
*/
- public void onViewAttachedToWindow(VH holder) {
+ public void onViewAttachedToWindow(@NonNull VH holder) {
}
/**
@@ -6782,7 +6817,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
*
* @param holder Holder of the view being detached
*/
- public void onViewDetachedFromWindow(VH holder) {
+ public void onViewDetachedFromWindow(@NonNull VH holder) {
}
/**
@@ -6810,7 +6845,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
*
* @see #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver)
*/
- public void registerAdapterDataObserver(AdapterDataObserver observer) {
+ public void registerAdapterDataObserver(@NonNull AdapterDataObserver observer) {
mObservable.registerObserver(observer);
}
@@ -6824,7 +6859,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
*
* @see #registerAdapterDataObserver(RecyclerView.AdapterDataObserver)
*/
- public void unregisterAdapterDataObserver(AdapterDataObserver observer) {
+ public void unregisterAdapterDataObserver(@NonNull AdapterDataObserver observer) {
mObservable.unregisterObserver(observer);
}
@@ -6836,7 +6871,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
* @param recyclerView The RecyclerView instance which started observing this adapter.
* @see #onDetachedFromRecyclerView(RecyclerView)
*/
- public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
}
/**
@@ -6845,7 +6880,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
* @param recyclerView The RecyclerView instance which stopped observing this adapter.
* @see #onAttachedToRecyclerView(RecyclerView)
*/
- public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
+ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
}
/**
@@ -6921,7 +6956,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
*
* @see #notifyItemRangeChanged(int, int)
*/
- public final void notifyItemChanged(int position, Object payload) {
+ public final void notifyItemChanged(int position, @Nullable Object payload) {
mObservable.notifyItemRangeChanged(position, 1, payload);
}
@@ -6969,7 +7004,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
*
* @see #notifyItemChanged(int)
*/
- public final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ public final void notifyItemRangeChanged(int positionStart, int itemCount,
+ @Nullable Object payload) {
mObservable.notifyItemRangeChanged(positionStart, itemCount, payload);
}
@@ -10072,7 +10108,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
if (vScroll == 0 && hScroll == 0) {
return false;
}
- mRecyclerView.smoothScrollBy(hScroll, vScroll);
+ mRecyclerView.scrollBy(hScroll, vScroll);
return true;
}
@@ -10118,7 +10154,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView,
defStyleAttr, defStyleRes);
properties.orientation = a.getInt(R.styleable.RecyclerView_android_orientation,
- VERTICAL);
+ DEFAULT_ORIENTATION);
properties.spanCount = a.getInt(R.styleable.RecyclerView_spanCount, 1);
properties.reverseLayout = a.getBoolean(R.styleable.RecyclerView_reverseLayout, false);
properties.stackFromEnd = a.getBoolean(R.styleable.RecyclerView_stackFromEnd, false);
@@ -10833,8 +10869,12 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
*/
private void onEnteredHiddenState(RecyclerView parent) {
// While the view item is in hidden state, make it invisible for the accessibility.
- mWasImportantForAccessibilityBeforeHidden =
- ViewCompat.getImportantForAccessibility(itemView);
+ if (mPendingAccessibilityState != PENDING_ACCESSIBILITY_STATE_NOT_SET) {
+ mWasImportantForAccessibilityBeforeHidden = mPendingAccessibilityState;
+ } else {
+ mWasImportantForAccessibilityBeforeHidden =
+ ViewCompat.getImportantForAccessibility(itemView);
+ }
parent.setChildImportantForAccessibilityInternal(this,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
}
@@ -11193,7 +11233,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
// do nothing
}
- public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
// fallback to onItemRangeChanged(positionStart, itemCount) if app
// does not override this method.
onItemRangeChanged(positionStart, itemCount);
@@ -11670,7 +11710,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
notifyItemRangeChanged(positionStart, itemCount, null);
}
- public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ public void notifyItemRangeChanged(int positionStart, int itemCount,
+ @Nullable Object payload) {
// since onItemRangeChanged() is implemented by the app, it could do anything, including
// removing itself from {@link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {@link mObservers}.
@@ -12063,6 +12104,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
+ "mTargetPosition=" + mTargetPosition
+ ", mData=" + mData
+ ", mItemCount=" + mItemCount
+ + ", mIsMeasuring=" + mIsMeasuring
+ ", mPreviousLayoutItemCount=" + mPreviousLayoutItemCount
+ ", mDeletedInvisibleItemCountSincePreviousLayout="
+ mDeletedInvisibleItemCountSincePreviousLayout
diff --git a/android/support/v7/widget/StaggeredGridLayoutManager.java b/android/support/v7/widget/StaggeredGridLayoutManager.java
index f3ea0453..55fb14e8 100644
--- a/android/support/v7/widget/StaggeredGridLayoutManager.java
+++ b/android/support/v7/widget/StaggeredGridLayoutManager.java
@@ -59,9 +59,9 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager imple
static final boolean DEBUG = false;
- public static final int HORIZONTAL = OrientationHelper.HORIZONTAL;
+ public static final int HORIZONTAL = RecyclerView.HORIZONTAL;
- public static final int VERTICAL = OrientationHelper.VERTICAL;
+ public static final int VERTICAL = RecyclerView.VERTICAL;
/**
* Does not do anything to hide gaps.
diff --git a/android/support/v7/widget/TooltipCompat.java b/android/support/v7/widget/TooltipCompat.java
index 470c3b2c..4a583da1 100644
--- a/android/support/v7/widget/TooltipCompat.java
+++ b/android/support/v7/widget/TooltipCompat.java
@@ -16,7 +16,6 @@
package android.support.v7.widget;
-import android.annotation.TargetApi;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -28,34 +27,6 @@ import android.view.View;
*
*/
public class TooltipCompat {
- private interface ViewCompatImpl {
- void setTooltipText(@NonNull View view, @Nullable CharSequence tooltipText);
- }
-
- private static class BaseViewCompatImpl implements ViewCompatImpl {
- @Override
- public void setTooltipText(@NonNull View view, @Nullable CharSequence tooltipText) {
- TooltipCompatHandler.setTooltipText(view, tooltipText);
- }
- }
-
- @TargetApi(26)
- private static class Api26ViewCompatImpl implements ViewCompatImpl {
- @Override
- public void setTooltipText(@NonNull View view, @Nullable CharSequence tooltipText) {
- view.setTooltipText(tooltipText);
- }
- }
-
- private static final ViewCompatImpl IMPL;
- static {
- if (Build.VERSION.SDK_INT >= 26) {
- IMPL = new Api26ViewCompatImpl();
- } else {
- IMPL = new BaseViewCompatImpl();
- }
- }
-
/**
* Sets the tooltip text for the view.
* <p> Prior to API 26 this method sets or clears (when tooltip is null) the view's
@@ -66,7 +37,11 @@ public class TooltipCompat {
* @param tooltipText the tooltip text
*/
public static void setTooltipText(@NonNull View view, @Nullable CharSequence tooltipText) {
- IMPL.setTooltipText(view, tooltipText);
+ if (Build.VERSION.SDK_INT >= 26) {
+ view.setTooltipText(tooltipText);
+ } else {
+ TooltipCompatHandler.setTooltipText(view, tooltipText);
+ }
}
private TooltipCompat() {}
diff --git a/android/support/v7/widget/ViewInfoStoreTest.java b/android/support/v7/widget/ViewInfoStoreTest.java
index 4a224a41..c4163140 100644
--- a/android/support/v7/widget/ViewInfoStoreTest.java
+++ b/android/support/v7/widget/ViewInfoStoreTest.java
@@ -21,6 +21,13 @@ import static android.support.v7.widget.ViewInfoStore.InfoRecord.FLAG_DISAPPEARE
import static android.support.v7.widget.ViewInfoStore.InfoRecord.FLAG_POST;
import static android.support.v7.widget.ViewInfoStore.InfoRecord.FLAG_PRE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.test.filters.SmallTest;
@@ -29,8 +36,6 @@ import android.support.v7.widget.RecyclerView.ItemAnimator.ItemHolderInfo;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.View;
-import junit.framework.TestCase;
-
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -44,7 +49,7 @@ import java.util.Map;
@SuppressWarnings("ConstantConditions")
@RunWith(JUnit4.class)
@SmallTest
-public class ViewInfoStoreTest extends TestCase {
+public class ViewInfoStoreTest {
ViewInfoStore mStore;
LoggingProcessCallback mCallback;
@Before
diff --git a/android/support/v7/widget/helper/ItemTouchHelper.java b/android/support/v7/widget/helper/ItemTouchHelper.java
index aee48dfa..d2b6a202 100644
--- a/android/support/v7/widget/helper/ItemTouchHelper.java
+++ b/android/support/v7/widget/helper/ItemTouchHelper.java
@@ -457,7 +457,7 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration
destroyCallbacks();
}
mRecyclerView = recyclerView;
- if (mRecyclerView != null) {
+ if (recyclerView != null) {
final Resources resources = recyclerView.getResources();
mSwipeEscapeVelocity = resources
.getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
diff --git a/android/support/wear/ambient/AmbientDelegateTest.java b/android/support/wear/ambient/AmbientDelegateTest.java
new file mode 100644
index 00000000..60332323
--- /dev/null
+++ b/android/support/wear/ambient/AmbientDelegateTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.wear.ambient;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.app.FragmentActivity;
+
+import com.google.android.wearable.compat.WearableActivityController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+/**
+ * Tests for {@link AmbientDelegate}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AmbientDelegateTest {
+
+ @Mock
+ AmbientDelegate.AmbientCallback mMockAmbientCallback;
+ @Mock
+ WearableControllerProvider mMockWearableControllerProvider;
+ @Mock
+ WearableActivityController mMockWearableController;
+ @Mock
+ FragmentActivity mMockActivity;
+
+ private AmbientDelegate mAmbientDelegateUnderTest;
+
+ @Before
+ public void setUp() {
+ mMockAmbientCallback = mock(AmbientDelegate.AmbientCallback.class);
+ mMockWearableControllerProvider = mock(WearableControllerProvider.class);
+ mMockWearableController = mock(WearableActivityController.class);
+ mMockActivity = mock(FragmentActivity.class);
+ when(mMockWearableControllerProvider
+ .getWearableController(mMockActivity, mMockAmbientCallback))
+ .thenReturn(mMockWearableController);
+ }
+
+ @Test
+ public void testNullActivity() {
+ mAmbientDelegateUnderTest = new AmbientDelegate(null,
+ mMockWearableControllerProvider, mMockAmbientCallback);
+ verifyZeroInteractions(mMockWearableControllerProvider);
+
+ assertFalse(mAmbientDelegateUnderTest.isAmbient());
+
+ }
+
+ @Test
+ public void testActivityPresent() {
+ mAmbientDelegateUnderTest = new AmbientDelegate(mMockActivity,
+ mMockWearableControllerProvider, mMockAmbientCallback);
+
+ mAmbientDelegateUnderTest.onCreate();
+ verify(mMockWearableController).onCreate();
+
+ mAmbientDelegateUnderTest.onResume();
+ verify(mMockWearableController).onResume();
+
+ mAmbientDelegateUnderTest.onPause();
+ verify(mMockWearableController).onPause();
+
+ mAmbientDelegateUnderTest.onStop();
+ verify(mMockWearableController).onStop();
+
+ mAmbientDelegateUnderTest.onDestroy();
+ verify(mMockWearableController).onDestroy();
+ }
+}
diff --git a/android/support/wear/ambient/AmbientMode.java b/android/support/wear/ambient/AmbientMode.java
index 0077a5bd..d3a8a435 100644
--- a/android/support/wear/ambient/AmbientMode.java
+++ b/android/support/wear/ambient/AmbientMode.java
@@ -48,7 +48,9 @@ import java.io.PrintWriter;
* AmbientMode.AmbientController controller = AmbientMode.attachAmbientSupport(this);
* boolean isAmbient = controller.isAmbient();
* }</pre>
+ * @deprecated please use {@link AmbientModeSupport} instead.
*/
+@Deprecated
public final class AmbientMode extends Fragment {
private static final String TAG = "AmbientMode";
diff --git a/android/support/wear/ambient/AmbientModeResumeTest.java b/android/support/wear/ambient/AmbientModeResumeTest.java
new file mode 100644
index 00000000..007c6194
--- /dev/null
+++ b/android/support/wear/ambient/AmbientModeResumeTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.ambient;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.wear.widget.util.WakeLockRule;
+
+import com.google.android.wearable.compat.WearableActivityController;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class AmbientModeResumeTest {
+ @Rule
+ public final WakeLockRule mWakeLock = new WakeLockRule();
+
+ @Rule
+ public final ActivityTestRule<AmbientModeResumeTestActivity> mActivityRule =
+ new ActivityTestRule<>(AmbientModeResumeTestActivity.class);
+
+ @Test
+ public void testActivityDefaults() throws Throwable {
+ assertTrue(WearableActivityController.getLastInstance().isAutoResumeEnabled());
+ assertFalse(WearableActivityController.getLastInstance().isAmbientEnabled());
+ }
+}
diff --git a/android/support/wear/ambient/AmbientModeResumeTestActivity.java b/android/support/wear/ambient/AmbientModeResumeTestActivity.java
new file mode 100644
index 00000000..0ca3c15b
--- /dev/null
+++ b/android/support/wear/ambient/AmbientModeResumeTestActivity.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.wear.ambient;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class AmbientModeResumeTestActivity extends Activity {
+ AmbientMode.AmbientController mAmbientController;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mAmbientController = AmbientMode.attachAmbientSupport(this);
+ }
+}
diff --git a/android/support/wear/ambient/AmbientModeSupport.java b/android/support/wear/ambient/AmbientModeSupport.java
new file mode 100644
index 00000000..97e26d9f
--- /dev/null
+++ b/android/support/wear/ambient/AmbientModeSupport.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.wear.ambient;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.CallSuper;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.util.Log;
+
+import com.google.android.wearable.compat.WearableActivityController;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Use this as a headless Fragment to add ambient support to an Activity on Wearable devices.
+ * <p>
+ * The application that uses this should add the {@link android.Manifest.permission#WAKE_LOCK}
+ * permission to its manifest.
+ * <p>
+ * The primary entry point for this code is the {@link #attach(FragmentActivity)} method.
+ * It should be called with an {@link FragmentActivity} as an argument and that
+ * {@link FragmentActivity} will then be able to receive ambient lifecycle events through
+ * an {@link AmbientCallback}. The {@link FragmentActivity} will also receive a
+ * {@link AmbientController} object from the attachment which can be used to query the current
+ * status of the ambient mode. An example of how to attach {@link AmbientModeSupport} to your
+ * {@link FragmentActivity} and use the {@link AmbientController} can be found below:
+ * <p>
+ * <pre class="prettyprint">{@code
+ * AmbientMode.AmbientController controller = AmbientMode.attachAmbientSupport(this);
+ * boolean isAmbient = controller.isAmbient();
+ * }</pre>
+ */
+public final class AmbientModeSupport extends Fragment {
+ private static final String TAG = "AmbientMode";
+
+ /**
+ * Property in bundle passed to {@code AmbientCallback#onEnterAmbient(Bundle)} to indicate
+ * whether burn-in protection is required. When this property is set to true, views must be
+ * shifted around periodically in ambient mode. To ensure that content isn't shifted off
+ * the screen, avoid placing content within 10 pixels of the edge of the screen. Activities
+ * should also avoid solid white areas to prevent pixel burn-in. Both of these requirements
+ * only apply in ambient mode, and only when this property is set to true.
+ */
+ public static final String EXTRA_BURN_IN_PROTECTION =
+ WearableActivityController.EXTRA_BURN_IN_PROTECTION;
+
+ /**
+ * Property in bundle passed to {@code AmbientCallback#onEnterAmbient(Bundle)} to indicate
+ * whether the device has low-bit ambient mode. When this property is set to true, the screen
+ * supports fewer bits for each color in ambient mode. In this case, activities should disable
+ * anti-aliasing in ambient mode.
+ */
+ public static final String EXTRA_LOWBIT_AMBIENT =
+ WearableActivityController.EXTRA_LOWBIT_AMBIENT;
+
+ /**
+ * Fragment tag used by default when adding {@link AmbientModeSupport} to add ambient support to
+ * a {@link FragmentActivity}.
+ */
+ public static final String FRAGMENT_TAG = "android.support.wearable.ambient.AmbientMode";
+
+ /**
+ * Interface for any {@link Activity} that wishes to implement Ambient Mode. Use the
+ * {@link #getAmbientCallback()} method to return and {@link AmbientCallback} which can be used
+ * to bind the {@link AmbientModeSupport} to the instantiation of this interface.
+ * <p>
+ * <pre class="prettyprint">{@code
+ * return new AmbientMode.AmbientCallback() {
+ * public void onEnterAmbient(Bundle ambientDetails) {...}
+ * public void onExitAmbient(Bundle ambientDetails) {...}
+ * }
+ * }</pre>
+ */
+ public interface AmbientCallbackProvider {
+ /**
+ * @return the {@link AmbientCallback} to be used by this class to communicate with the
+ * entity interested in ambient events.
+ */
+ AmbientCallback getAmbientCallback();
+ }
+
+ /**
+ * Callback to receive ambient mode state changes. It must be used by all users of AmbientMode.
+ */
+ public abstract static class AmbientCallback {
+ /**
+ * Called when an activity is entering ambient mode. This event is sent while an activity is
+ * running (after onResume, before onPause). All drawing should complete by the conclusion
+ * of this method. Note that {@code invalidate()} calls will be executed before resuming
+ * lower-power mode.
+ *
+ * @param ambientDetails bundle containing information about the display being used.
+ * It includes information about low-bit color and burn-in protection.
+ */
+ public void onEnterAmbient(Bundle ambientDetails) {}
+
+ /**
+ * Called when the system is updating the display for ambient mode. Activities may use this
+ * opportunity to update or invalidate views.
+ */
+ public void onUpdateAmbient() {}
+
+ /**
+ * Called when an activity should exit ambient mode. This event is sent while an activity is
+ * running (after onResume, before onPause).
+ */
+ public void onExitAmbient() {}
+ }
+
+ private final AmbientDelegate.AmbientCallback mCallback =
+ new AmbientDelegate.AmbientCallback() {
+ @Override
+ public void onEnterAmbient(Bundle ambientDetails) {
+ if (mSuppliedCallback != null) {
+ mSuppliedCallback.onEnterAmbient(ambientDetails);
+ }
+ }
+
+ @Override
+ public void onExitAmbient() {
+ if (mSuppliedCallback != null) {
+ mSuppliedCallback.onExitAmbient();
+ }
+ }
+
+ @Override
+ public void onUpdateAmbient() {
+ if (mSuppliedCallback != null) {
+ mSuppliedCallback.onUpdateAmbient();
+ }
+ }
+ };
+ private AmbientDelegate mDelegate;
+ @Nullable
+ private AmbientCallback mSuppliedCallback;
+ private AmbientController mController;
+
+ /**
+ * Constructor
+ */
+ public AmbientModeSupport() {
+ mController = new AmbientController();
+ }
+
+ @Override
+ @CallSuper
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mDelegate = new AmbientDelegate(getActivity(), new WearableControllerProvider(), mCallback);
+
+ if (context instanceof AmbientCallbackProvider) {
+ mSuppliedCallback = ((AmbientCallbackProvider) context).getAmbientCallback();
+ } else {
+ Log.w(TAG, "No callback provided - enabling only smart resume");
+ }
+ }
+
+ @Override
+ @CallSuper
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDelegate.onCreate();
+ if (mSuppliedCallback != null) {
+ mDelegate.setAmbientEnabled();
+ }
+ }
+
+ @Override
+ @CallSuper
+ public void onResume() {
+ super.onResume();
+ mDelegate.onResume();
+ }
+
+ @Override
+ @CallSuper
+ public void onPause() {
+ mDelegate.onPause();
+ super.onPause();
+ }
+
+ @Override
+ @CallSuper
+ public void onStop() {
+ mDelegate.onStop();
+ super.onStop();
+ }
+
+ @Override
+ @CallSuper
+ public void onDestroy() {
+ mDelegate.onDestroy();
+ super.onDestroy();
+ }
+
+ @Override
+ @CallSuper
+ public void onDetach() {
+ mDelegate = null;
+ super.onDetach();
+ }
+
+ /**
+ * Attach ambient support to the given activity. Calling this method with an Activity
+ * implementing the {@link AmbientCallbackProvider} interface will provide you with an
+ * opportunity to react to ambient events such as {@code onEnterAmbient}. Alternatively,
+ * you can call this method with an Activity which does not implement
+ * the {@link AmbientCallbackProvider} interface and that will only enable the auto-resume
+ * functionality. This is equivalent to providing (@code null} from
+ * the {@link AmbientCallbackProvider}.
+ *
+ * @param activity the activity to attach ambient support to.
+ * @return the associated {@link AmbientController} which can be used to query the state of
+ * ambient mode.
+ */
+ public static <T extends FragmentActivity> AmbientController attach(T activity) {
+ FragmentManager fragmentManager = activity.getSupportFragmentManager();
+ AmbientModeSupport ambientFragment =
+ (AmbientModeSupport) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
+ if (ambientFragment == null) {
+ AmbientModeSupport fragment = new AmbientModeSupport();
+ fragmentManager
+ .beginTransaction()
+ .add(fragment, FRAGMENT_TAG)
+ .commit();
+ ambientFragment = fragment;
+ }
+ return ambientFragment.mController;
+ }
+
+ @Override
+ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ if (mDelegate != null) {
+ mDelegate.dump(prefix, fd, writer, args);
+ }
+ }
+
+ @VisibleForTesting
+ void setAmbientDelegate(AmbientDelegate delegate) {
+ mDelegate = delegate;
+ }
+
+ /**
+ * A class for interacting with the ambient mode on a wearable device. This class can be used to
+ * query the current state of ambient mode. An instance of this class is returned to the user
+ * when they attach their {@link Activity} to {@link AmbientModeSupport}.
+ */
+ public final class AmbientController {
+ private static final String TAG = "AmbientController";
+
+ // Do not initialize outside of this class.
+ AmbientController() {}
+
+ /**
+ * @return {@code true} if the activity is currently in ambient.
+ */
+ public boolean isAmbient() {
+ return mDelegate == null ? false : mDelegate.isAmbient();
+ }
+ }
+}
diff --git a/android/support/wear/ambient/AmbientModeTest.java b/android/support/wear/ambient/AmbientModeTest.java
new file mode 100644
index 00000000..f96c0c25
--- /dev/null
+++ b/android/support/wear/ambient/AmbientModeTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.ambient;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.wear.widget.util.WakeLockRule;
+
+import com.google.android.wearable.compat.WearableActivityController;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class AmbientModeTest {
+ @Rule
+ public final WakeLockRule mWakeLock = new WakeLockRule();
+
+ @Rule
+ public final ActivityTestRule<AmbientModeTestActivity> mActivityRule = new ActivityTestRule<>(
+ AmbientModeTestActivity.class);
+
+ @Test
+ public void testEnterAmbientCallback() throws Throwable {
+ AmbientModeTestActivity activity = mActivityRule.getActivity();
+
+ WearableActivityController.getLastInstance().enterAmbient();
+ assertTrue(activity.mEnterAmbientCalled);
+ assertFalse(activity.mUpdateAmbientCalled);
+ assertFalse(activity.mExitAmbientCalled);
+ }
+
+ @Test
+ public void testUpdateAmbientCallback() throws Throwable {
+ AmbientModeTestActivity activity = mActivityRule.getActivity();
+
+ WearableActivityController.getLastInstance().updateAmbient();
+ assertFalse(activity.mEnterAmbientCalled);
+ assertTrue(activity.mUpdateAmbientCalled);
+ assertFalse(activity.mExitAmbientCalled);
+ }
+
+ @Test
+ public void testExitAmbientCallback() throws Throwable {
+ AmbientModeTestActivity activity = mActivityRule.getActivity();
+
+ WearableActivityController.getLastInstance().exitAmbient();
+ assertFalse(activity.mEnterAmbientCalled);
+ assertFalse(activity.mUpdateAmbientCalled);
+ assertTrue(activity.mExitAmbientCalled);
+ }
+
+ @Test
+ public void testIsAmbientEnabled() {
+ assertTrue(WearableActivityController.getLastInstance().isAmbientEnabled());
+ }
+
+ @Test
+ public void testCallsControllerIsAmbient() {
+ AmbientModeTestActivity activity = mActivityRule.getActivity();
+
+ WearableActivityController.getLastInstance().setAmbient(true);
+ assertTrue(activity.getAmbientController().isAmbient());
+
+ WearableActivityController.getLastInstance().setAmbient(false);
+ assertFalse(activity.getAmbientController().isAmbient());
+ }
+}
diff --git a/android/support/wear/ambient/AmbientModeTestActivity.java b/android/support/wear/ambient/AmbientModeTestActivity.java
new file mode 100644
index 00000000..26155d8f
--- /dev/null
+++ b/android/support/wear/ambient/AmbientModeTestActivity.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.wear.ambient;
+
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+
+public class AmbientModeTestActivity extends FragmentActivity
+ implements AmbientMode.AmbientCallbackProvider {
+ AmbientMode.AmbientController mAmbientController;
+
+ boolean mEnterAmbientCalled;
+ boolean mUpdateAmbientCalled;
+ boolean mExitAmbientCalled;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mAmbientController = AmbientMode.attachAmbientSupport(this);
+ }
+
+ @Override
+ public AmbientMode.AmbientCallback getAmbientCallback() {
+ return new MyAmbientCallback();
+ }
+
+ private class MyAmbientCallback extends AmbientMode.AmbientCallback {
+
+ @Override
+ public void onEnterAmbient(Bundle ambientDetails) {
+ mEnterAmbientCalled = true;
+ }
+
+ @Override
+ public void onUpdateAmbient() {
+ mUpdateAmbientCalled = true;
+ }
+
+ @Override
+ public void onExitAmbient() {
+ mExitAmbientCalled = true;
+ }
+ }
+
+ public AmbientMode.AmbientController getAmbientController() {
+ return mAmbientController;
+ }
+
+}
diff --git a/android/support/wear/utils/MetadataTestActivity.java b/android/support/wear/utils/MetadataTestActivity.java
new file mode 100644
index 00000000..f64247ee
--- /dev/null
+++ b/android/support/wear/utils/MetadataTestActivity.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 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.support.wear.utils;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.wear.test.R;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class MetadataTestActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ assertTrue(MetadataConstants.isStandalone(this));
+ assertTrue(MetadataConstants.isNotificationBridgingEnabled(this));
+ assertEquals(R.drawable.preview_face,
+ MetadataConstants.getPreviewDrawableResourceId(this, false));
+ assertEquals(R.drawable.preview_face_circular,
+ MetadataConstants.getPreviewDrawableResourceId(this, true));
+ }
+}
diff --git a/android/support/wear/widget/BoxInsetLayoutTest.java b/android/support/wear/widget/BoxInsetLayoutTest.java
new file mode 100644
index 00000000..731f57a1
--- /dev/null
+++ b/android/support/wear/widget/BoxInsetLayoutTest.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2016 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.support.wear.widget;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.wear.widget.util.MoreViewAssertions.approximateBottom;
+import static android.support.wear.widget.util.MoreViewAssertions.approximateTop;
+import static android.support.wear.widget.util.MoreViewAssertions.bottom;
+import static android.support.wear.widget.util.MoreViewAssertions.left;
+import static android.support.wear.widget.util.MoreViewAssertions.right;
+import static android.support.wear.widget.util.MoreViewAssertions.screenBottom;
+import static android.support.wear.widget.util.MoreViewAssertions.screenLeft;
+import static android.support.wear.widget.util.MoreViewAssertions.screenRight;
+import static android.support.wear.widget.util.MoreViewAssertions.screenTop;
+import static android.support.wear.widget.util.MoreViewAssertions.top;
+
+import static org.hamcrest.Matchers.closeTo;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+import android.content.Intent;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.wear.test.R;
+import android.support.wear.widget.util.WakeLockRule;
+import android.util.DisplayMetrics;
+import android.view.View;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class BoxInsetLayoutTest {
+ private static final float FACTOR = 0.146467f; //(1 - sqrt(2)/2)/2
+
+ @Rule
+ public final WakeLockRule mWakeLock = new WakeLockRule();
+
+ @Rule
+ public final ActivityTestRule<LayoutTestActivity> mActivityRule = new ActivityTestRule<>(
+ LayoutTestActivity.class, true, false);
+
+ @Test
+ public void testCase1() throws Throwable {
+ mActivityRule.launchActivity(new Intent().putExtra(LayoutTestActivity
+ .EXTRA_LAYOUT_RESOURCE_ID, R.layout.box_inset_layout_testcase_1));
+ DisplayMetrics dm = InstrumentationRegistry.getTargetContext().getResources()
+ .getDisplayMetrics();
+ int boxInset = (int) (FACTOR * Math.min(dm.widthPixels, dm.heightPixels));
+
+ int desiredPadding = 0;
+ if (mActivityRule.getActivity().getResources().getConfiguration().isScreenRound()) {
+ desiredPadding = boxInset;
+ }
+
+ ViewFetchingRunnable customRunnable = new ViewFetchingRunnable(){
+ @Override
+ public void run() {
+ View box = mActivityRule.getActivity().findViewById(R.id.box);
+ mIdViewMap.put(R.id.box, box);
+ }
+ };
+ mActivityRule.runOnUiThread(customRunnable);
+
+ View box = customRunnable.mIdViewMap.get(R.id.box);
+ // proxy for window location
+ View boxParent = (View) box.getParent();
+ int parentLeft = boxParent.getLeft();
+ int parentTop = boxParent.getTop();
+ int parentRight = boxParent.getLeft() + boxParent.getWidth();
+ int parentBottom = boxParent.getTop() + boxParent.getHeight();
+
+ // Child 1 is match_parent width and height
+ // layout_box=right|bottom
+ // Padding of boxInset should be added to the right and bottom sides only
+ onView(withId(R.id.child1))
+ .check(screenLeft(equalTo(parentLeft)))
+ .check(screenTop(equalTo(parentTop)))
+ .check(screenRight(equalTo(parentRight - desiredPadding)))
+ .check(screenBottom(equalTo(parentBottom - desiredPadding)));
+
+ // Content 1 is is width and height match_parent
+ // The bottom and right sides should be inset by boxInset pixels due to padding
+ // on the parent view
+ onView(withId(R.id.content1))
+ .check(screenLeft(equalTo(parentLeft)))
+ .check(screenTop(equalTo(parentTop)))
+ .check(screenRight(equalTo(parentRight - desiredPadding)))
+ .check(screenBottom(equalTo(parentBottom - desiredPadding)));
+ }
+
+ @Test
+ public void testCase2() throws Throwable {
+ mActivityRule.launchActivity(
+ new Intent().putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.box_inset_layout_testcase_2));
+ DisplayMetrics dm =
+ InstrumentationRegistry.getTargetContext().getResources().getDisplayMetrics();
+ int boxInset = (int) (FACTOR * Math.min(dm.widthPixels, dm.heightPixels));
+
+ int desiredPadding = 0;
+ if (mActivityRule.getActivity().getResources().getConfiguration().isScreenRound()) {
+ desiredPadding = boxInset;
+ }
+
+ ViewFetchingRunnable customRunnable = new ViewFetchingRunnable(){
+ @Override
+ public void run() {
+ View box = mActivityRule.getActivity().findViewById(R.id.box);
+ View child1 = mActivityRule.getActivity().findViewById(R.id.child1);
+ View child2 = mActivityRule.getActivity().findViewById(R.id.child2);
+ View child3 = mActivityRule.getActivity().findViewById(R.id.child3);
+ View child4 = mActivityRule.getActivity().findViewById(R.id.child4);
+ mIdViewMap.put(R.id.box, box);
+ mIdViewMap.put(R.id.child1, child1);
+ mIdViewMap.put(R.id.child2, child2);
+ mIdViewMap.put(R.id.child3, child3);
+ mIdViewMap.put(R.id.child4, child4);
+
+ }
+ };
+ mActivityRule.runOnUiThread(customRunnable);
+
+ View box = customRunnable.mIdViewMap.get(R.id.box);
+ View child1 = customRunnable.mIdViewMap.get(R.id.child1);
+ View child2 = customRunnable.mIdViewMap.get(R.id.child2);
+ View child3 = customRunnable.mIdViewMap.get(R.id.child3);
+ View child4 = customRunnable.mIdViewMap.get(R.id.child4);
+
+ // proxy for window location
+ View boxParent = (View) box.getParent();
+ int parentLeft = boxParent.getLeft();
+ int parentTop = boxParent.getTop();
+ int parentRight = boxParent.getLeft() + boxParent.getWidth();
+ int parentBottom = boxParent.getTop() + boxParent.getHeight();
+ int parentWidth = boxParent.getWidth();
+ int parentHeight = boxParent.getHeight();
+
+ // Child 1 is width match_parent, height=60dp, gravity top
+ // layout_box=all means it should have padding added to left, top and right
+ onView(withId(R.id.child1))
+ .check(screenLeft(is(equalTo(parentLeft + desiredPadding))))
+ .check(screenTop(is(equalTo(parentTop + desiredPadding))))
+ .check(screenRight(is(equalTo(parentRight - desiredPadding))))
+ .check(screenBottom(is(equalTo(parentTop + desiredPadding + child1.getHeight()))));
+
+ // Content 1 is width and height match_parent
+ // the left top and right edges should be inset by boxInset pixels, due to
+ // padding in the parent
+ onView(withId(R.id.content1))
+ .check(screenLeft(equalTo(parentLeft + desiredPadding)))
+ .check(screenTop(equalTo(parentTop + desiredPadding)))
+ .check(screenRight(equalTo(parentRight - desiredPadding)));
+
+ // Child 2 is width match_parent, height=60dp, gravity bottom
+ // layout_box=all means it should have padding added to left, bottom and right
+ onView(withId(R.id.child2))
+ .check(screenLeft(is(equalTo(parentLeft + desiredPadding))))
+ .check(screenTop(is(equalTo(parentBottom - desiredPadding - child2.getHeight()))))
+ .check(screenRight(is(equalTo(parentRight - desiredPadding))))
+ .check(screenBottom(is(equalTo(parentBottom - desiredPadding))));
+
+ // Content 2 is width and height match_parent
+ // the left bottom and right edges should be inset by boxInset pixels, due to
+ // padding in the parent
+ onView(withId(R.id.content2))
+ .check(screenLeft(equalTo(parentLeft + desiredPadding)))
+ .check(screenRight(equalTo(parentRight - desiredPadding)))
+ .check(screenBottom(equalTo(parentBottom - desiredPadding)));
+
+ // Child 3 is width wrap_content, height=20dp, gravity left|center_vertical.
+ // layout_box=all means it should have padding added to left
+ // marginLeft be ignored due to gravity and layout_box=all (screenLeft=0)
+ onView(withId(R.id.child3))
+ .check(screenLeft(is(equalTo(parentLeft + desiredPadding))))
+ .check(approximateTop(is(closeTo((parentHeight / 2 - child3.getHeight() / 2), 1))))
+ .check(screenRight(is(equalTo(parentLeft + desiredPadding + child3.getWidth()))))
+ .check(approximateBottom(is(
+ closeTo((parentHeight / 2 + child3.getHeight() / 2), 1))));
+
+ // Content 3 width and height match_parent
+ // the left edge should be offset from the screen edge by boxInset pixels, due to left on
+ // the parent
+ onView(withId(R.id.content3)).check(screenLeft(equalTo(desiredPadding)));
+
+ // Child 4 is width wrap_content, height=20dp, gravity right|center_vertical.
+ // layout_box=all means it should have padding added to right
+ // it should have marginRight ignored due to gravity and layout_box=all (screenRight=max)
+ onView(withId(R.id.child4))
+ .check(screenLeft(is(parentWidth - desiredPadding - child4.getWidth())))
+ .check(approximateTop(is(closeTo((parentHeight / 2 - child3.getHeight() / 2), 1))))
+ .check(screenRight(is(equalTo(parentWidth - desiredPadding))))
+ .check(approximateBottom(is(
+ closeTo((parentHeight / 2 + child4.getHeight() / 2), 1))));
+
+ // Content 4 width and height wrap_content
+ // the right edge should be offset from the screen edge by boxInset pixels, due to
+ // right on the parent
+ onView(withId(R.id.content4)).check(screenRight(equalTo(parentWidth - desiredPadding)));
+ }
+
+ @Test
+ public void testCase3() throws Throwable {
+ mActivityRule.launchActivity(
+ new Intent().putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.box_inset_layout_testcase_3));
+ DisplayMetrics dm =
+ InstrumentationRegistry.getTargetContext().getResources().getDisplayMetrics();
+ int boxInset = (int) (FACTOR * Math.min(dm.widthPixels, dm.heightPixels));
+
+ int desiredPadding = 0;
+ if (mActivityRule.getActivity().getResources().getConfiguration().isScreenRound()) {
+ desiredPadding = boxInset;
+ }
+
+ ViewFetchingRunnable customRunnable = new ViewFetchingRunnable(){
+ @Override
+ public void run() {
+ View box = mActivityRule.getActivity().findViewById(R.id.box);
+ View child1 = mActivityRule.getActivity().findViewById(R.id.child1);
+ View child2 = mActivityRule.getActivity().findViewById(R.id.child2);
+ View child3 = mActivityRule.getActivity().findViewById(R.id.child3);
+ View child4 = mActivityRule.getActivity().findViewById(R.id.child4);
+ mIdViewMap.put(R.id.box, box);
+ mIdViewMap.put(R.id.child1, child1);
+ mIdViewMap.put(R.id.child2, child2);
+ mIdViewMap.put(R.id.child3, child3);
+ mIdViewMap.put(R.id.child4, child4);
+ }
+ };
+ mActivityRule.runOnUiThread(customRunnable);
+
+ View box = customRunnable.mIdViewMap.get(R.id.box);
+ View child1 = customRunnable.mIdViewMap.get(R.id.child1);
+ View child2 = customRunnable.mIdViewMap.get(R.id.child2);
+ View child3 = customRunnable.mIdViewMap.get(R.id.child3);
+ View child4 = customRunnable.mIdViewMap.get(R.id.child4);
+ // proxy for window location
+ View boxParent = (View) box.getParent();
+ int parentLeft = boxParent.getLeft();
+ int parentTop = boxParent.getTop();
+ int parentBottom = boxParent.getTop() + boxParent.getHeight();
+ int parentWidth = boxParent.getWidth();
+
+ // Child 1 is width and height wrap_content
+ // gravity is top|left, position should be 0,0 on screen
+ onView(withId(R.id.child1))
+ .check(screenLeft(is(equalTo(parentLeft + desiredPadding))))
+ .check(screenTop(is(equalTo(parentTop + desiredPadding))))
+ .check(screenRight(is(equalTo(parentLeft + desiredPadding + child1.getWidth()))))
+ .check(screenBottom(is(equalTo(parentTop + desiredPadding + child1.getHeight()))));
+
+ // Content 1 is width and height wrap_content
+ // the left and top edges should be offset from the screen edges by boxInset pixels
+ onView(withId(R.id.content1))
+ .check(screenLeft(equalTo(parentLeft + desiredPadding)))
+ .check(screenTop(equalTo(parentTop + desiredPadding)));
+
+ // Child 2 is width and height wrap_content
+ // gravity is top|right, position should be 0,max on screen
+ onView(withId(R.id.child2))
+ .check(screenLeft(is(equalTo(parentWidth - desiredPadding - child2.getWidth()))))
+ .check(screenTop(is(equalTo(parentTop + desiredPadding))))
+ .check(screenRight(is(equalTo(parentWidth - desiredPadding))))
+ .check(screenBottom(is(equalTo(parentTop + desiredPadding + child2.getHeight()))));
+
+ // Content 2 is width and height wrap_content
+ // the top and right edges should be offset from the screen edges by boxInset pixels
+ onView(withId(R.id.content2))
+ .check(screenTop(equalTo(parentTop + desiredPadding)))
+ .check(screenRight(equalTo(parentWidth - desiredPadding)));
+
+ // Child 3 is width and height wrap_content
+ // gravity is bottom|right, position should be max,max on screen
+ onView(withId(R.id.child3))
+ .check(screenLeft(is(equalTo(parentWidth - desiredPadding - child3.getWidth()))))
+ .check(screenTop(is(
+ equalTo(parentBottom - desiredPadding - child3.getHeight()))))
+ .check(screenRight(is(equalTo(parentWidth - desiredPadding))))
+ .check(screenBottom(is(equalTo(parentBottom - desiredPadding))));
+
+ // Content 3 is width and height wrap_content
+ // the right and bottom edges should be offset from the screen edges by boxInset pixels
+ onView(withId(R.id.content3))
+ .check(screenBottom(equalTo(parentBottom - desiredPadding)))
+ .check(screenRight(equalTo(parentWidth - desiredPadding)));
+
+ // Child 4 is width and height wrap_content
+ // gravity is bottom|left, position should be max,0 on screen
+ onView(withId(R.id.child4))
+ .check(screenLeft(is(equalTo(parentLeft + desiredPadding))))
+ .check(screenTop(is(equalTo(parentBottom - desiredPadding - child4.getHeight()))))
+ .check(screenRight(is(equalTo(parentLeft + desiredPadding + child4.getWidth()))))
+ .check(screenBottom(is(equalTo(parentBottom - desiredPadding))));
+
+ // Content 3 is width and height wrap_content
+ // the bottom and left edges should be offset from the screen edges by boxInset pixels
+ onView(withId(R.id.content4)).check(
+ screenBottom(equalTo(parentBottom - desiredPadding)))
+ .check(screenLeft(equalTo(parentLeft + desiredPadding)));
+ }
+
+ @Test
+ public void testCase4() throws Throwable {
+ mActivityRule.launchActivity(new Intent().putExtra(LayoutTestActivity
+ .EXTRA_LAYOUT_RESOURCE_ID, R.layout.box_inset_layout_testcase_4));
+ DisplayMetrics dm = InstrumentationRegistry.getTargetContext().getResources()
+ .getDisplayMetrics();
+ int boxInset = (int) (FACTOR * Math.min(dm.widthPixels, dm.heightPixels));
+
+ int desiredPadding = 0;
+ if (mActivityRule.getActivity().getResources().getConfiguration().isScreenRound()) {
+ desiredPadding = boxInset;
+ }
+
+ ViewFetchingRunnable customRunnable = new ViewFetchingRunnable(){
+ @Override
+ public void run() {
+ View container = mActivityRule.getActivity().findViewById(R.id.container);
+ View child1 = mActivityRule.getActivity().findViewById(R.id.child1);
+ mIdViewMap.put(R.id.container, container);
+ mIdViewMap.put(R.id.child1, child1);
+
+ }
+ };
+ mActivityRule.runOnUiThread(customRunnable);
+
+ View container = customRunnable.mIdViewMap.get(R.id.container);
+ View child1 = customRunnable.mIdViewMap.get(R.id.child1);
+ // Child 1 is match_parent width and wrap_content height
+ // layout_box=right|left
+ // Padding of boxInset should be added to the right and bottom sides only
+ onView(withId(R.id.child1)).check(left(equalTo(desiredPadding))).check(
+ top(equalTo(container.getTop()))).check(
+ right(equalTo(dm.widthPixels - desiredPadding))).check(
+ bottom(equalTo(container.getTop() + child1.getHeight())));
+ }
+
+ private abstract class ViewFetchingRunnable implements Runnable {
+ Map<Integer, View> mIdViewMap = new HashMap<>();
+ }
+}
diff --git a/android/support/wear/widget/CircularProgressLayoutControllerTest.java b/android/support/wear/widget/CircularProgressLayoutControllerTest.java
new file mode 100644
index 00000000..2f625b6c
--- /dev/null
+++ b/android/support/wear/widget/CircularProgressLayoutControllerTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.widget.CircularProgressDrawable;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.concurrent.TimeUnit;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class CircularProgressLayoutControllerTest {
+
+ private static final long TOTAL_TIME = TimeUnit.SECONDS.toMillis(1);
+ private static final long UPDATE_INTERVAL = TimeUnit.MILLISECONDS.toMillis(30);
+
+ private CircularProgressLayoutController mControllerUnderTest;
+
+ @Mock
+ CircularProgressDrawable mMockDrawable;
+ @Mock
+ CircularProgressLayout mMockLayout;
+ @Mock
+ CircularProgressLayout.OnTimerFinishedListener mMockListener;
+
+ @Before
+ public void setUp() {
+ mMockDrawable = mock(CircularProgressDrawable.class);
+ mMockLayout = mock(CircularProgressLayout.class);
+ mMockListener = mock(CircularProgressLayout.OnTimerFinishedListener.class);
+ when(mMockLayout.getProgressDrawable()).thenReturn(mMockDrawable);
+ when(mMockLayout.getOnTimerFinishedListener()).thenReturn(mMockListener);
+ mControllerUnderTest = new CircularProgressLayoutController(mMockLayout);
+ }
+
+ @Test
+ public void testSetIndeterminate() {
+ mControllerUnderTest.setIndeterminate(true);
+
+ assertEquals(true, mControllerUnderTest.isIndeterminate());
+ verify(mMockDrawable).start();
+ }
+
+ @Test
+ public void testIsIndeterminateAfterSetToFalse() {
+ mControllerUnderTest.setIndeterminate(true);
+ mControllerUnderTest.setIndeterminate(false);
+
+ assertEquals(false, mControllerUnderTest.isIndeterminate());
+ verify(mMockDrawable).stop();
+ }
+
+ @LargeTest
+ @Test
+ @UiThreadTest
+ public void testIsTimerRunningAfterStart() {
+ mControllerUnderTest.startTimer(TOTAL_TIME, UPDATE_INTERVAL);
+
+ assertEquals(true, mControllerUnderTest.isTimerRunning());
+ }
+
+ @Test
+ @UiThreadTest
+ public void testIsTimerRunningAfterStop() {
+ mControllerUnderTest.startTimer(TOTAL_TIME, UPDATE_INTERVAL);
+ mControllerUnderTest.stopTimer();
+
+ assertEquals(false, mControllerUnderTest.isTimerRunning());
+ }
+
+ @Test
+ @UiThreadTest
+ public void testSwitchFromIndeterminateToDeterminate() {
+ mControllerUnderTest.setIndeterminate(true);
+ mControllerUnderTest.startTimer(TOTAL_TIME, UPDATE_INTERVAL);
+
+ assertEquals(false, mControllerUnderTest.isIndeterminate());
+ assertEquals(true, mControllerUnderTest.isTimerRunning());
+ verify(mMockDrawable).stop();
+ }
+
+ @Test
+ @UiThreadTest
+ public void testSwitchFromDeterminateToIndeterminate() {
+ mControllerUnderTest.startTimer(TOTAL_TIME, UPDATE_INTERVAL);
+ mControllerUnderTest.setIndeterminate(true);
+
+ assertEquals(true, mControllerUnderTest.isIndeterminate());
+ assertEquals(false, mControllerUnderTest.isTimerRunning());
+ verify(mMockDrawable).start();
+ }
+}
diff --git a/android/support/wear/widget/CircularProgressLayoutTest.java b/android/support/wear/widget/CircularProgressLayoutTest.java
new file mode 100644
index 00000000..ff98c30c
--- /dev/null
+++ b/android/support/wear/widget/CircularProgressLayoutTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Intent;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.wear.test.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeUnit;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class CircularProgressLayoutTest {
+
+ private static final long TOTAL_TIME = TimeUnit.SECONDS.toMillis(1);
+
+ @Rule
+ public final ActivityTestRule<LayoutTestActivity> mActivityRule = new ActivityTestRule<>(
+ LayoutTestActivity.class, true, false);
+ private CircularProgressLayout mLayoutUnderTest;
+
+ @Before
+ public void setUp() {
+ mActivityRule.launchActivity(new Intent().putExtra(LayoutTestActivity
+ .EXTRA_LAYOUT_RESOURCE_ID, R.layout.circular_progress_layout));
+ mLayoutUnderTest = mActivityRule.getActivity().findViewById(R.id.circular_progress_layout);
+ mLayoutUnderTest.setOnTimerFinishedListener(new FakeListener());
+ }
+
+ @Test
+ public void testListenerIsNotified() {
+ mLayoutUnderTest.setTotalTime(TOTAL_TIME);
+ startTimerOnUiThread();
+ waitForTimer(TOTAL_TIME + 100);
+ assertNotNull(mLayoutUnderTest.getOnTimerFinishedListener());
+ assertTrue(((FakeListener) mLayoutUnderTest.getOnTimerFinishedListener()).mFinished);
+ }
+
+ @Test
+ public void testListenerIsNotNotifiedWhenStopped() {
+ mLayoutUnderTest.setTotalTime(TOTAL_TIME);
+ startTimerOnUiThread();
+ stopTimerOnUiThread();
+ waitForTimer(TOTAL_TIME + 100);
+ assertNotNull(mLayoutUnderTest.getOnTimerFinishedListener());
+ assertFalse(((FakeListener) mLayoutUnderTest.getOnTimerFinishedListener()).mFinished);
+ }
+
+ private class FakeListener implements CircularProgressLayout.OnTimerFinishedListener {
+
+ boolean mFinished;
+
+ @Override
+ public void onTimerFinished(CircularProgressLayout layout) {
+ mFinished = true;
+ }
+ }
+
+ private void startTimerOnUiThread() {
+ mActivityRule.getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mLayoutUnderTest.startTimer();
+ }
+ });
+ }
+
+ private void stopTimerOnUiThread() {
+ mActivityRule.getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mLayoutUnderTest.stopTimer();
+ }
+ });
+ }
+
+ private void waitForTimer(long time) {
+ try {
+ Thread.sleep(time);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/android/support/wear/widget/LayoutTestActivity.java b/android/support/wear/widget/LayoutTestActivity.java
new file mode 100644
index 00000000..ec909dbf
--- /dev/null
+++ b/android/support/wear/widget/LayoutTestActivity.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 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.support.wear.widget;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+public class LayoutTestActivity extends Activity {
+ public static final String EXTRA_LAYOUT_RESOURCE_ID = "layout_resource_id";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+ if (!intent.hasExtra(EXTRA_LAYOUT_RESOURCE_ID)) {
+ throw new IllegalArgumentException(
+ "Intent extras must contain EXTRA_LAYOUT_RESOURCE_ID");
+ }
+ int layoutId = intent.getIntExtra(EXTRA_LAYOUT_RESOURCE_ID, -1);
+ setContentView(layoutId);
+ }
+}
diff --git a/android/support/wear/widget/RoundedDrawableTest.java b/android/support/wear/widget/RoundedDrawableTest.java
new file mode 100644
index 00000000..b01b3fad
--- /dev/null
+++ b/android/support/wear/widget/RoundedDrawableTest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.wear.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
+import android.os.Build;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.wear.test.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+/** Tests for {@link RoundedDrawable} */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RoundedDrawableTest {
+
+ @Rule
+ public final ActivityTestRule<LayoutTestActivity> mActivityRule = new ActivityTestRule<>(
+ LayoutTestActivity.class, true, false);
+ private static final int BITMAP_WIDTH = 64;
+ private static final int BITMAP_HEIGHT = 32;
+
+ private RoundedDrawable mRoundedDrawable;
+ private BitmapDrawable mBitmapDrawable;
+
+ @Mock
+ Canvas mMockCanvas;
+
+ @Before
+ public void setUp() {
+ mMockCanvas = mock(Canvas.class);
+ mActivityRule.launchActivity(new Intent().putExtra(LayoutTestActivity
+ .EXTRA_LAYOUT_RESOURCE_ID,
+ android.support.wear.test.R.layout.rounded_drawable_layout));
+ mRoundedDrawable = new RoundedDrawable();
+ mBitmapDrawable =
+ new BitmapDrawable(
+ mActivityRule.getActivity().getResources(),
+ Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888));
+ }
+
+ @Test
+ public void colorFilterIsAppliedCorrectly() {
+ ColorFilter cf = new ColorFilter();
+ mRoundedDrawable.setColorFilter(cf);
+ assertEquals(cf, mRoundedDrawable.mPaint.getColorFilter());
+ }
+
+ @Test
+ public void alphaIsAppliedCorrectly() {
+ int alpha = 128;
+ mRoundedDrawable.setAlpha(alpha);
+ assertEquals(alpha, mRoundedDrawable.mPaint.getAlpha());
+ }
+
+ @Test
+ public void radiusIsAppliedCorrectly() {
+ int radius = 10;
+ Rect bounds = new Rect(0, 0, BITMAP_WIDTH, BITMAP_HEIGHT);
+ mRoundedDrawable.setDrawable(mBitmapDrawable);
+ mRoundedDrawable.setClipEnabled(true);
+ mRoundedDrawable.setRadius(radius);
+ mRoundedDrawable.setBounds(bounds);
+ mRoundedDrawable.draw(mMockCanvas);
+ // One for background and one for the actual drawable, this should be called two times.
+ verify(mMockCanvas, times(2))
+ .drawRoundRect(
+ eq(new RectF(0, 0, bounds.width(), bounds.height())),
+ eq((float) radius),
+ eq((float) radius),
+ any(Paint.class));
+ }
+
+ @Test
+ public void scalingIsAppliedCorrectly() {
+ int radius = 14;
+ // 14 px radius should apply 5 px padding due to formula ceil(radius * 1 - 1 / sqrt(2))
+ Rect bounds = new Rect(0, 0, BITMAP_WIDTH, BITMAP_HEIGHT);
+ mRoundedDrawable.setDrawable(mBitmapDrawable);
+ mRoundedDrawable.setClipEnabled(false);
+ mRoundedDrawable.setRadius(radius);
+ mRoundedDrawable.setBounds(bounds);
+ mRoundedDrawable.draw(mMockCanvas);
+ assertEquals(BITMAP_WIDTH - 10, mBitmapDrawable.getBounds().width());
+ assertEquals(BITMAP_HEIGHT - 10, mBitmapDrawable.getBounds().height());
+ assertEquals(bounds.centerX(), mBitmapDrawable.getBounds().centerX());
+ assertEquals(bounds.centerY(), mBitmapDrawable.getBounds().centerY());
+ // Background should also be drawn
+ verify(mMockCanvas)
+ .drawRoundRect(
+ eq(new RectF(0, 0, bounds.width(), bounds.height())),
+ eq((float) radius),
+ eq((float) radius),
+ any(Paint.class));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+ public void inflate() {
+ RoundedDrawable roundedDrawable =
+ (RoundedDrawable) mActivityRule.getActivity().getDrawable(
+ R.drawable.rounded_drawable);
+ assertEquals(
+ mActivityRule.getActivity().getColor(R.color.rounded_drawable_background_color),
+ roundedDrawable.getBackgroundColor());
+ assertTrue(roundedDrawable.isClipEnabled());
+ assertNotNull(roundedDrawable.getDrawable());
+ assertEquals(mActivityRule.getActivity().getResources().getDimensionPixelSize(
+ R.dimen.rounded_drawable_radius), roundedDrawable.getRadius());
+ }
+}
diff --git a/android/support/wear/widget/ScrollManagerTest.java b/android/support/wear/widget/ScrollManagerTest.java
new file mode 100644
index 00000000..34faea3f
--- /dev/null
+++ b/android/support/wear/widget/ScrollManagerTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import static java.lang.Math.cos;
+import static java.lang.Math.sin;
+
+import android.os.SystemClock;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.wear.widget.util.WakeLockRule;
+import android.view.MotionEvent;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ScrollManagerTest {
+ private static final int TEST_WIDTH = 400;
+ private static final int TEST_HEIGHT = 400;
+ private static final int STEP_COUNT = 300;
+
+ private static final int EXPECTED_SCROLLS_FOR_STRAIGHT_GESTURE = 36;
+ private static final int EXPECTED_SCROLLS_FOR_CIRCULAR_GESTURE = 199;
+
+ @Rule
+ public final WakeLockRule wakeLock = new WakeLockRule();
+
+ @Rule
+ public final ActivityTestRule<WearableRecyclerViewTestActivity> mActivityRule =
+ new ActivityTestRule<>(WearableRecyclerViewTestActivity.class, true, true);
+
+ @Mock
+ WearableRecyclerView mMockWearableRecyclerView;
+
+ ScrollManager mScrollManagerUnderTest;
+
+ @Before
+ public void setUp() throws Throwable {
+ MockitoAnnotations.initMocks(this);
+ mScrollManagerUnderTest = new ScrollManager();
+ mScrollManagerUnderTest.setRecyclerView(mMockWearableRecyclerView, TEST_WIDTH, TEST_HEIGHT);
+ }
+
+ @Test
+ public void testStraightUpScrollingGestureLeft() throws Throwable {
+ // Pretend to scroll in a straight line from center left to upper left
+ scroll(mScrollManagerUnderTest, 30, 30, 200, 150);
+ // The scroll manager should require the recycler view to scroll up and only up
+ verify(mMockWearableRecyclerView, times(EXPECTED_SCROLLS_FOR_STRAIGHT_GESTURE))
+ .scrollBy(0, 1);
+ }
+
+ @Test
+ public void testStraightDownScrollingGestureLeft() throws Throwable {
+ // Pretend to scroll in a straight line upper left to center left
+ scroll(mScrollManagerUnderTest, 30, 30, 150, 200);
+ // The scroll manager should require the recycler view to scroll down and only down
+ verify(mMockWearableRecyclerView, times(EXPECTED_SCROLLS_FOR_STRAIGHT_GESTURE))
+ .scrollBy(0, -1);
+ }
+
+ @Test
+ public void testStraightUpScrollingGestureRight() throws Throwable {
+ // Pretend to scroll in a straight line from center right to upper right
+ scroll(mScrollManagerUnderTest, 370, 370, 200, 150);
+ // The scroll manager should require the recycler view to scroll down and only down
+ verify(mMockWearableRecyclerView, times(EXPECTED_SCROLLS_FOR_STRAIGHT_GESTURE))
+ .scrollBy(0, -1);
+ }
+
+ @Test
+ public void testStraightDownScrollingGestureRight() throws Throwable {
+ // Pretend to scroll in a straight line upper right to center right
+ scroll(mScrollManagerUnderTest, 370, 370, 150, 200);
+ // The scroll manager should require the recycler view to scroll up and only up
+ verify(mMockWearableRecyclerView, times(EXPECTED_SCROLLS_FOR_STRAIGHT_GESTURE))
+ .scrollBy(0, 1);
+ }
+
+ @Test
+ public void testCircularScrollingGestureLeft() throws Throwable {
+ // Pretend to scroll in an arch from center left to center right
+ scrollOnArch(mScrollManagerUnderTest, 30, 200, 180.0f);
+ // The scroll manager should never reverse the scroll direction and scroll up
+ verify(mMockWearableRecyclerView, times(EXPECTED_SCROLLS_FOR_CIRCULAR_GESTURE))
+ .scrollBy(0, 1);
+ }
+
+ @Test
+ public void testCircularScrollingGestureRight() throws Throwable {
+ // Pretend to scroll in an arch from center left to center right
+ scrollOnArch(mScrollManagerUnderTest, 370, 200, -180.0f);
+ // The scroll manager should never reverse the scroll direction and scroll down.
+ verify(mMockWearableRecyclerView, times(EXPECTED_SCROLLS_FOR_CIRCULAR_GESTURE))
+ .scrollBy(0, -1);
+ }
+
+ private static void scroll(ScrollManager scrollManager, float fromX, float toX, float fromY,
+ float toY) {
+ long downTime = SystemClock.uptimeMillis();
+ long eventTime = SystemClock.uptimeMillis();
+
+ float y = fromY;
+ float x = fromX;
+
+ float yStep = (toY - fromY) / STEP_COUNT;
+ float xStep = (toX - fromX) / STEP_COUNT;
+
+ MotionEvent event = MotionEvent.obtain(downTime, eventTime,
+ MotionEvent.ACTION_DOWN, x, y, 0);
+ scrollManager.onTouchEvent(event);
+ for (int i = 0; i < STEP_COUNT; ++i) {
+ y += yStep;
+ x += xStep;
+ eventTime = SystemClock.uptimeMillis();
+ event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0);
+ scrollManager.onTouchEvent(event);
+ }
+
+ eventTime = SystemClock.uptimeMillis();
+ event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0);
+ scrollManager.onTouchEvent(event);
+ }
+
+ private static void scrollOnArch(ScrollManager scrollManager, float fromX, float fromY,
+ float deltaAngle) {
+ long downTime = SystemClock.uptimeMillis();
+ long eventTime = SystemClock.uptimeMillis();
+
+ float stepAngle = deltaAngle / STEP_COUNT;
+ double relativeX = fromX - (TEST_WIDTH / 2);
+ double relativeY = fromY - (TEST_HEIGHT / 2);
+ float radius = (float) Math.sqrt(relativeX * relativeX + relativeY * relativeY);
+ float angle = getAngle(fromX, fromY, TEST_WIDTH, TEST_HEIGHT);
+
+ float y = fromY;
+ float x = fromX;
+
+ MotionEvent event = MotionEvent.obtain(downTime, eventTime,
+ MotionEvent.ACTION_DOWN, x, y, 0);
+ scrollManager.onTouchEvent(event);
+ for (int i = 0; i < STEP_COUNT; ++i) {
+ angle += stepAngle;
+ x = getX(angle, radius, TEST_WIDTH);
+ y = getY(angle, radius, TEST_HEIGHT);
+ eventTime = SystemClock.uptimeMillis();
+ event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0);
+ scrollManager.onTouchEvent(event);
+ }
+
+ eventTime = SystemClock.uptimeMillis();
+ event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0);
+ scrollManager.onTouchEvent(event);
+ }
+
+ private static float getX(double angle, double radius, double viewWidth) {
+ double radianAngle = Math.toRadians(angle - 90);
+ double relativeX = cos(radianAngle) * radius;
+ return (float) (relativeX + (viewWidth / 2));
+ }
+
+ private static float getY(double angle, double radius, double viewHeight) {
+ double radianAngle = Math.toRadians(angle - 90);
+ double relativeY = sin(radianAngle) * radius;
+ return (float) (relativeY + (viewHeight / 2));
+ }
+
+ private static float getAngle(double x, double y, double viewWidth, double viewHeight) {
+ double relativeX = x - (viewWidth / 2);
+ double relativeY = y - (viewHeight / 2);
+ double rowAngle = Math.atan2(relativeX, relativeY);
+ double angle = -Math.toDegrees(rowAngle) - 180;
+ if (angle < 0) {
+ angle += 360;
+ }
+ return (float) angle;
+ }
+}
diff --git a/android/support/wear/widget/SwipeDismissFrameLayoutTest.java b/android/support/wear/widget/SwipeDismissFrameLayoutTest.java
new file mode 100644
index 00000000..e4e47aaf
--- /dev/null
+++ b/android/support/wear/widget/SwipeDismissFrameLayoutTest.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.swipeRight;
+import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.wear.widget.util.AsyncViewActions.waitForMatchingView;
+import static android.support.wear.widget.util.MoreViewAssertions.withPositiveVerticalScrollOffset;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.RectF;
+import android.support.annotation.IdRes;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.espresso.ViewAction;
+import android.support.test.espresso.action.GeneralLocation;
+import android.support.test.espresso.action.GeneralSwipeAction;
+import android.support.test.espresso.action.Press;
+import android.support.test.espresso.action.Swipe;
+import android.support.test.espresso.matcher.ViewMatchers;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.support.wear.test.R;
+import android.support.wear.widget.util.ArcSwipe;
+import android.support.wear.widget.util.WakeLockRule;
+import android.view.View;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SwipeDismissFrameLayoutTest {
+
+ private static final long MAX_WAIT_TIME = 4000; //ms
+ private final SwipeDismissFrameLayout.Callback mDismissCallback = new DismissCallback();
+
+ @Rule
+ public final WakeLockRule wakeLock = new WakeLockRule();
+
+ @Rule
+ public final ActivityTestRule<SwipeDismissFrameLayoutTestActivity> activityRule =
+ new ActivityTestRule<>(
+ SwipeDismissFrameLayoutTestActivity.class,
+ true, /** initial touch mode */
+ false /** launchActivity */
+ );
+
+ private int mLayoutWidth;
+ private int mLayoutHeight;
+
+ @Test
+ @SmallTest
+ public void testCanScrollHorizontally() {
+ // GIVEN a freshly setup SwipeDismissFrameLayout
+ setUpSimpleLayout();
+ Activity activity = activityRule.getActivity();
+ SwipeDismissFrameLayout testLayout =
+ (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+ // WHEN we check that the layout is horizontally scrollable from left to right.
+ // THEN the layout is found to be horizontally swipeable from left to right.
+ assertTrue(testLayout.canScrollHorizontally(-20));
+ // AND the layout is found to NOT be horizontally swipeable from right to left.
+ assertFalse(testLayout.canScrollHorizontally(20));
+
+ // WHEN we switch off the swipe-to-dismiss functionality for the layout
+ testLayout.setSwipeable(false);
+ // THEN the layout is found NOT to be horizontally swipeable from left to right.
+ assertFalse(testLayout.canScrollHorizontally(-20));
+ // AND the layout is found to NOT be horizontally swipeable from right to left.
+ assertFalse(testLayout.canScrollHorizontally(20));
+ }
+
+ @Test
+ @SmallTest
+ public void canScrollHorizontallyShouldBeFalseWhenInvisible() {
+ // GIVEN a freshly setup SwipeDismissFrameLayout
+ setUpSimpleLayout();
+ Activity activity = activityRule.getActivity();
+ final SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root);
+ // GIVEN the layout is invisible
+ // Note: We have to run this on the main thread, because of thread checks in View.java.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ testLayout.setVisibility(View.INVISIBLE);
+ }
+ });
+ // WHEN we check that the layout is horizontally scrollable
+ // THEN the layout is found to be NOT horizontally swipeable from left to right.
+ assertFalse(testLayout.canScrollHorizontally(-20));
+ // AND the layout is found to NOT be horizontally swipeable from right to left.
+ assertFalse(testLayout.canScrollHorizontally(20));
+ }
+
+ @Test
+ @SmallTest
+ public void canScrollHorizontallyShouldBeFalseWhenGone() {
+ // GIVEN a freshly setup SwipeDismissFrameLayout
+ setUpSimpleLayout();
+ Activity activity = activityRule.getActivity();
+ final SwipeDismissFrameLayout testLayout =
+ (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+ // GIVEN the layout is gone
+ // Note: We have to run this on the main thread, because of thread checks in View.java.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ testLayout.setVisibility(View.GONE);
+ }
+ });
+ // WHEN we check that the layout is horizontally scrollable
+ // THEN the layout is found to be NOT horizontally swipeable from left to right.
+ assertFalse(testLayout.canScrollHorizontally(-20));
+ // AND the layout is found to NOT be horizontally swipeable from right to left.
+ assertFalse(testLayout.canScrollHorizontally(20));
+ }
+
+ @Test
+ @SmallTest
+ public void testSwipeDismissEnabledByDefault() {
+ // GIVEN a freshly setup SwipeDismissFrameLayout
+ setUpSimpleLayout();
+ Activity activity = activityRule.getActivity();
+ SwipeDismissFrameLayout testLayout =
+ (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+ // WHEN we check that the layout is dismissible
+ // THEN the layout is find to be dismissible
+ assertTrue(testLayout.isSwipeable());
+ }
+
+ @Test
+ @SmallTest
+ public void testSwipeDismissesViewIfEnabled() {
+ // GIVEN a freshly setup SwipeDismissFrameLayout
+ setUpSimpleLayout();
+ // WHEN we perform a swipe to dismiss
+ onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
+ // AND hidden
+ assertHidden(R.id.swipe_dismiss_root);
+ }
+
+ @Test
+ @SmallTest
+ public void testSwipeDoesNotDismissViewIfDisabled() {
+ // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off.
+ setUpSimpleLayout();
+ Activity activity = activityRule.getActivity();
+ SwipeDismissFrameLayout testLayout =
+ (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+ testLayout.setSwipeable(false);
+ // WHEN we perform a swipe to dismiss
+ onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
+ // THEN the layout is not hidden
+ assertNotHidden(R.id.swipe_dismiss_root);
+ }
+
+ @Test
+ @SmallTest
+ public void testAddRemoveCallback() {
+ // GIVEN a freshly setup SwipeDismissFrameLayout
+ setUpSimpleLayout();
+ Activity activity = activityRule.getActivity();
+ SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root);
+ // WHEN we remove the swipe callback
+ testLayout.removeCallback(mDismissCallback);
+ onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
+ // THEN the layout is not hidden
+ assertNotHidden(R.id.swipe_dismiss_root);
+ }
+
+ @Test
+ @SmallTest
+ public void testSwipeDoesNotDismissViewIfScrollable() throws Throwable {
+ // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off.
+ setUpSwipeDismissWithHorizontalRecyclerView();
+ activityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Activity activity = activityRule.getActivity();
+ RecyclerView testLayout = activity.findViewById(R.id.recycler_container);
+ // Scroll to a position from which the child is scrollable.
+ testLayout.scrollToPosition(50);
+ }
+ });
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ // WHEN we perform a swipe to dismiss from the center of the screen.
+ onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter());
+ // THEN the layout is not hidden
+ assertNotHidden(R.id.swipe_dismiss_root);
+ }
+
+
+ @Test
+ @SmallTest
+ public void testEdgeSwipeDoesDismissViewIfScrollable() {
+ // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off.
+ setUpSwipeDismissWithHorizontalRecyclerView();
+ // WHEN we perform a swipe to dismiss from the left edge of the screen.
+ onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftEdge());
+ // THEN the layout is hidden
+ assertHidden(R.id.swipe_dismiss_root);
+ }
+
+ @Test
+ @SmallTest
+ public void testSwipeDoesNotDismissViewIfStartsInWrongPosition() {
+ // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an
+ // inner circle.
+ setUpSwipeableRegion();
+ // WHEN we perform a swipe to dismiss from the left edge of the screen.
+ onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftEdge());
+ // THEN the layout is not not hidden
+ assertNotHidden(R.id.swipe_dismiss_root);
+ }
+
+ @Test
+ @SmallTest
+ public void testSwipeDoesDismissViewIfStartsInRightPosition() {
+ // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an
+ // inner circle.
+ setUpSwipeableRegion();
+ // WHEN we perform a swipe to dismiss from the center of the screen.
+ onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter());
+ // THEN the layout is hidden
+ assertHidden(R.id.swipe_dismiss_root);
+ }
+
+ /**
+ @Test public void testSwipeInPreferenceFragmentAndNavDrawer() {
+ // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an inner
+ // circle.
+ setUpPreferenceFragmentAndNavDrawer();
+ // WHEN we perform a swipe to dismiss from the center of the screen to the bottom.
+ onView(withId(R.id.drawer_layout)).perform(swipeBottomFromCenter());
+ // THEN the navigation drawer is shown.
+ assertPeeking(R.id.top_drawer);
+ }*/
+
+ @Test
+ @SmallTest
+ public void testArcSwipeDoesNotTriggerDismiss() throws Throwable {
+ // GIVEN a freshly setup SwipeDismissFrameLayout with vertically scrollable content
+ setUpSwipeDismissWithVerticalRecyclerView();
+ int center = mLayoutHeight / 2;
+ int halfBound = mLayoutWidth / 2;
+ RectF bounds = new RectF(0, center - halfBound, mLayoutWidth, center + halfBound);
+ // WHEN the view is scrolled on an arc from top to bottom.
+ onView(withId(R.id.swipe_dismiss_root)).perform(swipeTopFromBottomOnArc(bounds));
+ // THEN the layout is not dismissed and not hidden.
+ assertNotHidden(R.id.swipe_dismiss_root);
+ // AND the content view is scrolled.
+ assertScrolledY(R.id.recycler_container);
+ }
+
+ /**
+ * Set ups the simplest possible layout for test cases - a {@link SwipeDismissFrameLayout} with
+ * a single static child.
+ */
+ private void setUpSimpleLayout() {
+ activityRule.launchActivity(
+ new Intent()
+ .putExtra(
+ LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.swipe_dismiss_layout_testcase_1));
+ setDismissCallback();
+ }
+
+
+ /**
+ * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable
+ * containers. This layout contains a {@link SwipeDismissFrameLayout} with a horizontal {@link
+ * android.support.v7.widget.RecyclerView} as a child, ready to accept an adapter.
+ */
+ private void setUpSwipeDismissWithHorizontalRecyclerView() {
+ Intent launchIntent = new Intent();
+ launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.swipe_dismiss_layout_testcase_2);
+ launchIntent.putExtra(SwipeDismissFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true);
+ activityRule.launchActivity(launchIntent);
+ setDismissCallback();
+ }
+
+ /**
+ * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable
+ * containers. This layout contains a {@link SwipeDismissFrameLayout} with a vertical {@link
+ * WearableRecyclerView} as a child, ready to accept an adapter.
+ */
+ private void setUpSwipeDismissWithVerticalRecyclerView() {
+ Intent launchIntent = new Intent();
+ launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.swipe_dismiss_layout_testcase_2);
+ launchIntent.putExtra(SwipeDismissFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, false);
+ activityRule.launchActivity(launchIntent);
+ setDismissCallback();
+ }
+
+ /**
+ * Sets up a {@link SwipeDismissFrameLayout} in which only a certain region is allowed to react
+ * to swipe-dismiss gestures.
+ */
+ private void setUpSwipeableRegion() {
+ activityRule.launchActivity(
+ new Intent()
+ .putExtra(
+ LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.swipe_dismiss_layout_testcase_1));
+ setCallback(
+ new DismissCallback() {
+ @Override
+ public boolean onPreSwipeStart(SwipeDismissFrameLayout layout, float x,
+ float y) {
+ float normalizedX = x - mLayoutWidth / 2;
+ float normalizedY = y - mLayoutWidth / 2;
+ float squareX = normalizedX * normalizedX;
+ float squareY = normalizedY * normalizedY;
+ // 30 is an arbitrary number limiting the circle.
+ return Math.sqrt(squareX + squareY) < (mLayoutWidth / 2 - 30);
+ }
+ });
+ }
+
+ /**
+ * Sets up a more involved test case where the layout consists of a
+ * {@code WearableNavigationDrawer} and a
+ * {@code android.support.wear.internal.view.SwipeDismissPreferenceFragment}
+ */
+ /*
+ private void setUpPreferenceFragmentAndNavDrawer() {
+ activityRule.launchActivity(
+ new Intent()
+ .putExtra(
+ LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.swipe_dismiss_layout_testcase_3));
+ Activity activity = activityRule.getActivity();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ WearableNavigationDrawer wearableNavigationDrawer =
+ (WearableNavigationDrawer) activity.findViewById(R.id.top_drawer);
+ wearableNavigationDrawer.setAdapter(
+ new WearableNavigationDrawer.WearableNavigationDrawerAdapter() {
+ @Override
+ public String getItemText(int pos) {
+ return "test";
+ }
+
+ @Override
+ public Drawable getItemDrawable(int pos) {
+ return null;
+ }
+
+ @Override
+ public void onItemSelected(int pos) {
+ return;
+ }
+
+ @Override
+ public int getCount() {
+ return 3;
+ }
+ });
+ });
+ }*/
+ private void setDismissCallback() {
+ setCallback(mDismissCallback);
+ }
+
+ private void setCallback(SwipeDismissFrameLayout.Callback callback) {
+ Activity activity = activityRule.getActivity();
+ SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root);
+ mLayoutWidth = testLayout.getWidth();
+ mLayoutHeight = testLayout.getHeight();
+ testLayout.addCallback(callback);
+ }
+
+ /**
+ * private static void assertPeeking(@IdRes int layoutId) {
+ * onView(withId(layoutId))
+ * .perform(
+ * waitForMatchingView(
+ * allOf(withId(layoutId), isOpened(true)), MAX_WAIT_TIME));
+ * }
+ */
+
+ private static void assertHidden(@IdRes int layoutId) {
+ onView(withId(layoutId))
+ .perform(
+ waitForMatchingView(
+ allOf(withId(layoutId),
+ withEffectiveVisibility(ViewMatchers.Visibility.GONE)),
+ MAX_WAIT_TIME));
+ }
+
+ private static void assertNotHidden(@IdRes int layoutId) {
+ onView(withId(layoutId))
+ .perform(
+ waitForMatchingView(
+ allOf(withId(layoutId),
+ withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)),
+ MAX_WAIT_TIME));
+ }
+
+ private static void assertScrolledY(@IdRes int layoutId) {
+ onView(withId(layoutId))
+ .perform(
+ waitForMatchingView(
+ allOf(withId(layoutId), withPositiveVerticalScrollOffset()),
+ MAX_WAIT_TIME));
+ }
+
+ private static ViewAction swipeRightFromCenter() {
+ return new GeneralSwipeAction(
+ Swipe.SLOW, GeneralLocation.CENTER, GeneralLocation.CENTER_RIGHT, Press.FINGER);
+ }
+
+ private static ViewAction swipeRightFromLeftEdge() {
+ return new GeneralSwipeAction(
+ Swipe.SLOW, GeneralLocation.CENTER_LEFT, GeneralLocation.CENTER_RIGHT,
+ Press.FINGER);
+ }
+
+ private static ViewAction swipeTopFromBottomOnArc(RectF bounds) {
+ return new GeneralSwipeAction(
+ new ArcSwipe(ArcSwipe.Gesture.SLOW_ANTICLOCKWISE, bounds),
+ GeneralLocation.BOTTOM_CENTER,
+ GeneralLocation.TOP_CENTER,
+ Press.FINGER);
+ }
+
+ /** Helper class hiding the view after a successful swipe-to-dismiss. */
+ private static class DismissCallback extends SwipeDismissFrameLayout.Callback {
+
+ @Override
+ public void onDismissed(SwipeDismissFrameLayout layout) {
+ layout.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/android/support/wear/widget/SwipeDismissFrameLayoutTestActivity.java b/android/support/wear/widget/SwipeDismissFrameLayoutTestActivity.java
new file mode 100644
index 00000000..5d868324
--- /dev/null
+++ b/android/support/wear/widget/SwipeDismissFrameLayoutTestActivity.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget;
+
+import android.os.Bundle;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.wear.test.R;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+public class SwipeDismissFrameLayoutTestActivity extends LayoutTestActivity {
+
+ public static final String EXTRA_LAYOUT_HORIZONTAL = "layout_horizontal";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ int layoutId = getIntent().getIntExtra(EXTRA_LAYOUT_RESOURCE_ID, -1);
+ boolean horizontal = getIntent().getBooleanExtra(EXTRA_LAYOUT_HORIZONTAL, false);
+
+ if (layoutId == R.layout.swipe_dismiss_layout_testcase_2) {
+ createScrollableContent(horizontal);
+ }
+ }
+
+ private void createScrollableContent(boolean horizontal) {
+ RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_container);
+ if (recyclerView == null) {
+ throw new NullPointerException("There has to be a relevant container defined");
+ }
+ recyclerView.setLayoutManager(
+ new LinearLayoutManager(
+ this,
+ horizontal ? LinearLayoutManager.HORIZONTAL : LinearLayoutManager.VERTICAL,
+ false));
+ recyclerView.setAdapter(new MyRecyclerViewAdapter());
+ }
+
+ private static class MyRecyclerViewAdapter
+ extends RecyclerView.Adapter<MyRecyclerViewAdapter.CustomViewHolder> {
+ @Override
+ public CustomViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ TextView textView = new TextView(parent.getContext());
+ textView.setText("A LOT OF TEXT VIEW");
+ textView.setGravity(Gravity.CENTER_VERTICAL);
+ return new CustomViewHolder(textView);
+ }
+
+ @Override
+ public void onBindViewHolder(CustomViewHolder holder, int position) {
+ }
+
+ @Override
+ public int getItemCount() {
+ return 100;
+ }
+
+ static class CustomViewHolder extends RecyclerView.ViewHolder {
+
+ CustomViewHolder(View view) {
+ super(view);
+ }
+ }
+ }
+}
diff --git a/android/support/wear/widget/SwipeDismissPreferenceFragment.java b/android/support/wear/widget/SwipeDismissPreferenceFragment.java
new file mode 100644
index 00000000..a892cb68
--- /dev/null
+++ b/android/support/wear/widget/SwipeDismissPreferenceFragment.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget;
+
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+import android.support.wear.widget.SwipeDismissFrameLayout.Callback;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * {@link PreferenceFragment} that supports swipe-to-dismiss.
+ *
+ * <p>Unlike a regular PreferenceFragment, this Fragment has a solid color background using the
+ * background color from the theme. This allows the fragment to be layered on top of other
+ * fragments so that the previous layer is seen when this fragment is swiped away.
+ */
+public class SwipeDismissPreferenceFragment extends PreferenceFragment {
+
+ private SwipeDismissFrameLayout mSwipeLayout;
+
+ private final Callback mCallback =
+ new Callback() {
+ @Override
+ public void onSwipeStarted(SwipeDismissFrameLayout layout) {
+ SwipeDismissPreferenceFragment.this.onSwipeStart();
+ }
+
+ @Override
+ public void onSwipeCanceled(SwipeDismissFrameLayout layout) {
+ SwipeDismissPreferenceFragment.this.onSwipeCancelled();
+ }
+
+ @Override
+ public void onDismissed(SwipeDismissFrameLayout layout) {
+ SwipeDismissPreferenceFragment.this.onDismiss();
+ }
+ };
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ mSwipeLayout = new SwipeDismissFrameLayout(getActivity());
+ mSwipeLayout.addCallback(mCallback);
+
+ View contents = super.onCreateView(inflater, mSwipeLayout, savedInstanceState);
+
+ mSwipeLayout.setBackgroundColor(getBackgroundColor());
+ mSwipeLayout.addView(contents);
+
+ return mSwipeLayout;
+ }
+
+ /** Called when the fragment is dismissed with a swipe. */
+ public void onDismiss() {
+ }
+
+ /** Called when a swipe-to-dismiss gesture is started. */
+ public void onSwipeStart() {
+ }
+
+ /** Called when a swipe-to-dismiss gesture is cancelled. */
+ public void onSwipeCancelled() {
+ }
+
+ /**
+ * Sets whether or not the preferences list can be focused. If {@code focusable} is false, any
+ * existing focus will be cleared.
+ */
+ public void setFocusable(boolean focusable) {
+ if (focusable) {
+ mSwipeLayout.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
+ mSwipeLayout.setFocusable(true);
+ } else {
+ // Prevent any child views from receiving focus.
+ mSwipeLayout.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+
+ mSwipeLayout.setFocusable(false);
+ mSwipeLayout.clearFocus();
+ }
+ }
+
+ private int getBackgroundColor() {
+ TypedValue value = new TypedValue();
+ getActivity().getTheme().resolveAttribute(android.R.attr.colorBackground, value, true);
+ return value.data;
+ }
+}
diff --git a/android/support/wear/widget/WearableLinearLayoutManagerTest.java b/android/support/wear/widget/WearableLinearLayoutManagerTest.java
new file mode 100644
index 00000000..49da7b21
--- /dev/null
+++ b/android/support/wear/widget/WearableLinearLayoutManagerTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.Activity;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.wear.test.R;
+import android.support.wear.widget.util.WakeLockRule;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WearableLinearLayoutManagerTest {
+
+ @Rule
+ public final WakeLockRule wakeLock = new WakeLockRule();
+
+ @Rule
+ public final ActivityTestRule<WearableRecyclerViewTestActivity> mActivityRule =
+ new ActivityTestRule<>(WearableRecyclerViewTestActivity.class, true, true);
+
+ WearableLinearLayoutManager mWearableLinearLayoutManagerUnderTest;
+
+ @Before
+ public void setUp() throws Throwable {
+ Activity activity = mActivityRule.getActivity();
+ CurvingLayoutCallback mCurvingCallback = new CurvingLayoutCallback(activity);
+ mCurvingCallback.setOffset(10);
+ mWearableLinearLayoutManagerUnderTest =
+ new WearableLinearLayoutManager(mActivityRule.getActivity(), mCurvingCallback);
+ }
+
+ @Test
+ public void testRoundOffsetting() throws Throwable {
+ ((CurvingLayoutCallback) mWearableLinearLayoutManagerUnderTest.getLayoutCallback())
+ .setRound(true);
+ final AtomicReference<WearableRecyclerView> wrvReference = new AtomicReference<>();
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
+ // Set a fixed layout so that the test adapts to different device screens.
+ wrv.setLayoutParams(new FrameLayout.LayoutParams(390, 390));
+ }
+ });
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
+ wrv.setLayoutManager(mWearableLinearLayoutManagerUnderTest);
+ wrvReference.set(wrv);
+ }
+ });
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ WearableRecyclerView wrv = wrvReference.get();
+
+ View child1 = wrv.getChildAt(0);
+ View child2 = wrv.getChildAt(1);
+ View child3 = wrv.getChildAt(2);
+ View child4 = wrv.getChildAt(3);
+ View child5 = wrv.getChildAt(4);
+
+ // The left position and the translation of the child is modified if the screen is round.
+ // Check if the 5th child is not null as some devices will not be able to display 5 views.
+ assertEquals(136, child1.getLeft());
+ assertEquals(-6.3, child1.getTranslationY(), 0.1);
+
+ assertEquals(91, child2.getLeft(), 1);
+ assertEquals(-15.21, child2.getTranslationY(), 0.1);
+
+ assertEquals(58, child3.getLeft(), 1);
+ assertEquals(-13.5, child3.getTranslationY(), 0.1);
+
+ assertEquals(42, child4.getLeft(), 1);
+ assertEquals(-4.5, child4.getTranslationY(), 0.1);
+
+ if (child5 != null) {
+ assertEquals(43, child5.getLeft(), 1);
+ assertEquals(6.7, child5.getTranslationY(), 0.1);
+ }
+ }
+
+ @Test
+ public void testStraightOffsetting() throws Throwable {
+ ((CurvingLayoutCallback) mWearableLinearLayoutManagerUnderTest.getLayoutCallback())
+ .setRound(
+ false);
+ final AtomicReference<WearableRecyclerView> wrvReference = new AtomicReference<>();
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
+ wrv.setLayoutManager(mWearableLinearLayoutManagerUnderTest);
+ wrvReference.set(wrv);
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ WearableRecyclerView wrv = wrvReference.get();
+
+ View child1 = wrv.getChildAt(0);
+ View child2 = wrv.getChildAt(1);
+ View child3 = wrv.getChildAt(2);
+ View child4 = wrv.getChildAt(3);
+ View child5 = wrv.getChildAt(4);
+
+ // The left position and the translation of the child is not modified if the screen is
+ // straight. Check if the 5th child is not null as some devices will not be able to display
+ // 5 views.
+ assertEquals(0, child1.getLeft());
+ assertEquals(0.0f, child1.getTranslationY(), 0);
+
+ assertEquals(0, child2.getLeft());
+ assertEquals(0.0f, child2.getTranslationY(), 0);
+
+ assertEquals(0, child3.getLeft());
+ assertEquals(0.0f, child3.getTranslationY(), 0);
+
+ assertEquals(0, child4.getLeft());
+ assertEquals(0.0f, child4.getTranslationY(), 0);
+
+ if (child5 != null) {
+ assertEquals(0, child5.getLeft());
+ assertEquals(0.0f, child5.getTranslationY(), 0);
+ }
+ }
+}
diff --git a/android/support/wear/widget/WearableRecyclerViewTest.java b/android/support/wear/widget/WearableRecyclerViewTest.java
new file mode 100644
index 00000000..5c176386
--- /dev/null
+++ b/android/support/wear/widget/WearableRecyclerViewTest.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.wear.widget.util.AsyncViewActions.waitForMatchingView;
+import static android.support.wear.widget.util.MoreViewAssertions.withNoVerticalScrollOffset;
+import static android.support.wear.widget.util.MoreViewAssertions.withPositiveVerticalScrollOffset;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.support.annotation.IdRes;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.espresso.ViewAction;
+import android.support.test.espresso.action.GeneralLocation;
+import android.support.test.espresso.action.GeneralSwipeAction;
+import android.support.test.espresso.action.Press;
+import android.support.test.espresso.action.Swipe;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.support.wear.test.R;
+import android.support.wear.widget.util.WakeLockRule;
+import android.view.View;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WearableRecyclerViewTest {
+
+ private static final long MAX_WAIT_TIME = 10000;
+ @Mock
+ WearableRecyclerView.LayoutManager mMockChildLayoutManager;
+
+ @Rule
+ public final WakeLockRule wakeLock = new WakeLockRule();
+
+ @Rule
+ public final ActivityTestRule<WearableRecyclerViewTestActivity> mActivityRule =
+ new ActivityTestRule<>(WearableRecyclerViewTestActivity.class, true, true);
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testCaseInitState() {
+ WearableRecyclerView wrv = new WearableRecyclerView(mActivityRule.getActivity());
+ wrv.setLayoutManager(new WearableLinearLayoutManager(wrv.getContext()));
+
+ assertFalse(wrv.isEdgeItemsCenteringEnabled());
+ assertFalse(wrv.isCircularScrollingGestureEnabled());
+ assertEquals(1.0f, wrv.getBezelFraction(), 0.01f);
+ assertEquals(180.0f, wrv.getScrollDegreesPerScreen(), 0.01f);
+ }
+
+ @Test
+ public void testEdgeItemsCenteringOnAndOff() throws Throwable {
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
+ wrv.setEdgeItemsCenteringEnabled(true);
+ }
+ });
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
+ View child = wrv.getChildAt(0);
+ assertNotNull("child", child);
+ assertEquals((wrv.getHeight() - child.getHeight()) / 2, child.getTop());
+ }
+ });
+
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
+ wrv.setEdgeItemsCenteringEnabled(false);
+ }
+ });
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
+ View child = wrv.getChildAt(0);
+ assertNotNull("child", child);
+ assertEquals(0, child.getTop());
+
+ }
+ });
+ }
+
+ @Test
+ public void testEdgeItemsCenteringBeforeChildrenDrawn() throws Throwable {
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Activity activity = mActivityRule.getActivity();
+ WearableRecyclerView wrv = (WearableRecyclerView) activity.findViewById(R.id.wrv);
+ RecyclerView.Adapter<WearableRecyclerView.ViewHolder> adapter = wrv.getAdapter();
+ wrv.setAdapter(null);
+ wrv.setEdgeItemsCenteringEnabled(true);
+ wrv.setAdapter(adapter);
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
+ // Verify the first child
+ View child = wrv.getChildAt(0);
+ assertNotNull("child", child);
+ assertEquals((wrv.getHeight() - child.getHeight()) / 2, child.getTop());
+ }
+ });
+ }
+
+ @Test
+ public void testCircularScrollingGesture() throws Throwable {
+ onView(withId(R.id.wrv)).perform(swipeDownFromTopRight());
+ assertNotScrolledY(R.id.wrv);
+
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
+ wrv.setCircularScrollingGestureEnabled(true);
+ }
+ });
+
+ onView(withId(R.id.wrv)).perform(swipeDownFromTopRight());
+ assertScrolledY(R.id.wrv);
+ }
+
+ @Test
+ public void testCurvedOffsettingHelper() throws Throwable {
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
+ wrv.setLayoutManager(new WearableLinearLayoutManager(wrv.getContext()));
+ }
+ });
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ onView(withId(R.id.wrv)).perform(swipeDownFromTopRight());
+
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Activity activity = mActivityRule.getActivity();
+ WearableRecyclerView wrv = (WearableRecyclerView) activity.findViewById(R.id.wrv);
+ if (activity.getResources().getConfiguration().isScreenRound()) {
+ View child = wrv.getChildAt(0);
+ assertTrue(child.getLeft() > 0);
+ } else {
+ for (int i = 0; i < wrv.getChildCount(); i++) {
+ assertEquals(0, wrv.getChildAt(i).getLeft());
+ }
+ }
+ }
+ });
+ }
+
+ private static ViewAction swipeDownFromTopRight() {
+ return new GeneralSwipeAction(
+ Swipe.FAST, GeneralLocation.TOP_RIGHT, GeneralLocation.BOTTOM_RIGHT,
+ Press.FINGER);
+ }
+
+ private void assertScrolledY(@IdRes int layoutId) {
+ onView(withId(layoutId)).perform(waitForMatchingView(
+ allOf(withId(layoutId), withPositiveVerticalScrollOffset()), MAX_WAIT_TIME));
+ }
+
+ private void assertNotScrolledY(@IdRes int layoutId) {
+ onView(withId(layoutId)).perform(waitForMatchingView(
+ allOf(withId(layoutId), withNoVerticalScrollOffset()), MAX_WAIT_TIME));
+ }
+}
diff --git a/android/support/wear/widget/WearableRecyclerViewTestActivity.java b/android/support/wear/widget/WearableRecyclerViewTestActivity.java
new file mode 100644
index 00000000..2329fc51
--- /dev/null
+++ b/android/support/wear/widget/WearableRecyclerViewTestActivity.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v7.widget.RecyclerView;
+import android.support.wear.test.R;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+public class WearableRecyclerViewTestActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.wearable_recycler_view_basic);
+ WearableRecyclerView wrv = findViewById(R.id.wrv);
+ wrv.setLayoutManager(new WearableLinearLayoutManager(this));
+ wrv.setAdapter(new TestAdapter());
+ }
+
+ private class ViewHolder extends RecyclerView.ViewHolder {
+ TextView mView;
+ ViewHolder(TextView itemView) {
+ super(itemView);
+ mView = itemView;
+ }
+ }
+
+ private class TestAdapter extends WearableRecyclerView.Adapter<ViewHolder> {
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ TextView view = new TextView(parent.getContext());
+ view.setLayoutParams(new RecyclerView.LayoutParams(200, 50));
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ holder.mView.setText("holder at position " + position);
+ holder.mView.setTag(position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return 100;
+ }
+ }
+}
diff --git a/android/support/wear/widget/drawer/DrawerTestActivity.java b/android/support/wear/widget/drawer/DrawerTestActivity.java
new file mode 100644
index 00000000..414b97b9
--- /dev/null
+++ b/android/support/wear/widget/drawer/DrawerTestActivity.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget.drawer;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.IntDef;
+import android.support.wear.test.R;
+import android.support.wear.widget.drawer.WearableDrawerLayout.DrawerStateCallback;
+import android.support.wear.widget.drawer.WearableNavigationDrawerView.WearableNavigationDrawerAdapter;
+import android.util.ArrayMap;
+import android.view.Gravity;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Map;
+
+/**
+ * Test {@link Activity} for {@link WearableDrawerLayout} and implementations of {@link
+ * android.support.wear.widget.drawer.WearableDrawerView}.
+ */
+public class DrawerTestActivity extends Activity {
+
+ private static final int DRAWER_SIZE = 5;
+ private static final String STYLE_EXTRA = "style";
+ private static final String OPEN_TOP_IN_ONCREATE_EXTRA = "openTopInOnCreate";
+ private static final String OPEN_BOTTOM_IN_ONCREATE_EXTRA = "openBottomInOnCreate";
+ private static final String CLOSE_FIRST_DRAWER_OPENED = "closeFirstDrawerOpened";
+ private static final Map<Integer, Integer> STYLE_TO_RES_ID = new ArrayMap<>();
+
+ static {
+ STYLE_TO_RES_ID.put(
+ DrawerStyle.BOTH_DRAWER_NAV_MULTI_PAGE,
+ R.layout.test_multi_page_nav_drawer_layout);
+ STYLE_TO_RES_ID.put(
+ DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE,
+ R.layout.test_single_page_nav_drawer_layout);
+ STYLE_TO_RES_ID.put(
+ DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE,
+ R.layout.test_only_action_drawer_with_title_layout);
+
+ }
+
+ private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+ private final WearableNavigationDrawerAdapter mDrawerAdapter =
+ new WearableNavigationDrawerAdapter() {
+ @Override
+ public String getItemText(int pos) {
+ return Integer.toString(pos);
+ }
+
+ @Override
+ public Drawable getItemDrawable(int pos) {
+ return getDrawable(android.R.drawable.star_on);
+ }
+
+ @Override
+ public int getCount() {
+ return DRAWER_SIZE;
+ }
+ };
+ private WearableActionDrawerView mActionDrawer;
+ private WearableDrawerLayout mDrawerLayout;
+ private WearableNavigationDrawerView mNavigationDrawer;
+ private final Runnable mCloseTopDrawerRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ mNavigationDrawer.getController().closeDrawer();
+ }
+ };
+ private final DrawerStateCallback mCloseFirstDrawerOpenedCallback =
+ new DrawerStateCallback() {
+ @Override
+ public void onDrawerOpened(WearableDrawerLayout layout,
+ WearableDrawerView drawerView) {
+ mMainThreadHandler.postDelayed(mCloseTopDrawerRunnable, 1000);
+ }
+ };
+ @DrawerStyle private int mNavigationStyle;
+ private boolean mOpenTopDrawerInOnCreate;
+ private boolean mOpenBottomDrawerInOnCreate;
+ private boolean mCloseFirstDrawerOpened;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ parseIntent(getIntent());
+
+ setContentView(STYLE_TO_RES_ID.get(mNavigationStyle));
+
+ mDrawerLayout = (WearableDrawerLayout) findViewById(R.id.drawer_layout);
+ mNavigationDrawer = (WearableNavigationDrawerView) findViewById(R.id.navigation_drawer);
+ mActionDrawer = (WearableActionDrawerView) findViewById(R.id.action_drawer);
+
+ if (mCloseFirstDrawerOpened) {
+ mDrawerLayout.setDrawerStateCallback(mCloseFirstDrawerOpenedCallback);
+ }
+
+ if (mNavigationDrawer != null) {
+ mNavigationDrawer.setAdapter(mDrawerAdapter);
+ if (mOpenTopDrawerInOnCreate) {
+ mDrawerLayout.openDrawer(Gravity.TOP);
+ } else {
+ mDrawerLayout.peekDrawer(Gravity.TOP);
+ }
+ }
+
+ if (mActionDrawer != null) {
+ if (mOpenBottomDrawerInOnCreate) {
+ mDrawerLayout.openDrawer(Gravity.BOTTOM);
+ } else {
+ mDrawerLayout.peekDrawer(Gravity.BOTTOM);
+ }
+ }
+ }
+
+ private void parseIntent(Intent intent) {
+ //noinspection WrongConstant - Linter doesn't know intent contains a NavigationStyle
+ mNavigationStyle = intent.getIntExtra(STYLE_EXTRA, DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE);
+ mOpenTopDrawerInOnCreate = intent.getBooleanExtra(OPEN_TOP_IN_ONCREATE_EXTRA, false);
+ mOpenBottomDrawerInOnCreate = intent.getBooleanExtra(OPEN_BOTTOM_IN_ONCREATE_EXTRA, false);
+ mCloseFirstDrawerOpened = intent.getBooleanExtra(CLOSE_FIRST_DRAWER_OPENED, false);
+ }
+
+ /**
+ * Which configuration of drawers should be used.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE,
+ DrawerStyle.BOTH_DRAWER_NAV_MULTI_PAGE,
+ DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE
+ })
+ public @interface DrawerStyle {
+ int BOTH_DRAWER_NAV_SINGLE_PAGE = 0;
+ int BOTH_DRAWER_NAV_MULTI_PAGE = 1;
+ int ONLY_ACTION_DRAWER_WITH_TITLE = 2;
+ }
+
+ /**
+ * Builds an {@link Intent} to start this {@link Activity} with the appropriate extras.
+ */
+ public static class Builder {
+
+ @DrawerStyle private int mStyle = DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE;
+ private boolean mOpenTopDrawerInOnCreate = false;
+ private boolean mOpenBottomDrawerInOnCreate = false;
+ private boolean mCloseFirstDrawerOpened = false;
+
+ public Builder setStyle(@DrawerStyle int style) {
+ mStyle = style;
+ return this;
+ }
+
+ public Builder openTopDrawerInOnCreate() {
+ mOpenTopDrawerInOnCreate = true;
+ return this;
+ }
+
+ public Builder openBottomDrawerInOnCreate() {
+ mOpenBottomDrawerInOnCreate = true;
+ return this;
+ }
+
+ public Builder closeFirstDrawerOpened() {
+ mCloseFirstDrawerOpened = true;
+ return this;
+ }
+
+ public Intent build() {
+ return new Intent()
+ .putExtra(STYLE_EXTRA, mStyle)
+ .putExtra(OPEN_TOP_IN_ONCREATE_EXTRA, mOpenTopDrawerInOnCreate)
+ .putExtra(OPEN_BOTTOM_IN_ONCREATE_EXTRA, mOpenBottomDrawerInOnCreate)
+ .putExtra(CLOSE_FIRST_DRAWER_OPENED, mCloseFirstDrawerOpened);
+ }
+ }
+}
diff --git a/android/support/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java b/android/support/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java
new file mode 100644
index 00000000..d1e44e67
--- /dev/null
+++ b/android/support/wear/widget/drawer/WearableDrawerLayoutEspressoTest.java
@@ -0,0 +1,668 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget.drawer;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
+import static android.support.test.espresso.action.ViewActions.swipeDown;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.test.espresso.matcher.ViewMatchers.withParent;
+import static android.support.test.espresso.matcher.ViewMatchers.withText;
+import static android.support.wear.widget.util.AsyncViewActions.waitForMatchingView;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.content.Intent;
+import android.support.test.espresso.PerformException;
+import android.support.test.espresso.UiController;
+import android.support.test.espresso.ViewAction;
+import android.support.test.espresso.util.HumanReadables;
+import android.support.test.espresso.util.TreeIterables;
+import android.support.test.filters.LargeTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.support.wear.test.R;
+import android.support.wear.widget.drawer.DrawerTestActivity.DrawerStyle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.TimeoutException;
+
+import javax.annotation.Nullable;
+
+/**
+ * Espresso tests for {@link WearableDrawerLayout}.
+ */
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class WearableDrawerLayoutEspressoTest {
+
+ private static final long MAX_WAIT_MS = 4000;
+
+ @Rule public final ActivityTestRule<DrawerTestActivity> activityRule =
+ new ActivityTestRule<>(
+ DrawerTestActivity.class, true /* touchMode */, false /* initialLaunch*/);
+
+ private final Intent mSinglePageIntent =
+ new DrawerTestActivity.Builder().setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE)
+ .build();
+ @Mock WearableNavigationDrawerView.OnItemSelectedListener mNavDrawerItemSelectedListener;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void openingNavigationDrawerDoesNotCloseActionDrawer() {
+ // GIVEN a drawer layout with a peeking action and navigation drawer
+ activityRule.launchActivity(mSinglePageIntent);
+ DrawerTestActivity activity = activityRule.getActivity();
+ WearableDrawerView actionDrawer =
+ (WearableDrawerView) activity.findViewById(R.id.action_drawer);
+ WearableDrawerView navigationDrawer =
+ (WearableDrawerView) activity.findViewById(R.id.navigation_drawer);
+ assertTrue(actionDrawer.isPeeking());
+ assertTrue(navigationDrawer.isPeeking());
+
+ // WHEN the top drawer is opened
+ openDrawer(navigationDrawer);
+ onView(withId(R.id.navigation_drawer))
+ .perform(
+ waitForMatchingView(
+ allOf(withId(R.id.navigation_drawer), isOpened(true)),
+ MAX_WAIT_MS));
+
+ // THEN the action drawer should still be peeking
+ assertTrue(actionDrawer.isPeeking());
+ }
+
+ @Test
+ public void swipingDownNavigationDrawerDoesNotCloseActionDrawer() {
+ // GIVEN a drawer layout with a peeking action and navigation drawer
+ activityRule.launchActivity(mSinglePageIntent);
+ onView(withId(R.id.action_drawer)).check(matches(isPeeking()));
+ onView(withId(R.id.navigation_drawer)).check(matches(isPeeking()));
+
+ // WHEN the top drawer is opened by swiping down
+ onView(withId(R.id.drawer_layout)).perform(swipeDown());
+ onView(withId(R.id.navigation_drawer))
+ .perform(
+ waitForMatchingView(
+ allOf(withId(R.id.navigation_drawer), isOpened(true)),
+ MAX_WAIT_MS));
+
+ // THEN the action drawer should still be peeking
+ onView(withId(R.id.action_drawer)).check(matches(isPeeking()));
+ }
+
+
+ @Test
+ public void firstNavDrawerItemShouldBeSelectedInitially() {
+ // GIVEN a top drawer
+ // WHEN it is first opened
+ activityRule.launchActivity(mSinglePageIntent);
+ onView(withId(R.id.drawer_layout)).perform(swipeDown());
+ onView(withId(R.id.navigation_drawer))
+ .perform(
+ waitForMatchingView(
+ allOf(withId(R.id.navigation_drawer), isOpened(true)),
+ MAX_WAIT_MS));
+
+ // THEN the text should display "0".
+ onView(withId(R.id.ws_nav_drawer_text)).check(matches(withText("0")));
+ }
+
+ @Test
+ public void selectingNavItemChangesTextAndClosedDrawer() {
+ // GIVEN an open top drawer
+ activityRule.launchActivity(mSinglePageIntent);
+ onView(withId(R.id.drawer_layout)).perform(swipeDown());
+ onView(withId(R.id.navigation_drawer))
+ .perform(
+ waitForMatchingView(
+ allOf(withId(R.id.navigation_drawer), isOpened(true)),
+ MAX_WAIT_MS));
+
+ // WHEN the second item is selected
+ onView(withId(R.id.ws_nav_drawer_icon_1)).perform(click());
+
+ // THEN the text should display "1" and it should close.
+ onView(withId(R.id.ws_nav_drawer_text))
+ .perform(
+ waitForMatchingView(
+ allOf(withId(R.id.ws_nav_drawer_text), withText("1")),
+ MAX_WAIT_MS));
+ onView(withId(R.id.navigation_drawer))
+ .perform(
+ waitForMatchingView(
+ allOf(withId(R.id.navigation_drawer), isClosed(true)),
+ MAX_WAIT_MS));
+ }
+
+ @Test
+ public void programmaticallySelectingNavItemChangesTextInSinglePage() {
+ // GIVEN an open top drawer
+ activityRule.launchActivity(new DrawerTestActivity.Builder()
+ .setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE)
+ .openTopDrawerInOnCreate()
+ .build());
+ final WearableNavigationDrawerView navDrawer =
+ activityRule.getActivity().findViewById(R.id.navigation_drawer);
+ navDrawer.addOnItemSelectedListener(mNavDrawerItemSelectedListener);
+
+ // WHEN the second item is selected programmatically
+ selectNavItem(navDrawer, 1);
+
+ // THEN the text should display "1" and the listener should be notified.
+ onView(withId(R.id.ws_nav_drawer_text))
+ .check(matches(withText("1")));
+ verify(mNavDrawerItemSelectedListener).onItemSelected(1);
+ }
+
+ @Test
+ public void programmaticallySelectingNavItemChangesTextInMultiPage() {
+ // GIVEN an open top drawer
+ activityRule.launchActivity(new DrawerTestActivity.Builder()
+ .setStyle(DrawerStyle.BOTH_DRAWER_NAV_MULTI_PAGE)
+ .openTopDrawerInOnCreate()
+ .build());
+ final WearableNavigationDrawerView navDrawer =
+ activityRule.getActivity().findViewById(R.id.navigation_drawer);
+ navDrawer.addOnItemSelectedListener(mNavDrawerItemSelectedListener);
+
+ // WHEN the second item is selected programmatically
+ selectNavItem(navDrawer, 1);
+
+ // THEN the text should display "1" and the listener should be notified.
+ onView(allOf(withId(R.id.ws_navigation_drawer_item_text), isDisplayed()))
+ .check(matches(withText("1")));
+ verify(mNavDrawerItemSelectedListener).onItemSelected(1);
+ }
+
+ @Test
+ public void navDrawerShouldOpenWhenCalledInOnCreate() {
+ // GIVEN an activity which calls openDrawer(Gravity.TOP) in onCreate
+ // WHEN it is launched
+ activityRule.launchActivity(
+ new DrawerTestActivity.Builder()
+ .setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE)
+ .openTopDrawerInOnCreate()
+ .build());
+
+ // THEN the nav drawer should be open
+ onView(withId(R.id.navigation_drawer)).check(matches(isOpened(true)));
+ }
+
+ @Test
+ public void actionDrawerShouldOpenWhenCalledInOnCreate() {
+ // GIVEN an activity with only an action drawer which is opened in onCreate
+ // WHEN it is launched
+ activityRule.launchActivity(
+ new DrawerTestActivity.Builder()
+ .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
+ .openBottomDrawerInOnCreate()
+ .build());
+
+ // THEN the action drawer should be open
+ onView(withId(R.id.action_drawer)).check(matches(isOpened(true)));
+ }
+
+ @Test
+ public void navDrawerShouldOpenWhenCalledInOnCreateAndThenCloseWhenRequested() {
+ // GIVEN an activity which calls openDrawer(Gravity.TOP) in onCreate, then closes it
+ // WHEN it is launched
+ activityRule.launchActivity(
+ new DrawerTestActivity.Builder()
+ .setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE)
+ .openTopDrawerInOnCreate()
+ .closeFirstDrawerOpened()
+ .build());
+
+ // THEN the nav drawer should be open and then close
+ onView(withId(R.id.navigation_drawer))
+ .check(matches(isOpened(true)))
+ .perform(
+ waitForMatchingView(
+ allOf(withId(R.id.navigation_drawer), isClosed(true)),
+ MAX_WAIT_MS));
+ }
+
+ @Test
+ public void openedNavDrawerShouldPreventSwipeToClose() {
+ // GIVEN an activity which calls openDrawer(Gravity.TOP) in onCreate
+ activityRule.launchActivity(
+ new DrawerTestActivity.Builder()
+ .setStyle(DrawerStyle.BOTH_DRAWER_NAV_SINGLE_PAGE)
+ .openTopDrawerInOnCreate()
+ .build());
+
+ // THEN the view should prevent swipe to close
+ onView(withId(R.id.navigation_drawer)).check(matches(not(allowsSwipeToClose())));
+ }
+
+ @Test
+ public void closedNavDrawerShouldNotPreventSwipeToClose() {
+ // GIVEN an activity which doesn't start with the nav drawer open
+ activityRule.launchActivity(mSinglePageIntent);
+
+ // THEN the view should allow swipe to close
+ onView(withId(R.id.navigation_drawer)).check(matches(allowsSwipeToClose()));
+ }
+
+ @Test
+ public void scrolledDownActionDrawerCanScrollUpWhenReOpened() {
+ // GIVEN a freshly launched activity
+ activityRule.launchActivity(mSinglePageIntent);
+ WearableActionDrawerView actionDrawer =
+ (WearableActionDrawerView) activityRule.getActivity()
+ .findViewById(R.id.action_drawer);
+ RecyclerView recyclerView = (RecyclerView) actionDrawer.getDrawerContent();
+
+ // WHEN the action drawer is opened and scrolled to the last item (Item 6)
+ openDrawer(actionDrawer);
+ scrollToPosition(recyclerView, 5);
+ onView(withId(R.id.action_drawer))
+ .perform(
+ waitForMatchingView(allOf(withId(R.id.action_drawer), isOpened(true)),
+ MAX_WAIT_MS))
+ .perform(
+ waitForMatchingView(allOf(withText("Item 6"), isCompletelyDisplayed()),
+ MAX_WAIT_MS));
+ // and then it is peeked
+ peekDrawer(actionDrawer);
+ onView(withId(R.id.action_drawer))
+ .perform(waitForMatchingView(allOf(withId(R.id.action_drawer), isPeeking()),
+ MAX_WAIT_MS));
+ // and re-opened
+ openDrawer(actionDrawer);
+ onView(withId(R.id.action_drawer))
+ .perform(
+ waitForMatchingView(allOf(withId(R.id.action_drawer), isOpened(true)),
+ MAX_WAIT_MS));
+
+ // THEN item 6 should be visible, but swiping down should scroll up, not close the drawer.
+ onView(withText("Item 6")).check(matches(isDisplayed()));
+ onView(withId(R.id.action_drawer)).perform(swipeDown()).check(matches(isOpened(true)));
+ }
+
+ @Test
+ public void actionDrawerPeekIconShouldNotBeNull() {
+ // GIVEN a drawer layout with a peeking action drawer whose menu is initialized in XML
+ activityRule.launchActivity(mSinglePageIntent);
+ DrawerTestActivity activity = activityRule.getActivity();
+ ImageView peekIconView =
+ (ImageView) activity
+ .findViewById(R.id.ws_action_drawer_peek_action_icon);
+ // THEN its peek icon should not be null
+ assertNotNull(peekIconView.getDrawable());
+ }
+
+ @Test
+ public void tappingActionDrawerPeekIconShouldTriggerFirstAction() {
+ // GIVEN a drawer layout with a peeking action drawer, title, and mock click listener
+ activityRule.launchActivity(
+ new DrawerTestActivity.Builder()
+ .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
+ .build());
+ WearableActionDrawerView actionDrawer =
+ (WearableActionDrawerView) activityRule.getActivity()
+ .findViewById(R.id.action_drawer);
+ OnMenuItemClickListener mockClickListener = mock(OnMenuItemClickListener.class);
+ actionDrawer.setOnMenuItemClickListener(mockClickListener);
+ // WHEN the action drawer peek view is tapped
+ onView(withId(R.id.ws_drawer_view_peek_container))
+ .perform(waitForMatchingView(
+ allOf(
+ withId(R.id.ws_drawer_view_peek_container),
+ isCompletelyDisplayed()),
+ MAX_WAIT_MS))
+ .perform(click());
+ // THEN its click listener should be notified
+ verify(mockClickListener).onMenuItemClick(any(MenuItem.class));
+ }
+
+ @Test
+ public void tappingActionDrawerPeekIconShouldTriggerFirstActionAfterItWasOpened() {
+ // GIVEN a drawer layout with an open action drawer with a title, and mock click listener
+ activityRule.launchActivity(
+ new DrawerTestActivity.Builder()
+ .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
+ .openBottomDrawerInOnCreate()
+ .build());
+ WearableActionDrawerView actionDrawer =
+ (WearableActionDrawerView) activityRule.getActivity()
+ .findViewById(R.id.action_drawer);
+ OnMenuItemClickListener mockClickListener = mock(OnMenuItemClickListener.class);
+ actionDrawer.setOnMenuItemClickListener(mockClickListener);
+
+ // WHEN the action drawer is closed to its peek state and then tapped
+ peekDrawer(actionDrawer);
+ onView(withId(R.id.action_drawer))
+ .perform(waitForMatchingView(allOf(withId(R.id.action_drawer), isPeeking()),
+ MAX_WAIT_MS));
+ actionDrawer.getPeekContainer().callOnClick();
+
+ // THEN its click listener should be notified
+ verify(mockClickListener).onMenuItemClick(any(MenuItem.class));
+ }
+
+ @Test
+ public void changingActionDrawerItemShouldUpdateView() {
+ // GIVEN a drawer layout with an open action drawer
+ activityRule.launchActivity(
+ new DrawerTestActivity.Builder()
+ .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
+ .openBottomDrawerInOnCreate()
+ .build());
+ WearableActionDrawerView actionDrawer =
+ activityRule.getActivity().findViewById(R.id.action_drawer);
+ final MenuItem secondItem = actionDrawer.getMenu().getItem(1);
+
+ // WHEN its second item is changed
+ actionDrawer.post(new Runnable() {
+ @Override
+ public void run() {
+ secondItem.setTitle("Modified item");
+ }
+ });
+
+ // THEN the new item should be displayed
+ onView(withText("Modified item")).check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void removingActionDrawerItemShouldUpdateView() {
+ // GIVEN a drawer layout with an open action drawer
+ activityRule.launchActivity(
+ new DrawerTestActivity.Builder()
+ .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
+ .openBottomDrawerInOnCreate()
+ .build());
+ final WearableActionDrawerView actionDrawer =
+ activityRule.getActivity().findViewById(R.id.action_drawer);
+ MenuItem secondItem = actionDrawer.getMenu().getItem(1);
+ final int itemId = secondItem.getItemId();
+ final String title = secondItem.getTitle().toString();
+ final int initialSize = getChildByType(actionDrawer, RecyclerView.class)
+ .getAdapter()
+ .getItemCount();
+
+ // WHEN its second item is removed
+ actionDrawer.post(new Runnable() {
+ @Override
+ public void run() {
+ actionDrawer.getMenu().removeItem(itemId);
+ }
+ });
+
+ // THEN it should decrease by 1 in size and it should no longer contain the item's text
+ onView(allOf(withParent(withId(R.id.action_drawer)), isAssignableFrom(RecyclerView.class)))
+ .perform(waitForRecyclerToBeSize(initialSize - 1, MAX_WAIT_MS))
+ .perform(waitForMatchingView(recyclerWithoutText(is(title)), MAX_WAIT_MS));
+ }
+
+ @Test
+ public void addingActionDrawerItemShouldUpdateView() {
+ // GIVEN a drawer layout with an open action drawer
+ activityRule.launchActivity(
+ new DrawerTestActivity.Builder()
+ .setStyle(DrawerStyle.ONLY_ACTION_DRAWER_WITH_TITLE)
+ .openBottomDrawerInOnCreate()
+ .build());
+ final WearableActionDrawerView actionDrawer =
+ activityRule.getActivity().findViewById(R.id.action_drawer);
+
+ RecyclerView recycler = getChildByType(actionDrawer, RecyclerView.class);
+ final RecyclerView.LayoutManager layoutManager = recycler.getLayoutManager();
+ final int initialSize = recycler.getAdapter().getItemCount();
+
+ // WHEN an item is added and the view is scrolled down (to make sure the view is created)
+ actionDrawer.post(new Runnable() {
+ @Override
+ public void run() {
+ actionDrawer.getMenu().add(0, 42, Menu.NONE, "New Item");
+ layoutManager.scrollToPosition(initialSize);
+ }
+ });
+
+ // THEN it should decrease by 1 in size and the there should be a view with the item's text
+ onView(allOf(withParent(withId(R.id.action_drawer)), isAssignableFrom(RecyclerView.class)))
+ .perform(waitForRecyclerToBeSize(initialSize + 1, MAX_WAIT_MS))
+ .perform(waitForMatchingView(withText("New Item"), MAX_WAIT_MS));
+ }
+
+ private void scrollToPosition(final RecyclerView recyclerView, final int position) {
+ recyclerView.post(new Runnable() {
+ @Override
+ public void run() {
+ recyclerView.scrollToPosition(position);
+ }
+ });
+ }
+
+ private void selectNavItem(final WearableNavigationDrawerView navDrawer, final int index) {
+ navDrawer.post(new Runnable() {
+ @Override
+ public void run() {
+ navDrawer.setCurrentItem(index, false);
+ }
+ });
+ }
+
+ private void peekDrawer(final WearableDrawerView drawer) {
+ drawer.post(new Runnable() {
+ @Override
+ public void run() {
+ drawer.getController().peekDrawer();
+ }
+ });
+ }
+
+ private void openDrawer(final WearableDrawerView drawer) {
+ drawer.post(new Runnable() {
+ @Override
+ public void run() {
+ drawer.getController().openDrawer();
+ }
+ });
+ }
+
+ private static TypeSafeMatcher<View> isOpened(final boolean isOpened) {
+ return new TypeSafeMatcher<View>() {
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("is opened == " + isOpened);
+ }
+
+ @Override
+ public boolean matchesSafely(View view) {
+ return ((WearableDrawerView) view).isOpened() == isOpened;
+ }
+ };
+ }
+
+ private static TypeSafeMatcher<View> isClosed(final boolean isClosed) {
+ return new TypeSafeMatcher<View>() {
+ @Override
+ protected boolean matchesSafely(View view) {
+ WearableDrawerView drawer = (WearableDrawerView) view;
+ return drawer.isClosed() == isClosed;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("is closed");
+ }
+ };
+ }
+
+ private TypeSafeMatcher<View> isPeeking() {
+ return new TypeSafeMatcher<View>() {
+ @Override
+ protected boolean matchesSafely(View view) {
+ WearableDrawerView drawer = (WearableDrawerView) view;
+ return drawer.isPeeking();
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("is peeking");
+ }
+ };
+ }
+
+ private TypeSafeMatcher<View> allowsSwipeToClose() {
+ return new TypeSafeMatcher<View>() {
+ @Override
+ protected boolean matchesSafely(View view) {
+ return !view.canScrollHorizontally(-2) && !view.canScrollHorizontally(2);
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("can be swiped closed");
+ }
+ };
+ }
+
+ /**
+ * Returns a {@link TypeSafeMatcher} that returns {@code true} when the {@link RecyclerView}
+ * does not contain a {@link TextView} with text matched by {@code textMatcher}.
+ */
+ private TypeSafeMatcher<View> recyclerWithoutText(final Matcher<String> textMatcher) {
+ return new TypeSafeMatcher<View>() {
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("Without recycler text ");
+ textMatcher.describeTo(description);
+ }
+
+ @Override
+ public boolean matchesSafely(View view) {
+ if (!(view instanceof RecyclerView)) {
+ return false;
+ }
+
+ RecyclerView recycler = ((RecyclerView) view);
+ if (recycler.isAnimating()) {
+ // While the RecyclerView is animating, it will return null ViewHolders and we
+ // won't be able to tell whether the item has been removed or not.
+ return false;
+ }
+
+ for (int i = 0; i < recycler.getAdapter().getItemCount(); i++) {
+ RecyclerView.ViewHolder holder = recycler.findViewHolderForAdapterPosition(i);
+ if (holder != null) {
+ TextView text = getChildByType(holder.itemView, TextView.class);
+ if (text != null && textMatcher.matches(text.getText())) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+ };
+ }
+
+ /**
+ * Waits for the {@link RecyclerView} to contain {@code targetCount} items, up to {@code millis}
+ * milliseconds. Throws exception if the time limit is reached before reaching the desired
+ * number of items.
+ */
+ public ViewAction waitForRecyclerToBeSize(final int targetCount, final long millis) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isAssignableFrom(RecyclerView.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "Waiting for recycler to be size=" + targetCount;
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ if (!(view instanceof RecyclerView)) {
+ return;
+ }
+
+ RecyclerView recycler = (RecyclerView) view;
+ uiController.loopMainThreadUntilIdle();
+ final long startTime = System.currentTimeMillis();
+ final long endTime = startTime + millis;
+ do {
+ if (recycler.getAdapter().getItemCount() == targetCount) {
+ return;
+ }
+ uiController.loopMainThreadForAtLeast(100); // at least 3 frames
+ } while (System.currentTimeMillis() < endTime);
+
+ // timeout happens
+ throw new PerformException.Builder()
+ .withActionDescription(this.getDescription())
+ .withViewDescription(HumanReadables.describe(view))
+ .withCause(new TimeoutException())
+ .build();
+ }
+ };
+ }
+
+ /**
+ * Returns the first child of {@code root} to be an instance of class {@code T}, or {@code null}
+ * if none were found.
+ */
+ @Nullable
+ private <T> T getChildByType(View root, Class<T> classOfChildToFind) {
+ for (View child : TreeIterables.breadthFirstViewTraversal(root)) {
+ if (classOfChildToFind.isInstance(child)) {
+ return (T) child;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/android/support/wear/widget/util/ArcSwipe.java b/android/support/wear/widget/util/ArcSwipe.java
new file mode 100644
index 00000000..2630d19f
--- /dev/null
+++ b/android/support/wear/widget/util/ArcSwipe.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget.util;
+
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.graphics.RectF;
+import android.os.SystemClock;
+import android.support.annotation.VisibleForTesting;
+import android.support.test.espresso.UiController;
+import android.support.test.espresso.action.MotionEvents;
+import android.support.test.espresso.action.Swiper;
+import android.support.v4.util.Preconditions;
+import android.util.Log;
+import android.view.MotionEvent;
+
+/**
+ * Swiper for gestures meant to be performed on an arc - part of a circle - not a straight line.
+ * This class assumes a square bounding box with the radius of the circle being half the height of
+ * the box.
+ */
+public class ArcSwipe implements Swiper {
+
+ /** Enum describing the exact gesture which will perform the curved swipe. */
+ public enum Gesture {
+ /** Swipes quickly between the co-ordinates, clockwise. */
+ FAST_CLOCKWISE(SWIPE_FAST_DURATION_MS, true),
+ /** Swipes deliberately slowly between the co-ordinates, clockwise. */
+ SLOW_CLOCKWISE(SWIPE_SLOW_DURATION_MS, true),
+ /** Swipes quickly between the co-ordinates, anticlockwise. */
+ FAST_ANTICLOCKWISE(SWIPE_FAST_DURATION_MS, false),
+ /** Swipes deliberately slowly between the co-ordinates, anticlockwise. */
+ SLOW_ANTICLOCKWISE(SWIPE_SLOW_DURATION_MS, false);
+
+ private final int mDuration;
+ private final boolean mClockwise;
+
+ Gesture(int duration, boolean clockwise) {
+ mDuration = duration;
+ mClockwise = clockwise;
+ }
+ }
+
+ /** The number of motion events to send for each swipe. */
+ private static final int SWIPE_EVENT_COUNT = 10;
+
+ /** Length of time a "fast" swipe should last for, in milliseconds. */
+ private static final int SWIPE_FAST_DURATION_MS = 100;
+
+ /** Length of time a "slow" swipe should last for, in milliseconds. */
+ private static final int SWIPE_SLOW_DURATION_MS = 1500;
+
+ private static final String TAG = ArcSwipe.class.getSimpleName();
+ private final RectF mBounds;
+ private final Gesture mGesture;
+
+ public ArcSwipe(Gesture gesture, RectF bounds) {
+ Preconditions.checkArgument(bounds.height() == bounds.width());
+ mGesture = gesture;
+ mBounds = bounds;
+ }
+
+ @Override
+ public Swiper.Status sendSwipe(
+ UiController uiController,
+ float[] startCoordinates,
+ float[] endCoordinates,
+ float[] precision) {
+ return sendArcSwipe(
+ uiController,
+ startCoordinates,
+ endCoordinates,
+ precision,
+ mGesture.mDuration,
+ mGesture.mClockwise);
+ }
+
+ private float[][] interpolate(float[] start, float[] end, int steps, boolean isClockwise) {
+ float startAngle = getAngle(start[0], start[1]);
+ float endAngle = getAngle(end[0], end[1]);
+
+ Path path = new Path();
+ PathMeasure pathMeasure = new PathMeasure();
+ path.moveTo(start[0], start[1]);
+ path.arcTo(mBounds, startAngle, getSweepAngle(startAngle, endAngle, isClockwise));
+ pathMeasure.setPath(path, false);
+ float pathLength = pathMeasure.getLength();
+
+ float[][] res = new float[steps][2];
+ float[] mPathTangent = new float[2];
+
+ for (int i = 1; i < steps + 1; i++) {
+ pathMeasure.getPosTan((pathLength * i) / (steps + 2f), res[i - 1], mPathTangent);
+ }
+
+ return res;
+ }
+
+ private Swiper.Status sendArcSwipe(
+ UiController uiController,
+ float[] startCoordinates,
+ float[] endCoordinates,
+ float[] precision,
+ int duration,
+ boolean isClockwise) {
+
+ float[][] steps = interpolate(startCoordinates, endCoordinates, SWIPE_EVENT_COUNT,
+ isClockwise);
+ final int delayBetweenMovements = duration / steps.length;
+
+ MotionEvent downEvent = MotionEvents.sendDown(uiController, startCoordinates,
+ precision).down;
+ try {
+ for (int i = 0; i < steps.length; i++) {
+ if (!MotionEvents.sendMovement(uiController, downEvent, steps[i])) {
+ Log.e(TAG,
+ "Injection of move event as part of the swipe failed. Sending cancel "
+ + "event.");
+ MotionEvents.sendCancel(uiController, downEvent);
+ return Swiper.Status.FAILURE;
+ }
+
+ long desiredTime = downEvent.getDownTime() + delayBetweenMovements * i;
+ long timeUntilDesired = desiredTime - SystemClock.uptimeMillis();
+ if (timeUntilDesired > 10) {
+ uiController.loopMainThreadForAtLeast(timeUntilDesired);
+ }
+ }
+
+ if (!MotionEvents.sendUp(uiController, downEvent, endCoordinates)) {
+ Log.e(TAG,
+ "Injection of up event as part of the swipe failed. Sending cancel event.");
+ MotionEvents.sendCancel(uiController, downEvent);
+ return Swiper.Status.FAILURE;
+ }
+ } finally {
+ downEvent.recycle();
+ }
+ return Swiper.Status.SUCCESS;
+ }
+
+ @VisibleForTesting
+ float getAngle(double x, double y) {
+ double relativeX = x - (mBounds.width() / 2);
+ double relativeY = y - (mBounds.height() / 2);
+ double rowAngle = Math.atan2(relativeX, relativeY);
+ double angle = -Math.toDegrees(rowAngle) - 180;
+ if (angle < 0) {
+ angle += 360;
+ }
+ return (float) angle;
+ }
+
+ @VisibleForTesting
+ float getSweepAngle(float startAngle, float endAngle, boolean isClockwise) {
+ float sweepAngle = endAngle - startAngle;
+ if (sweepAngle < 0) {
+ sweepAngle += 360;
+ }
+ return isClockwise ? sweepAngle : (360 - sweepAngle);
+ }
+}
diff --git a/android/support/wear/widget/util/ArcSwipeTest.java b/android/support/wear/widget/util/ArcSwipeTest.java
new file mode 100644
index 00000000..f403e1b3
--- /dev/null
+++ b/android/support/wear/widget/util/ArcSwipeTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget.util;
+
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.RectF;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link ArcSwipe}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ArcSwipeTest {
+ private ArcSwipe mArcSwipeUnderTest;
+ private final RectF mFakeBounds = new RectF(0, 0, 400, 400);
+
+ @Before
+ public void setup() {
+ mArcSwipeUnderTest = new ArcSwipe(ArcSwipe.Gesture.FAST_CLOCKWISE, mFakeBounds);
+ }
+
+ @Test
+ public void testSweepAngleClockwise() {
+ assertEquals(0, mArcSwipeUnderTest.getSweepAngle(0, 0, true), 0.0f);
+ assertEquals(360, mArcSwipeUnderTest.getSweepAngle(0, 360, true), 0.0f);
+ assertEquals(90, mArcSwipeUnderTest.getSweepAngle(0, 90, true), 0.0f);
+ assertEquals(90, mArcSwipeUnderTest.getSweepAngle(90, 180, true), 0.0f);
+ assertEquals(225, mArcSwipeUnderTest.getSweepAngle(45, 270, true), 0.0f);
+ assertEquals(270, mArcSwipeUnderTest.getSweepAngle(90, 0, true), 0.0f);
+ assertEquals(170, mArcSwipeUnderTest.getSweepAngle(280, 90, true), 0.0f);
+ }
+
+ @Test
+ public void testSweepAngleAntiClockwise() {
+ assertEquals(360, mArcSwipeUnderTest.getSweepAngle(0, 0, false), 0.0f);
+ assertEquals(0, mArcSwipeUnderTest.getSweepAngle(0, 360, false), 0.0f);
+ assertEquals(270, mArcSwipeUnderTest.getSweepAngle(0, 90, false), 0.0f);
+ assertEquals(270, mArcSwipeUnderTest.getSweepAngle(90, 180, false), 0.0f);
+ assertEquals(135, mArcSwipeUnderTest.getSweepAngle(45, 270, false), 0.0f);
+ assertEquals(90, mArcSwipeUnderTest.getSweepAngle(90, 0, false), 0.0f);
+ assertEquals(190, mArcSwipeUnderTest.getSweepAngle(280, 90, false), 0.0f);
+ }
+
+ @Test
+ public void testGetAngle() {
+ assertEquals(0, mArcSwipeUnderTest.getAngle(200, 0), 0.0f);
+ assertEquals(90, mArcSwipeUnderTest.getAngle(400, 200), 0.0f);
+ assertEquals(180, mArcSwipeUnderTest.getAngle(200, 400), 0.0f);
+ assertEquals(270, mArcSwipeUnderTest.getAngle(0, 200), 0.0f);
+ }
+}
diff --git a/android/support/wear/widget/util/AsyncViewActions.java b/android/support/wear/widget/util/AsyncViewActions.java
new file mode 100644
index 00000000..3db4619b
--- /dev/null
+++ b/android/support/wear/widget/util/AsyncViewActions.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.wear.widget.util;
+
+import android.support.test.espresso.PerformException;
+import android.support.test.espresso.UiController;
+import android.support.test.espresso.ViewAction;
+import android.support.test.espresso.util.HumanReadables;
+import android.support.test.espresso.util.TreeIterables;
+import android.view.View;
+
+import org.hamcrest.Matchers;
+import org.hamcrest.StringDescription;
+
+import java.util.concurrent.TimeoutException;
+
+public class AsyncViewActions {
+
+ /** Perform action of waiting for a specific view id. */
+ public static ViewAction waitForMatchingView(
+ final org.hamcrest.Matcher<? extends View> viewMatcher, final long millis) {
+ return new ViewAction() {
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+ final long startTime = System.currentTimeMillis();
+ final long endTime = startTime + millis;
+ do {
+ for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
+ // found matching view
+ if (viewMatcher.matches(child)) {
+ return;
+ }
+ }
+ uiController.loopMainThreadForAtLeast(100); // at least 3 frames
+ } while (System.currentTimeMillis() < endTime);
+
+ // timeout happens
+ throw new PerformException.Builder()
+ .withActionDescription(this.getDescription())
+ .withViewDescription(HumanReadables.describe(view))
+ .withCause(new TimeoutException())
+ .build();
+ }
+
+ @Override
+ public String getDescription() {
+ return "Wait for view which matches " + StringDescription.asString(viewMatcher);
+ }
+
+ @Override
+ public org.hamcrest.Matcher<View> getConstraints() {
+ return Matchers.any(View.class);
+ }
+ };
+ }
+}
diff --git a/android/support/wear/widget/util/MoreViewAssertions.java b/android/support/wear/widget/util/MoreViewAssertions.java
new file mode 100644
index 00000000..503336ba
--- /dev/null
+++ b/android/support/wear/widget/util/MoreViewAssertions.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2016 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.support.wear.widget.util;
+
+import static android.support.test.espresso.matcher.ViewMatchers.assertThat;
+
+import android.support.test.espresso.NoMatchingViewException;
+import android.support.test.espresso.ViewAssertion;
+import android.support.test.espresso.util.HumanReadables;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+public class MoreViewAssertions {
+
+ public static ViewAssertion left(final Matcher<Integer> matcher) {
+ return new ViewAssertion() {
+ @Override
+ public void check(View view, NoMatchingViewException noViewException) {
+ assertThat("View left: " + HumanReadables.describe(view), view.getLeft(), matcher);
+ }
+ };
+ }
+
+ public static ViewAssertion approximateTop(final Matcher<Double> matcher) {
+ return new ViewAssertion() {
+ @Override
+ public void check(View view, NoMatchingViewException noViewException) {
+ assertThat("View top: " + HumanReadables.describe(view), ((double) view.getTop()),
+ matcher);
+ }
+ };
+ }
+
+ public static ViewAssertion top(final Matcher<Integer> matcher) {
+ return new ViewAssertion() {
+ @Override
+ public void check(View view, NoMatchingViewException noViewException) {
+ assertThat("View top: " + HumanReadables.describe(view), view.getTop(), matcher);
+ }
+ };
+ }
+
+ public static ViewAssertion right(final Matcher<Integer> matcher) {
+ return new ViewAssertion() {
+ @Override
+ public void check(View view, NoMatchingViewException noViewException) {
+ assertThat("View right: " + HumanReadables.describe(view), view.getRight(),
+ matcher);
+ }
+ };
+ }
+
+ public static ViewAssertion bottom(final Matcher<Integer> matcher) {
+ return new ViewAssertion() {
+ @Override
+ public void check(View view, NoMatchingViewException noViewException) {
+ assertThat("View bottom: " + HumanReadables.describe(view), view.getBottom(),
+ matcher);
+ }
+ };
+ }
+
+ public static ViewAssertion approximateBottom(final Matcher<Double> matcher) {
+ return new ViewAssertion() {
+ @Override
+ public void check(View view, NoMatchingViewException noViewException) {
+ assertThat("View bottom: " + HumanReadables.describe(view), ((double) view
+ .getBottom()), matcher);
+ }
+ };
+ }
+
+ /**
+ * Returns a new ViewAssertion against a match of the view's left position, relative to the
+ * left
+ * edge of the containing window.
+ *
+ * @param matcher matcher for the left position
+ */
+ public static ViewAssertion screenLeft(final Matcher<Integer> matcher) {
+ return new ViewAssertion() {
+ @Override
+ public void check(View view, NoMatchingViewException noViewException) {
+ int[] screenXy = {0, 0};
+ view.getLocationInWindow(screenXy);
+ assertThat("View screenLeft: " + HumanReadables.describe(view), screenXy[0],
+ matcher);
+ }
+ };
+ }
+
+ /**
+ * Returns a new ViewAssertion against a match of the view's top position, relative to the top
+ * edge of the containing window.
+ *
+ * @param matcher matcher for the top position
+ */
+ public static ViewAssertion screenTop(final Matcher<Integer> matcher) {
+ return new ViewAssertion() {
+ @Override
+ public void check(View view, NoMatchingViewException noViewException) {
+ int[] screenXy = {0, 0};
+ view.getLocationInWindow(screenXy);
+ assertThat("View screenTop: " + HumanReadables.describe(view), screenXy[1],
+ matcher);
+ }
+ };
+ }
+
+ /**
+ * Returns a new ViewAssertion against a match of the view's right position, relative to the
+ * left
+ * edge of the containing window.
+ *
+ * @param matcher matcher for the right position
+ */
+ public static ViewAssertion screenRight(final Matcher<Integer> matcher) {
+ return new ViewAssertion() {
+ @Override
+ public void check(View view, NoMatchingViewException noViewException) {
+ int[] screenXy = {0, 0};
+ view.getLocationInWindow(screenXy);
+ assertThat("View screenRight: " + HumanReadables.describe(view),
+ screenXy[0] + view.getWidth(), matcher);
+ }
+ };
+ }
+
+ /**
+ * Returns a new ViewAssertion against a match of the view's bottom position, relative to the
+ * top
+ * edge of the containing window.
+ *
+ * @param matcher matcher for the bottom position
+ */
+ public static ViewAssertion screenBottom(final Matcher<Integer> matcher) {
+ return new ViewAssertion() {
+ @Override
+ public void check(View view, NoMatchingViewException noViewException) {
+ int[] screenXy = {0, 0};
+ view.getLocationInWindow(screenXy);
+ assertThat("View screenBottom: " + HumanReadables.describe(view),
+ screenXy[1] + view.getHeight(), matcher);
+ }
+ };
+ }
+
+ public static Matcher<View> withTranslationX(final int xTranslation) {
+ return new TypeSafeMatcher<View>() {
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("with x translation == " + xTranslation);
+ }
+
+ @Override
+ public boolean matchesSafely(View view) {
+ return view.getTranslationX() == xTranslation;
+ }
+ };
+ }
+
+ public static Matcher<RecyclerView> withPositiveVerticalScrollOffset() {
+ return new TypeSafeMatcher<RecyclerView>() {
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("with positive y scroll offset");
+ }
+
+ @Override
+ public boolean matchesSafely(RecyclerView view) {
+ return view.computeVerticalScrollOffset() > 0;
+ }
+ };
+ }
+
+ public static Matcher<RecyclerView> withNoVerticalScrollOffset() {
+ return new TypeSafeMatcher<RecyclerView>() {
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("with no y scroll offset");
+ }
+
+ @Override
+ public boolean matchesSafely(RecyclerView view) {
+ return view.computeVerticalScrollOffset() == 0;
+ }
+ };
+ }
+}
diff --git a/android/support/wear/widget/util/WakeLockRule.java b/android/support/wear/widget/util/WakeLockRule.java
new file mode 100644
index 00000000..13b627e7
--- /dev/null
+++ b/android/support/wear/widget/util/WakeLockRule.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 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.support.wear.widget.util;
+
+import android.content.Context;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.support.test.InstrumentationRegistry;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Rule which holds a wake lock for the duration of the test.
+ */
+public class WakeLockRule implements TestRule {
+ @SuppressWarnings("deprecation")
+ private static final int WAKELOCK_FLAGS =
+ PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP;
+
+ @Override
+ public Statement apply(final Statement statement, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ WakeLock wakeLock = createWakeLock();
+ wakeLock.acquire();
+ try {
+ statement.evaluate();
+ } finally {
+ wakeLock.release();
+ }
+ }
+ };
+ }
+
+ private WakeLock createWakeLock() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ PowerManager power = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ return power.newWakeLock(WAKELOCK_FLAGS, context.getPackageName());
+ }
+}
diff --git a/android/system/OsConstants.java b/android/system/OsConstants.java
index 3e1f007d..83a1b41a 100644
--- a/android/system/OsConstants.java
+++ b/android/system/OsConstants.java
@@ -368,6 +368,7 @@ public final class OsConstants {
public static final int O_APPEND = placeholder();
public static final int O_CLOEXEC = placeholder();
public static final int O_CREAT = placeholder();
+ /** @hide */ public static final int O_DIRECT = placeholder();
public static final int O_EXCL = placeholder();
public static final int O_NOCTTY = placeholder();
public static final int O_NOFOLLOW = placeholder();
diff --git a/android/telecom/Call.java b/android/telecom/Call.java
index a07f2bbf..20911012 100644
--- a/android/telecom/Call.java
+++ b/android/telecom/Call.java
@@ -416,8 +416,15 @@ public final class Call {
*/
public static final int PROPERTY_SELF_MANAGED = 0x00000100;
+ /**
+ * Indicates the call used Assisted Dialing.
+ * See also {@link Connection#PROPERTY_ASSISTED_DIALING_USED}
+ * @hide
+ */
+ public static final int PROPERTY_ASSISTED_DIALING_USED = 0x00000200;
+
//******************************************************************************************
- // Next PROPERTY value: 0x00000200
+ // Next PROPERTY value: 0x00000400
//******************************************************************************************
private final String mTelecomCallId;
@@ -577,6 +584,9 @@ public final class Call {
if(hasProperty(properties, PROPERTY_HAS_CDMA_VOICE_PRIVACY)) {
builder.append(" PROPERTY_HAS_CDMA_VOICE_PRIVACY");
}
+ if(hasProperty(properties, PROPERTY_ASSISTED_DIALING_USED)) {
+ builder.append(" PROPERTY_ASSISTED_DIALING_USED");
+ }
builder.append("]");
return builder.toString();
}
@@ -858,7 +868,8 @@ public final class Call {
* @hide
*/
@IntDef({HANDOVER_FAILURE_DEST_APP_REJECTED, HANDOVER_FAILURE_DEST_NOT_SUPPORTED,
- HANDOVER_FAILURE_DEST_INVALID_PERM, HANDOVER_FAILURE_DEST_USER_REJECTED})
+ HANDOVER_FAILURE_DEST_INVALID_PERM, HANDOVER_FAILURE_DEST_USER_REJECTED,
+ HANDOVER_FAILURE_ONGOING_EMERG_CALL})
@Retention(RetentionPolicy.SOURCE)
public @interface HandoverFailureErrors {}
@@ -886,6 +897,12 @@ public final class Call {
*/
public static final int HANDOVER_FAILURE_DEST_USER_REJECTED = 4;
+ /**
+ * Handover failure reason returned via {@link #onHandoverFailed(Call, int)} when there
+ * is ongoing emergency call.
+ */
+ public static final int HANDOVER_FAILURE_ONGOING_EMERG_CALL = 5;
+
/**
* Invoked when the state of this {@code Call} has changed. See {@link #getState()}.
@@ -1935,6 +1952,15 @@ public final class Call {
}
}
+ /** {@hide} */
+ final void internalOnHandoverFailed(int error) {
+ for (CallbackRecord<Callback> record : mCallbackRecords) {
+ final Call call = this;
+ final Callback callback = record.getCallback();
+ record.getHandler().post(() -> callback.onHandoverFailed(call, error));
+ }
+ }
+
private void fireStateChanged(final int newState) {
for (CallbackRecord<Callback> record : mCallbackRecords) {
final Call call = this;
diff --git a/android/telecom/Connection.java b/android/telecom/Connection.java
index 2bb1c4ed..aaef8d3d 100644
--- a/android/telecom/Connection.java
+++ b/android/telecom/Connection.java
@@ -23,7 +23,6 @@ import com.android.internal.telecom.IVideoProvider;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
-import android.annotation.TestApi;
import android.app.Notification;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
@@ -397,13 +396,17 @@ public abstract class Connection extends Conferenceable {
/**
* Set by the framework to indicate that a connection has an active RTT session associated with
* it.
- * @hide
*/
- @TestApi
public static final int PROPERTY_IS_RTT = 1 << 8;
+ /**
+ * Set by the framework to indicate that a connection is using assisted dialing.
+ * @hide
+ */
+ public static final int PROPERTY_ASSISTED_DIALING_USED = 1 << 9;
+
//**********************************************************************************************
- // Next PROPERTY value: 1<<9
+ // Next PROPERTY value: 1<<10
//**********************************************************************************************
/**
@@ -831,9 +834,7 @@ public abstract class Connection extends Conferenceable {
/**
* Provides methods to read and write RTT data to/from the in-call app.
- * @hide
*/
- @TestApi
public static final class RttTextStream {
private static final int READ_BUFFER_SIZE = 1000;
private final InputStreamReader mPipeFromInCall;
@@ -2608,10 +2609,8 @@ public abstract class Connection extends Conferenceable {
/**
* Informs listeners that a previously requested RTT session via
* {@link ConnectionRequest#isRequestingRtt()} or
- * {@link #onStartRtt(ParcelFileDescriptor, ParcelFileDescriptor)} has succeeded.
- * @hide
+ * {@link #onStartRtt(RttTextStream)} has succeeded.
*/
- @TestApi
public final void sendRttInitiationSuccess() {
setRttProperty();
mListeners.forEach((l) -> l.onRttInitiationSuccess(Connection.this));
@@ -2619,14 +2618,11 @@ public abstract class Connection extends Conferenceable {
/**
* Informs listeners that a previously requested RTT session via
- * {@link ConnectionRequest#isRequestingRtt()} or
- * {@link #onStartRtt(ParcelFileDescriptor, ParcelFileDescriptor)}
+ * {@link ConnectionRequest#isRequestingRtt()} or {@link #onStartRtt(RttTextStream)}
* has failed.
* @param reason One of the reason codes defined in {@link RttModifyStatus}, with the
* exception of {@link RttModifyStatus#SESSION_MODIFY_REQUEST_SUCCESS}.
- * @hide
*/
- @TestApi
public final void sendRttInitiationFailure(int reason) {
unsetRttProperty();
mListeners.forEach((l) -> l.onRttInitiationFailure(Connection.this, reason));
@@ -2635,9 +2631,7 @@ public abstract class Connection extends Conferenceable {
/**
* Informs listeners that a currently active RTT session has been terminated by the remote
* side of the coll.
- * @hide
*/
- @TestApi
public final void sendRttSessionRemotelyTerminated() {
mListeners.forEach((l) -> l.onRttSessionRemotelyTerminated(Connection.this));
}
@@ -2645,9 +2639,7 @@ public abstract class Connection extends Conferenceable {
/**
* Informs listeners that the remote side of the call has requested an upgrade to include an
* RTT session in the call.
- * @hide
*/
- @TestApi
public final void sendRemoteRttRequest() {
mListeners.forEach((l) -> l.onRemoteRttRequest(Connection.this));
}
@@ -2864,17 +2856,13 @@ public abstract class Connection extends Conferenceable {
* request, respectively.
* @param rttTextStream The object that should be used to send text to or receive text from
* the in-call app.
- * @hide
*/
- @TestApi
public void onStartRtt(@NonNull RttTextStream rttTextStream) {}
/**
* Notifies this {@link Connection} that it should terminate any existing RTT communication
* channel. No response to Telecom is needed for this method.
- * @hide
*/
- @TestApi
public void onStopRtt() {}
/**
@@ -2882,11 +2870,9 @@ public abstract class Connection extends Conferenceable {
* request sent via {@link #sendRemoteRttRequest}. Acceptance of the request is
* indicated by the supplied {@link RttTextStream} being non-null, and rejection is
* indicated by {@code rttTextStream} being {@code null}
- * @hide
* @param rttTextStream The object that should be used to send text to or receive text from
* the in-call app.
*/
- @TestApi
public void handleRttUpgradeResponse(@Nullable RttTextStream rttTextStream) {}
/**
diff --git a/android/telecom/ConnectionRequest.java b/android/telecom/ConnectionRequest.java
index e169e5f8..658b4734 100644
--- a/android/telecom/ConnectionRequest.java
+++ b/android/telecom/ConnectionRequest.java
@@ -16,7 +16,6 @@
package android.telecom;
-import android.annotation.TestApi;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
@@ -310,9 +309,7 @@ public final class ConnectionRequest implements Parcelable {
* send and receive RTT text to/from the in-call app.
* @return An instance of {@link android.telecom.Connection.RttTextStream}, or {@code null}
* if this connection request is not requesting an RTT session upon connection establishment.
- * @hide
*/
- @TestApi
public Connection.RttTextStream getRttTextStream() {
if (isRequestingRtt()) {
return new Connection.RttTextStream(mRttPipeToInCall, mRttPipeFromInCall);
@@ -324,9 +321,7 @@ public final class ConnectionRequest implements Parcelable {
/**
* Convenience method for determining whether the ConnectionRequest is requesting an RTT session
* @return {@code true} if RTT is requested, {@code false} otherwise.
- * @hide
*/
- @TestApi
public boolean isRequestingRtt() {
return mRttPipeFromInCall != null && mRttPipeToInCall != null;
}
diff --git a/android/telecom/ConnectionService.java b/android/telecom/ConnectionService.java
index 7e833066..6af01aee 100644
--- a/android/telecom/ConnectionService.java
+++ b/android/telecom/ConnectionService.java
@@ -21,6 +21,7 @@ import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -143,6 +144,9 @@ public abstract class ConnectionService extends Service {
private static final String SESSION_START_RTT = "CS.+RTT";
private static final String SESSION_STOP_RTT = "CS.-RTT";
private static final String SESSION_RTT_UPGRADE_RESPONSE = "CS.rTRUR";
+ private static final String SESSION_CONNECTION_SERVICE_FOCUS_LOST = "CS.cSFL";
+ private static final String SESSION_CONNECTION_SERVICE_FOCUS_GAINED = "CS.cSFG";
+ private static final String SESSION_HANDOVER_FAILED = "CS.haF";
private static final int MSG_ADD_CONNECTION_SERVICE_ADAPTER = 1;
private static final int MSG_CREATE_CONNECTION = 2;
@@ -172,6 +176,9 @@ public abstract class ConnectionService extends Service {
private static final int MSG_ON_STOP_RTT = 27;
private static final int MSG_RTT_UPGRADE_RESPONSE = 28;
private static final int MSG_CREATE_CONNECTION_COMPLETE = 29;
+ private static final int MSG_CONNECTION_SERVICE_FOCUS_LOST = 30;
+ private static final int MSG_CONNECTION_SERVICE_FOCUS_GAINED = 31;
+ private static final int MSG_HANDOVER_FAILED = 32;
private static Connection sNullConnection;
@@ -275,6 +282,22 @@ public abstract class ConnectionService extends Service {
}
@Override
+ public void handoverFailed(String callId, ConnectionRequest request, int reason,
+ Session.Info sessionInfo) {
+ Log.startSession(sessionInfo, SESSION_HANDOVER_FAILED);
+ try {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = request;
+ args.arg3 = Log.createSubsession();
+ args.arg4 = reason;
+ mHandler.obtainMessage(MSG_HANDOVER_FAILED, args).sendToTarget();
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
public void abort(String callId, Session.Info sessionInfo) {
Log.startSession(sessionInfo, SESSION_ABORT);
try {
@@ -591,6 +614,26 @@ public abstract class ConnectionService extends Service {
Log.endSession();
}
}
+
+ @Override
+ public void connectionServiceFocusLost(Session.Info sessionInfo) throws RemoteException {
+ Log.startSession(sessionInfo, SESSION_CONNECTION_SERVICE_FOCUS_LOST);
+ try {
+ mHandler.obtainMessage(MSG_CONNECTION_SERVICE_FOCUS_LOST).sendToTarget();
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
+ public void connectionServiceFocusGained(Session.Info sessionInfo) throws RemoteException {
+ Log.startSession(sessionInfo, SESSION_CONNECTION_SERVICE_FOCUS_GAINED);
+ try {
+ mHandler.obtainMessage(MSG_CONNECTION_SERVICE_FOCUS_GAINED).sendToTarget();
+ } finally {
+ Log.endSession();
+ }
+ }
};
private final Handler mHandler = new Handler(Looper.getMainLooper()) {
@@ -723,6 +766,36 @@ public abstract class ConnectionService extends Service {
}
break;
}
+ case MSG_HANDOVER_FAILED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ Log.continueSession((Session) args.arg3, SESSION_HANDLER +
+ SESSION_HANDOVER_FAILED);
+ try {
+ final String id = (String) args.arg1;
+ final ConnectionRequest request = (ConnectionRequest) args.arg2;
+ final int reason = (int) args.arg4;
+ if (!mAreAccountsInitialized) {
+ Log.d(this, "Enqueueing pre-init request %s", id);
+ mPreInitializationConnectionRequests.add(
+ new android.telecom.Logging.Runnable(
+ SESSION_HANDLER
+ + SESSION_HANDOVER_FAILED + ".pICR",
+ null /*lock*/) {
+ @Override
+ public void loggedRun() {
+ handoverFailed(id, request, reason);
+ }
+ }.prepare());
+ } else {
+ Log.i(this, "createConnectionFailed %s", id);
+ handoverFailed(id, request, reason);
+ }
+ } finally {
+ args.recycle();
+ Log.endSession();
+ }
+ break;
+ }
case MSG_ABORT: {
SomeArgs args = (SomeArgs) msg.obj;
Log.continueSession((Session) args.arg2, SESSION_HANDLER + SESSION_ABORT);
@@ -1012,6 +1085,12 @@ public abstract class ConnectionService extends Service {
}
break;
}
+ case MSG_CONNECTION_SERVICE_FOCUS_GAINED:
+ onConnectionServiceFocusGained();
+ break;
+ case MSG_CONNECTION_SERVICE_FOCUS_LOST:
+ onConnectionServiceFocusLost();
+ break;
default:
break;
}
@@ -1371,13 +1450,25 @@ public abstract class ConnectionService extends Service {
isIncoming,
isUnknown);
- Connection connection = isUnknown ? onCreateUnknownConnection(callManagerAccount, request)
- : isIncoming ? onCreateIncomingConnection(callManagerAccount, request)
- : onCreateOutgoingConnection(callManagerAccount, request);
+ Connection connection = null;
+ if (getApplicationContext().getApplicationInfo().targetSdkVersion >
+ Build.VERSION_CODES.O_MR1 && request.getExtras() != null &&
+ request.getExtras().getBoolean(TelecomManager.EXTRA_IS_HANDOVER,false)) {
+ if (!isIncoming) {
+ connection = onCreateOutgoingHandoverConnection(callManagerAccount, request);
+ } else {
+ connection = onCreateIncomingHandoverConnection(callManagerAccount, request);
+ }
+ } else {
+ connection = isUnknown ? onCreateUnknownConnection(callManagerAccount, request)
+ : isIncoming ? onCreateIncomingConnection(callManagerAccount, request)
+ : onCreateOutgoingConnection(callManagerAccount, request);
+ }
Log.d(this, "createConnection, connection: %s", connection);
if (connection == null) {
+ Log.i(this, "createConnection, implementation returned null connection.");
connection = Connection.createFailedConnection(
- new DisconnectCause(DisconnectCause.ERROR));
+ new DisconnectCause(DisconnectCause.ERROR, "IMPL_RETURNED_NULL_CONNECTION"));
}
connection.setTelecomCallId(callId);
@@ -1442,6 +1533,13 @@ public abstract class ConnectionService extends Service {
}
}
+ private void handoverFailed(final String callId, final ConnectionRequest request,
+ int reason) {
+
+ Log.i(this, "handoverFailed %s", callId);
+ onHandoverFailed(request, reason);
+ }
+
/**
* Called by Telecom when the creation of a new Connection has completed and it is now added
* to Telecom.
@@ -1863,6 +1961,16 @@ public abstract class ConnectionService extends Service {
}
/**
+ * Call to inform Telecom that your {@link ConnectionService} has released call resources (e.g
+ * microphone, camera).
+ *
+ * @see ConnectionService#onConnectionServiceFocusLost()
+ */
+ public final void connectionServiceFocusReleased() {
+ mAdapter.onConnectionServiceFocusReleased();
+ }
+
+ /**
* Adds a connection created by the {@link ConnectionService} and informs telecom of the new
* connection.
*
@@ -2136,6 +2244,20 @@ public abstract class ConnectionService extends Service {
public void onRemoteExistingConnectionAdded(RemoteConnection connection) {}
/**
+ * Called when the {@link ConnectionService} has lost the call focus.
+ * The {@link ConnectionService} should release the call resources and invokes
+ * {@link ConnectionService#connectionServiceFocusReleased()} to inform telecom that it has
+ * released the call resources.
+ */
+ public void onConnectionServiceFocusLost() {}
+
+ /**
+ * Called when the {@link ConnectionService} has gained the call focus. The
+ * {@link ConnectionService} can acquire the call resources at this time.
+ */
+ public void onConnectionServiceFocusGained() {}
+
+ /**
* @hide
*/
public boolean containsConference(Conference conference) {
diff --git a/android/telecom/ConnectionServiceAdapter.java b/android/telecom/ConnectionServiceAdapter.java
index 92a9dc23..0d319bbc 100644
--- a/android/telecom/ConnectionServiceAdapter.java
+++ b/android/telecom/ConnectionServiceAdapter.java
@@ -628,4 +628,17 @@ final class ConnectionServiceAdapter implements DeathRecipient {
}
}
}
+
+ /**
+ * Notifies Telecom that the {@link ConnectionService} has released the call resource.
+ */
+ void onConnectionServiceFocusReleased() {
+ for (IConnectionServiceAdapter adapter : mAdapters) {
+ try {
+ Log.d(this, "onConnectionServiceFocusReleased");
+ adapter.onConnectionServiceFocusReleased(Log.getExternalSession());
+ } catch (RemoteException ignored) {
+ }
+ }
+ }
}
diff --git a/android/telecom/ConnectionServiceAdapterServant.java b/android/telecom/ConnectionServiceAdapterServant.java
index 3fbdeb1e..3e1bf779 100644
--- a/android/telecom/ConnectionServiceAdapterServant.java
+++ b/android/telecom/ConnectionServiceAdapterServant.java
@@ -73,6 +73,7 @@ final class ConnectionServiceAdapterServant {
private static final int MSG_ON_RTT_REMOTELY_TERMINATED = 32;
private static final int MSG_ON_RTT_UPGRADE_REQUEST = 33;
private static final int MSG_SET_PHONE_ACCOUNT_CHANGED = 34;
+ private static final int MSG_CONNECTION_SERVICE_FOCUS_RELEASED = 35;
private final IConnectionServiceAdapter mDelegate;
@@ -329,6 +330,9 @@ final class ConnectionServiceAdapterServant {
}
break;
}
+ case MSG_CONNECTION_SERVICE_FOCUS_RELEASED:
+ mDelegate.onConnectionServiceFocusReleased(null /*Session.Info*/);
+ break;
}
}
};
@@ -601,6 +605,11 @@ final class ConnectionServiceAdapterServant {
args.arg2 = pHandle;
mHandler.obtainMessage(MSG_SET_PHONE_ACCOUNT_CHANGED, args).sendToTarget();
}
+
+ @Override
+ public void onConnectionServiceFocusReleased(Session.Info sessionInfo) {
+ mHandler.obtainMessage(MSG_CONNECTION_SERVICE_FOCUS_RELEASED).sendToTarget();
+ }
};
public ConnectionServiceAdapterServant(IConnectionServiceAdapter delegate) {
diff --git a/android/telecom/InCallService.java b/android/telecom/InCallService.java
index d558bbae..74fa62d6 100644
--- a/android/telecom/InCallService.java
+++ b/android/telecom/InCallService.java
@@ -80,6 +80,7 @@ public abstract class InCallService extends Service {
private static final int MSG_ON_CONNECTION_EVENT = 9;
private static final int MSG_ON_RTT_UPGRADE_REQUEST = 10;
private static final int MSG_ON_RTT_INITIATION_FAILURE = 11;
+ private static final int MSG_ON_HANDOVER_FAILED = 12;
/** Default Handler used to consolidate binder method calls onto a single thread. */
private final Handler mHandler = new Handler(Looper.getMainLooper()) {
@@ -150,6 +151,12 @@ public abstract class InCallService extends Service {
mPhone.internalOnRttInitiationFailure(callId, reason);
break;
}
+ case MSG_ON_HANDOVER_FAILED: {
+ String callId = (String) msg.obj;
+ int error = msg.arg1;
+ mPhone.internalOnHandoverFailed(callId, error);
+ break;
+ }
default:
break;
}
@@ -225,6 +232,11 @@ public abstract class InCallService extends Service {
public void onRttInitiationFailure(String callId, int reason) {
mHandler.obtainMessage(MSG_ON_RTT_INITIATION_FAILURE, reason, 0, callId).sendToTarget();
}
+
+ @Override
+ public void onHandoverFailed(String callId, int error) {
+ mHandler.obtainMessage(MSG_ON_HANDOVER_FAILED, error, 0, callId).sendToTarget();
+ }
}
private Phone.Listener mPhoneListener = new Phone.Listener() {
diff --git a/android/telecom/Phone.java b/android/telecom/Phone.java
index 421b1a4b..b5394b9b 100644
--- a/android/telecom/Phone.java
+++ b/android/telecom/Phone.java
@@ -223,6 +223,13 @@ public final class Phone {
}
}
+ final void internalOnHandoverFailed(String callId, int error) {
+ Call call = mCallByTelecomCallId.get(callId);
+ if (call != null) {
+ call.internalOnHandoverFailed(error);
+ }
+ }
+
/**
* Called to destroy the phone and cleanup any lingering calls.
*/
diff --git a/android/telecom/PhoneAccount.java b/android/telecom/PhoneAccount.java
index 74b94650..fcfc5931 100644
--- a/android/telecom/PhoneAccount.java
+++ b/android/telecom/PhoneAccount.java
@@ -964,6 +964,9 @@ public final class PhoneAccount implements Parcelable {
if (hasCapabilities(CAPABILITY_SIM_SUBSCRIPTION)) {
sb.append("SimSub ");
}
+ if (hasCapabilities(CAPABILITY_RTT)) {
+ sb.append("Rtt");
+ }
return sb.toString();
}
diff --git a/android/telecom/RemoteConnectionService.java b/android/telecom/RemoteConnectionService.java
index 85906ad1..59ce5908 100644
--- a/android/telecom/RemoteConnectionService.java
+++ b/android/telecom/RemoteConnectionService.java
@@ -213,6 +213,9 @@ final class RemoteConnectionService {
}
@Override
+ public void onConnectionServiceFocusReleased(Session.Info sessionInfo) {}
+
+ @Override
public void addConferenceCall(
final String callId, ParcelableConference parcel, Session.Info sessionInfo) {
RemoteConference conference = new RemoteConference(callId,
diff --git a/android/telecom/TelecomManager.java b/android/telecom/TelecomManager.java
index 92d458f1..15355ac7 100644
--- a/android/telecom/TelecomManager.java
+++ b/android/telecom/TelecomManager.java
@@ -582,13 +582,29 @@ public class TelecomManager {
"android.telecom.extra.CALL_BACK_INTENT";
/**
+ * The dialer activity responsible for placing emergency calls from, for example, a locked
+ * keyguard.
+ * @hide
+ */
+ public static final ComponentName EMERGENCY_DIALER_COMPONENT =
+ ComponentName.createRelative("com.android.phone", ".EmergencyDialer");
+
+ /**
+ * The boolean indicated by this extra controls whether or not a call is eligible to undergo
+ * assisted dialing. This extra is stored under {@link #EXTRA_OUTGOING_CALL_EXTRAS}.
+ * @hide
+ */
+ public static final String EXTRA_USE_ASSISTED_DIALING =
+ "android.telecom.extra.USE_ASSISTED_DIALING";
+
+ /**
* The following 4 constants define how properties such as phone numbers and names are
* displayed to the user.
*/
/**
* Indicates that the address or number of a call is allowed to be displayed for caller ID.
- */
+ */
public static final int PRESENTATION_ALLOWED = 1;
/**
diff --git a/android/telephony/CarrierConfigManager.java b/android/telephony/CarrierConfigManager.java
index 69371a18..6a47d050 100644
--- a/android/telephony/CarrierConfigManager.java
+++ b/android/telephony/CarrierConfigManager.java
@@ -300,10 +300,10 @@ public class CarrierConfigManager {
* If true all networks are considered as home network a.k.a non-roaming. When false,
* the 2 pairs of CMDA and GSM roaming/non-roaming arrays are consulted.
*
- * @see KEY_GSM_ROAMING_NETWORKS_STRING_ARRAY
- * @see KEY_GSM_NONROAMING_NETWORKS_STRING_ARRAY
- * @see KEY_CDMA_ROAMING_NETWORKS_STRING_ARRAY
- * @see KEY_CDMA_NONROAMING_NETWORKS_STRING_ARRAY
+ * @see #KEY_GSM_ROAMING_NETWORKS_STRING_ARRAY
+ * @see #KEY_GSM_NONROAMING_NETWORKS_STRING_ARRAY
+ * @see #KEY_CDMA_ROAMING_NETWORKS_STRING_ARRAY
+ * @see #KEY_CDMA_NONROAMING_NETWORKS_STRING_ARRAY
*/
public static final String
KEY_FORCE_HOME_NETWORK_BOOL = "force_home_network_bool";
@@ -735,6 +735,13 @@ public class CarrierConfigManager {
public static final String KEY_SHOW_ICCID_IN_SIM_STATUS_BOOL = "show_iccid_in_sim_status_bool";
/**
+ * Flag specifying whether the {@link android.telephony.SignalStrength} is shown in the SIM
+ * Status screen. The default value is true.
+ */
+ public static final String KEY_SHOW_SIGNAL_STRENGTH_IN_SIM_STATUS_BOOL =
+ "show_signal_strength_in_sim_status_bool";
+
+ /**
* Flag specifying whether an additional (client initiated) intent needs to be sent on System
* update
*/
@@ -989,6 +996,12 @@ public class CarrierConfigManager {
public static final String KEY_STK_DISABLE_LAUNCH_BROWSER_BOOL =
"stk_disable_launch_browser_bool";
+ /**
+ * Boolean indicating if show data RAT icon on status bar even when data is disabled
+ * @hide
+ */
+ public static final String KEY_ALWAYS_SHOW_DATA_RAT_ICON_BOOL =
+ "always_show_data_rat_icon_bool";
// These variables are used by the MMS service and exposed through another API, {@link
// SmsManager}. The variable names and string values are copied from there.
@@ -1296,6 +1309,19 @@ public class CarrierConfigManager {
*/
public static final String KEY_ALLOW_HOLD_IN_IMS_CALL_BOOL = "allow_hold_in_ims_call";
+
+ /**
+ * Flag indicating whether the carrier always wants to play an "on-hold" tone when a call has
+ * been remotely held.
+ * <p>
+ * When {@code true}, if the IMS stack indicates that the call session has been held, a signal
+ * will be sent from Telephony to play an audible "on-hold" tone played to the user.
+ * When {@code false}, a hold tone will only be played if the audio session becomes inactive.
+ * @hide
+ */
+ public static final String KEY_ALWAYS_PLAY_REMOTE_HOLD_TONE_BOOL =
+ "always_play_remote_hold_tone_bool";
+
/**
* When true, indicates that adding a call is disabled when there is an ongoing video call
* or when there is an ongoing call on wifi which was downgraded from video and VoWifi is
@@ -1634,6 +1660,11 @@ public class CarrierConfigManager {
"show_ims_registration_status_bool";
/**
+ * Flag indicating whether the carrier supports RTT over IMS.
+ */
+ public static final String KEY_RTT_SUPPORTED_BOOL = "rtt_supported_bool";
+
+ /**
* The flag to disable the popup dialog which warns the user of data charges.
* @hide
*/
@@ -1686,12 +1717,20 @@ public class CarrierConfigManager {
public static final String KEY_SPN_DISPLAY_RULE_USE_ROAMING_FROM_SERVICE_STATE_BOOL =
"spn_display_rule_use_roaming_from_service_state_bool";
+ /**
+ * Determines whether any carrier has been identified and its specific config has been applied,
+ * default to false.
+ * @hide
+ */
+ public static final String KEY_CARRIER_CONFIG_APPLIED_BOOL = "carrier_config_applied_bool";
+
/** The default value for every variable. */
private final static PersistableBundle sDefaults;
static {
sDefaults = new PersistableBundle();
sDefaults.putBoolean(KEY_ALLOW_HOLD_IN_IMS_CALL_BOOL, true);
+ sDefaults.putBoolean(KEY_ALWAYS_PLAY_REMOTE_HOLD_TONE_BOOL, false);
sDefaults.putBoolean(KEY_ADDITIONAL_CALL_SETTING_BOOL, true);
sDefaults.putBoolean(KEY_ALLOW_EMERGENCY_NUMBERS_IN_CALL_LOG_BOOL, false);
sDefaults.putBoolean(KEY_ALLOW_LOCAL_DTMF_TONES_BOOL, true);
@@ -1768,6 +1807,7 @@ public class CarrierConfigManager {
sDefaults.putString(KEY_CARRIER_VVM_PACKAGE_NAME_STRING, "");
sDefaults.putStringArray(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY, null);
sDefaults.putBoolean(KEY_SHOW_ICCID_IN_SIM_STATUS_BOOL, false);
+ sDefaults.putBoolean(KEY_SHOW_SIGNAL_STRENGTH_IN_SIM_STATUS_BOOL, true);
sDefaults.putBoolean(KEY_CI_ACTION_ON_SYS_UPDATE_BOOL, false);
sDefaults.putString(KEY_CI_ACTION_ON_SYS_UPDATE_INTENT_STRING, "");
sDefaults.putString(KEY_CI_ACTION_ON_SYS_UPDATE_EXTRA_STRING, "");
@@ -1963,10 +2003,13 @@ public class CarrierConfigManager {
sDefaults.putStringArray(KEY_NON_ROAMING_OPERATOR_STRING_ARRAY, null);
sDefaults.putStringArray(KEY_ROAMING_OPERATOR_STRING_ARRAY, null);
sDefaults.putBoolean(KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL, false);
+ sDefaults.putBoolean(KEY_RTT_SUPPORTED_BOOL, false);
sDefaults.putBoolean(KEY_DISABLE_CHARGE_INDICATION_BOOL, false);
sDefaults.putStringArray(KEY_FEATURE_ACCESS_CODES_STRING_ARRAY, null);
sDefaults.putBoolean(KEY_IDENTIFY_HIGH_DEFINITION_CALLS_IN_CALL_LOG_BOOL, false);
sDefaults.putBoolean(KEY_SPN_DISPLAY_RULE_USE_ROAMING_FROM_SERVICE_STATE_BOOL, false);
+ sDefaults.putBoolean(KEY_ALWAYS_SHOW_DATA_RAT_ICON_BOOL, false);
+ sDefaults.putBoolean(KEY_CARRIER_CONFIG_APPLIED_BOOL, false);
}
/**
@@ -2012,6 +2055,33 @@ public class CarrierConfigManager {
}
/**
+ * Determines whether a configuration {@link PersistableBundle} obtained from
+ * {@link #getConfig()} or {@link #getConfigForSubId(int)} corresponds to an identified carrier.
+ * <p>
+ * When an app receives the {@link CarrierConfigManager#ACTION_CARRIER_CONFIG_CHANGED}
+ * broadcast which informs it that the carrier configuration has changed, it is possible
+ * that another reload of the carrier configuration has begun since the intent was sent.
+ * In this case, the carrier configuration the app fetches (e.g. via {@link #getConfig()})
+ * may not represent the configuration for the current carrier. It should be noted that it
+ * does not necessarily mean the configuration belongs to current carrier when this function
+ * return true because it may belong to another previous identified carrier. Users should
+ * always call {@link #getConfig()} or {@link #getConfigForSubId(int)} after receiving the
+ * broadcast {@link #ACTION_CARRIER_CONFIG_CHANGED}.
+ * </p>
+ * <p>
+ * After using {@link #getConfig()} or {@link #getConfigForSubId(int)} an app should always
+ * use this method to confirm whether any carrier specific configuration has been applied.
+ * </p>
+ *
+ * @param bundle the configuration bundle to be checked.
+ * @return boolean true if any carrier specific configuration bundle has been applied, false
+ * otherwise or the bundle is null.
+ */
+ public static boolean isConfigForIdentifiedCarrier(PersistableBundle bundle) {
+ return bundle != null && bundle.getBoolean(KEY_CARRIER_CONFIG_APPLIED_BOOL);
+ }
+
+ /**
* Calling this method triggers telephony services to fetch the current carrier configuration.
* <p>
* Normally this does not need to be called because the platform reloads config on its own.
@@ -2024,7 +2094,7 @@ public class CarrierConfigManager {
* {@link android.service.carrier.CarrierService#onLoadConfig} will be called from an
* arbitrary thread.
* </p>
- * @see #hasCarrierPrivileges
+ * @see TelephonyManager#hasCarrierPrivileges
*/
public void notifyConfigChangedForSubId(int subId) {
try {
diff --git a/android/telephony/CellIdentityGsm.java b/android/telephony/CellIdentityGsm.java
index c9684062..376e6aa7 100644
--- a/android/telephony/CellIdentityGsm.java
+++ b/android/telephony/CellIdentityGsm.java
@@ -202,7 +202,7 @@ public final class CellIdentityGsm implements Parcelable {
* @return a 5 or 6 character string (MCC+MNC), null if any field is unknown
*/
public String getMobileNetworkOperator() {
- return (mMncStr == null || mMncStr == null) ? null : mMccStr + mMncStr;
+ return (mMccStr == null || mMncStr == null) ? null : mMccStr + mMncStr;
}
/**
diff --git a/android/telephony/CellIdentityLte.java b/android/telephony/CellIdentityLte.java
index 825dcc33..6ca5daf6 100644
--- a/android/telephony/CellIdentityLte.java
+++ b/android/telephony/CellIdentityLte.java
@@ -213,7 +213,7 @@ public final class CellIdentityLte implements Parcelable {
* @return a 5 or 6 character string (MCC+MNC), null if any field is unknown
*/
public String getMobileNetworkOperator() {
- return (mMncStr == null || mMncStr == null) ? null : mMccStr + mMncStr;
+ return (mMccStr == null || mMncStr == null) ? null : mMccStr + mMncStr;
}
/**
diff --git a/android/telephony/CellIdentityWcdma.java b/android/telephony/CellIdentityWcdma.java
index e74b5701..e4bb4f29 100644
--- a/android/telephony/CellIdentityWcdma.java
+++ b/android/telephony/CellIdentityWcdma.java
@@ -208,7 +208,7 @@ public final class CellIdentityWcdma implements Parcelable {
* @return a 5 or 6 character string (MCC+MNC), null if any field is unknown
*/
public String getMobileNetworkOperator() {
- return (mMncStr == null || mMncStr == null) ? null : mMccStr + mMncStr;
+ return (mMccStr == null || mMncStr == null) ? null : mMccStr + mMncStr;
}
/**
diff --git a/android/telephony/DisconnectCause.java b/android/telephony/DisconnectCause.java
index c3a2ceb1..56e1e640 100644
--- a/android/telephony/DisconnectCause.java
+++ b/android/telephony/DisconnectCause.java
@@ -280,6 +280,36 @@ public class DisconnectCause {
* {@hide}
*/
public static final int NORMAL_UNSPECIFIED = 65;
+
+ /**
+ * Stk Call Control modified DIAL request to video DIAL request.
+ * {@hide}
+ */
+ public static final int DIAL_MODIFIED_TO_DIAL_VIDEO = 66;
+
+ /**
+ * Stk Call Control modified Video DIAL request to SS request.
+ * {@hide}
+ */
+ public static final int DIAL_VIDEO_MODIFIED_TO_SS = 67;
+
+ /**
+ * Stk Call Control modified Video DIAL request to USSD request.
+ * {@hide}
+ */
+ public static final int DIAL_VIDEO_MODIFIED_TO_USSD = 68;
+
+ /**
+ * Stk Call Control modified Video DIAL request to DIAL request.
+ * {@hide}
+ */
+ public static final int DIAL_VIDEO_MODIFIED_TO_DIAL = 69;
+
+ /**
+ * Stk Call Control modified Video DIAL request to Video DIAL request.
+ * {@hide}
+ */
+ public static final int DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO = 70;
//*********************************************************************************************
// When adding a disconnect type:
// 1) Update toString() with the newly added disconnect type.
@@ -382,6 +412,16 @@ public class DisconnectCause {
return "DIAL_MODIFIED_TO_SS";
case DIAL_MODIFIED_TO_DIAL:
return "DIAL_MODIFIED_TO_DIAL";
+ case DIAL_MODIFIED_TO_DIAL_VIDEO:
+ return "DIAL_MODIFIED_TO_DIAL_VIDEO";
+ case DIAL_VIDEO_MODIFIED_TO_SS:
+ return "DIAL_VIDEO_MODIFIED_TO_SS";
+ case DIAL_VIDEO_MODIFIED_TO_USSD:
+ return "DIAL_VIDEO_MODIFIED_TO_USSD";
+ case DIAL_VIDEO_MODIFIED_TO_DIAL:
+ return "DIAL_VIDEO_MODIFIED_TO_DIAL";
+ case DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO:
+ return "DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO";
case ERROR_UNSPECIFIED:
return "ERROR_UNSPECIFIED";
case OUTGOING_FAILURE:
diff --git a/android/telephony/MbmsDownloadSession.java b/android/telephony/MbmsDownloadSession.java
index f392570e..059a2d07 100644
--- a/android/telephony/MbmsDownloadSession.java
+++ b/android/telephony/MbmsDownloadSession.java
@@ -347,6 +347,7 @@ public class MbmsDownloadSession implements AutoCloseable {
@Override
public void onServiceDisconnected(ComponentName name) {
+ Log.w(LOG_TAG, "bindAndInitialize: Remote service disconnected");
sIsInitialized.set(false);
mService.set(null);
}
@@ -385,6 +386,7 @@ public class MbmsDownloadSession implements AutoCloseable {
} catch (RemoteException e) {
Log.w(LOG_TAG, "Remote process died");
mService.set(null);
+ sIsInitialized.set(false);
sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
}
}
@@ -438,6 +440,7 @@ public class MbmsDownloadSession implements AutoCloseable {
}
} catch (RemoteException e) {
mService.set(null);
+ sIsInitialized.set(false);
sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
return;
}
@@ -499,8 +502,10 @@ public class MbmsDownloadSession implements AutoCloseable {
* Asynchronous errors through the callback may include any error not specific to the
* streaming use-case.
* @param request The request that specifies what should be downloaded.
+ * @return {@link MbmsErrors#SUCCESS} if the operation did not encounter a synchronous error,
+ * and some other error code otherwise.
*/
- public void download(@NonNull DownloadRequest request) {
+ public int download(@NonNull DownloadRequest request) {
IMbmsDownloadService downloadService = mService.get();
if (downloadService == null) {
throw new IllegalStateException("Middleware not yet bound");
@@ -516,12 +521,16 @@ public class MbmsDownloadSession implements AutoCloseable {
setTempFileRootDirectory(tempRootDirectory);
}
- writeDownloadRequestToken(request);
try {
- downloadService.download(request);
+ int result = downloadService.download(request);
+ if (result == MbmsErrors.SUCCESS) {
+ writeDownloadRequestToken(request);
+ }
+ return result;
} catch (RemoteException e) {
mService.set(null);
- sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
+ sIsInitialized.set(false);
+ return MbmsErrors.ERROR_MIDDLEWARE_LOST;
}
}
@@ -542,6 +551,7 @@ public class MbmsDownloadSession implements AutoCloseable {
return downloadService.listPendingDownloads(mSubscriptionId);
} catch (RemoteException e) {
mService.set(null);
+ sIsInitialized.set(false);
sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
return Collections.emptyList();
}
@@ -560,8 +570,10 @@ public class MbmsDownloadSession implements AutoCloseable {
* @param callback The callback that should be called when the middleware has information to
* share on the download.
* @param handler The {@link Handler} on which calls to {@code callback} should be enqueued on.
+ * @return {@link MbmsErrors#SUCCESS} if the operation did not encounter a synchronous error,
+ * and some other error code otherwise.
*/
- public void registerStateCallback(@NonNull DownloadRequest request,
+ public int registerStateCallback(@NonNull DownloadRequest request,
@NonNull DownloadStateCallback callback, @NonNull Handler handler) {
IMbmsDownloadService downloadService = mService.get();
if (downloadService == null) {
@@ -578,15 +590,15 @@ public class MbmsDownloadSession implements AutoCloseable {
if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
throw new IllegalArgumentException("Unknown download request.");
}
- sendErrorToApp(result, null);
- return;
+ return result;
}
} catch (RemoteException e) {
mService.set(null);
- sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
- return;
+ sIsInitialized.set(false);
+ return MbmsErrors.ERROR_MIDDLEWARE_LOST;
}
mInternalDownloadCallbacks.put(callback, internalCallback);
+ return MbmsErrors.SUCCESS;
}
/**
@@ -600,8 +612,10 @@ public class MbmsDownloadSession implements AutoCloseable {
*
* @param request The {@link DownloadRequest} provided during registration
* @param callback The callback provided during registration.
+ * @return {@link MbmsErrors#SUCCESS} if the operation did not encounter a synchronous error,
+ * and some other error code otherwise.
*/
- public void unregisterStateCallback(@NonNull DownloadRequest request,
+ public int unregisterStateCallback(@NonNull DownloadRequest request,
@NonNull DownloadStateCallback callback) {
try {
IMbmsDownloadService downloadService = mService.get();
@@ -611,6 +625,9 @@ public class MbmsDownloadSession implements AutoCloseable {
InternalDownloadStateCallback internalCallback =
mInternalDownloadCallbacks.get(callback);
+ if (internalCallback == null) {
+ throw new IllegalArgumentException("Provided callback was never registered");
+ }
try {
int result = downloadService.unregisterStateCallback(request, internalCallback);
@@ -618,11 +635,12 @@ public class MbmsDownloadSession implements AutoCloseable {
if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
throw new IllegalArgumentException("Unknown download request.");
}
- sendErrorToApp(result, null);
+ return result;
}
} catch (RemoteException e) {
mService.set(null);
- sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
+ sIsInitialized.set(false);
+ return MbmsErrors.ERROR_MIDDLEWARE_LOST;
}
} finally {
InternalDownloadStateCallback internalCallback =
@@ -631,6 +649,7 @@ public class MbmsDownloadSession implements AutoCloseable {
internalCallback.stop();
}
}
+ return MbmsErrors.SUCCESS;
}
/**
@@ -640,8 +659,10 @@ public class MbmsDownloadSession implements AutoCloseable {
* this method will throw an {@link IllegalArgumentException}.
*
* @param downloadRequest The download request that you wish to cancel.
+ * @return {@link MbmsErrors#SUCCESS} if the operation did not encounter a synchronous error,
+ * and some other error code otherwise.
*/
- public void cancelDownload(@NonNull DownloadRequest downloadRequest) {
+ public int cancelDownload(@NonNull DownloadRequest downloadRequest) {
IMbmsDownloadService downloadService = mService.get();
if (downloadService == null) {
throw new IllegalStateException("Middleware not yet bound");
@@ -653,15 +674,15 @@ public class MbmsDownloadSession implements AutoCloseable {
if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
throw new IllegalArgumentException("Unknown download request.");
}
- sendErrorToApp(result, null);
- return;
+ } else {
+ deleteDownloadRequestToken(downloadRequest);
}
+ return result;
} catch (RemoteException e) {
mService.set(null);
- sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
- return;
+ sIsInitialized.set(false);
+ return MbmsErrors.ERROR_MIDDLEWARE_LOST;
}
- deleteDownloadRequestToken(downloadRequest);
}
/**
@@ -686,6 +707,7 @@ public class MbmsDownloadSession implements AutoCloseable {
return downloadService.getDownloadStatus(downloadRequest, fileInfo);
} catch (RemoteException e) {
mService.set(null);
+ sIsInitialized.set(false);
sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
return STATUS_UNKNOWN;
}
@@ -727,6 +749,7 @@ public class MbmsDownloadSession implements AutoCloseable {
}
} catch (RemoteException e) {
mService.set(null);
+ sIsInitialized.set(false);
sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
}
}
diff --git a/android/telephony/NetworkScan.java b/android/telephony/NetworkScan.java
index f15fde8f..a2772126 100644
--- a/android/telephony/NetworkScan.java
+++ b/android/telephony/NetworkScan.java
@@ -19,50 +19,92 @@ package android.telephony;
import android.content.Context;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.annotation.IntDef;
import android.util.Log;
import com.android.internal.telephony.ITelephony;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
- * Allows applications to request the system to perform a network scan.
- *
- * The caller of {@link #requestNetworkScan(NetworkScanRequest, NetworkScanCallback)} will
- * receive a NetworkScan which contains the callback method to stop the scan requested.
- * @hide
+ * The caller of
+ * {@link TelephonyManager#requestNetworkScan(NetworkScanRequest, NetworkScanCallback)}
+ * will receive an instance of {@link NetworkScan}, which contains a callback method
+ * {@link #stop()} for stopping the in-progress scan.
*/
public class NetworkScan {
- public static final String TAG = "NetworkScan";
+ private static final String TAG = "NetworkScan";
// Below errors are mapped from RadioError which is returned from RIL. We will consolidate
// RadioErrors during the mapping if those RadioErrors mean no difference to the users.
+
+ /**
+ * Defines acceptable values of scan error code.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ERROR_MODEM_ERROR, ERROR_INVALID_SCAN, ERROR_MODEM_UNAVAILABLE, ERROR_UNSUPPORTED,
+ ERROR_RADIO_INTERFACE_ERROR, ERROR_INVALID_SCANID, ERROR_INTERRUPTED})
+ public @interface ScanErrorCode {}
+
+ /**
+ * The RIL has successfully performed the network scan.
+ */
public static final int SUCCESS = 0; // RadioError:NONE
+
+ /**
+ * The scan has failed due to some modem errors.
+ */
public static final int ERROR_MODEM_ERROR = 1; // RadioError:RADIO_NOT_AVAILABLE
// RadioError:NO_MEMORY
// RadioError:INTERNAL_ERR
// RadioError:MODEM_ERR
// RadioError:OPERATION_NOT_ALLOWED
+
+ /**
+ * The parameters of the scan is invalid.
+ */
public static final int ERROR_INVALID_SCAN = 2; // RadioError:INVALID_ARGUMENTS
- public static final int ERROR_MODEM_BUSY = 3; // RadioError:DEVICE_IN_USE
+
+ /**
+ * The modem can not perform the scan because it is doing something else.
+ */
+ public static final int ERROR_MODEM_UNAVAILABLE = 3; // RadioError:DEVICE_IN_USE
+
+ /**
+ * The modem does not support the request scan.
+ */
public static final int ERROR_UNSUPPORTED = 4; // RadioError:REQUEST_NOT_SUPPORTED
+
// Below errors are generated at the Telephony.
- public static final int ERROR_RIL_ERROR = 10000; // Nothing or only exception is
- // returned from RIL.
- public static final int ERROR_INVALID_SCANID = 10001; // The scanId is invalid. The user is
- // either trying to stop a scan which
- // does not exist or started by others.
- public static final int ERROR_INTERRUPTED = 10002; // Scan was interrupted by another scan
- // with higher priority.
+
+ /**
+ * The RIL returns nothing or exceptions.
+ */
+ public static final int ERROR_RADIO_INTERFACE_ERROR = 10000;
+
+ /**
+ * The scan ID is invalid. The user is either trying to stop a scan which does not exist
+ * or started by others.
+ */
+ public static final int ERROR_INVALID_SCANID = 10001;
+
+ /**
+ * The scan has been interrupted by another scan with higher priority.
+ */
+ public static final int ERROR_INTERRUPTED = 10002;
+
private final int mScanId;
private final int mSubId;
/**
* Stops the network scan
*
- * This is the callback method to stop an ongoing scan. When user requests a new scan,
- * a NetworkScan object will be returned, and the user can stop the scan by calling this
- * method.
+ * Use this method to stop an ongoing scan. When user requests a new scan, a {@link NetworkScan}
+ * object will be returned, and the user can stop the scan by calling this method.
*/
public void stop() throws RemoteException {
try {
diff --git a/android/telephony/NetworkScanRequest.java b/android/telephony/NetworkScanRequest.java
index 9674c930..ea503c3e 100644
--- a/android/telephony/NetworkScanRequest.java
+++ b/android/telephony/NetworkScanRequest.java
@@ -16,11 +16,14 @@
package android.telephony;
+import android.annotation.IntDef;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.ArrayList;
import java.util.Arrays;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/**
* Defines a request to peform a network scan.
@@ -28,7 +31,6 @@ import java.util.Arrays;
* This class defines whether the network scan will be performed only once or periodically until
* cancelled, when the scan is performed periodically, the time interval is not controlled by the
* user but defined by the modem vendor.
- * @hide
*/
public final class NetworkScanRequest implements Parcelable {
@@ -54,6 +56,14 @@ public final class NetworkScanRequest implements Parcelable {
/** @hide */
public static final int MAX_INCREMENTAL_PERIODICITY_SEC = 10;
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SCAN_TYPE_ONE_SHOT,
+ SCAN_TYPE_PERIODIC,
+ })
+ public @interface ScanType {}
+
/** Performs the scan only once */
public static final int SCAN_TYPE_ONE_SHOT = 0;
/**
@@ -65,21 +75,21 @@ public final class NetworkScanRequest implements Parcelable {
public static final int SCAN_TYPE_PERIODIC = 1;
/** Defines the type of the scan. */
- public int scanType;
+ private int mScanType;
/**
* Search periodicity (in seconds).
* Expected range for the input is [5s - 300s]
- * This value must be less than or equal to maxSearchTime
+ * This value must be less than or equal to mMaxSearchTime
*/
- public int searchPeriodicity;
+ private int mSearchPeriodicity;
/**
* Maximum duration of the periodic search (in seconds).
* Expected range for the input is [60s - 3600s]
* If the search lasts this long, it will be terminated.
*/
- public int maxSearchTime;
+ private int mMaxSearchTime;
/**
* Indicates whether the modem should report incremental
@@ -87,18 +97,18 @@ public final class NetworkScanRequest implements Parcelable {
* FALSE – Incremental results are not reported.
* TRUE (default) – Incremental results are reported
*/
- public boolean incrementalResults;
+ private boolean mIncrementalResults;
/**
* Indicates the periodicity with which the modem should
* report incremental results to the client (in seconds).
* Expected range for the input is [1s - 10s]
- * This value must be less than or equal to maxSearchTime
+ * This value must be less than or equal to mMaxSearchTime
*/
- public int incrementalResultsPeriodicity;
+ private int mIncrementalResultsPeriodicity;
/** Describes the radio access technologies with bands or channels that need to be scanned. */
- public RadioAccessSpecifier[] specifiers;
+ private RadioAccessSpecifier[] mSpecifiers;
/**
* Describes the List of PLMN ids (MCC-MNC)
@@ -107,20 +117,24 @@ public final class NetworkScanRequest implements Parcelable {
* If list not sent, search to be completed till end and all PLMNs found to be reported.
* Max size of array is MAX_MCC_MNC_LIST_SIZE
*/
- public ArrayList<String> mccMncs;
+ private ArrayList<String> mMccMncs;
/**
- * Creates a new NetworkScanRequest with scanType and network specifiers
+ * Creates a new NetworkScanRequest with mScanType and network mSpecifiers
*
- * @param scanType The type of the scan
+ * @param scanType The type of the scan, can be either one shot or periodic
* @param specifiers the radio network with bands / channels to be scanned
- * @param searchPeriodicity Search periodicity (in seconds)
- * @param maxSearchTime Maximum duration of the periodic search (in seconds)
+ * @param searchPeriodicity The modem will restart the scan every searchPeriodicity seconds if
+ * no network has been found, until it reaches the maxSearchTime. Only
+ * valid when scan type is periodic scan.
+ * @param maxSearchTime Maximum duration of the search (in seconds)
* @param incrementalResults Indicates whether the modem should report incremental
* results of the network scan to the client
* @param incrementalResultsPeriodicity Indicates the periodicity with which the modem should
- * report incremental results to the client (in seconds)
- * @param mccMncs Describes the List of PLMN ids (MCC-MNC)
+ * report incremental results to the client (in seconds),
+ * only valid when incrementalResults is true
+ * @param mccMncs Describes the list of PLMN ids (MCC-MNC), once any network in the list has
+ * been found, the scan will be terminated by the modem.
*/
public NetworkScanRequest(int scanType, RadioAccessSpecifier[] specifiers,
int searchPeriodicity,
@@ -128,19 +142,63 @@ public final class NetworkScanRequest implements Parcelable {
boolean incrementalResults,
int incrementalResultsPeriodicity,
ArrayList<String> mccMncs) {
- this.scanType = scanType;
- this.specifiers = specifiers;
- this.searchPeriodicity = searchPeriodicity;
- this.maxSearchTime = maxSearchTime;
- this.incrementalResults = incrementalResults;
- this.incrementalResultsPeriodicity = incrementalResultsPeriodicity;
- if (mccMncs != null) {
- this.mccMncs = mccMncs;
+ this.mScanType = scanType;
+ this.mSpecifiers = specifiers.clone();
+ this.mSearchPeriodicity = searchPeriodicity;
+ this.mMaxSearchTime = maxSearchTime;
+ this.mIncrementalResults = incrementalResults;
+ this.mIncrementalResultsPeriodicity = incrementalResultsPeriodicity;
+ if (mMccMncs != null) {
+ this.mMccMncs = (ArrayList<String>) mccMncs.clone();
} else {
- this.mccMncs = new ArrayList<>();
+ this.mMccMncs = new ArrayList<>();
}
}
+ /** Returns the type of the scan. */
+ @ScanType
+ public int getScanType() {
+ return mScanType;
+ }
+
+ /** Returns the search periodicity in seconds. */
+ public int getSearchPeriodicity() {
+ return mSearchPeriodicity;
+ }
+
+ /** Returns maximum duration of the periodic search in seconds. */
+ public int getMaxSearchTime() {
+ return mMaxSearchTime;
+ }
+
+ /**
+ * Returns whether incremental result is enabled.
+ * FALSE – Incremental results is not enabled.
+ * TRUE – Incremental results is reported.
+ */
+ public boolean getIncrementalResults() {
+ return mIncrementalResults;
+ }
+
+ /** Returns the periodicity in seconds of incremental results. */
+ public int getIncrementalResultsPeriodicity() {
+ return mIncrementalResultsPeriodicity;
+ }
+
+ /** Returns the radio access technologies with bands or channels that need to be scanned. */
+ public RadioAccessSpecifier[] getSpecifiers() {
+ return mSpecifiers.clone();
+ }
+
+ /**
+ * Returns the List of PLMN ids (MCC-MNC) for early termination of scan.
+ * If any PLMN of this list is found, search should end at that point and
+ * results with all PLMN found till that point should be sent as response.
+ */
+ public ArrayList<String> getPlmns() {
+ return (ArrayList<String>) mMccMncs.clone();
+ }
+
@Override
public int describeContents() {
return 0;
@@ -148,26 +206,26 @@ public final class NetworkScanRequest implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(scanType);
- dest.writeParcelableArray(specifiers, flags);
- dest.writeInt(searchPeriodicity);
- dest.writeInt(maxSearchTime);
- dest.writeBoolean(incrementalResults);
- dest.writeInt(incrementalResultsPeriodicity);
- dest.writeStringList(mccMncs);
+ dest.writeInt(mScanType);
+ dest.writeParcelableArray(mSpecifiers, flags);
+ dest.writeInt(mSearchPeriodicity);
+ dest.writeInt(mMaxSearchTime);
+ dest.writeBoolean(mIncrementalResults);
+ dest.writeInt(mIncrementalResultsPeriodicity);
+ dest.writeStringList(mMccMncs);
}
private NetworkScanRequest(Parcel in) {
- scanType = in.readInt();
- specifiers = (RadioAccessSpecifier[]) in.readParcelableArray(
+ mScanType = in.readInt();
+ mSpecifiers = (RadioAccessSpecifier[]) in.readParcelableArray(
Object.class.getClassLoader(),
RadioAccessSpecifier.class);
- searchPeriodicity = in.readInt();
- maxSearchTime = in.readInt();
- incrementalResults = in.readBoolean();
- incrementalResultsPeriodicity = in.readInt();
- mccMncs = new ArrayList<>();
- in.readStringList(mccMncs);
+ mSearchPeriodicity = in.readInt();
+ mMaxSearchTime = in.readInt();
+ mIncrementalResults = in.readBoolean();
+ mIncrementalResultsPeriodicity = in.readInt();
+ mMccMncs = new ArrayList<>();
+ in.readStringList(mMccMncs);
}
@Override
@@ -184,25 +242,25 @@ public final class NetworkScanRequest implements Parcelable {
return false;
}
- return (scanType == nsr.scanType
- && Arrays.equals(specifiers, nsr.specifiers)
- && searchPeriodicity == nsr.searchPeriodicity
- && maxSearchTime == nsr.maxSearchTime
- && incrementalResults == nsr.incrementalResults
- && incrementalResultsPeriodicity == nsr.incrementalResultsPeriodicity
- && (((mccMncs != null)
- && mccMncs.equals(nsr.mccMncs))));
+ return (mScanType == nsr.mScanType
+ && Arrays.equals(mSpecifiers, nsr.mSpecifiers)
+ && mSearchPeriodicity == nsr.mSearchPeriodicity
+ && mMaxSearchTime == nsr.mMaxSearchTime
+ && mIncrementalResults == nsr.mIncrementalResults
+ && mIncrementalResultsPeriodicity == nsr.mIncrementalResultsPeriodicity
+ && (((mMccMncs != null)
+ && mMccMncs.equals(nsr.mMccMncs))));
}
@Override
public int hashCode () {
- return ((scanType * 31)
- + (Arrays.hashCode(specifiers)) * 37
- + (searchPeriodicity * 41)
- + (maxSearchTime * 43)
- + ((incrementalResults == true? 1 : 0) * 47)
- + (incrementalResultsPeriodicity * 53)
- + (mccMncs.hashCode() * 59));
+ return ((mScanType * 31)
+ + (Arrays.hashCode(mSpecifiers)) * 37
+ + (mSearchPeriodicity * 41)
+ + (mMaxSearchTime * 43)
+ + ((mIncrementalResults == true? 1 : 0) * 47)
+ + (mIncrementalResultsPeriodicity * 53)
+ + (mMccMncs.hashCode() * 59));
}
public static final Creator<NetworkScanRequest> CREATOR =
diff --git a/android/telephony/PhoneStateListener.java b/android/telephony/PhoneStateListener.java
index 9ccfa942..c7e51310 100644
--- a/android/telephony/PhoneStateListener.java
+++ b/android/telephony/PhoneStateListener.java
@@ -408,7 +408,7 @@ public class PhoneStateListener {
/**
* Callback invoked when device call state changes.
* @param state call state
- * @param incomingNumber incoming call phone number. If application does not have
+ * @param phoneNumber call phone number. If application does not have
* {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE} permission, an empty
* string will be passed as an argument.
*
@@ -416,7 +416,7 @@ public class PhoneStateListener {
* @see TelephonyManager#CALL_STATE_RINGING
* @see TelephonyManager#CALL_STATE_OFFHOOK
*/
- public void onCallStateChanged(int state, String incomingNumber) {
+ public void onCallStateChanged(int state, String phoneNumber) {
// default implementation empty
}
diff --git a/android/telephony/RadioAccessSpecifier.java b/android/telephony/RadioAccessSpecifier.java
index 33ce8b42..5412c617 100644
--- a/android/telephony/RadioAccessSpecifier.java
+++ b/android/telephony/RadioAccessSpecifier.java
@@ -25,34 +25,40 @@ import java.util.Arrays;
* Describes a particular radio access network to be scanned.
*
* The scan can be performed on either bands or channels for a specific radio access network type.
- * @hide
*/
public final class RadioAccessSpecifier implements Parcelable {
/**
* The radio access network that needs to be scanned
*
+ * This parameter must be provided or else the scan will be rejected.
+ *
* See {@link RadioNetworkConstants.RadioAccessNetworks} for details.
*/
- public int radioAccessNetwork;
+ private int mRadioAccessNetwork;
/**
* The frequency bands that need to be scanned
*
- * bands must be used together with radioAccessNetwork
+ * When no specific bands are specified (empty array or null), all the frequency bands
+ * supported by the modem will be scanned.
*
* See {@link RadioNetworkConstants} for details.
*/
- public int[] bands;
+ private int[] mBands;
/**
* The frequency channels that need to be scanned
*
- * channels must be used together with radioAccessNetwork
+ * When any specific channels are provided for scan, the corresponding frequency bands that
+ * contains those channels must also be provided, or else the channels will be ignored.
*
- * See {@link RadioNetworkConstants.RadioAccessNetworks} for details.
+ * When no specific channels are specified (empty array or null), all the frequency channels
+ * supported by the modem will be scanned.
+ *
+ * See {@link RadioNetworkConstants} for details.
*/
- public int[] channels;
+ private int[] mChannels;
/**
* Creates a new RadioAccessSpecifier with radio network, bands and channels
@@ -65,9 +71,34 @@ public final class RadioAccessSpecifier implements Parcelable {
* @param channels the frequency bands to be scanned
*/
public RadioAccessSpecifier(int ran, int[] bands, int[] channels) {
- this.radioAccessNetwork = ran;
- this.bands = bands;
- this.channels = channels;
+ this.mRadioAccessNetwork = ran;
+ this.mBands = bands.clone();
+ this.mChannels = channels.clone();
+ }
+
+ /**
+ * Returns the radio access network that needs to be scanned.
+ *
+ * The returned value is define in {@link RadioNetworkConstants.RadioAccessNetworks};
+ */
+ public int getRadioAccessNetwork() {
+ return mRadioAccessNetwork;
+ }
+
+ /**
+ * Returns the frequency bands that need to be scanned.
+ *
+ * The returned value is defined in either of {@link RadioNetworkConstants.GeranBands},
+ * {@link RadioNetworkConstants.UtranBands} and {@link RadioNetworkConstants.EutranBands}, and
+ * it depends on the returned value of {@link #getRadioAccessNetwork()}.
+ */
+ public int[] getBands() {
+ return mBands.clone();
+ }
+
+ /** Returns the frequency channels that need to be scanned. */
+ public int[] getChannels() {
+ return mChannels.clone();
}
public static final Parcelable.Creator<RadioAccessSpecifier> CREATOR =
@@ -90,15 +121,15 @@ public final class RadioAccessSpecifier implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(radioAccessNetwork);
- dest.writeIntArray(bands);
- dest.writeIntArray(channels);
+ dest.writeInt(mRadioAccessNetwork);
+ dest.writeIntArray(mBands);
+ dest.writeIntArray(mChannels);
}
private RadioAccessSpecifier(Parcel in) {
- radioAccessNetwork = in.readInt();
- bands = in.createIntArray();
- channels = in.createIntArray();
+ mRadioAccessNetwork = in.readInt();
+ mBands = in.createIntArray();
+ mChannels = in.createIntArray();
}
@Override
@@ -115,15 +146,15 @@ public final class RadioAccessSpecifier implements Parcelable {
return false;
}
- return (radioAccessNetwork == ras.radioAccessNetwork
- && Arrays.equals(bands, ras.bands)
- && Arrays.equals(channels, ras.channels));
+ return (mRadioAccessNetwork == ras.mRadioAccessNetwork
+ && Arrays.equals(mBands, ras.mBands)
+ && Arrays.equals(mChannels, ras.mChannels));
}
@Override
public int hashCode () {
- return ((radioAccessNetwork * 31)
- + (Arrays.hashCode(bands) * 37)
- + (Arrays.hashCode(channels)) * 39);
+ return ((mRadioAccessNetwork * 31)
+ + (Arrays.hashCode(mBands) * 37)
+ + (Arrays.hashCode(mChannels)) * 39);
}
}
diff --git a/android/telephony/RadioNetworkConstants.java b/android/telephony/RadioNetworkConstants.java
index 1a9072d3..5f5dd82e 100644
--- a/android/telephony/RadioNetworkConstants.java
+++ b/android/telephony/RadioNetworkConstants.java
@@ -18,7 +18,6 @@ package android.telephony;
/**
* Contains radio access network related constants.
- * @hide
*/
public final class RadioNetworkConstants {
diff --git a/android/telephony/SmsManager.java b/android/telephony/SmsManager.java
index fdedf758..5d88cf07 100644
--- a/android/telephony/SmsManager.java
+++ b/android/telephony/SmsManager.java
@@ -344,6 +344,7 @@ public final class SmsManager {
* </p>
*
* <p>Requires Permission:
+ * {@link android.Manifest.permission#SEND_SMS} and
* {@link android.Manifest.permission#MODIFY_PHONE_STATE} or the calling app has carrier
* privileges.
* </p>
@@ -351,6 +352,10 @@ public final class SmsManager {
* @see #sendTextMessage(String, String, String, PendingIntent, PendingIntent)
*/
@SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.MODIFY_PHONE_STATE,
+ android.Manifest.permission.SEND_SMS
+ })
public void sendTextMessageWithoutPersisting(
String destinationAddress, String scAddress, String text,
PendingIntent sentIntent, PendingIntent deliveryIntent) {
@@ -390,112 +395,6 @@ public final class SmsManager {
}
/**
- * Send a text based SMS with messaging options.
- *
- * @param destinationAddress the address to send the message to
- * @param scAddress is the service center address or null to use
- * the current default SMSC
- * @param text the body of the message to send
- * @param sentIntent if not NULL this <code>PendingIntent</code> is
- * broadcast when the message is successfully sent, or failed.
- * The result code will be <code>Activity.RESULT_OK</code> for success,
- * or one of these errors:<br>
- * <code>RESULT_ERROR_GENERIC_FAILURE</code><br>
- * <code>RESULT_ERROR_RADIO_OFF</code><br>
- * <code>RESULT_ERROR_NULL_PDU</code><br>
- * For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include
- * the extra "errorCode" containing a radio technology specific value,
- * generally only useful for troubleshooting.<br>
- * The per-application based SMS control checks sentIntent. If sentIntent
- * is NULL the caller will be checked against all unknown applications,
- * which cause smaller number of SMS to be sent in checking period.
- * @param deliveryIntent if not NULL this <code>PendingIntent</code> is
- * broadcast when the message is delivered to the recipient. The
- * raw pdu of the status report is in the extended data ("pdu").
- * @param priority Priority level of the message
- * Refer specification See 3GPP2 C.S0015-B, v2.0, table 4.5.9-1
- * ---------------------------------
- * PRIORITY | Level of Priority
- * ---------------------------------
- * '00' | Normal
- * '01' | Interactive
- * '10' | Urgent
- * '11' | Emergency
- * ----------------------------------
- * Any Other values included Negative considered as Invalid Priority Indicator of the message.
- * @param expectMore is a boolean to indicate the sending messages through same link or not.
- * @param validityPeriod Validity Period of the message in mins.
- * Refer specification 3GPP TS 23.040 V6.8.1 section 9.2.3.12.1.
- * Validity Period(Minimum) -> 5 mins
- * Validity Period(Maximum) -> 635040 mins(i.e.63 weeks).
- * Any Other values included Negative considered as Invalid Validity Period of the message.
- *
- * @throws IllegalArgumentException if destinationAddress or text are empty
- * {@hide}
- */
- public void sendTextMessage(
- String destinationAddress, String scAddress, String text,
- PendingIntent sentIntent, PendingIntent deliveryIntent,
- int priority, boolean expectMore, int validityPeriod) {
- sendTextMessageInternal(destinationAddress, scAddress, text, sentIntent, deliveryIntent,
- true /* persistMessage*/, priority, expectMore, validityPeriod);
- }
-
- private void sendTextMessageInternal(
- String destinationAddress, String scAddress, String text,
- PendingIntent sentIntent, PendingIntent deliveryIntent, boolean persistMessage,
- int priority, boolean expectMore, int validityPeriod) {
- if (TextUtils.isEmpty(destinationAddress)) {
- throw new IllegalArgumentException("Invalid destinationAddress");
- }
-
- if (TextUtils.isEmpty(text)) {
- throw new IllegalArgumentException("Invalid message body");
- }
-
- if (priority < 0x00 || priority > 0x03) {
- throw new IllegalArgumentException("Invalid priority");
- }
-
- if (validityPeriod < 0x05 || validityPeriod > 0x09b0a0) {
- throw new IllegalArgumentException("Invalid validity period");
- }
-
- try {
- ISms iccISms = getISmsServiceOrThrow();
- if (iccISms != null) {
- iccISms.sendTextForSubscriberWithOptions(getSubscriptionId(),
- ActivityThread.currentPackageName(), destinationAddress, scAddress, text,
- sentIntent, deliveryIntent, persistMessage, priority, expectMore,
- validityPeriod);
- }
- } catch (RemoteException ex) {
- // ignore it
- }
- }
-
- /**
- * Send a text based SMS without writing it into the SMS Provider.
- *
- * <p>Requires Permission:
- * {@link android.Manifest.permission#MODIFY_PHONE_STATE} or the calling app has carrier
- * privileges.
- * </p>
- *
- * @see #sendTextMessage(String, String, String, PendingIntent,
- * PendingIntent, int, boolean, int)
- * @hide
- */
- public void sendTextMessageWithoutPersisting(
- String destinationAddress, String scAddress, String text,
- PendingIntent sentIntent, PendingIntent deliveryIntent, int priority,
- boolean expectMore, int validityPeriod) {
- sendTextMessageInternal(destinationAddress, scAddress, text, sentIntent, deliveryIntent,
- false /* persistMessage */, priority, expectMore, validityPeriod);
- }
-
- /**
- *
* Inject an SMS PDU into the android application framework.
*
* <p>Requires permission: {@link android.Manifest.permission#MODIFY_PHONE_STATE} or carrier
@@ -653,140 +552,6 @@ public final class SmsManager {
}
/**
- * Send a multi-part text based SMS with messaging options. The callee should have already
- * divided the message into correctly sized parts by calling
- * <code>divideMessage</code>.
- *
- * <p class="note"><strong>Note:</strong> Using this method requires that your app has the
- * {@link android.Manifest.permission#SEND_SMS} permission.</p>
- *
- * <p class="note"><strong>Note:</strong> Beginning with Android 4.4 (API level 19), if
- * <em>and only if</em> an app is not selected as the default SMS app, the system automatically
- * writes messages sent using this method to the SMS Provider (the default SMS app is always
- * responsible for writing its sent messages to the SMS Provider). For information about
- * how to behave as the default SMS app, see {@link android.provider.Telephony}.</p>
- *
- * @param destinationAddress the address to send the message to
- * @param scAddress is the service center address or null to use
- * the current default SMSC
- * @param parts an <code>ArrayList</code> of strings that, in order,
- * comprise the original message
- * @param sentIntents if not null, an <code>ArrayList</code> of
- * <code>PendingIntent</code>s (one for each message part) that is
- * broadcast when the corresponding message part has been sent.
- * The result code will be <code>Activity.RESULT_OK</code> for success,
- * or one of these errors:<br>
- * <code>RESULT_ERROR_GENERIC_FAILURE</code><br>
- * <code>RESULT_ERROR_RADIO_OFF</code><br>
- * <code>RESULT_ERROR_NULL_PDU</code><br>
- * For <code>RESULT_ERROR_GENERIC_FAILURE</code> each sentIntent may include
- * the extra "errorCode" containing a radio technology specific value,
- * generally only useful for troubleshooting.<br>
- * The per-application based SMS control checks sentIntent. If sentIntent
- * is NULL the caller will be checked against all unknown applications,
- * which cause smaller number of SMS to be sent in checking period.
- * @param deliveryIntents if not null, an <code>ArrayList</code> of
- * <code>PendingIntent</code>s (one for each message part) that is
- * broadcast when the corresponding message part has been delivered
- * to the recipient. The raw pdu of the status report is in the
- * extended data ("pdu").
- * @param priority Priority level of the message
- * Refer specification See 3GPP2 C.S0015-B, v2.0, table 4.5.9-1
- * ---------------------------------
- * PRIORITY | Level of Priority
- * ---------------------------------
- * '00' | Normal
- * '01' | Interactive
- * '10' | Urgent
- * '11' | Emergency
- * ----------------------------------
- * Any Other values included Negative considered as Invalid Priority Indicator of the message.
- * @param expectMore is a boolean to indicate the sending messages through same link or not.
- * @param validityPeriod Validity Period of the message in mins.
- * Refer specification 3GPP TS 23.040 V6.8.1 section 9.2.3.12.1.
- * Validity Period(Minimum) -> 5 mins
- * Validity Period(Maximum) -> 635040 mins(i.e.63 weeks).
- * Any Other values included Negative considered as Invalid Validity Period of the message.
- *
- * @throws IllegalArgumentException if destinationAddress or data are empty
- * {@hide}
- */
- public void sendMultipartTextMessage(
- String destinationAddress, String scAddress, ArrayList<String> parts,
- ArrayList<PendingIntent> sentIntents, ArrayList<PendingIntent> deliveryIntents,
- int priority, boolean expectMore, int validityPeriod) {
- sendMultipartTextMessageInternal(destinationAddress, scAddress, parts, sentIntents,
- deliveryIntents, true /* persistMessage*/);
- }
-
- private void sendMultipartTextMessageInternal(
- String destinationAddress, String scAddress, List<String> parts,
- List<PendingIntent> sentIntents, List<PendingIntent> deliveryIntents,
- boolean persistMessage, int priority, boolean expectMore, int validityPeriod) {
- if (TextUtils.isEmpty(destinationAddress)) {
- throw new IllegalArgumentException("Invalid destinationAddress");
- }
- if (parts == null || parts.size() < 1) {
- throw new IllegalArgumentException("Invalid message body");
- }
-
- if (priority < 0x00 || priority > 0x03) {
- throw new IllegalArgumentException("Invalid priority");
- }
-
- if (validityPeriod < 0x05 || validityPeriod > 0x09b0a0) {
- throw new IllegalArgumentException("Invalid validity period");
- }
-
- if (parts.size() > 1) {
- try {
- ISms iccISms = getISmsServiceOrThrow();
- if (iccISms != null) {
- iccISms.sendMultipartTextForSubscriberWithOptions(getSubscriptionId(),
- ActivityThread.currentPackageName(), destinationAddress, scAddress,
- parts, sentIntents, deliveryIntents, persistMessage, priority,
- expectMore, validityPeriod);
- }
- } catch (RemoteException ex) {
- // ignore it
- }
- } else {
- PendingIntent sentIntent = null;
- PendingIntent deliveryIntent = null;
- if (sentIntents != null && sentIntents.size() > 0) {
- sentIntent = sentIntents.get(0);
- }
- if (deliveryIntents != null && deliveryIntents.size() > 0) {
- deliveryIntent = deliveryIntents.get(0);
- }
- sendTextMessageInternal(destinationAddress, scAddress, parts.get(0),
- sentIntent, deliveryIntent, persistMessage, priority, expectMore,
- validityPeriod);
- }
- }
-
- /**
- * Send a multi-part text based SMS without writing it into the SMS Provider.
- *
- * <p>Requires Permission:
- * {@link android.Manifest.permission#MODIFY_PHONE_STATE} or the calling app has carrier
- * privileges.
- * </p>
- *
- * @see #sendMultipartTextMessage(String, String, ArrayList, ArrayList,
- * ArrayList, int, boolean, int)
- * @hide
- **/
- public void sendMultipartTextMessageWithoutPersisting(
- String destinationAddress, String scAddress, List<String> parts,
- List<PendingIntent> sentIntents, List<PendingIntent> deliveryIntents,
- int priority, boolean expectMore, int validityPeriod) {
- sendMultipartTextMessageInternal(destinationAddress, scAddress, parts, sentIntents,
- deliveryIntents, false /* persistMessage*/, priority, expectMore,
- validityPeriod);
- }
-
- /**
* Send a data based SMS to a specific application port.
*
* <p class="note"><strong>Note:</strong> Using this method requires that your app has the
@@ -1249,7 +1014,7 @@ public final class SmsManager {
* <code>getAllMessagesFromIcc</code>
* @return <code>ArrayList</code> of <code>SmsMessage</code> objects.
*/
- private ArrayList<SmsMessage> createMessageListFromRawRecords(List<SmsRawData> records) {
+ private static ArrayList<SmsMessage> createMessageListFromRawRecords(List<SmsRawData> records) {
ArrayList<SmsMessage> messages = new ArrayList<SmsMessage>();
if (records != null) {
int count = records.size();
@@ -1257,8 +1022,7 @@ public final class SmsManager {
SmsRawData data = records.get(i);
// List contains all records, including "free" records (null)
if (data != null) {
- SmsMessage sms = SmsMessage.createFromEfRecord(i+1, data.getBytes(),
- getSubscriptionId());
+ SmsMessage sms = SmsMessage.createFromEfRecord(i+1, data.getBytes());
if (sms != null) {
messages.add(sms);
}
@@ -1370,6 +1134,8 @@ public final class SmsManager {
// SMS send failure result codes
+ /** No error. {@hide}*/
+ static public final int RESULT_ERROR_NONE = 0;
/** Generic failure cause */
static public final int RESULT_ERROR_GENERIC_FAILURE = 1;
/** Failed because radio was explicitly turned off */
diff --git a/android/telephony/SmsMessage.java b/android/telephony/SmsMessage.java
index a5d67c60..a6dbc066 100644
--- a/android/telephony/SmsMessage.java
+++ b/android/telephony/SmsMessage.java
@@ -19,6 +19,7 @@ package android.telephony;
import static android.telephony.TelephonyManager.PHONE_TYPE_CDMA;
import android.annotation.StringDef;
+import android.app.PendingIntent;
import android.content.res.Resources;
import android.os.Binder;
import android.text.TextUtils;
@@ -83,17 +84,22 @@ public class SmsMessage {
public static final int MAX_USER_DATA_SEPTETS_WITH_HEADER = 153;
/** @hide */
- @StringDef({FORMAT_3GPP, FORMAT_3GPP2})
+ @StringDef(prefix = { "FORMAT_" }, value = {
+ FORMAT_3GPP,
+ FORMAT_3GPP2
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface Format {}
/**
* Indicates a 3GPP format SMS message.
+ * @see SmsManager#injectSmsPdu(byte[], String, PendingIntent)
*/
public static final String FORMAT_3GPP = "3gpp";
/**
* Indicates a 3GPP2 format SMS message.
+ * @see SmsManager#injectSmsPdu(byte[], String, PendingIntent)
*/
public static final String FORMAT_3GPP2 = "3gpp2";
@@ -271,31 +277,6 @@ public class SmsMessage {
}
/**
- * Create an SmsMessage from an SMS EF record.
- *
- * @param index Index of SMS record. This should be index in ArrayList
- * returned by SmsManager.getAllMessagesFromSim + 1.
- * @param data Record data.
- * @param subId Subscription Id of the SMS
- * @return An SmsMessage representing the record.
- *
- * @hide
- */
- public static SmsMessage createFromEfRecord(int index, byte[] data, int subId) {
- SmsMessageBase wrappedMessage;
-
- if (isCdmaVoice(subId)) {
- wrappedMessage = com.android.internal.telephony.cdma.SmsMessage.createFromEfRecord(
- index, data);
- } else {
- wrappedMessage = com.android.internal.telephony.gsm.SmsMessage.createFromEfRecord(
- index, data);
- }
-
- return wrappedMessage != null ? new SmsMessage(wrappedMessage) : null;
- }
-
- /**
* Get the TP-Layer-Length for the given SMS-SUBMIT PDU Basically, the
* length in bytes (not hex chars) less the SMSC header
*
@@ -847,7 +828,6 @@ public class SmsMessage {
int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(subId);
return (PHONE_TYPE_CDMA == activePhone);
}
-
/**
* Decide if the carrier supports long SMS.
* {@hide}
diff --git a/android/telephony/TelephonyManager.java b/android/telephony/TelephonyManager.java
index 81806e52..af5b1908 100644
--- a/android/telephony/TelephonyManager.java
+++ b/android/telephony/TelephonyManager.java
@@ -29,7 +29,6 @@ import android.annotation.SystemService;
import android.annotation.WorkerThread;
import android.app.ActivityThread;
import android.app.PendingIntent;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
@@ -43,7 +42,6 @@ import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.SystemProperties;
-import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.service.carrier.CarrierIdentifier;
import android.telecom.PhoneAccount;
@@ -979,6 +977,63 @@ public class TelephonyManager {
*/
public static final int CDMA_ROAMING_MODE_ANY = 2;
+ /**
+ * An unknown carrier id. It could either be subscription unavailable or the subscription
+ * carrier cannot be recognized. Unrecognized carriers here means
+ * {@link #getSimOperator() MCC+MNC} cannot be identified.
+ */
+ public static final int UNKNOWN_CARRIER_ID = -1;
+
+ /**
+ * Broadcast Action: The subscription carrier identity has changed.
+ * This intent could be sent on the following events:
+ * <ul>
+ * <li>Subscription absent. Carrier identity could change from a valid id to
+ * {@link TelephonyManager#UNKNOWN_CARRIER_ID}.</li>
+ * <li>Subscription loaded. Carrier identity could change from
+ * {@link TelephonyManager#UNKNOWN_CARRIER_ID} to a valid id.</li>
+ * <li>The subscription carrier is recognized after a remote update.</li>
+ * </ul>
+ * The intent will have the following extra values:
+ * <ul>
+ * <li>{@link #EXTRA_CARRIER_ID} The up-to-date carrier id of the current subscription id.
+ * </li>
+ * <li>{@link #EXTRA_CARRIER_NAME} The up-to-date carrier name of the current subscription.
+ * </li>
+ * <li>{@link #EXTRA_SUBSCRIPTION_ID} The subscription id associated with the changed carrier
+ * identity.
+ * </li>
+ * </ul>
+ * <p class="note">This is a protected intent that can only be sent by the system.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED =
+ "android.telephony.action.SUBSCRIPTION_CARRIER_IDENTITY_CHANGED";
+
+ /**
+ * An int extra used with {@link #ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED} which indicates
+ * the updated carrier id {@link TelephonyManager#getSubscriptionCarrierId()} of the current
+ * subscription.
+ * <p>Will be {@link TelephonyManager#UNKNOWN_CARRIER_ID} if the subscription is unavailable or
+ * the carrier cannot be identified.
+ */
+ public static final String EXTRA_CARRIER_ID = "android.telephony.extra.CARRIER_ID";
+
+ /**
+ * An string extra used with {@link #ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED} which
+ * indicates the updated carrier name of the current subscription.
+ * {@see TelephonyManager#getSubscriptionCarrierName()}
+ * <p>Carrier name is a user-facing name of the carrier id {@link #EXTRA_CARRIER_ID},
+ * usually the brand name of the subsidiary (e.g. T-Mobile).
+ */
+ public static final String EXTRA_CARRIER_NAME = "android.telephony.extra.CARRIER_NAME";
+
+ /**
+ * An int extra used with {@link #ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED} to indicate the
+ * subscription which has changed.
+ */
+ public static final String EXTRA_SUBSCRIPTION_ID = "android.telephony.extra.SUBSCRIPTION_ID";
+
//
//
// Device Info
@@ -1121,12 +1176,14 @@ public class TelephonyManager {
}
/**
- * Returns the NAI. Return null if NAI is not available.
- *
+ * Returns the Network Access Identifier (NAI). Return null if NAI is not available.
+ * <p>
+ * Requires Permission:
+ * {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
*/
- /** {@hide}*/
+ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
public String getNai() {
- return getNai(getSlotIndex());
+ return getNaiBySubscriberId(getSubId());
}
/**
@@ -1137,11 +1194,18 @@ public class TelephonyManager {
/** {@hide}*/
public String getNai(int slotIndex) {
int[] subId = SubscriptionManager.getSubId(slotIndex);
+ if (subId == null) {
+ return null;
+ }
+ return getNaiBySubscriberId(subId[0]);
+ }
+
+ private String getNaiBySubscriberId(int subId) {
try {
IPhoneSubInfo info = getSubscriberInfo();
if (info == null)
return null;
- String nai = info.getNaiForSubscriber(subId[0], mContext.getOpPackageName());
+ String nai = info.getNaiForSubscriber(subId, mContext.getOpPackageName());
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Rlog.v(TAG, "Nai = " + nai);
}
@@ -3091,6 +3155,7 @@ public class TelephonyManager {
* Initial SIM activation state, unknown. Not set by any carrier apps.
* @hide
*/
+ @SystemApi
public static final int SIM_ACTIVATION_STATE_UNKNOWN = 0;
/**
@@ -3101,12 +3166,14 @@ public class TelephonyManager {
* @see #SIM_ACTIVATION_STATE_RESTRICTED
* @hide
*/
+ @SystemApi
public static final int SIM_ACTIVATION_STATE_ACTIVATING = 1;
/**
* Indicate SIM has been successfully activated with full service
* @hide
*/
+ @SystemApi
public static final int SIM_ACTIVATION_STATE_ACTIVATED = 2;
/**
@@ -3116,6 +3183,7 @@ public class TelephonyManager {
* deactivated sim state and set it back to activated after successfully run activation service.
* @hide
*/
+ @SystemApi
public static final int SIM_ACTIVATION_STATE_DEACTIVATED = 3;
/**
@@ -3123,14 +3191,47 @@ public class TelephonyManager {
* note this is currently available for data activation state. For example out of byte sim.
* @hide
*/
+ @SystemApi
public static final int SIM_ACTIVATION_STATE_RESTRICTED = 4;
+ /** @hide */
+ @IntDef({
+ SIM_ACTIVATION_STATE_UNKNOWN,
+ SIM_ACTIVATION_STATE_ACTIVATING,
+ SIM_ACTIVATION_STATE_ACTIVATED,
+ SIM_ACTIVATION_STATE_DEACTIVATED,
+ SIM_ACTIVATION_STATE_RESTRICTED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SimActivationState{}
+
+ /**
+ * Sets the voice activation state
+ *
+ * <p>Requires Permission:
+ * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
+ * Or the calling app has carrier privileges.
+ *
+ * @param activationState The voice activation state
+ * @see #SIM_ACTIVATION_STATE_UNKNOWN
+ * @see #SIM_ACTIVATION_STATE_ACTIVATING
+ * @see #SIM_ACTIVATION_STATE_ACTIVATED
+ * @see #SIM_ACTIVATION_STATE_DEACTIVATED
+ * @see #hasCarrierPrivileges
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ public void setVoiceActivationState(@SimActivationState int activationState) {
+ setVoiceActivationState(getSubId(), activationState);
+ }
+
/**
* Sets the voice activation state for the given subscriber.
*
* <p>Requires Permission:
* {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
- * Or the calling app has carrier privileges. @see #hasCarrierPrivileges
+ * Or the calling app has carrier privileges.
*
* @param subId The subscription id.
* @param activationState The voice activation state of the given subscriber.
@@ -3138,24 +3239,48 @@ public class TelephonyManager {
* @see #SIM_ACTIVATION_STATE_ACTIVATING
* @see #SIM_ACTIVATION_STATE_ACTIVATED
* @see #SIM_ACTIVATION_STATE_DEACTIVATED
+ * @see #hasCarrierPrivileges
* @hide
*/
- public void setVoiceActivationState(int subId, int activationState) {
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ public void setVoiceActivationState(int subId, @SimActivationState int activationState) {
try {
- ITelephony telephony = getITelephony();
- if (telephony != null)
- telephony.setVoiceActivationState(subId, activationState);
- } catch (RemoteException ex) {
- } catch (NullPointerException ex) {
- }
+ ITelephony telephony = getITelephony();
+ if (telephony != null)
+ telephony.setVoiceActivationState(subId, activationState);
+ } catch (RemoteException ex) {
+ } catch (NullPointerException ex) {
+ }
+ }
+
+ /**
+ * Sets the data activation state
+ *
+ * <p>Requires Permission:
+ * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
+ * Or the calling app has carrier privileges.
+ *
+ * @param activationState The data activation state
+ * @see #SIM_ACTIVATION_STATE_UNKNOWN
+ * @see #SIM_ACTIVATION_STATE_ACTIVATING
+ * @see #SIM_ACTIVATION_STATE_ACTIVATED
+ * @see #SIM_ACTIVATION_STATE_DEACTIVATED
+ * @see #SIM_ACTIVATION_STATE_RESTRICTED
+ * @see #hasCarrierPrivileges
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ public void setDataActivationState(@SimActivationState int activationState) {
+ setDataActivationState(getSubId(), activationState);
}
/**
* Sets the data activation state for the given subscriber.
*
* <p>Requires Permission:
- * {@link android.Manifest.permission#MODIFY_PHONE_STATE}
- * Or the calling app has carrier privileges. @see #hasCarrierPrivileges
+ * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
+ * Or the calling app has carrier privileges.
*
* @param subId The subscription id.
* @param activationState The data activation state of the given subscriber.
@@ -3164,9 +3289,11 @@ public class TelephonyManager {
* @see #SIM_ACTIVATION_STATE_ACTIVATED
* @see #SIM_ACTIVATION_STATE_DEACTIVATED
* @see #SIM_ACTIVATION_STATE_RESTRICTED
+ * @see #hasCarrierPrivileges
* @hide
*/
- public void setDataActivationState(int subId, int activationState) {
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ public void setDataActivationState(int subId, @SimActivationState int activationState) {
try {
ITelephony telephony = getITelephony();
if (telephony != null)
@@ -3177,8 +3304,33 @@ public class TelephonyManager {
}
/**
+ * Returns the voice activation state
+ *
+ * <p>Requires Permission:
+ * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE}
+ * Or the calling app has carrier privileges.
+ *
+ * @return voiceActivationState
+ * @see #SIM_ACTIVATION_STATE_UNKNOWN
+ * @see #SIM_ACTIVATION_STATE_ACTIVATING
+ * @see #SIM_ACTIVATION_STATE_ACTIVATED
+ * @see #SIM_ACTIVATION_STATE_DEACTIVATED
+ * @see #hasCarrierPrivileges
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ public @SimActivationState int getVoiceActivationState() {
+ return getVoiceActivationState(getSubId());
+ }
+
+ /**
* Returns the voice activation state for the given subscriber.
*
+ * <p>Requires Permission:
+ * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE}
+ * Or the calling app has carrier privileges.
+ *
* @param subId The subscription id.
*
* @return voiceActivationState for the given subscriber
@@ -3186,10 +3338,11 @@ public class TelephonyManager {
* @see #SIM_ACTIVATION_STATE_ACTIVATING
* @see #SIM_ACTIVATION_STATE_ACTIVATED
* @see #SIM_ACTIVATION_STATE_DEACTIVATED
+ * @see #hasCarrierPrivileges
* @hide
*/
- @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
- public int getVoiceActivationState(int subId) {
+ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ public @SimActivationState int getVoiceActivationState(int subId) {
try {
ITelephony telephony = getITelephony();
if (telephony != null)
@@ -3201,8 +3354,34 @@ public class TelephonyManager {
}
/**
+ * Returns the data activation state
+ *
+ * <p>Requires Permission:
+ * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE}
+ * Or the calling app has carrier privileges.
+ *
+ * @return dataActivationState for the given subscriber
+ * @see #SIM_ACTIVATION_STATE_UNKNOWN
+ * @see #SIM_ACTIVATION_STATE_ACTIVATING
+ * @see #SIM_ACTIVATION_STATE_ACTIVATED
+ * @see #SIM_ACTIVATION_STATE_DEACTIVATED
+ * @see #SIM_ACTIVATION_STATE_RESTRICTED
+ * @see #hasCarrierPrivileges
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ public @SimActivationState int getDataActivationState() {
+ return getDataActivationState(getSubId());
+ }
+
+ /**
* Returns the data activation state for the given subscriber.
*
+ * <p>Requires Permission:
+ * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE}
+ * Or the calling app has carrier privileges.
+ *
* @param subId The subscription id.
*
* @return dataActivationState for the given subscriber
@@ -3211,10 +3390,11 @@ public class TelephonyManager {
* @see #SIM_ACTIVATION_STATE_ACTIVATED
* @see #SIM_ACTIVATION_STATE_DEACTIVATED
* @see #SIM_ACTIVATION_STATE_RESTRICTED
+ * @see #hasCarrierPrivileges
* @hide
*/
- @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
- public int getDataActivationState(int subId) {
+ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ public @SimActivationState int getDataActivationState(int subId) {
try {
ITelephony telephony = getITelephony();
if (telephony != null)
@@ -4785,15 +4965,14 @@ public class TelephonyManager {
* Requires Permission:
* {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
* Or the calling app has carrier privileges. @see #hasCarrierPrivileges
- *
- * @hide
- * TODO: Add an overload that takes no args.
*/
- public void setNetworkSelectionModeAutomatic(int subId) {
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ public void setNetworkSelectionModeAutomatic() {
try {
ITelephony telephony = getITelephony();
- if (telephony != null)
- telephony.setNetworkSelectionModeAutomatic(subId);
+ if (telephony != null) {
+ telephony.setNetworkSelectionModeAutomatic(getSubId());
+ }
} catch (RemoteException ex) {
Rlog.e(TAG, "setNetworkSelectionModeAutomatic RemoteException", ex);
} catch (NullPointerException ex) {
@@ -4841,9 +5020,9 @@ public class TelephonyManager {
*
* @param request Contains all the RAT with bands/channels that need to be scanned.
* @param callback Returns network scan results or errors.
- * @return A NetworkScan obj which contains a callback which can stop the scan.
- * @hide
+ * @return A NetworkScan obj which contains a callback which can be used to stop the scan.
*/
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
public NetworkScan requestNetworkScan(
NetworkScanRequest request, TelephonyScanManager.NetworkScanCallback callback) {
synchronized (this) {
@@ -4862,15 +5041,20 @@ public class TelephonyManager {
* {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
* Or the calling app has carrier privileges. @see #hasCarrierPrivileges
*
- * @hide
- * TODO: Add an overload that takes no args.
+ * @param operatorNumeric the PLMN ID of the network to select.
+ * @param persistSelection whether the selection will persist until reboot. If true, only allows
+ * attaching to the selected PLMN until reboot; otherwise, attach to the chosen PLMN and resume
+ * normal network selection next time.
+ * @return true on success; false on any failure.
*/
- public boolean setNetworkSelectionModeManual(int subId, OperatorInfo operator,
- boolean persistSelection) {
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ public boolean setNetworkSelectionModeManual(String operatorNumeric, boolean persistSelection) {
try {
ITelephony telephony = getITelephony();
- if (telephony != null)
- return telephony.setNetworkSelectionModeManual(subId, operator, persistSelection);
+ if (telephony != null) {
+ return telephony.setNetworkSelectionModeManual(
+ getSubId(), operatorNumeric, persistSelection);
+ }
} catch (RemoteException ex) {
Rlog.e(TAG, "setNetworkSelectionModeManual RemoteException", ex);
} catch (NullPointerException ex) {
@@ -4895,8 +5079,9 @@ public class TelephonyManager {
public boolean setPreferredNetworkType(int subId, int networkType) {
try {
ITelephony telephony = getITelephony();
- if (telephony != null)
+ if (telephony != null) {
return telephony.setPreferredNetworkType(subId, networkType);
+ }
} catch (RemoteException ex) {
Rlog.e(TAG, "setPreferredNetworkType RemoteException", ex);
} catch (NullPointerException ex) {
@@ -5692,39 +5877,38 @@ public class TelephonyManager {
* @param enable Whether to enable mobile data.
*
* @see #hasCarrierPrivileges
+ * @deprecated use {@link #setUserMobileDataEnabled(boolean)} instead.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
public void setDataEnabled(boolean enable) {
- setDataEnabled(getSubId(SubscriptionManager.getDefaultDataSubscriptionId()), enable);
+ setUserMobileDataEnabled(enable);
}
- /** @hide */
+ /**
+ * @hide
+ * @deprecated use {@link #setUserMobileDataEnabled(boolean)} instead.
+ */
@SystemApi
+ @Deprecated
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
public void setDataEnabled(int subId, boolean enable) {
- try {
- Log.d(TAG, "setDataEnabled: enabled=" + enable);
- ITelephony telephony = getITelephony();
- if (telephony != null)
- telephony.setDataEnabled(subId, enable);
- } catch (RemoteException e) {
- Log.e(TAG, "Error calling ITelephony#setDataEnabled", e);
- }
+ setUserMobileDataEnabled(subId, enable);
}
-
/**
- * @deprecated use {@link #isDataEnabled()} instead.
+ * @deprecated use {@link #isUserMobileDataEnabled()} instead.
* @hide
*/
@SystemApi
@Deprecated
public boolean getDataEnabled() {
- return isDataEnabled();
+ return isUserMobileDataEnabled();
}
/**
- * Returns whether mobile data is enabled or not.
+ * Returns whether mobile data is enabled or not per user setting. There are other factors
+ * that could disable mobile data, but they are not considered here.
*
* If this object has been created with {@link #createForSubscriptionId}, applies to the given
* subId. Otherwise, applies to {@link SubscriptionManager#getDefaultDataSubscriptionId()}
@@ -5741,28 +5925,21 @@ public class TelephonyManager {
* @return true if mobile data is enabled.
*
* @see #hasCarrierPrivileges
+ * @deprecated use {@link #isUserMobileDataEnabled()} instead.
*/
- @SuppressWarnings("deprecation")
+ @Deprecated
public boolean isDataEnabled() {
- return getDataEnabled(getSubId(SubscriptionManager.getDefaultDataSubscriptionId()));
+ return isUserMobileDataEnabled();
}
/**
- * @deprecated use {@link #isDataEnabled(int)} instead.
+ * @deprecated use {@link #isUserMobileDataEnabled()} instead.
* @hide
*/
+ @Deprecated
@SystemApi
public boolean getDataEnabled(int subId) {
- boolean retVal = false;
- try {
- ITelephony telephony = getITelephony();
- if (telephony != null)
- retVal = telephony.getDataEnabled(subId);
- } catch (RemoteException e) {
- Log.e(TAG, "Error calling ITelephony#getDataEnabled", e);
- } catch (NullPointerException e) {
- }
- return retVal;
+ return isUserMobileDataEnabled(subId);
}
/** @hide */
@@ -6509,6 +6686,9 @@ public class TelephonyManager {
* @param uri The URI for the ringtone to play when receiving a voicemail from a specific
* PhoneAccount.
* @see #hasCarrierPrivileges
+ *
+ * @deprecated Use {@link android.provider.Settings#ACTION_CHANNEL_NOTIFICATION_SETTINGS}
+ * instead.
*/
public void setVoicemailRingtoneUri(PhoneAccountHandle phoneAccountHandle, Uri uri) {
try {
@@ -6551,6 +6731,9 @@ public class TelephonyManager {
* @param enabled Whether to enable or disable vibration for voicemail notifications from a
* specific PhoneAccount.
* @see #hasCarrierPrivileges
+ *
+ * @deprecated Use {@link android.provider.Settings#ACTION_CHANNEL_NOTIFICATION_SETTINGS}
+ * instead.
*/
public void setVoicemailVibrationEnabled(PhoneAccountHandle phoneAccountHandle,
boolean enabled) {
@@ -6566,6 +6749,55 @@ public class TelephonyManager {
}
/**
+ * Returns carrier id of the current subscription.
+ * <p>To recognize a carrier (including MVNO) as a first class identity, assign each carrier
+ * with a canonical integer a.k.a carrier id.
+ *
+ * @return Carrier id of the current subscription. Return {@link #UNKNOWN_CARRIER_ID} if the
+ * subscription is unavailable or the carrier cannot be identified.
+ * @throws IllegalStateException if telephony service is unavailable.
+ */
+ public int getSubscriptionCarrierId() {
+ try {
+ ITelephony service = getITelephony();
+ return service.getSubscriptionCarrierId(getSubId());
+ } catch (RemoteException ex) {
+ // This could happen if binder process crashes.
+ ex.rethrowAsRuntimeException();
+ } catch (NullPointerException ex) {
+ // This could happen before phone restarts due to crashing.
+ throw new IllegalStateException("Telephony service unavailable");
+ }
+ return UNKNOWN_CARRIER_ID;
+ }
+
+ /**
+ * Returns carrier name of the current subscription.
+ * <p>Carrier name is a user-facing name of carrier id {@link #getSubscriptionCarrierId()},
+ * usually the brand name of the subsidiary (e.g. T-Mobile). Each carrier could configure
+ * multiple {@link #getSimOperatorName() SPN} but should have a single carrier name.
+ * Carrier name is not a canonical identity, use {@link #getSubscriptionCarrierId()} instead.
+ * <p>The returned carrier name is unlocalized.
+ *
+ * @return Carrier name of the current subscription. Return {@code null} if the subscription is
+ * unavailable or the carrier cannot be identified.
+ * @throws IllegalStateException if telephony service is unavailable.
+ */
+ public String getSubscriptionCarrierName() {
+ try {
+ ITelephony service = getITelephony();
+ return service.getSubscriptionCarrierName(getSubId());
+ } catch (RemoteException ex) {
+ // This could happen if binder process crashes.
+ ex.rethrowAsRuntimeException();
+ } catch (NullPointerException ex) {
+ // This could happen before phone restarts due to crashing.
+ throw new IllegalStateException("Telephony service unavailable");
+ }
+ return null;
+ }
+
+ /**
* Return the application ID for the app type like {@link APPTYPE_CSIM}.
*
* Requires that the calling app has READ_PRIVILEGED_PHONE_STATE permission
@@ -6641,6 +6873,7 @@ public class TelephonyManager {
* @return PRLVersion or null if error.
* @hide
*/
+ @SystemApi
public String getCdmaPrlVersion() {
return getCdmaPrlVersion(getSubId());
}
@@ -6866,6 +7099,8 @@ public class TelephonyManager {
* @return true if phone is in emergency callback mode
* @hide
*/
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
public boolean getEmergencyCallbackMode() {
return getEmergencyCallbackMode(getSubId());
}
@@ -6908,4 +7143,101 @@ public class TelephonyManager {
}
return null;
}
+
+ /**
+ * Turns mobile data on or off.
+ * If the {@link TelephonyManager} object has been created with
+ * {@link #createForSubscriptionId}, this API applies to the given subId.
+ * Otherwise, it applies to {@link SubscriptionManager#getDefaultDataSubscriptionId()}
+ *
+ * <p>Requires Permission:
+ * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE} or that the
+ * calling app has carrier privileges.
+ *
+ * @param enable Whether to enable mobile data.
+ *
+ * @see #hasCarrierPrivileges
+ */
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ public void setUserMobileDataEnabled(boolean enable) {
+ setUserMobileDataEnabled(
+ getSubId(SubscriptionManager.getDefaultDataSubscriptionId()), enable);
+ }
+
+ /**
+ * Returns whether mobile data is enabled or not per user setting. There are other factors
+ * that could disable mobile data, but they are not considered here.
+ *
+ * If this object has been created with {@link #createForSubscriptionId}, applies to the given
+ * subId. Otherwise, applies to {@link SubscriptionManager#getDefaultDataSubscriptionId()}
+ *
+ * <p>Requires one of the following permissions:
+ * {@link android.Manifest.permission#ACCESS_NETWORK_STATE ACCESS_NETWORK_STATE},
+ * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}, or that the
+ * calling app has carrier privileges.
+ *
+ * <p>Note that this does not take into account any data restrictions that may be present on the
+ * calling app. Such restrictions may be inspected with
+ * {@link ConnectivityManager#getRestrictBackgroundStatus}.
+ *
+ * @return true if mobile data is enabled.
+ *
+ * @see #hasCarrierPrivileges
+ */
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.ACCESS_NETWORK_STATE,
+ android.Manifest.permission.MODIFY_PHONE_STATE
+ })
+ public boolean isUserMobileDataEnabled() {
+ return isUserMobileDataEnabled(
+ getSubId(SubscriptionManager.getDefaultDataSubscriptionId()));
+ }
+
+ /**
+ * @hide
+ * Unlike isUserMobileDataEnabled, this API also evaluates carrierDataEnabled,
+ * policyDataEnabled etc to give a final decision.
+ */
+ public boolean isMobileDataEnabled() {
+ boolean retVal = false;
+ try {
+ int subId = getSubId(SubscriptionManager.getDefaultDataSubscriptionId());
+ ITelephony telephony = getITelephony();
+ if (telephony != null)
+ retVal = telephony.isDataEnabled(subId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling ITelephony#isDataEnabled", e);
+ } catch (NullPointerException e) {
+ }
+ return retVal;
+ }
+
+ /**
+ * Utility class of {@link #isUserMobileDataEnabled()};
+ */
+ private boolean isUserMobileDataEnabled(int subId) {
+ boolean retVal = false;
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null)
+ retVal = telephony.isUserDataEnabled(subId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling ITelephony#isUserDataEnabled", e);
+ } catch (NullPointerException e) {
+ }
+ return retVal;
+ }
+
+ /** Utility method of {@link #setUserMobileDataEnabled(boolean)} */
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ private void setUserMobileDataEnabled(int subId, boolean enable) {
+ try {
+ Log.d(TAG, "setUserMobileDataEnabled: enabled=" + enable);
+ ITelephony telephony = getITelephony();
+ if (telephony != null)
+ telephony.setUserDataEnabled(subId, enable);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling ITelephony#setUserDataEnabled", e);
+ }
+ }
}
diff --git a/android/telephony/TelephonyScanManager.java b/android/telephony/TelephonyScanManager.java
index 7bcdcdcc..c182e349 100644
--- a/android/telephony/TelephonyScanManager.java
+++ b/android/telephony/TelephonyScanManager.java
@@ -38,7 +38,6 @@ import com.android.internal.telephony.ITelephony;
/**
* Manages the radio access network scan requests and callbacks.
- * @hide
*/
public final class TelephonyScanManager {
@@ -55,7 +54,8 @@ public final class TelephonyScanManager {
public static final int CALLBACK_SCAN_COMPLETE = 3;
/**
- * The caller of {@link #requestNetworkScan(NetworkScanRequest, NetworkScanCallback)} should
+ * The caller of
+ * {@link TelephonyManager#requestNetworkScan(NetworkScanRequest, NetworkScanCallback)} should
* implement and provide this callback so that the scan results or errors can be returned.
*/
public static abstract class NetworkScanCallback {
@@ -75,8 +75,10 @@ public final class TelephonyScanManager {
*
* This callback will be called whenever there is any error about the scan, and the scan
* will be terminated. onComplete() will NOT be called.
+ *
+ * @param error Error code when the scan is failed, as defined in {@link NetworkScan}.
*/
- public void onError(int error) {}
+ public void onError(@NetworkScan.ScanErrorCode int error) {}
}
private static class NetworkScanInfo {
diff --git a/android/telephony/data/DataCallResponse.java b/android/telephony/data/DataCallResponse.java
new file mode 100644
index 00000000..da51c861
--- /dev/null
+++ b/android/telephony/data/DataCallResponse.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2009 Qualcomm Innovation Center, Inc. All Rights Reserved.
+ * Copyright (C) 2009 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.telephony.data;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Description of the response of a setup data call connection request.
+ *
+ * @hide
+ */
+@SystemApi
+public final class DataCallResponse implements Parcelable {
+ private final int mStatus;
+ private final int mSuggestedRetryTime;
+ private final int mCid;
+ private final int mActive;
+ private final String mType;
+ private final String mIfname;
+ private final List<InterfaceAddress> mAddresses;
+ private final List<InetAddress> mDnses;
+ private final List<InetAddress> mGateways;
+ private final List<String> mPcscfs;
+ private final int mMtu;
+
+ /**
+ * @param status Data call fail cause. 0 indicates no error.
+ * @param suggestedRetryTime The suggested data retry time in milliseconds.
+ * @param cid The unique id of the data connection.
+ * @param active Data connection active status. 0 = inactive, 1 = active/physical link down,
+ * 2 = active/physical link up.
+ * @param type The connection protocol, should be one of the PDP_type values in TS 27.007
+ * section 10.1.1. For example, "IP", "IPV6", "IPV4V6", or "PPP".
+ * @param ifname The network interface name.
+ * @param addresses A list of addresses with optional "/" prefix length, e.g.,
+ * "192.0.1.3" or "192.0.1.11/16 2001:db8::1/64". Typically 1 IPv4 or 1 IPv6 or
+ * one of each. If the prefix length is absent the addresses are assumed to be
+ * point to point with IPv4 having a prefix length of 32 and IPv6 128.
+ * @param dnses A list of DNS server addresses, e.g., "192.0.1.3" or
+ * "192.0.1.11 2001:db8::1". Null if no dns server addresses returned.
+ * @param gateways A list of default gateway addresses, e.g., "192.0.1.3" or
+ * "192.0.1.11 2001:db8::1". When null, the addresses represent point to point
+ * connections.
+ * @param pcscfs A list of Proxy Call State Control Function address via PCO(Protocol
+ * Configuration Option) for IMS client.
+ * @param mtu MTU (Maximum transmission unit) received from network Value <= 0 means network has
+ * either not sent a value or sent an invalid value.
+ */
+ public DataCallResponse(int status, int suggestedRetryTime, int cid, int active,
+ @Nullable String type, @Nullable String ifname,
+ @Nullable List<InterfaceAddress> addresses,
+ @Nullable List<InetAddress> dnses,
+ @Nullable List<InetAddress> gateways,
+ @Nullable List<String> pcscfs, int mtu) {
+ mStatus = status;
+ mSuggestedRetryTime = suggestedRetryTime;
+ mCid = cid;
+ mActive = active;
+ mType = (type == null) ? "" : type;
+ mIfname = (ifname == null) ? "" : ifname;
+ mAddresses = (addresses == null) ? new ArrayList<>() : addresses;
+ mDnses = (dnses == null) ? new ArrayList<>() : dnses;
+ mGateways = (gateways == null) ? new ArrayList<>() : gateways;
+ mPcscfs = (pcscfs == null) ? new ArrayList<>() : pcscfs;
+ mMtu = mtu;
+ }
+
+ public DataCallResponse(Parcel source) {
+ mStatus = source.readInt();
+ mSuggestedRetryTime = source.readInt();
+ mCid = source.readInt();
+ mActive = source.readInt();
+ mType = source.readString();
+ mIfname = source.readString();
+ mAddresses = new ArrayList<>();
+ source.readList(mAddresses, InterfaceAddress.class.getClassLoader());
+ mDnses = new ArrayList<>();
+ source.readList(mDnses, InetAddress.class.getClassLoader());
+ mGateways = new ArrayList<>();
+ source.readList(mGateways, InetAddress.class.getClassLoader());
+ mPcscfs = new ArrayList<>();
+ source.readList(mPcscfs, InetAddress.class.getClassLoader());
+ mMtu = source.readInt();
+ }
+
+ /**
+ * @return Data call fail cause. 0 indicates no error.
+ */
+ public int getStatus() { return mStatus; }
+
+ /**
+ * @return The suggested data retry time in milliseconds.
+ */
+ public int getSuggestedRetryTime() { return mSuggestedRetryTime; }
+
+ /**
+ * @return The unique id of the data connection.
+ */
+ public int getCallId() { return mCid; }
+
+ /**
+ * @return 0 = inactive, 1 = active/physical link down, 2 = active/physical link up.
+ */
+ public int getActive() { return mActive; }
+
+ /**
+ * @return The connection protocol, should be one of the PDP_type values in TS 27.007 section
+ * 10.1.1. For example, "IP", "IPV6", "IPV4V6", or "PPP".
+ */
+ @NonNull
+ public String getType() { return mType; }
+
+ /**
+ * @return The network interface name.
+ */
+ @NonNull
+ public String getIfname() { return mIfname; }
+
+ /**
+ * @return A list of {@link InterfaceAddress}
+ */
+ @NonNull
+ public List<InterfaceAddress> getAddresses() { return mAddresses; }
+
+ /**
+ * @return A list of DNS server addresses, e.g., "192.0.1.3" or
+ * "192.0.1.11 2001:db8::1". Empty list if no dns server addresses returned.
+ */
+ @NonNull
+ public List<InetAddress> getDnses() { return mDnses; }
+
+ /**
+ * @return A list of default gateway addresses, e.g., "192.0.1.3" or
+ * "192.0.1.11 2001:db8::1". Empty list if the addresses represent point to point connections.
+ */
+ @NonNull
+ public List<InetAddress> getGateways() { return mGateways; }
+
+ /**
+ * @return A list of Proxy Call State Control Function address via PCO(Protocol Configuration
+ * Option) for IMS client.
+ */
+ @NonNull
+ public List<String> getPcscfs() { return mPcscfs; }
+
+ /**
+ * @return MTU received from network Value <= 0 means network has either not sent a value or
+ * sent an invalid value
+ */
+ public int getMtu() { return mMtu; }
+
+ @Override
+ public String toString() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("DataCallResponse: {")
+ .append(" status=").append(mStatus)
+ .append(" retry=").append(mSuggestedRetryTime)
+ .append(" cid=").append(mCid)
+ .append(" active=").append(mActive)
+ .append(" type=").append(mType)
+ .append(" ifname=").append(mIfname)
+ .append(" addresses=").append(mAddresses)
+ .append(" dnses=").append(mDnses)
+ .append(" gateways=").append(mGateways)
+ .append(" pcscf=").append(mPcscfs)
+ .append(" mtu=").append(mMtu)
+ .append("}");
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals (Object o) {
+ if (this == o) return true;
+
+ if (o == null || !(o instanceof DataCallResponse)) {
+ return false;
+ }
+
+ DataCallResponse other = (DataCallResponse) o;
+ return this.mStatus == other.mStatus
+ && this.mSuggestedRetryTime == other.mSuggestedRetryTime
+ && this.mCid == other.mCid
+ && this.mActive == other.mActive
+ && this.mType.equals(other.mType)
+ && this.mIfname.equals(other.mIfname)
+ && mAddresses.size() == other.mAddresses.size()
+ && mAddresses.containsAll(other.mAddresses)
+ && mDnses.size() == other.mDnses.size()
+ && mDnses.containsAll(other.mDnses)
+ && mGateways.size() == other.mGateways.size()
+ && mGateways.containsAll(other.mGateways)
+ && mPcscfs.size() == other.mPcscfs.size()
+ && mPcscfs.containsAll(other.mPcscfs)
+ && mMtu == other.mMtu;
+ }
+
+ @Override
+ public int hashCode() {
+ return mStatus * 31
+ + mSuggestedRetryTime * 37
+ + mCid * 41
+ + mActive * 43
+ + mType.hashCode() * 47
+ + mIfname.hashCode() * 53
+ + mAddresses.hashCode() * 59
+ + mDnses.hashCode() * 61
+ + mGateways.hashCode() * 67
+ + mPcscfs.hashCode() * 71
+ + mMtu * 73;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mStatus);
+ dest.writeInt(mSuggestedRetryTime);
+ dest.writeInt(mCid);
+ dest.writeInt(mActive);
+ dest.writeString(mType);
+ dest.writeString(mIfname);
+ dest.writeList(mAddresses);
+ dest.writeList(mDnses);
+ dest.writeList(mGateways);
+ dest.writeList(mPcscfs);
+ dest.writeInt(mMtu);
+ }
+
+ public static final Parcelable.Creator<DataCallResponse> CREATOR =
+ new Parcelable.Creator<DataCallResponse>() {
+ @Override
+ public DataCallResponse createFromParcel(Parcel source) {
+ return new DataCallResponse(source);
+ }
+
+ @Override
+ public DataCallResponse[] newArray(int size) {
+ return new DataCallResponse[size];
+ }
+ };
+} \ No newline at end of file
diff --git a/android/telephony/data/InterfaceAddress.java b/android/telephony/data/InterfaceAddress.java
new file mode 100644
index 00000000..00d212a5
--- /dev/null
+++ b/android/telephony/data/InterfaceAddress.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.data;
+
+import android.annotation.SystemApi;
+import android.net.NetworkUtils;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * This class represents a Network Interface address. In short it's an IP address, a subnet mask
+ * when the address is an IPv4 one. An IP address and a network prefix length in the case of IPv6
+ * address.
+ *
+ * @hide
+ */
+@SystemApi
+public final class InterfaceAddress implements Parcelable {
+
+ private final InetAddress mInetAddress;
+
+ private final int mPrefixLength;
+
+ /**
+ * @param inetAddress A {@link InetAddress} of the address
+ * @param prefixLength The network prefix length for this address.
+ */
+ public InterfaceAddress(InetAddress inetAddress, int prefixLength) {
+ mInetAddress = inetAddress;
+ mPrefixLength = prefixLength;
+ }
+
+ /**
+ * @param address The address in string format
+ * @param prefixLength The network prefix length for this address.
+ * @throws UnknownHostException
+ */
+ public InterfaceAddress(String address, int prefixLength) throws UnknownHostException {
+ InetAddress ia;
+ try {
+ ia = NetworkUtils.numericToInetAddress(address);
+ } catch (IllegalArgumentException e) {
+ throw new UnknownHostException("Non-numeric ip addr=" + address);
+ }
+ mInetAddress = ia;
+ mPrefixLength = prefixLength;
+ }
+
+ public InterfaceAddress(Parcel source) {
+ mInetAddress = (InetAddress) source.readSerializable();
+ mPrefixLength = source.readInt();
+ }
+
+ /**
+ * @return an InetAddress for this address.
+ */
+ public InetAddress getAddress() { return mInetAddress; }
+
+ /**
+ * @return The network prefix length for this address.
+ */
+ public int getNetworkPrefixLength() { return mPrefixLength; }
+
+ @Override
+ public boolean equals (Object o) {
+ if (this == o) return true;
+
+ if (o == null || !(o instanceof InterfaceAddress)) {
+ return false;
+ }
+
+ InterfaceAddress other = (InterfaceAddress) o;
+ return this.mInetAddress.equals(other.mInetAddress)
+ && this.mPrefixLength == other.mPrefixLength;
+ }
+
+ @Override
+ public int hashCode() {
+ return mInetAddress.hashCode() * 31 + mPrefixLength * 37;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return mInetAddress + "/" + mPrefixLength;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeSerializable(mInetAddress);
+ dest.writeInt(mPrefixLength);
+ }
+
+ public static final Parcelable.Creator<InterfaceAddress> CREATOR =
+ new Parcelable.Creator<InterfaceAddress>() {
+ @Override
+ public InterfaceAddress createFromParcel(Parcel source) {
+ return new InterfaceAddress(source);
+ }
+
+ @Override
+ public InterfaceAddress[] newArray(int size) {
+ return new InterfaceAddress[size];
+ }
+ };
+}
diff --git a/android/telephony/euicc/EuiccManager.java b/android/telephony/euicc/EuiccManager.java
index a13af5f4..176057dd 100644
--- a/android/telephony/euicc/EuiccManager.java
+++ b/android/telephony/euicc/EuiccManager.java
@@ -15,8 +15,10 @@
*/
package android.telephony.euicc;
+import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
@@ -29,6 +31,9 @@ import android.os.ServiceManager;
import com.android.internal.telephony.euicc.IEuiccController;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* EuiccManager is the application interface to eUICCs, or eSIMs/embedded SIMs.
*
@@ -63,7 +68,7 @@ public class EuiccManager {
* embedded SIM.
*
* <p>The activity will immediately finish with {@link android.app.Activity#RESULT_CANCELED} if
- * {@link #isEnabled} is false or if the device is already provisioned.
+ * {@link #isEnabled} is false.
*
* TODO(b/35851809): Make this a SystemApi.
*/
@@ -167,13 +172,40 @@ public class EuiccManager {
*/
public static final String META_DATA_CARRIER_ICON = "android.telephony.euicc.carriericon";
+ /**
+ * Euicc OTA update status which can be got by {@link #getOtaStatus}
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"EUICC_OTA_"}, value = {
+ EUICC_OTA_IN_PROGRESS,
+ EUICC_OTA_FAILED,
+ EUICC_OTA_SUCCEEDED,
+ EUICC_OTA_NOT_NEEDED,
+ EUICC_OTA_STATUS_UNAVAILABLE
+
+ })
+ public @interface OtaStatus{}
+
+ /**
+ * An OTA is in progress. During this time, the eUICC is not available and the user may lose
+ * network access.
+ */
+ public static final int EUICC_OTA_IN_PROGRESS = 1;
+ /** The OTA update failed. */
+ public static final int EUICC_OTA_FAILED = 2;
+ /** The OTA update finished successfully. */
+ public static final int EUICC_OTA_SUCCEEDED = 3;
+ /** The OTA update not needed since current eUICC OS is latest. */
+ public static final int EUICC_OTA_NOT_NEEDED = 4;
+ /** The OTA status is unavailable since eUICC service is unavailable. */
+ public static final int EUICC_OTA_STATUS_UNAVAILABLE = 5;
+
private final Context mContext;
- private final IEuiccController mController;
/** @hide */
public EuiccManager(Context context) {
mContext = context;
- mController = IEuiccController.Stub.asInterface(ServiceManager.getService("econtroller"));
}
/**
@@ -189,7 +221,7 @@ public class EuiccManager {
public boolean isEnabled() {
// In the future, this may reach out to IEuiccController (if non-null) to check any dynamic
// restrictions.
- return mController != null;
+ return getIEuiccController() != null;
}
/**
@@ -206,7 +238,27 @@ public class EuiccManager {
return null;
}
try {
- return mController.getEid();
+ return getIEuiccController().getEid();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the current status of eUICC OTA.
+ *
+ * <p>Requires the {@link android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission.
+ *
+ * @return the status of eUICC OTA. If {@link #isEnabled()} is false or the eUICC is not ready,
+ * {@link OtaStatus#EUICC_OTA_STATUS_UNAVAILABLE} will be returned.
+ */
+ @SystemApi
+ public int getOtaStatus() {
+ if (!isEnabled()) {
+ return EUICC_OTA_STATUS_UNAVAILABLE;
+ }
+ try {
+ return getIEuiccController().getOtaStatus();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -232,7 +284,7 @@ public class EuiccManager {
return;
}
try {
- mController.downloadSubscription(subscription, switchAfterDownload,
+ getIEuiccController().downloadSubscription(subscription, switchAfterDownload,
mContext.getOpPackageName(), callbackIntent);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -296,7 +348,7 @@ public class EuiccManager {
return;
}
try {
- mController.continueOperation(resolutionIntent, resolutionExtras);
+ getIEuiccController().continueOperation(resolutionIntent, resolutionExtras);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -328,7 +380,7 @@ public class EuiccManager {
return;
}
try {
- mController.getDownloadableSubscriptionMetadata(
+ getIEuiccController().getDownloadableSubscriptionMetadata(
subscription, mContext.getOpPackageName(), callbackIntent);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -358,7 +410,7 @@ public class EuiccManager {
return;
}
try {
- mController.getDefaultDownloadableSubscriptionList(
+ getIEuiccController().getDefaultDownloadableSubscriptionList(
mContext.getOpPackageName(), callbackIntent);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -377,7 +429,7 @@ public class EuiccManager {
return null;
}
try {
- return mController.getEuiccInfo();
+ return getIEuiccController().getEuiccInfo();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -402,7 +454,7 @@ public class EuiccManager {
return;
}
try {
- mController.deleteSubscription(
+ getIEuiccController().deleteSubscription(
subscriptionId, mContext.getOpPackageName(), callbackIntent);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -429,7 +481,7 @@ public class EuiccManager {
return;
}
try {
- mController.switchToSubscription(
+ getIEuiccController().switchToSubscription(
subscriptionId, mContext.getOpPackageName(), callbackIntent);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -455,7 +507,8 @@ public class EuiccManager {
return;
}
try {
- mController.updateSubscriptionNickname(subscriptionId, nickname, callbackIntent);
+ getIEuiccController().updateSubscriptionNickname(
+ subscriptionId, nickname, callbackIntent);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -477,7 +530,7 @@ public class EuiccManager {
return;
}
try {
- mController.eraseSubscriptions(callbackIntent);
+ getIEuiccController().eraseSubscriptions(callbackIntent);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -507,7 +560,7 @@ public class EuiccManager {
return;
}
try {
- mController.retainSubscriptionsForFactoryReset(callbackIntent);
+ getIEuiccController().retainSubscriptionsForFactoryReset(callbackIntent);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -520,4 +573,8 @@ public class EuiccManager {
// Caller canceled the callback; do nothing.
}
}
+
+ private static IEuiccController getIEuiccController() {
+ return IEuiccController.Stub.asInterface(ServiceManager.getService("econtroller"));
+ }
}
diff --git a/android/telephony/ims/feature/ImsFeature.java b/android/telephony/ims/feature/ImsFeature.java
index 062858d4..ca4a210e 100644
--- a/android/telephony/ims/feature/ImsFeature.java
+++ b/android/telephony/ims/feature/ImsFeature.java
@@ -188,6 +188,11 @@ public abstract class ImsFeature {
}
/**
+ * Called when the feature is ready to use.
+ */
+ public abstract void onFeatureReady();
+
+ /**
* Called when the feature is being removed and must be cleaned up.
*/
public abstract void onFeatureRemoved();
diff --git a/android/telephony/ims/feature/MMTelFeature.java b/android/telephony/ims/feature/MMTelFeature.java
index e790d146..4e095e3a 100644
--- a/android/telephony/ims/feature/MMTelFeature.java
+++ b/android/telephony/ims/feature/MMTelFeature.java
@@ -346,6 +346,11 @@ public class MMTelFeature extends ImsFeature {
return null;
}
+ @Override
+ public void onFeatureReady() {
+
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/android/telephony/ims/feature/RcsFeature.java b/android/telephony/ims/feature/RcsFeature.java
index a82e6086..40c5181d 100644
--- a/android/telephony/ims/feature/RcsFeature.java
+++ b/android/telephony/ims/feature/RcsFeature.java
@@ -36,6 +36,11 @@ public class RcsFeature extends ImsFeature {
}
@Override
+ public void onFeatureReady() {
+
+ }
+
+ @Override
public void onFeatureRemoved() {
}
diff --git a/android/telephony/ims/internal/ImsCallSessionListener.java b/android/telephony/ims/internal/ImsCallSessionListener.java
new file mode 100644
index 00000000..5d16dd5b
--- /dev/null
+++ b/android/telephony/ims/internal/ImsCallSessionListener.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.telephony.ims.internal;
+
+import android.os.RemoteException;
+import android.telephony.ims.internal.aidl.IImsCallSessionListener;
+
+import com.android.ims.ImsCallProfile;
+import com.android.ims.ImsConferenceState;
+import com.android.ims.ImsReasonInfo;
+import com.android.ims.ImsStreamMediaProfile;
+import com.android.ims.ImsSuppServiceNotification;
+import com.android.ims.internal.ImsCallSession;
+
+/**
+ * Proxy class for interfacing with the framework's Call session for an ongoing IMS call.
+ *
+ * DO NOT remove or change the existing APIs, only add new ones to this Base implementation or you
+ * will break other implementations of ImsCallSessionListener maintained by other ImsServices.
+ *
+ * @hide
+ */
+public class ImsCallSessionListener {
+
+ private final IImsCallSessionListener mListener;
+
+ public ImsCallSessionListener(IImsCallSessionListener l) {
+ mListener = l;
+ }
+
+ /**
+ * Called when a request is sent out to initiate a new session
+ * and 1xx response is received from the network.
+ */
+ public void callSessionProgressing(ImsStreamMediaProfile profile)
+ throws RemoteException {
+ mListener.callSessionProgressing(profile);
+ }
+
+ /**
+ * Called when the session is initiated.
+ *
+ * @param profile the associated {@link ImsCallSession}.
+ */
+ public void callSessionInitiated(ImsCallProfile profile) throws RemoteException {
+ mListener.callSessionInitiated(profile);
+ }
+
+ /**
+ * Called when the session establishment has failed.
+ *
+ * @param reasonInfo detailed reason of the session establishment failure
+ */
+ public void callSessionInitiatedFailed(ImsReasonInfo reasonInfo) throws RemoteException {
+ mListener.callSessionInitiatedFailed(reasonInfo);
+ }
+
+ /**
+ * Called when the session is terminated.
+ *
+ * @param reasonInfo detailed reason of the session termination
+ */
+ public void callSessionTerminated(ImsReasonInfo reasonInfo) throws RemoteException {
+ mListener.callSessionTerminated(reasonInfo);
+ }
+
+ /**
+ * Called when the session is on hold.
+ */
+ public void callSessionHeld(ImsCallProfile profile) throws RemoteException {
+ mListener.callSessionHeld(profile);
+ }
+
+ /**
+ * Called when the session hold has failed.
+ *
+ * @param reasonInfo detailed reason of the session hold failure
+ */
+ public void callSessionHoldFailed(ImsReasonInfo reasonInfo) throws RemoteException {
+ mListener.callSessionHoldFailed(reasonInfo);
+ }
+
+ /**
+ * Called when the session hold is received from the remote user.
+ */
+ public void callSessionHoldReceived(ImsCallProfile profile) throws RemoteException {
+ mListener.callSessionHoldReceived(profile);
+ }
+
+ /**
+ * Called when the session resume is done.
+ */
+ public void callSessionResumed(ImsCallProfile profile) throws RemoteException {
+ mListener.callSessionResumed(profile);
+ }
+
+ /**
+ * Called when the session resume has failed.
+ *
+ * @param reasonInfo detailed reason of the session resume failure
+ */
+ public void callSessionResumeFailed(ImsReasonInfo reasonInfo) throws RemoteException {
+ mListener.callSessionResumeFailed(reasonInfo);
+ }
+
+ /**
+ * Called when the session resume is received from the remote user.
+ */
+ public void callSessionResumeReceived(ImsCallProfile profile) throws RemoteException {
+ mListener.callSessionResumeReceived(profile);
+ }
+
+ /**
+ * Called when the session merge has been started. At this point, the {@code newSession}
+ * represents the session which has been initiated to the IMS conference server for the
+ * new merged conference.
+ *
+ * @param newSession the session object that is merged with an active & hold session
+ */
+ public void callSessionMergeStarted(ImsCallSession newSession, ImsCallProfile profile)
+ throws RemoteException {
+ mListener.callSessionMergeStarted(newSession != null ? newSession.getSession() : null,
+ profile);
+ }
+
+ /**
+ * Called when the session merge is successful and the merged session is active.
+ *
+ * @param newSession the new session object that is used for the conference
+ */
+ public void callSessionMergeComplete(ImsCallSession newSession) throws RemoteException {
+ mListener.callSessionMergeComplete(newSession != null ? newSession.getSession() : null);
+ }
+
+ /**
+ * Called when the session merge has failed.
+ *
+ * @param reasonInfo detailed reason of the call merge failure
+ */
+ public void callSessionMergeFailed(ImsReasonInfo reasonInfo) throws RemoteException {
+ mListener.callSessionMergeFailed(reasonInfo);
+ }
+
+ /**
+ * Called when the session is updated (except for hold/unhold).
+ */
+ public void callSessionUpdated(ImsCallProfile profile) throws RemoteException {
+ mListener.callSessionUpdated(profile);
+ }
+
+ /**
+ * Called when the session update has failed.
+ *
+ * @param reasonInfo detailed reason of the session update failure
+ */
+ public void callSessionUpdateFailed(ImsReasonInfo reasonInfo) throws RemoteException {
+ mListener.callSessionUpdateFailed(reasonInfo);
+ }
+
+ /**
+ * Called when the session update is received from the remote user.
+ */
+ public void callSessionUpdateReceived(ImsCallProfile profile) throws RemoteException {
+ mListener.callSessionUpdateReceived(profile);
+ }
+
+ /**
+ * Called when the session has been extended to a conference session.
+ *
+ * @param newSession the session object that is extended to the conference
+ * from the active session
+ */
+ public void callSessionConferenceExtended(ImsCallSession newSession, ImsCallProfile profile)
+ throws RemoteException {
+ mListener.callSessionConferenceExtended(newSession != null ? newSession.getSession() : null,
+ profile);
+ }
+
+ /**
+ * Called when the conference extension has failed.
+ *
+ * @param reasonInfo detailed reason of the conference extension failure
+ */
+ public void callSessionConferenceExtendFailed(ImsReasonInfo reasonInfo) throws RemoteException {
+ mListener.callSessionConferenceExtendFailed(reasonInfo);
+ }
+
+ /**
+ * Called when the conference extension is received from the remote user.
+ */
+ public void callSessionConferenceExtendReceived(ImsCallSession newSession,
+ ImsCallProfile profile) throws RemoteException {
+ mListener.callSessionConferenceExtendReceived(newSession != null
+ ? newSession.getSession() : null, profile);
+ }
+
+ /**
+ * Called when the invitation request of the participants is delivered to the conference
+ * server.
+ */
+ public void callSessionInviteParticipantsRequestDelivered() throws RemoteException {
+ mListener.callSessionInviteParticipantsRequestDelivered();
+ }
+
+ /**
+ * Called when the invitation request of the participants has failed.
+ *
+ * @param reasonInfo detailed reason of the conference invitation failure
+ */
+ public void callSessionInviteParticipantsRequestFailed(ImsReasonInfo reasonInfo)
+ throws RemoteException {
+ mListener.callSessionInviteParticipantsRequestFailed(reasonInfo);
+ }
+
+ /**
+ * Called when the removal request of the participants is delivered to the conference
+ * server.
+ */
+ public void callSessionRemoveParticipantsRequestDelivered() throws RemoteException {
+ mListener.callSessionRemoveParticipantsRequestDelivered();
+ }
+
+ /**
+ * Called when the removal request of the participants has failed.
+ *
+ * @param reasonInfo detailed reason of the conference removal failure
+ */
+ public void callSessionRemoveParticipantsRequestFailed(ImsReasonInfo reasonInfo)
+ throws RemoteException {
+ mListener.callSessionInviteParticipantsRequestFailed(reasonInfo);
+ }
+
+ /**
+ * Notifies the framework of the updated Call session conference state.
+ *
+ * @param state the new {@link ImsConferenceState} associated with the conference.
+ */
+ public void callSessionConferenceStateUpdated(ImsConferenceState state) throws RemoteException {
+ mListener.callSessionConferenceStateUpdated(state);
+ }
+
+ /**
+ * Notifies the incoming USSD message.
+ */
+ public void callSessionUssdMessageReceived(int mode, String ussdMessage)
+ throws RemoteException {
+ mListener.callSessionUssdMessageReceived(mode, ussdMessage);
+ }
+
+ /**
+ * Notifies of a case where a {@link com.android.ims.internal.ImsCallSession} may potentially
+ * handover from one radio technology to another.
+ *
+ * @param srcAccessTech The source radio access technology; one of the access technology
+ * constants defined in {@link android.telephony.ServiceState}. For
+ * example
+ * {@link android.telephony.ServiceState#RIL_RADIO_TECHNOLOGY_LTE}.
+ * @param targetAccessTech The target radio access technology; one of the access technology
+ * constants defined in {@link android.telephony.ServiceState}. For
+ * example
+ * {@link android.telephony.ServiceState#RIL_RADIO_TECHNOLOGY_LTE}.
+ */
+ public void callSessionMayHandover(int srcAccessTech, int targetAccessTech)
+ throws RemoteException {
+ mListener.callSessionMayHandover(srcAccessTech, targetAccessTech);
+ }
+
+ /**
+ * Called when session access technology changes.
+ *
+ * @param srcAccessTech original access technology
+ * @param targetAccessTech new access technology
+ * @param reasonInfo
+ */
+ public void callSessionHandover(int srcAccessTech, int targetAccessTech,
+ ImsReasonInfo reasonInfo) throws RemoteException {
+ mListener.callSessionHandover(srcAccessTech, targetAccessTech, reasonInfo);
+ }
+
+ /**
+ * Called when session access technology change fails.
+ *
+ * @param srcAccessTech original access technology
+ * @param targetAccessTech new access technology
+ * @param reasonInfo handover failure reason
+ */
+ public void callSessionHandoverFailed(int srcAccessTech, int targetAccessTech,
+ ImsReasonInfo reasonInfo) throws RemoteException {
+ mListener.callSessionHandoverFailed(srcAccessTech, targetAccessTech, reasonInfo);
+ }
+
+ /**
+ * Called when the TTY mode is changed by the remote party.
+ *
+ * @param mode one of the following: -
+ * {@link com.android.internal.telephony.Phone#TTY_MODE_OFF} -
+ * {@link com.android.internal.telephony.Phone#TTY_MODE_FULL} -
+ * {@link com.android.internal.telephony.Phone#TTY_MODE_HCO} -
+ * {@link com.android.internal.telephony.Phone#TTY_MODE_VCO}
+ */
+ public void callSessionTtyModeReceived(int mode) throws RemoteException {
+ mListener.callSessionTtyModeReceived(mode);
+ }
+
+ /**
+ * Called when the multiparty state is changed for this {@code ImsCallSession}.
+ *
+ * @param isMultiParty {@code true} if the session became multiparty,
+ * {@code false} otherwise.
+ */
+
+ public void callSessionMultipartyStateChanged(boolean isMultiParty) throws RemoteException {
+ mListener.callSessionMultipartyStateChanged(isMultiParty);
+ }
+
+ /**
+ * Called when the supplementary service information is received for the current session.
+ */
+ public void callSessionSuppServiceReceived(ImsSuppServiceNotification suppSrvNotification)
+ throws RemoteException {
+ mListener.callSessionSuppServiceReceived(suppSrvNotification);
+ }
+
+ /**
+ * Received RTT modify request from the remote party.
+ *
+ * @param callProfile ImsCallProfile with updated attributes
+ */
+ public void callSessionRttModifyRequestReceived(ImsCallProfile callProfile)
+ throws RemoteException {
+ mListener.callSessionRttModifyRequestReceived(callProfile);
+ }
+
+ /**
+ * @param status the received response for RTT modify request.
+ */
+ public void callSessionRttModifyResponseReceived(int status) throws RemoteException {
+ mListener.callSessionRttModifyResponseReceived(status);
+ }
+
+ /**
+ * Device received RTT message from Remote UE.
+ *
+ * @param rttMessage RTT message received
+ */
+ public void callSessionRttMessageReceived(String rttMessage) throws RemoteException {
+ mListener.callSessionRttMessageReceived(rttMessage);
+ }
+}
+
diff --git a/android/telephony/ims/internal/ImsService.java b/android/telephony/ims/internal/ImsService.java
new file mode 100644
index 00000000..b7c8ca0f
--- /dev/null
+++ b/android/telephony/ims/internal/ImsService.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.telephony.ims.internal;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.telephony.CarrierConfigManager;
+import android.telephony.ims.internal.aidl.IImsConfig;
+import android.telephony.ims.internal.aidl.IImsMmTelFeature;
+import android.telephony.ims.internal.aidl.IImsRcsFeature;
+import android.telephony.ims.internal.aidl.IImsRegistration;
+import android.telephony.ims.internal.aidl.IImsServiceController;
+import android.telephony.ims.internal.aidl.IImsServiceControllerListener;
+import android.telephony.ims.internal.feature.ImsFeature;
+import android.telephony.ims.internal.feature.MmTelFeature;
+import android.telephony.ims.internal.feature.RcsFeature;
+import android.telephony.ims.internal.stub.ImsConfigImplBase;
+import android.telephony.ims.internal.stub.ImsFeatureConfiguration;
+import android.telephony.ims.internal.stub.ImsRegistrationImplBase;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.ims.internal.IImsFeatureStatusCallback;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Main ImsService implementation, which binds via the Telephony ImsResolver. Services that extend
+ * ImsService must register the service in their AndroidManifest to be detected by the framework.
+ * First, the application must declare that they use the "android.permission.BIND_IMS_SERVICE"
+ * permission. Then, the ImsService definition in the manifest must follow the following format:
+ *
+ * ...
+ * <service android:name=".EgImsService"
+ * android:permission="android.permission.BIND_IMS_SERVICE" >
+ * <!-- Apps must declare which features they support as metadata. The different categories are
+ * defined below. In this example, the RCS_FEATURE feature is supported. -->
+ * <meta-data android:name="android.telephony.ims.RCS_FEATURE" android:value="true" />
+ * <intent-filter>
+ * <action android:name="android.telephony.ims.ImsService" />
+ * </intent-filter>
+ * </service>
+ * ...
+ *
+ * The telephony framework will then bind to the ImsService you have defined in your manifest
+ * if you are either:
+ * 1) Defined as the default ImsService for the device in the device overlay using
+ * "config_ims_package".
+ * 2) Defined as a Carrier Provided ImsService in the Carrier Configuration using
+ * {@link CarrierConfigManager#KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING}.
+ *
+ * The features that are currently supported in an ImsService are:
+ * - RCS_FEATURE: This ImsService implements the RcsFeature class.
+ * - MMTEL_FEATURE: This ImsService implements the MmTelFeature class.
+ * @hide
+ */
+public class ImsService extends Service {
+
+ private static final String LOG_TAG = "ImsService";
+
+ /**
+ * The intent that must be defined as an intent-filter in the AndroidManifest of the ImsService.
+ * @hide
+ */
+ public static final String SERVICE_INTERFACE = "android.telephony.ims.ImsService";
+
+ // A map of slot Id -> map of features (indexed by ImsFeature feature id) corresponding to that
+ // slot.
+ // We keep track of this to facilitate cleanup of the IImsFeatureStatusCallback and
+ // call ImsFeature#onFeatureRemoved.
+ private final SparseArray<SparseArray<ImsFeature>> mFeaturesBySlot = new SparseArray<>();
+
+ private IImsServiceControllerListener mListener;
+
+
+ /**
+ * Listener that notifies the framework of ImsService changes.
+ */
+ public static class Listener extends IImsServiceControllerListener.Stub {
+ /**
+ * The IMS features that this ImsService supports has changed.
+ * @param c a new {@link ImsFeatureConfiguration} containing {@link ImsFeature.FeatureType}s
+ * that this ImsService supports. This may trigger the addition/removal of feature
+ * in this service.
+ */
+ public void onUpdateSupportedImsFeatures(ImsFeatureConfiguration c) {
+ }
+ }
+
+ /**
+ * @hide
+ */
+ protected final IBinder mImsServiceController = new IImsServiceController.Stub() {
+ @Override
+ public void setListener(IImsServiceControllerListener l) {
+ mListener = l;
+ }
+
+ @Override
+ public IImsMmTelFeature createMmTelFeature(int slotId, IImsFeatureStatusCallback c) {
+ return createMmTelFeatureInternal(slotId, c);
+ }
+
+ @Override
+ public IImsRcsFeature createRcsFeature(int slotId, IImsFeatureStatusCallback c) {
+ return createRcsFeatureInternal(slotId, c);
+ }
+
+ @Override
+ public void removeImsFeature(int slotId, int featureType, IImsFeatureStatusCallback c)
+ throws RemoteException {
+ ImsService.this.removeImsFeature(slotId, featureType, c);
+ }
+
+ @Override
+ public ImsFeatureConfiguration querySupportedImsFeatures() {
+ return ImsService.this.querySupportedImsFeatures();
+ }
+
+ @Override
+ public void notifyImsServiceReadyForFeatureCreation() {
+ ImsService.this.readyForFeatureCreation();
+ }
+
+ @Override
+ public void notifyImsFeatureReady(int slotId, int featureType)
+ throws RemoteException {
+ ImsService.this.notifyImsFeatureReady(slotId, featureType);
+ }
+
+ @Override
+ public IImsConfig getConfig(int slotId) throws RemoteException {
+ ImsConfigImplBase c = ImsService.this.getConfig(slotId);
+ return c != null ? c.getBinder() : null;
+ }
+
+ @Override
+ public IImsRegistration getRegistration(int slotId) throws RemoteException {
+ ImsRegistrationImplBase r = ImsService.this.getRegistration(slotId);
+ return r != null ? r.getBinder() : null;
+ }
+ };
+
+ /**
+ * @hide
+ */
+ @Override
+ public IBinder onBind(Intent intent) {
+ if(SERVICE_INTERFACE.equals(intent.getAction())) {
+ Log.i(LOG_TAG, "ImsService Bound.");
+ return mImsServiceController;
+ }
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public SparseArray<ImsFeature> getFeatures(int slotId) {
+ return mFeaturesBySlot.get(slotId);
+ }
+
+ private IImsMmTelFeature createMmTelFeatureInternal(int slotId,
+ IImsFeatureStatusCallback c) {
+ MmTelFeature f = createMmTelFeature(slotId);
+ if (f != null) {
+ setupFeature(f, slotId, ImsFeature.FEATURE_MMTEL, c);
+ return f.getBinder();
+ } else {
+ Log.e(LOG_TAG, "createMmTelFeatureInternal: null feature returned.");
+ return null;
+ }
+ }
+
+ private IImsRcsFeature createRcsFeatureInternal(int slotId,
+ IImsFeatureStatusCallback c) {
+ RcsFeature f = createRcsFeature(slotId);
+ if (f != null) {
+ setupFeature(f, slotId, ImsFeature.FEATURE_RCS, c);
+ return f.getBinder();
+ } else {
+ Log.e(LOG_TAG, "createRcsFeatureInternal: null feature returned.");
+ return null;
+ }
+ }
+
+ private void setupFeature(ImsFeature f, int slotId, int featureType,
+ IImsFeatureStatusCallback c) {
+ f.addImsFeatureStatusCallback(c);
+ f.initialize(this, slotId);
+ addImsFeature(slotId, featureType, f);
+ }
+
+ private void addImsFeature(int slotId, int featureType, ImsFeature f) {
+ synchronized (mFeaturesBySlot) {
+ // Get SparseArray for Features, by querying slot Id
+ SparseArray<ImsFeature> features = mFeaturesBySlot.get(slotId);
+ if (features == null) {
+ // Populate new SparseArray of features if it doesn't exist for this slot yet.
+ features = new SparseArray<>();
+ mFeaturesBySlot.put(slotId, features);
+ }
+ features.put(featureType, f);
+ }
+ }
+
+ private void removeImsFeature(int slotId, int featureType,
+ IImsFeatureStatusCallback c) {
+ synchronized (mFeaturesBySlot) {
+ // get ImsFeature associated with the slot/feature
+ SparseArray<ImsFeature> features = mFeaturesBySlot.get(slotId);
+ if (features == null) {
+ Log.w(LOG_TAG, "Can not remove ImsFeature. No ImsFeatures exist on slot "
+ + slotId);
+ return;
+ }
+ ImsFeature f = features.get(featureType);
+ if (f == null) {
+ Log.w(LOG_TAG, "Can not remove ImsFeature. No feature with type "
+ + featureType + " exists on slot " + slotId);
+ return;
+ }
+ f.removeImsFeatureStatusCallback(c);
+ f.onFeatureRemoved();
+ features.remove(featureType);
+ }
+ }
+
+ private void notifyImsFeatureReady(int slotId, int featureType) {
+ synchronized (mFeaturesBySlot) {
+ // get ImsFeature associated with the slot/feature
+ SparseArray<ImsFeature> features = mFeaturesBySlot.get(slotId);
+ if (features == null) {
+ Log.w(LOG_TAG, "Can not notify ImsFeature ready. No ImsFeatures exist on " +
+ "slot " + slotId);
+ return;
+ }
+ ImsFeature f = features.get(featureType);
+ if (f == null) {
+ Log.w(LOG_TAG, "Can not notify ImsFeature ready. No feature with type "
+ + featureType + " exists on slot " + slotId);
+ return;
+ }
+ f.onFeatureReady();
+ }
+ }
+
+ /**
+ * When called, provide the {@link ImsFeatureConfiguration} that this ImsService currently
+ * supports. This will trigger the framework to set up the {@link ImsFeature}s that correspond
+ * to the {@link ImsFeature.FeatureType}s configured here.
+ * @return an {@link ImsFeatureConfiguration} containing Features this ImsService supports,
+ * defined in {@link ImsFeature.FeatureType}.
+ */
+ public ImsFeatureConfiguration querySupportedImsFeatures() {
+ // Return empty for base implementation
+ return new ImsFeatureConfiguration();
+ }
+
+ /**
+ * Updates the framework with a new {@link ImsFeatureConfiguration} containing the updated
+ * features, defined in {@link ImsFeature.FeatureType} that this ImsService supports. This may
+ * trigger the framework to add/remove new ImsFeatures, depending on the configuration.
+ */
+ public final void onUpdateSupportedImsFeatures(ImsFeatureConfiguration c)
+ throws RemoteException {
+ if (mListener == null) {
+ throw new IllegalStateException("Framework is not ready");
+ }
+ mListener.onUpdateSupportedImsFeatures(c);
+ }
+
+ /**
+ * The ImsService has been bound and is ready for ImsFeature creation based on the Features that
+ * the ImsService has registered for with the framework, either in the manifest or via
+ * The ImsService should use this signal instead of onCreate/onBind or similar to perform
+ * feature initialization because the framework may bind to this service multiple times to
+ * query the ImsService's {@link ImsFeatureConfiguration} via
+ * {@link #querySupportedImsFeatures()}before creating features.
+ */
+ public void readyForFeatureCreation() {
+ }
+
+ /**
+ * When called, the framework is requesting that a new MmTelFeature is created for the specified
+ * slot.
+ *
+ * @param slotId The slot ID that the MMTel Feature is being created for.
+ * @return The newly created MmTelFeature associated with the slot or null if the feature is not
+ * supported.
+ */
+ public MmTelFeature createMmTelFeature(int slotId) {
+ return null;
+ }
+
+ /**
+ * When called, the framework is requesting that a new RcsFeature is created for the specified
+ * slot
+ *
+ * @param slotId The slot ID that the RCS Feature is being created for.
+ * @return The newly created RcsFeature associated with the slot or null if the feature is not
+ * supported.
+ */
+ public RcsFeature createRcsFeature(int slotId) {
+ return null;
+ }
+
+ /**
+ * @param slotId The slot that the IMS configuration is associated with.
+ * @return ImsConfig implementation that is associated with the specified slot.
+ */
+ public ImsConfigImplBase getConfig(int slotId) {
+ return new ImsConfigImplBase();
+ }
+
+ /**
+ * @param slotId The slot that is associated with the IMS Registration.
+ * @return the ImsRegistration implementation associated with the slot.
+ */
+ public ImsRegistrationImplBase getRegistration(int slotId) {
+ return new ImsRegistrationImplBase();
+ }
+}
diff --git a/android/telephony/ims/internal/SmsImplBase.java b/android/telephony/ims/internal/SmsImplBase.java
new file mode 100644
index 00000000..47414cf7
--- /dev/null
+++ b/android/telephony/ims/internal/SmsImplBase.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.telephony.ims.internal;
+
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
+import android.os.RemoteException;
+import android.telephony.SmsManager;
+import android.telephony.SmsMessage;
+import android.telephony.ims.internal.aidl.IImsSmsListener;
+import android.telephony.ims.internal.feature.MmTelFeature;
+import android.util.Log;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Base implementation for SMS over IMS.
+ *
+ * Any service wishing to provide SMS over IMS should extend this class and implement all methods
+ * that the service supports.
+ * @hide
+ */
+public class SmsImplBase {
+ private static final String LOG_TAG = "SmsImplBase";
+
+ @IntDef({
+ SEND_STATUS_OK,
+ SEND_STATUS_ERROR,
+ SEND_STATUS_ERROR_RETRY,
+ SEND_STATUS_ERROR_FALLBACK
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SendStatusResult {}
+ /**
+ * Message was sent successfully.
+ */
+ public static final int SEND_STATUS_OK = 1;
+
+ /**
+ * IMS provider failed to send the message and platform should not retry falling back to sending
+ * the message using the radio.
+ */
+ public static final int SEND_STATUS_ERROR = 2;
+
+ /**
+ * IMS provider failed to send the message and platform should retry again after setting TP-RD bit
+ * to high.
+ */
+ public static final int SEND_STATUS_ERROR_RETRY = 3;
+
+ /**
+ * IMS provider failed to send the message and platform should retry falling back to sending
+ * the message using the radio.
+ */
+ public static final int SEND_STATUS_ERROR_FALLBACK = 4;
+
+ @IntDef({
+ DELIVER_STATUS_OK,
+ DELIVER_STATUS_ERROR
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DeliverStatusResult {}
+ /**
+ * Message was delivered successfully.
+ */
+ public static final int DELIVER_STATUS_OK = 1;
+
+ /**
+ * Message was not delivered.
+ */
+ public static final int DELIVER_STATUS_ERROR = 2;
+
+ @IntDef({
+ STATUS_REPORT_STATUS_OK,
+ STATUS_REPORT_STATUS_ERROR
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StatusReportResult {}
+
+ /**
+ * Status Report was set successfully.
+ */
+ public static final int STATUS_REPORT_STATUS_OK = 1;
+
+ /**
+ * Error while setting status report.
+ */
+ public static final int STATUS_REPORT_STATUS_ERROR = 2;
+
+
+ // Lock for feature synchronization
+ private final Object mLock = new Object();
+ private IImsSmsListener mListener;
+
+ /**
+ * Registers a listener responsible for handling tasks like delivering messages.
+ *
+ * @param listener listener to register.
+ *
+ * @hide
+ */
+ public final void registerSmsListener(IImsSmsListener listener) {
+ synchronized (mLock) {
+ mListener = listener;
+ }
+ }
+
+ /**
+ * This method will be triggered by the platform when the user attempts to send an SMS. This
+ * method should be implemented by the IMS providers to provide implementation of sending an SMS
+ * over IMS.
+ *
+ * @param smsc the Short Message Service Center address.
+ * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and
+ * {@link SmsMessage#FORMAT_3GPP2}.
+ * @param messageRef the message reference.
+ * @param isRetry whether it is a retry of an already attempted message or not.
+ * @param pdu PDUs representing the contents of the message.
+ */
+ public void sendSms(int messageRef, String format, String smsc, boolean isRetry, byte[] pdu) {
+ // Base implementation returns error. Should be overridden.
+ try {
+ onSendSmsResult(messageRef, SEND_STATUS_ERROR, SmsManager.RESULT_ERROR_GENERIC_FAILURE);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Can not send sms: " + e.getMessage());
+ }
+ }
+
+ /**
+ * This method will be triggered by the platform after {@link #onSmsReceived(String, byte[])} has
+ * been called to deliver the result to the IMS provider.
+ *
+ * @param result result of delivering the message. Valid values are defined in
+ * {@link DeliverStatusResult}
+ * @param messageRef the message reference or -1 of unavailable.
+ */
+ public void acknowledgeSms(int messageRef, @DeliverStatusResult int result) {
+
+ }
+
+ /**
+ * This method will be triggered by the platform after
+ * {@link #onSmsStatusReportReceived(int, int, byte[])} has been called to provide the result to
+ * the IMS provider.
+ *
+ * @param result result of delivering the message. Valid values are defined in
+ * {@link StatusReportResult}
+ * @param messageRef the message reference or -1 of unavailable.
+ */
+ public void acknowledgeSmsReport(int messageRef, @StatusReportResult int result) {
+
+ }
+
+ /**
+ * This method should be triggered by the IMS providers when there is an incoming message. The
+ * platform will deliver the message to the messages database and notify the IMS provider of the
+ * result by calling {@link #acknowledgeSms(int, int)}.
+ *
+ * This method must not be called before {@link MmTelFeature#onFeatureReady()} is called.
+ *
+ * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and
+ * {@link SmsMessage#FORMAT_3GPP2}.
+ * @param pdu PDUs representing the contents of the message.
+ * @throws IllegalStateException if called before {@link MmTelFeature#onFeatureReady()}
+ */
+ public final void onSmsReceived(String format, byte[] pdu) throws IllegalStateException {
+ synchronized (mLock) {
+ if (mListener == null) {
+ throw new IllegalStateException("Feature not ready.");
+ }
+ try {
+ mListener.onSmsReceived(format, pdu);
+ acknowledgeSms(-1, DELIVER_STATUS_OK);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Can not deliver sms: " + e.getMessage());
+ acknowledgeSms(-1, DELIVER_STATUS_ERROR);
+ }
+ }
+ }
+
+ /**
+ * This method should be triggered by the IMS providers to pass the result of the sent message
+ * to the platform.
+ *
+ * This method must not be called before {@link MmTelFeature#onFeatureReady()} is called.
+ *
+ * @param messageRef the message reference. Should be between 0 and 255 per TS.123.040
+ * @param status result of sending the SMS. Valid values are defined in {@link SendStatusResult}
+ * @param reason reason in case status is failure. Valid values are:
+ * {@link SmsManager#RESULT_ERROR_NONE},
+ * {@link SmsManager#RESULT_ERROR_GENERIC_FAILURE},
+ * {@link SmsManager#RESULT_ERROR_RADIO_OFF},
+ * {@link SmsManager#RESULT_ERROR_NULL_PDU},
+ * {@link SmsManager#RESULT_ERROR_NO_SERVICE},
+ * {@link SmsManager#RESULT_ERROR_LIMIT_EXCEEDED},
+ * {@link SmsManager#RESULT_ERROR_SHORT_CODE_NOT_ALLOWED},
+ * {@link SmsManager#RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED}
+ * @throws IllegalStateException if called before {@link MmTelFeature#onFeatureReady()}
+ * @throws RemoteException if the connection to the framework is not available. If this happens
+ * attempting to send the SMS should be aborted.
+ */
+ public final void onSendSmsResult(int messageRef, @SendStatusResult int status, int reason)
+ throws IllegalStateException, RemoteException {
+ synchronized (mLock) {
+ if (mListener == null) {
+ throw new IllegalStateException("Feature not ready.");
+ }
+ mListener.onSendSmsResult(messageRef, status, reason);
+ }
+ }
+
+ /**
+ * Sets the status report of the sent message.
+ *
+ * @param messageRef the message reference.
+ * @param format the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and
+ * {@link SmsMessage#FORMAT_3GPP2}.
+ * @param pdu PDUs representing the content of the status report.
+ * @throws IllegalStateException if called before {@link MmTelFeature#onFeatureReady()}
+ */
+ public final void onSmsStatusReportReceived(int messageRef, String format, byte[] pdu) {
+ synchronized (mLock) {
+ if (mListener == null) {
+ throw new IllegalStateException("Feature not ready.");
+ }
+ try {
+ mListener.onSmsStatusReportReceived(messageRef, format, pdu);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Can not process sms status report: " + e.getMessage());
+ acknowledgeSmsReport(messageRef, STATUS_REPORT_STATUS_ERROR);
+ }
+ }
+ }
+
+ /**
+ * Returns the SMS format. Default is {@link SmsMessage#FORMAT_3GPP} unless overridden by IMS
+ * Provider.
+ *
+ * @return the format of the message. Valid values are {@link SmsMessage#FORMAT_3GPP} and
+ * {@link SmsMessage#FORMAT_3GPP2}.
+ */
+ public String getSmsFormat() {
+ return SmsMessage.FORMAT_3GPP;
+ }
+
+}
diff --git a/android/telephony/ims/internal/feature/CapabilityChangeRequest.java b/android/telephony/ims/internal/feature/CapabilityChangeRequest.java
new file mode 100644
index 00000000..4d188734
--- /dev/null
+++ b/android/telephony/ims/internal/feature/CapabilityChangeRequest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.telephony.ims.internal.feature;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.ims.internal.stub.ImsRegistrationImplBase;
+import android.util.ArraySet;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Request to send to IMS provider, which will try to enable/disable capabilities that are added to
+ * the request.
+ * {@hide}
+ */
+public class CapabilityChangeRequest implements Parcelable {
+
+ public static class CapabilityPair {
+ private final int mCapability;
+ private final int radioTech;
+
+ public CapabilityPair(int capability, int radioTech) {
+ this.mCapability = capability;
+ this.radioTech = radioTech;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof CapabilityPair)) return false;
+
+ CapabilityPair that = (CapabilityPair) o;
+
+ if (getCapability() != that.getCapability()) return false;
+ return getRadioTech() == that.getRadioTech();
+ }
+
+ @Override
+ public int hashCode() {
+ int result = getCapability();
+ result = 31 * result + getRadioTech();
+ return result;
+ }
+
+ public @MmTelFeature.MmTelCapabilities.MmTelCapability int getCapability() {
+ return mCapability;
+ }
+
+ public @ImsRegistrationImplBase.ImsRegistrationTech int getRadioTech() {
+ return radioTech;
+ }
+ }
+
+ // Pair contains <radio tech, mCapability>
+ private final Set<CapabilityPair> mCapabilitiesToEnable;
+ // Pair contains <radio tech, mCapability>
+ private final Set<CapabilityPair> mCapabilitiesToDisable;
+
+ public CapabilityChangeRequest() {
+ mCapabilitiesToEnable = new ArraySet<>();
+ mCapabilitiesToDisable = new ArraySet<>();
+ }
+
+ /**
+ * Add one or many capabilities to the request to be enabled.
+ *
+ * @param capabilities A bitfield of capabilities to enable, valid values are defined in
+ * {@link MmTelFeature.MmTelCapabilities.MmTelCapability}.
+ * @param radioTech the radio tech that these capabilities should be enabled for, valid
+ * values are in {@link ImsRegistrationImplBase.ImsRegistrationTech}.
+ */
+ public void addCapabilitiesToEnableForTech(
+ @MmTelFeature.MmTelCapabilities.MmTelCapability int capabilities,
+ @ImsRegistrationImplBase.ImsRegistrationTech int radioTech) {
+ addAllCapabilities(mCapabilitiesToEnable, capabilities, radioTech);
+ }
+
+ /**
+ * Add one or many capabilities to the request to be disabled.
+ * @param capabilities A bitfield of capabilities to diable, valid values are defined in
+ * {@link MmTelFeature.MmTelCapabilities.MmTelCapability}.
+ * @param radioTech the radio tech that these capabilities should be disabled for, valid
+ * values are in {@link ImsRegistrationImplBase.ImsRegistrationTech}.
+ */
+ public void addCapabilitiesToDisableForTech(
+ @MmTelFeature.MmTelCapabilities.MmTelCapability int capabilities,
+ @ImsRegistrationImplBase.ImsRegistrationTech int radioTech) {
+ addAllCapabilities(mCapabilitiesToDisable, capabilities, radioTech);
+ }
+
+ /**
+ * @return a {@link List} of {@link CapabilityPair}s that are requesting to be enabled.
+ */
+ public List<CapabilityPair> getCapabilitiesToEnable() {
+ return new ArrayList<>(mCapabilitiesToEnable);
+ }
+
+ /**
+ * @return a {@link List} of {@link CapabilityPair}s that are requesting to be disabled.
+ */
+ public List<CapabilityPair> getCapabilitiesToDisable() {
+ return new ArrayList<>(mCapabilitiesToDisable);
+ }
+
+ // Iterate through capabilities bitfield and add each one as a pair associated with the radio
+ // technology
+ private void addAllCapabilities(Set<CapabilityPair> set, int capabilities, int tech) {
+ long highestCapability = Long.highestOneBit(capabilities);
+ for (int i = 1; i <= highestCapability; i *= 2) {
+ if ((i & capabilities) > 0) {
+ set.add(new CapabilityPair(/*capability*/ i, /*radioTech*/ tech));
+ }
+ }
+ }
+
+ protected CapabilityChangeRequest(Parcel in) {
+ int enableSize = in.readInt();
+ mCapabilitiesToEnable = new ArraySet<>(enableSize);
+ for (int i = 0; i < enableSize; i++) {
+ mCapabilitiesToEnable.add(new CapabilityPair(/*capability*/ in.readInt(),
+ /*radioTech*/ in.readInt()));
+ }
+ int disableSize = in.readInt();
+ mCapabilitiesToDisable = new ArraySet<>(disableSize);
+ for (int i = 0; i < disableSize; i++) {
+ mCapabilitiesToDisable.add(new CapabilityPair(/*capability*/ in.readInt(),
+ /*radioTech*/ in.readInt()));
+ }
+ }
+
+ public static final Creator<CapabilityChangeRequest> CREATOR =
+ new Creator<CapabilityChangeRequest>() {
+ @Override
+ public CapabilityChangeRequest createFromParcel(Parcel in) {
+ return new CapabilityChangeRequest(in);
+ }
+
+ @Override
+ public CapabilityChangeRequest[] newArray(int size) {
+ return new CapabilityChangeRequest[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mCapabilitiesToEnable.size());
+ for (CapabilityPair pair : mCapabilitiesToEnable) {
+ dest.writeInt(pair.getCapability());
+ dest.writeInt(pair.getRadioTech());
+ }
+ dest.writeInt(mCapabilitiesToDisable.size());
+ for (CapabilityPair pair : mCapabilitiesToDisable) {
+ dest.writeInt(pair.getCapability());
+ dest.writeInt(pair.getRadioTech());
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof CapabilityChangeRequest)) return false;
+
+ CapabilityChangeRequest that = (CapabilityChangeRequest) o;
+
+ if (!mCapabilitiesToEnable.equals(that.mCapabilitiesToEnable)) return false;
+ return mCapabilitiesToDisable.equals(that.mCapabilitiesToDisable);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mCapabilitiesToEnable.hashCode();
+ result = 31 * result + mCapabilitiesToDisable.hashCode();
+ return result;
+ }
+}
diff --git a/android/telephony/ims/internal/feature/ImsFeature.java b/android/telephony/ims/internal/feature/ImsFeature.java
new file mode 100644
index 00000000..9f82ad24
--- /dev/null
+++ b/android/telephony/ims/internal/feature/ImsFeature.java
@@ -0,0 +1,462 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.telephony.ims.internal.feature;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IInterface;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.internal.aidl.IImsCapabilityCallback;
+import android.util.Log;
+
+import com.android.ims.internal.IImsFeatureStatusCallback;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+/**
+ * Base class for all IMS features that are supported by the framework.
+ *
+ * @hide
+ */
+public abstract class ImsFeature {
+
+ private static final String LOG_TAG = "ImsFeature";
+
+ /**
+ * Action to broadcast when ImsService is up.
+ * Internal use only.
+ * Only defined here separately for compatibility purposes with the old ImsService.
+ *
+ * @hide
+ */
+ public static final String ACTION_IMS_SERVICE_UP =
+ "com.android.ims.IMS_SERVICE_UP";
+
+ /**
+ * Action to broadcast when ImsService is down.
+ * Internal use only.
+ * Only defined here separately for compatibility purposes with the old ImsService.
+ *
+ * @hide
+ */
+ public static final String ACTION_IMS_SERVICE_DOWN =
+ "com.android.ims.IMS_SERVICE_DOWN";
+
+ /**
+ * Part of the ACTION_IMS_SERVICE_UP or _DOWN intents.
+ * A long value; the phone ID corresponding to the IMS service coming up or down.
+ * Only defined here separately for compatibility purposes with the old ImsService.
+ *
+ * @hide
+ */
+ public static final String EXTRA_PHONE_ID = "android:phone_id";
+
+ // Invalid feature value
+ public static final int FEATURE_INVALID = -1;
+ // ImsFeatures that are defined in the Manifests. Ensure that these values match the previously
+ // defined values in ImsServiceClass for compatibility purposes.
+ public static final int FEATURE_EMERGENCY_MMTEL = 0;
+ public static final int FEATURE_MMTEL = 1;
+ public static final int FEATURE_RCS = 2;
+ // Total number of features defined
+ public static final int FEATURE_MAX = 3;
+
+ // Integer values defining IMS features that are supported in ImsFeature.
+ @IntDef(flag = true,
+ value = {
+ FEATURE_EMERGENCY_MMTEL,
+ FEATURE_MMTEL,
+ FEATURE_RCS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FeatureType {}
+
+ // Integer values defining the state of the ImsFeature at any time.
+ @IntDef(flag = true,
+ value = {
+ STATE_UNAVAILABLE,
+ STATE_INITIALIZING,
+ STATE_READY,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ImsState {}
+
+ public static final int STATE_UNAVAILABLE = 0;
+ public static final int STATE_INITIALIZING = 1;
+ public static final int STATE_READY = 2;
+
+ // Integer values defining the result codes that should be returned from
+ // {@link changeEnabledCapabilities} when the framework tries to set a feature's capability.
+ @IntDef(flag = true,
+ value = {
+ CAPABILITY_ERROR_GENERIC,
+ CAPABILITY_SUCCESS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ImsCapabilityError {}
+
+ public static final int CAPABILITY_ERROR_GENERIC = -1;
+ public static final int CAPABILITY_SUCCESS = 0;
+
+
+ /**
+ * The framework implements this callback in order to register for Feature Capability status
+ * updates, via {@link #onCapabilitiesStatusChanged(Capabilities)}, query Capability
+ * configurations, via {@link #onQueryCapabilityConfiguration}, as well as to receive error
+ * callbacks when the ImsService can not change the capability as requested, via
+ * {@link #onChangeCapabilityConfigurationError}.
+ */
+ public static class CapabilityCallback extends IImsCapabilityCallback.Stub {
+
+ @Override
+ public final void onCapabilitiesStatusChanged(int config) throws RemoteException {
+ onCapabilitiesStatusChanged(new Capabilities(config));
+ }
+
+ /**
+ * Returns the result of a query for the capability configuration of a requested capability.
+ *
+ * @param capability The capability that was requested.
+ * @param radioTech The IMS radio technology associated with the capability.
+ * @param isEnabled true if the capability is enabled, false otherwise.
+ */
+ @Override
+ public void onQueryCapabilityConfiguration(int capability, int radioTech,
+ boolean isEnabled) {
+
+ }
+
+ /**
+ * Called when a change to the capability configuration has returned an error.
+ *
+ * @param capability The capability that was requested to be changed.
+ * @param radioTech The IMS radio technology associated with the capability.
+ * @param reason error associated with the failure to change configuration.
+ */
+ @Override
+ public void onChangeCapabilityConfigurationError(int capability, int radioTech,
+ int reason) {
+ }
+
+ /**
+ * The status of the feature's capabilities has changed to either available or unavailable.
+ * If unavailable, the feature is not able to support the unavailable capability at this
+ * time.
+ *
+ * @param config The new availability of the capabilities.
+ */
+ public void onCapabilitiesStatusChanged(Capabilities config) {
+ }
+ }
+
+ /**
+ * Used by the ImsFeature to call back to the CapabilityCallback that the framework has
+ * provided.
+ */
+ protected static class CapabilityCallbackProxy {
+ private final IImsCapabilityCallback mCallback;
+
+ public CapabilityCallbackProxy(IImsCapabilityCallback c) {
+ mCallback = c;
+ }
+
+ /**
+ * This method notifies the provided framework callback that the request to change the
+ * indicated capability has failed and has not changed.
+ *
+ * @param capability The Capability that will be notified to the framework.
+ * @param radioTech The radio tech that this capability failed for.
+ * @param reason The reason this capability was unable to be changed.
+ */
+ public void onChangeCapabilityConfigurationError(int capability, int radioTech,
+ @ImsCapabilityError int reason) {
+ try {
+ mCallback.onChangeCapabilityConfigurationError(capability, radioTech, reason);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "onChangeCapabilityConfigurationError called on dead binder.");
+ }
+ }
+
+ public void onQueryCapabilityConfiguration(int capability, int radioTech,
+ boolean isEnabled) {
+ try {
+ mCallback.onQueryCapabilityConfiguration(capability, radioTech, isEnabled);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "onQueryCapabilityConfiguration called on dead binder.");
+ }
+ }
+ }
+
+ /**
+ * Contains the capabilities defined and supported by an ImsFeature in the form of a bit mask.
+ */
+ public static class Capabilities {
+ protected int mCapabilities = 0;
+
+ public Capabilities() {
+ }
+
+ protected Capabilities(int capabilities) {
+ mCapabilities = capabilities;
+ }
+
+ /**
+ * @param capabilities Capabilities to be added to the configuration in the form of a
+ * bit mask.
+ */
+ public void addCapabilities(int capabilities) {
+ mCapabilities |= capabilities;
+ }
+
+ /**
+ * @param capabilities Capabilities to be removed to the configuration in the form of a
+ * bit mask.
+ */
+ public void removeCapabilities(int capabilities) {
+ mCapabilities &= ~capabilities;
+ }
+
+ /**
+ * @return true if all of the capabilities specified are capable.
+ */
+ public boolean isCapable(int capabilities) {
+ return (mCapabilities & capabilities) == capabilities;
+ }
+
+ public Capabilities copy() {
+ return new Capabilities(mCapabilities);
+ }
+
+ /**
+ * @return a bitmask containing the capability flags directly.
+ */
+ public int getMask() {
+ return mCapabilities;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Capabilities)) return false;
+
+ Capabilities that = (Capabilities) o;
+
+ return mCapabilities == that.mCapabilities;
+ }
+
+ @Override
+ public int hashCode() {
+ return mCapabilities;
+ }
+ }
+
+ private final Set<IImsFeatureStatusCallback> mStatusCallbacks = Collections.newSetFromMap(
+ new WeakHashMap<IImsFeatureStatusCallback, Boolean>());
+ private @ImsState int mState = STATE_UNAVAILABLE;
+ private int mSlotId = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
+ private Context mContext;
+ private final Object mLock = new Object();
+ private final RemoteCallbackList<IImsCapabilityCallback> mCapabilityCallbacks
+ = new RemoteCallbackList<>();
+ private Capabilities mCapabilityStatus = new Capabilities();
+
+ public final void initialize(Context context, int slotId) {
+ mContext = context;
+ mSlotId = slotId;
+ }
+
+ public final int getFeatureState() {
+ synchronized (mLock) {
+ return mState;
+ }
+ }
+
+ protected final void setFeatureState(@ImsState int state) {
+ synchronized (mLock) {
+ if (mState != state) {
+ mState = state;
+ notifyFeatureState(state);
+ }
+ }
+ }
+
+ // Not final for testing, but shouldn't be extended!
+ @VisibleForTesting
+ public void addImsFeatureStatusCallback(@NonNull IImsFeatureStatusCallback c) {
+ try {
+ // If we have just connected, send queued status.
+ c.notifyImsFeatureStatus(getFeatureState());
+ // Add the callback if the callback completes successfully without a RemoteException.
+ synchronized (mLock) {
+ mStatusCallbacks.add(c);
+ }
+ } catch (RemoteException e) {
+ Log.w(LOG_TAG, "Couldn't notify feature state: " + e.getMessage());
+ }
+ }
+
+ @VisibleForTesting
+ // Not final for testing, but should not be extended!
+ public void removeImsFeatureStatusCallback(@NonNull IImsFeatureStatusCallback c) {
+ synchronized (mLock) {
+ mStatusCallbacks.remove(c);
+ }
+ }
+
+ /**
+ * Internal method called by ImsFeature when setFeatureState has changed.
+ */
+ private void notifyFeatureState(@ImsState int state) {
+ synchronized (mLock) {
+ for (Iterator<IImsFeatureStatusCallback> iter = mStatusCallbacks.iterator();
+ iter.hasNext(); ) {
+ IImsFeatureStatusCallback callback = iter.next();
+ try {
+ Log.i(LOG_TAG, "notifying ImsFeatureState=" + state);
+ callback.notifyImsFeatureStatus(state);
+ } catch (RemoteException e) {
+ // remove if the callback is no longer alive.
+ iter.remove();
+ Log.w(LOG_TAG, "Couldn't notify feature state: " + e.getMessage());
+ }
+ }
+ }
+ sendImsServiceIntent(state);
+ }
+
+ /**
+ * Provide backwards compatibility using deprecated service UP/DOWN intents.
+ */
+ private void sendImsServiceIntent(@ImsState int state) {
+ if (mContext == null || mSlotId == SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
+ return;
+ }
+ Intent intent;
+ switch (state) {
+ case ImsFeature.STATE_UNAVAILABLE:
+ case ImsFeature.STATE_INITIALIZING:
+ intent = new Intent(ACTION_IMS_SERVICE_DOWN);
+ break;
+ case ImsFeature.STATE_READY:
+ intent = new Intent(ACTION_IMS_SERVICE_UP);
+ break;
+ default:
+ intent = new Intent(ACTION_IMS_SERVICE_DOWN);
+ }
+ intent.putExtra(EXTRA_PHONE_ID, mSlotId);
+ mContext.sendBroadcast(intent);
+ }
+
+ public final void addCapabilityCallback(IImsCapabilityCallback c) {
+ mCapabilityCallbacks.register(c);
+ }
+
+ public final void removeCapabilityCallback(IImsCapabilityCallback c) {
+ mCapabilityCallbacks.unregister(c);
+ }
+
+ /**
+ * @return the cached capabilities status for this feature.
+ */
+ @VisibleForTesting
+ public Capabilities queryCapabilityStatus() {
+ synchronized (mLock) {
+ return mCapabilityStatus.copy();
+ }
+ }
+
+ // Called internally to request the change of enabled capabilities.
+ @VisibleForTesting
+ public final void requestChangeEnabledCapabilities(CapabilityChangeRequest request,
+ IImsCapabilityCallback c) throws RemoteException {
+ if (request == null) {
+ throw new IllegalArgumentException(
+ "ImsFeature#requestChangeEnabledCapabilities called with invalid params.");
+ }
+ changeEnabledCapabilities(request, new CapabilityCallbackProxy(c));
+ }
+
+ /**
+ * Called by the ImsFeature when the capabilities status has changed.
+ *
+ * @param c A {@link Capabilities} containing the new Capabilities status.
+ */
+ protected final void notifyCapabilitiesStatusChanged(Capabilities c) {
+ synchronized (mLock) {
+ mCapabilityStatus = c.copy();
+ }
+ int count = mCapabilityCallbacks.beginBroadcast();
+ try {
+ for (int i = 0; i < count; i++) {
+ try {
+ mCapabilityCallbacks.getBroadcastItem(i).onCapabilitiesStatusChanged(
+ c.mCapabilities);
+ } catch (RemoteException e) {
+ Log.w(LOG_TAG, e + " " + "notifyCapabilitiesStatusChanged() - Skipping " +
+ "callback.");
+ }
+ }
+ } finally {
+ mCapabilityCallbacks.finishBroadcast();
+ }
+ }
+
+ /**
+ * Features should override this method to receive Capability preference change requests from
+ * the framework using the provided {@link CapabilityChangeRequest}. If any of the capabilities
+ * in the {@link CapabilityChangeRequest} are not able to be completed due to an error,
+ * {@link CapabilityCallbackProxy#onChangeCapabilityConfigurationError} should be called for
+ * each failed capability.
+ *
+ * @param request A {@link CapabilityChangeRequest} containing requested capabilities to
+ * enable/disable.
+ * @param c A {@link CapabilityCallbackProxy}, which will be used to call back to the framework
+ * setting a subset of these capabilities fail, using
+ * {@link CapabilityCallbackProxy#onChangeCapabilityConfigurationError}.
+ */
+ public abstract void changeEnabledCapabilities(CapabilityChangeRequest request,
+ CapabilityCallbackProxy c);
+
+ /**
+ * Called when the framework is removing this feature and it needs to be cleaned up.
+ */
+ public abstract void onFeatureRemoved();
+
+ /**
+ * Called when the feature has been initialized and communication with the framework is set up.
+ * Any attempt by this feature to access the framework before this method is called will return
+ * with an {@link IllegalStateException}.
+ * The IMS provider should use this method to trigger registration for this feature on the IMS
+ * network, if needed.
+ */
+ public abstract void onFeatureReady();
+
+ /**
+ * @return Binder instance that the framework will use to communicate with this feature.
+ */
+ protected abstract IInterface getBinder();
+}
diff --git a/android/telephony/ims/internal/feature/MmTelFeature.java b/android/telephony/ims/internal/feature/MmTelFeature.java
new file mode 100644
index 00000000..2f350c86
--- /dev/null
+++ b/android/telephony/ims/internal/feature/MmTelFeature.java
@@ -0,0 +1,495 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.telephony.ims.internal.feature;
+
+import android.annotation.IntDef;
+import android.os.Message;
+import android.os.RemoteException;
+import android.telecom.TelecomManager;
+import android.telephony.ims.internal.ImsCallSessionListener;
+import android.telephony.ims.internal.SmsImplBase;
+import android.telephony.ims.internal.SmsImplBase.DeliverStatusResult;
+import android.telephony.ims.internal.SmsImplBase.StatusReportResult;
+import android.telephony.ims.internal.aidl.IImsCallSessionListener;
+import android.telephony.ims.internal.aidl.IImsCapabilityCallback;
+import android.telephony.ims.internal.aidl.IImsMmTelFeature;
+import android.telephony.ims.internal.aidl.IImsMmTelListener;
+import android.telephony.ims.internal.stub.ImsRegistrationImplBase;
+import android.telephony.ims.internal.aidl.IImsSmsListener;
+import android.telephony.ims.stub.ImsEcbmImplBase;
+import android.telephony.ims.stub.ImsMultiEndpointImplBase;
+import android.telephony.ims.stub.ImsUtImplBase;
+import android.util.Log;
+
+import com.android.ims.ImsCallProfile;
+import com.android.ims.internal.IImsCallSession;
+import com.android.ims.internal.IImsEcbm;
+import com.android.ims.internal.IImsMultiEndpoint;
+import com.android.ims.internal.IImsUt;
+import com.android.ims.internal.ImsCallSession;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Base implementation for Voice and SMS (IR-92) and Video (IR-94) IMS support.
+ *
+ * Any class wishing to use MmTelFeature should extend this class and implement all methods that the
+ * service supports.
+ * @hide
+ */
+
+public class MmTelFeature extends ImsFeature {
+
+ private static final String LOG_TAG = "MmTelFeature";
+
+ private final IImsMmTelFeature mImsMMTelBinder = new IImsMmTelFeature.Stub() {
+
+ @Override
+ public void setListener(IImsMmTelListener l) throws RemoteException {
+ synchronized (mLock) {
+ MmTelFeature.this.setListener(l);
+ }
+ }
+
+ @Override
+ public void setSmsListener(IImsSmsListener l) throws RemoteException {
+ MmTelFeature.this.setSmsListener(l);
+ }
+
+ @Override
+ public int getFeatureState() throws RemoteException {
+ synchronized (mLock) {
+ return MmTelFeature.this.getFeatureState();
+ }
+ }
+
+
+ @Override
+ public ImsCallProfile createCallProfile(int callSessionType, int callType)
+ throws RemoteException {
+ synchronized (mLock) {
+ return MmTelFeature.this.createCallProfile(callSessionType, callType);
+ }
+ }
+
+ @Override
+ public IImsCallSession createCallSession(ImsCallProfile profile,
+ IImsCallSessionListener listener) throws RemoteException {
+ synchronized (mLock) {
+ ImsCallSession s = MmTelFeature.this.createCallSession(profile,
+ new ImsCallSessionListener(listener));
+ return s != null ? s.getSession() : null;
+ }
+ }
+
+ @Override
+ public IImsUt getUtInterface() throws RemoteException {
+ synchronized (mLock) {
+ return MmTelFeature.this.getUt();
+ }
+ }
+
+ @Override
+ public IImsEcbm getEcbmInterface() throws RemoteException {
+ synchronized (mLock) {
+ return MmTelFeature.this.getEcbm();
+ }
+ }
+
+ @Override
+ public void setUiTtyMode(int uiTtyMode, Message onCompleteMessage) throws RemoteException {
+ synchronized (mLock) {
+ MmTelFeature.this.setUiTtyMode(uiTtyMode, onCompleteMessage);
+ }
+ }
+
+ @Override
+ public IImsMultiEndpoint getMultiEndpointInterface() throws RemoteException {
+ synchronized (mLock) {
+ return MmTelFeature.this.getMultiEndpoint();
+ }
+ }
+
+ @Override
+ public int queryCapabilityStatus() throws RemoteException {
+ return MmTelFeature.this.queryCapabilityStatus().mCapabilities;
+ }
+
+ @Override
+ public void addCapabilityCallback(IImsCapabilityCallback c) {
+ MmTelFeature.this.addCapabilityCallback(c);
+ }
+
+ @Override
+ public void removeCapabilityCallback(IImsCapabilityCallback c) {
+ MmTelFeature.this.removeCapabilityCallback(c);
+ }
+
+ @Override
+ public void changeCapabilitiesConfiguration(CapabilityChangeRequest request,
+ IImsCapabilityCallback c) throws RemoteException {
+ MmTelFeature.this.requestChangeEnabledCapabilities(request, c);
+ }
+
+ @Override
+ public void queryCapabilityConfiguration(int capability, int radioTech,
+ IImsCapabilityCallback c) {
+ queryCapabilityConfigurationInternal(capability, radioTech, c);
+ }
+
+ @Override
+ public void sendSms(int messageRef, String format, String smsc, boolean retry, byte[] pdu) {
+ synchronized (mLock) {
+ MmTelFeature.this.sendSms(messageRef, format, smsc, retry, pdu);
+ }
+ }
+
+ @Override
+ public void acknowledgeSms(int messageRef, int result) {
+ synchronized (mLock) {
+ MmTelFeature.this.acknowledgeSms(messageRef, result);
+ }
+ }
+
+ @Override
+ public void acknowledgeSmsReport(int messageRef, int result) {
+ synchronized (mLock) {
+ MmTelFeature.this.acknowledgeSmsReport(messageRef, result);
+ }
+ }
+
+ @Override
+ public String getSmsFormat() {
+ synchronized (mLock) {
+ return MmTelFeature.this.getSmsFormat();
+ }
+ }
+ };
+
+ /**
+ * Contains the capabilities defined and supported by a MmTelFeature in the form of a Bitmask.
+ * The capabilities that are used in MmTelFeature are defined by {@link MmTelCapability}.
+ *
+ * The capabilities of this MmTelFeature will be set by the framework and can be queried with
+ * {@link #queryCapabilityStatus()}.
+ *
+ * This MmTelFeature can then return the status of each of these capabilities (enabled or not)
+ * by sending a {@link #notifyCapabilitiesStatusChanged} callback to the framework. The current
+ * status can also be queried using {@link #queryCapabilityStatus()}.
+ */
+ public static class MmTelCapabilities extends Capabilities {
+
+ @VisibleForTesting
+ public MmTelCapabilities() {
+ super();
+ }
+
+ public MmTelCapabilities(Capabilities c) {
+ mCapabilities = c.mCapabilities;
+ }
+
+ @IntDef(flag = true,
+ value = {
+ CAPABILITY_TYPE_VOICE,
+ CAPABILITY_TYPE_VIDEO,
+ CAPABILITY_TYPE_UT,
+ CAPABILITY_TYPE_SMS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface MmTelCapability {}
+
+ /**
+ * This MmTelFeature supports Voice calling (IR.92)
+ */
+ public static final int CAPABILITY_TYPE_VOICE = 1 << 0;
+
+ /**
+ * This MmTelFeature supports Video (IR.94)
+ */
+ public static final int CAPABILITY_TYPE_VIDEO = 1 << 1;
+
+ /**
+ * This MmTelFeature supports XCAP over Ut for supplementary services. (IR.92)
+ */
+ public static final int CAPABILITY_TYPE_UT = 1 << 2;
+
+ /**
+ * This MmTelFeature supports SMS (IR.92)
+ */
+ public static final int CAPABILITY_TYPE_SMS = 1 << 3;
+
+ @Override
+ public final void addCapabilities(@MmTelCapability int capabilities) {
+ super.addCapabilities(capabilities);
+ }
+
+ @Override
+ public final void removeCapabilities(@MmTelCapability int capability) {
+ super.removeCapabilities(capability);
+ }
+
+ @Override
+ public final boolean isCapable(@MmTelCapability int capabilities) {
+ return super.isCapable(capabilities);
+ }
+ }
+
+ /**
+ * Listener that the framework implements for communication from the MmTelFeature.
+ */
+ public static class Listener extends IImsMmTelListener.Stub {
+
+ @Override
+ public final void onIncomingCall(IImsCallSession c) {
+ onIncomingCall(new ImsCallSession(c));
+ }
+
+ /**
+ * Called when the IMS provider receives an incoming call.
+ * @param c The {@link ImsCallSession} associated with the new call.
+ */
+ public void onIncomingCall(ImsCallSession c) {
+ }
+ }
+
+ // Lock for feature synchronization
+ private final Object mLock = new Object();
+ private IImsMmTelListener mListener;
+
+ /**
+ * @param listener A {@link Listener} used when the MmTelFeature receives an incoming call and
+ * notifies the framework.
+ */
+ private void setListener(IImsMmTelListener listener) {
+ synchronized (mLock) {
+ mListener = listener;
+ }
+ }
+
+ private void setSmsListener(IImsSmsListener listener) {
+ getSmsImplementation().registerSmsListener(listener);
+ }
+
+ private void queryCapabilityConfigurationInternal(int capability, int radioTech,
+ IImsCapabilityCallback c) {
+ boolean enabled = queryCapabilityConfiguration(capability, radioTech);
+ try {
+ if (c != null) {
+ c.onQueryCapabilityConfiguration(capability, radioTech, enabled);
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "queryCapabilityConfigurationInternal called on dead binder!");
+ }
+ }
+
+ /**
+ * The current capability status that this MmTelFeature has defined is available. This
+ * configuration will be used by the platform to figure out which capabilities are CURRENTLY
+ * available to be used.
+ *
+ * Should be a subset of the capabilities that are enabled by the framework in
+ * {@link #changeEnabledCapabilities}.
+ * @return A copy of the current MmTelFeature capability status.
+ */
+ @Override
+ public final MmTelCapabilities queryCapabilityStatus() {
+ return new MmTelCapabilities(super.queryCapabilityStatus());
+ }
+
+ /**
+ * Notify the framework that the status of the Capabilities has changed. Even though the
+ * MmTelFeature capability may be enabled by the framework, the status may be disabled due to
+ * the feature being unavailable from the network.
+ * @param c The current capability status of the MmTelFeature. If a capability is disabled, then
+ * the status of that capability is disabled. This can happen if the network does not currently
+ * support the capability that is enabled. A capability that is disabled by the framework (via
+ * {@link #changeEnabledCapabilities}) should also show the status as disabled.
+ */
+ protected final void notifyCapabilitiesStatusChanged(MmTelCapabilities c) {
+ super.notifyCapabilitiesStatusChanged(c);
+ }
+
+ /**
+ * Notify the framework of an incoming call.
+ * @param c The {@link ImsCallSession} of the new incoming call.
+ *
+ * @throws RemoteException if the connection to the framework is not available. If this happens,
+ * the call should be no longer considered active and should be cleaned up.
+ * */
+ protected final void notifyIncomingCall(ImsCallSession c) throws RemoteException {
+ synchronized (mLock) {
+ if (mListener == null) {
+ throw new IllegalStateException("Session is not available.");
+ }
+ mListener.onIncomingCall(c.getSession());
+ }
+ }
+
+ /**
+ * Provides the MmTelFeature with the ability to return the framework Capability Configuration
+ * for a provided Capability. If the framework calls {@link #changeEnabledCapabilities} and
+ * includes a capability A to enable or disable, this method should return the correct enabled
+ * status for capability A.
+ * @param capability The capability that we are querying the configuration for.
+ * @return true if the capability is enabled, false otherwise.
+ */
+ public boolean queryCapabilityConfiguration(@MmTelCapabilities.MmTelCapability int capability,
+ @ImsRegistrationImplBase.ImsRegistrationTech int radioTech) {
+ // Base implementation - Override to provide functionality
+ return false;
+ }
+
+ /**
+ * The MmTelFeature should override this method to handle the enabling/disabling of
+ * MmTel Features, defined in {@link MmTelCapabilities.MmTelCapability}. The framework assumes
+ * the {@link CapabilityChangeRequest} was processed successfully. If a subset of capabilities
+ * could not be set to their new values,
+ * {@link CapabilityCallbackProxy#onChangeCapabilityConfigurationError} must be called
+ * individually for each capability whose processing resulted in an error.
+ *
+ * Enabling/Disabling a capability here indicates that the capability should be registered or
+ * deregistered (depending on the capability change) and become available or unavailable to
+ * the framework.
+ */
+ @Override
+ public void changeEnabledCapabilities(CapabilityChangeRequest request,
+ CapabilityCallbackProxy c) {
+ // Base implementation, no-op
+ }
+
+ /**
+ * Creates a {@link ImsCallProfile} from the service capabilities & IMS registration state.
+ *
+ * @param callSessionType a service type that is specified in {@link ImsCallProfile}
+ * {@link ImsCallProfile#SERVICE_TYPE_NONE}
+ * {@link ImsCallProfile#SERVICE_TYPE_NORMAL}
+ * {@link ImsCallProfile#SERVICE_TYPE_EMERGENCY}
+ * @param callType a call type that is specified in {@link ImsCallProfile}
+ * {@link ImsCallProfile#CALL_TYPE_VOICE}
+ * {@link ImsCallProfile#CALL_TYPE_VT}
+ * {@link ImsCallProfile#CALL_TYPE_VT_TX}
+ * {@link ImsCallProfile#CALL_TYPE_VT_RX}
+ * {@link ImsCallProfile#CALL_TYPE_VT_NODIR}
+ * {@link ImsCallProfile#CALL_TYPE_VS}
+ * {@link ImsCallProfile#CALL_TYPE_VS_TX}
+ * {@link ImsCallProfile#CALL_TYPE_VS_RX}
+ * @return a {@link ImsCallProfile} object
+ */
+ public ImsCallProfile createCallProfile(int callSessionType, int callType) {
+ // Base Implementation - Should be overridden
+ return null;
+ }
+
+ /**
+ * Creates an {@link ImsCallSession} with the specified call profile.
+ * Use other methods, if applicable, instead of interacting with
+ * {@link ImsCallSession} directly.
+ *
+ * @param profile a call profile to make the call
+ * @param listener An implementation of IImsCallSessionListener.
+ */
+ public ImsCallSession createCallSession(ImsCallProfile profile,
+ ImsCallSessionListener listener) {
+ // Base Implementation - Should be overridden
+ return null;
+ }
+
+ /**
+ * @return The Ut interface for the supplementary service configuration.
+ */
+ public ImsUtImplBase getUt() {
+ // Base Implementation - Should be overridden
+ return null;
+ }
+
+ /**
+ * @return The Emergency call-back mode interface for emergency VoLTE calls that support it.
+ */
+ public ImsEcbmImplBase getEcbm() {
+ // Base Implementation - Should be overridden
+ return null;
+ }
+
+ /**
+ * @return The Emergency call-back mode interface for emergency VoLTE calls that support it.
+ */
+ public ImsMultiEndpointImplBase getMultiEndpoint() {
+ // Base Implementation - Should be overridden
+ return null;
+ }
+
+ /**
+ * Sets the current UI TTY mode for the MmTelFeature.
+ * @param mode An integer containing the new UI TTY Mode, can consist of
+ * {@link TelecomManager#TTY_MODE_OFF},
+ * {@link TelecomManager#TTY_MODE_FULL},
+ * {@link TelecomManager#TTY_MODE_HCO},
+ * {@link TelecomManager#TTY_MODE_VCO}
+ * @param onCompleteMessage A {@link Message} to be used when the mode has been set.
+ */
+ void setUiTtyMode(int mode, Message onCompleteMessage) {
+ // Base Implementation - Should be overridden
+ }
+
+ private void sendSms(int messageRef, String format, String smsc, boolean isRetry, byte[] pdu) {
+ getSmsImplementation().sendSms(messageRef, format, smsc, isRetry, pdu);
+ }
+
+ private void acknowledgeSms(int messageRef, @DeliverStatusResult int result) {
+ getSmsImplementation().acknowledgeSms(messageRef, result);
+ }
+
+ private void acknowledgeSmsReport(int messageRef, @StatusReportResult int result) {
+ getSmsImplementation().acknowledgeSmsReport(messageRef, result);
+ }
+
+ private String getSmsFormat() {
+ return getSmsImplementation().getSmsFormat();
+ }
+
+ /**
+ * Must be overridden by IMS Provider to be able to support SMS over IMS. Otherwise a default
+ * non-functional implementation is returned.
+ *
+ * @return an instance of {@link SmsImplBase} which should be implemented by the IMS Provider.
+ */
+ protected SmsImplBase getSmsImplementation() {
+ return new SmsImplBase();
+ }
+
+ /**{@inheritDoc}*/
+ @Override
+ public void onFeatureRemoved() {
+ // Base Implementation - Should be overridden
+ }
+
+ /**{@inheritDoc}*/
+ @Override
+ public void onFeatureReady() {
+ // Base Implementation - Should be overridden
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public final IImsMmTelFeature getBinder() {
+ return mImsMMTelBinder;
+ }
+}
diff --git a/android/telephony/ims/internal/feature/RcsFeature.java b/android/telephony/ims/internal/feature/RcsFeature.java
new file mode 100644
index 00000000..8d1bd9d2
--- /dev/null
+++ b/android/telephony/ims/internal/feature/RcsFeature.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.telephony.ims.internal.feature;
+
+import android.telephony.ims.internal.aidl.IImsRcsFeature;
+
+/**
+ * Base implementation of the RcsFeature APIs. Any ImsService wishing to support RCS should extend
+ * this class and provide implementations of the RcsFeature methods that they support.
+ * @hide
+ */
+
+public class RcsFeature extends ImsFeature {
+
+ private final IImsRcsFeature mImsRcsBinder = new IImsRcsFeature.Stub() {
+ // Empty Default Implementation.
+ };
+
+
+ public RcsFeature() {
+ super();
+ }
+
+ @Override
+ public void changeEnabledCapabilities(CapabilityChangeRequest request,
+ CapabilityCallbackProxy c) {
+ // Do nothing for base implementation.
+ }
+
+ @Override
+ public void onFeatureRemoved() {
+
+ }
+
+ /**{@inheritDoc}*/
+ @Override
+ public void onFeatureReady() {
+
+ }
+
+ @Override
+ public final IImsRcsFeature getBinder() {
+ return mImsRcsBinder;
+ }
+}
diff --git a/android/telephony/ims/internal/stub/ImsConfigImplBase.java b/android/telephony/ims/internal/stub/ImsConfigImplBase.java
new file mode 100644
index 00000000..33aec5df
--- /dev/null
+++ b/android/telephony/ims/internal/stub/ImsConfigImplBase.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.telephony.ims.internal.stub;
+
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.telephony.ims.internal.aidl.IImsConfig;
+import android.telephony.ims.internal.aidl.IImsConfigCallback;
+
+import com.android.ims.ImsConfig;
+
+/**
+ * Controls the modification of IMS specific configurations. For more information on the supported
+ * IMS configuration constants, see {@link ImsConfig}.
+ *
+ * @hide
+ */
+
+public class ImsConfigImplBase {
+
+ //TODO: Implement the Binder logic to call base APIs. Need to finish other ImsService Config
+ // work first.
+ private final IImsConfig mBinder = new IImsConfig.Stub() {
+
+ @Override
+ public void addImsConfigCallback(IImsConfigCallback c) throws RemoteException {
+ ImsConfigImplBase.this.addImsConfigCallback(c);
+ }
+
+ @Override
+ public void removeImsConfigCallback(IImsConfigCallback c) throws RemoteException {
+ ImsConfigImplBase.this.removeImsConfigCallback(c);
+ }
+
+ @Override
+ public int getConfigInt(int item) throws RemoteException {
+ return Integer.MIN_VALUE;
+ }
+
+ @Override
+ public String getConfigString(int item) throws RemoteException {
+ return null;
+ }
+
+ @Override
+ public int setConfigInt(int item, int value) throws RemoteException {
+ return Integer.MIN_VALUE;
+ }
+
+ @Override
+ public int setConfigString(int item, String value) throws RemoteException {
+ return Integer.MIN_VALUE;
+ }
+ };
+
+ public class Callback extends IImsConfigCallback.Stub {
+
+ @Override
+ public final void onIntConfigChanged(int item, int value) throws RemoteException {
+ onConfigChanged(item, value);
+ }
+
+ @Override
+ public final void onStringConfigChanged(int item, String value) throws RemoteException {
+ onConfigChanged(item, value);
+ }
+
+ /**
+ * Called when the IMS configuration has changed.
+ * @param item the IMS configuration key constant, as defined in ImsConfig.
+ * @param value the new integer value of the IMS configuration constant.
+ */
+ public void onConfigChanged(int item, int value) {
+ // Base Implementation
+ }
+
+ /**
+ * Called when the IMS configuration has changed.
+ * @param item the IMS configuration key constant, as defined in ImsConfig.
+ * @param value the new String value of the IMS configuration constant.
+ */
+ public void onConfigChanged(int item, String value) {
+ // Base Implementation
+ }
+ }
+
+ private final RemoteCallbackList<IImsConfigCallback> mCallbacks = new RemoteCallbackList<>();
+
+ /**
+ * Adds a {@link Callback} to the list of callbacks notified when a value in the configuration
+ * changes.
+ * @param c callback to add.
+ */
+ private void addImsConfigCallback(IImsConfigCallback c) {
+ mCallbacks.register(c);
+ }
+ /**
+ * Removes a {@link Callback} to the list of callbacks notified when a value in the
+ * configuration changes.
+ *
+ * @param c callback to remove.
+ */
+ private void removeImsConfigCallback(IImsConfigCallback c) {
+ mCallbacks.unregister(c);
+ }
+
+ public final IImsConfig getBinder() {
+ return mBinder;
+ }
+
+ /**
+ * Sets the value for IMS service/capabilities parameters by the operator device
+ * management entity. It sets the config item value in the provisioned storage
+ * from which the master value is derived.
+ *
+ * @param item as defined in com.android.ims.ImsConfig#ConfigConstants.
+ * @param value in Integer format.
+ * @return as defined in com.android.ims.ImsConfig#OperationStatusConstants.
+ */
+ public int setConfig(int item, int value) {
+ // Base Implementation - To be overridden.
+ return ImsConfig.OperationStatusConstants.FAILED;
+ }
+
+ /**
+ * Sets the value for IMS service/capabilities parameters by the operator device
+ * management entity. It sets the config item value in the provisioned storage
+ * from which the master value is derived.
+ *
+ * @param item as defined in com.android.ims.ImsConfig#ConfigConstants.
+ * @param value in String format.
+ * @return as defined in com.android.ims.ImsConfig#OperationStatusConstants.
+ */
+ public int setConfig(int item, String value) {
+ return ImsConfig.OperationStatusConstants.FAILED;
+ }
+
+ /**
+ * Gets the value for ims service/capabilities parameters from the provisioned
+ * value storage.
+ *
+ * @param item as defined in com.android.ims.ImsConfig#ConfigConstants.
+ * @return value in Integer format.
+ */
+ public int getConfigInt(int item) {
+ return ImsConfig.OperationStatusConstants.FAILED;
+ }
+
+ /**
+ * Gets the value for ims service/capabilities parameters from the provisioned
+ * value storage.
+ *
+ * @param item as defined in com.android.ims.ImsConfig#ConfigConstants.
+ * @return value in String format.
+ */
+ public String getConfigString(int item) {
+ return null;
+ }
+}
diff --git a/android/telephony/ims/internal/stub/ImsFeatureConfiguration.java b/android/telephony/ims/internal/stub/ImsFeatureConfiguration.java
new file mode 100644
index 00000000..244c9578
--- /dev/null
+++ b/android/telephony/ims/internal/stub/ImsFeatureConfiguration.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.telephony.ims.internal.stub;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.ims.internal.feature.ImsFeature;
+import android.util.ArraySet;
+
+import java.util.Arrays;
+import java.util.Set;
+
+/**
+ * Container class for IMS Feature configuration. This class contains the features that the
+ * ImsService supports, which are defined in {@link ImsFeature.FeatureType}.
+ * @hide
+ */
+public class ImsFeatureConfiguration implements Parcelable {
+ /**
+ * Features that this ImsService supports.
+ */
+ private final Set<Integer> mFeatures;
+
+ /**
+ * Creates an ImsFeatureConfiguration with the features
+ */
+ public static class Builder {
+ ImsFeatureConfiguration mConfig;
+ public Builder() {
+ mConfig = new ImsFeatureConfiguration();
+ }
+
+ /**
+ * @param feature A feature defined in {@link ImsFeature.FeatureType} that this service
+ * supports.
+ * @return a {@link Builder} to continue constructing the ImsFeatureConfiguration.
+ */
+ public Builder addFeature(@ImsFeature.FeatureType int feature) {
+ mConfig.addFeature(feature);
+ return this;
+ }
+
+ public ImsFeatureConfiguration build() {
+ return mConfig;
+ }
+ }
+
+ /**
+ * Creates with all registration features empty.
+ *
+ * Consider using the provided {@link Builder} to create this configuration instead.
+ */
+ public ImsFeatureConfiguration() {
+ mFeatures = new ArraySet<>();
+ }
+
+ /**
+ * Configuration of the ImsService, which describes which features the ImsService supports
+ * (for registration).
+ * @param features an array of feature integers defined in {@link ImsFeature} that describe
+ * which features this ImsService supports.
+ */
+ public ImsFeatureConfiguration(int[] features) {
+ mFeatures = new ArraySet<>();
+
+ if (features != null) {
+ for (int i : features) {
+ mFeatures.add(i);
+ }
+ }
+ }
+
+ /**
+ * @return an int[] containing the features that this ImsService supports.
+ */
+ public int[] getServiceFeatures() {
+ return mFeatures.stream().mapToInt(i->i).toArray();
+ }
+
+ void addFeature(int feature) {
+ mFeatures.add(feature);
+ }
+
+ protected ImsFeatureConfiguration(Parcel in) {
+ int[] features = in.createIntArray();
+ if (features != null) {
+ mFeatures = new ArraySet<>(features.length);
+ for(Integer i : features) {
+ mFeatures.add(i);
+ }
+ } else {
+ mFeatures = new ArraySet<>();
+ }
+ }
+
+ public static final Creator<ImsFeatureConfiguration> CREATOR
+ = new Creator<ImsFeatureConfiguration>() {
+ @Override
+ public ImsFeatureConfiguration createFromParcel(Parcel in) {
+ return new ImsFeatureConfiguration(in);
+ }
+
+ @Override
+ public ImsFeatureConfiguration[] newArray(int size) {
+ return new ImsFeatureConfiguration[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeIntArray(mFeatures.stream().mapToInt(i->i).toArray());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof ImsFeatureConfiguration)) return false;
+
+ ImsFeatureConfiguration that = (ImsFeatureConfiguration) o;
+
+ return mFeatures.equals(that.mFeatures);
+ }
+
+ @Override
+ public int hashCode() {
+ return mFeatures.hashCode();
+ }
+}
diff --git a/android/telephony/ims/internal/stub/ImsRegistrationImplBase.java b/android/telephony/ims/internal/stub/ImsRegistrationImplBase.java
new file mode 100644
index 00000000..558b009a
--- /dev/null
+++ b/android/telephony/ims/internal/stub/ImsRegistrationImplBase.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.telephony.ims.internal.stub;
+
+import android.annotation.IntDef;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.telephony.ims.internal.aidl.IImsRegistration;
+import android.telephony.ims.internal.aidl.IImsRegistrationCallback;
+import android.util.Log;
+
+import com.android.ims.ImsReasonInfo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Controls IMS registration for this ImsService and notifies the framework when the IMS
+ * registration for this ImsService has changed status.
+ * @hide
+ */
+
+public class ImsRegistrationImplBase {
+
+ private static final String LOG_TAG = "ImsRegistrationImplBase";
+
+ // Defines the underlying radio technology type that we have registered for IMS over.
+ @IntDef(flag = true,
+ value = {
+ REGISTRATION_TECH_NONE,
+ REGISTRATION_TECH_LTE,
+ REGISTRATION_TECH_IWLAN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ImsRegistrationTech {}
+ /**
+ * No registration technology specified, used when we are not registered.
+ */
+ public static final int REGISTRATION_TECH_NONE = -1;
+ /**
+ * IMS is registered to IMS via LTE.
+ */
+ public static final int REGISTRATION_TECH_LTE = 0;
+ /**
+ * IMS is registered to IMS via IWLAN.
+ */
+ public static final int REGISTRATION_TECH_IWLAN = 1;
+
+ // Registration states, used to notify new ImsRegistrationImplBase#Callbacks of the current
+ // state.
+ private static final int REGISTRATION_STATE_NOT_REGISTERED = 0;
+ private static final int REGISTRATION_STATE_REGISTERING = 1;
+ private static final int REGISTRATION_STATE_REGISTERED = 2;
+
+
+ /**
+ * Callback class for receiving Registration callback events.
+ */
+ public static class Callback extends IImsRegistrationCallback.Stub {
+
+ /**
+ * Notifies the framework when the IMS Provider is connected to the IMS network.
+ *
+ * @param imsRadioTech the radio access technology. Valid values are defined in
+ * {@link ImsRegistrationTech}.
+ */
+ @Override
+ public void onRegistered(@ImsRegistrationTech int imsRadioTech) {
+ }
+
+ /**
+ * Notifies the framework when the IMS Provider is trying to connect the IMS network.
+ *
+ * @param imsRadioTech the radio access technology. Valid values are defined in
+ * {@link ImsRegistrationTech}.
+ */
+ @Override
+ public void onRegistering(@ImsRegistrationTech int imsRadioTech) {
+ }
+
+ /**
+ * Notifies the framework when the IMS Provider is disconnected from the IMS network.
+ *
+ * @param info the {@link ImsReasonInfo} associated with why registration was disconnected.
+ */
+ @Override
+ public void onDeregistered(ImsReasonInfo info) {
+ }
+
+ /**
+ * A failure has occurred when trying to handover registration to another technology type,
+ * defined in {@link ImsRegistrationTech}
+ *
+ * @param imsRadioTech The {@link ImsRegistrationTech} type that has failed
+ * @param info A {@link ImsReasonInfo} that identifies the reason for failure.
+ */
+ @Override
+ public void onTechnologyChangeFailed(@ImsRegistrationTech int imsRadioTech,
+ ImsReasonInfo info) {
+ }
+ }
+
+ private final IImsRegistration mBinder = new IImsRegistration.Stub() {
+
+ @Override
+ public @ImsRegistrationTech int getRegistrationTechnology() throws RemoteException {
+ return getConnectionType();
+ }
+
+ @Override
+ public void addRegistrationCallback(IImsRegistrationCallback c) throws RemoteException {
+ ImsRegistrationImplBase.this.addRegistrationCallback(c);
+ }
+
+ @Override
+ public void removeRegistrationCallback(IImsRegistrationCallback c) throws RemoteException {
+ ImsRegistrationImplBase.this.removeRegistrationCallback(c);
+ }
+ };
+
+ private final RemoteCallbackList<IImsRegistrationCallback> mCallbacks
+ = new RemoteCallbackList<>();
+ private final Object mLock = new Object();
+ // Locked on mLock
+ private @ImsRegistrationTech
+ int mConnectionType = REGISTRATION_TECH_NONE;
+ // Locked on mLock
+ private int mRegistrationState = REGISTRATION_STATE_NOT_REGISTERED;
+ // Locked on mLock
+ private ImsReasonInfo mLastDisconnectCause;
+
+ public final IImsRegistration getBinder() {
+ return mBinder;
+ }
+
+ private void addRegistrationCallback(IImsRegistrationCallback c) throws RemoteException {
+ mCallbacks.register(c);
+ updateNewCallbackWithState(c);
+ }
+
+ private void removeRegistrationCallback(IImsRegistrationCallback c) {
+ mCallbacks.unregister(c);
+ }
+
+ /**
+ * Notify the framework that the device is connected to the IMS network.
+ *
+ * @param imsRadioTech the radio access technology. Valid values are defined in
+ * {@link ImsRegistrationTech}.
+ */
+ public final void onRegistered(@ImsRegistrationTech int imsRadioTech) {
+ updateToState(imsRadioTech, REGISTRATION_STATE_REGISTERED);
+ mCallbacks.broadcast((c) -> {
+ try {
+ c.onRegistered(imsRadioTech);
+ } catch (RemoteException e) {
+ Log.w(LOG_TAG, e + " " + "onRegistrationConnected() - Skipping " +
+ "callback.");
+ }
+ });
+ }
+
+ /**
+ * Notify the framework that the device is trying to connect the IMS network.
+ *
+ * @param imsRadioTech the radio access technology. Valid values are defined in
+ * {@link ImsRegistrationTech}.
+ */
+ public final void onRegistering(@ImsRegistrationTech int imsRadioTech) {
+ updateToState(imsRadioTech, REGISTRATION_STATE_REGISTERING);
+ mCallbacks.broadcast((c) -> {
+ try {
+ c.onRegistering(imsRadioTech);
+ } catch (RemoteException e) {
+ Log.w(LOG_TAG, e + " " + "onRegistrationProcessing() - Skipping " +
+ "callback.");
+ }
+ });
+ }
+
+ /**
+ * Notify the framework that the device is disconnected from the IMS network.
+ *
+ * @param info the {@link ImsReasonInfo} associated with why registration was disconnected.
+ */
+ public final void onDeregistered(ImsReasonInfo info) {
+ updateToDisconnectedState(info);
+ mCallbacks.broadcast((c) -> {
+ try {
+ c.onDeregistered(info);
+ } catch (RemoteException e) {
+ Log.w(LOG_TAG, e + " " + "onRegistrationDisconnected() - Skipping " +
+ "callback.");
+ }
+ });
+ }
+
+ public final void onTechnologyChangeFailed(@ImsRegistrationTech int imsRadioTech,
+ ImsReasonInfo info) {
+ mCallbacks.broadcast((c) -> {
+ try {
+ c.onTechnologyChangeFailed(imsRadioTech, info);
+ } catch (RemoteException e) {
+ Log.w(LOG_TAG, e + " " + "onRegistrationChangeFailed() - Skipping " +
+ "callback.");
+ }
+ });
+ }
+
+ private void updateToState(@ImsRegistrationTech int connType, int newState) {
+ synchronized (mLock) {
+ mConnectionType = connType;
+ mRegistrationState = newState;
+ mLastDisconnectCause = null;
+ }
+ }
+
+ private void updateToDisconnectedState(ImsReasonInfo info) {
+ synchronized (mLock) {
+ updateToState(REGISTRATION_TECH_NONE, REGISTRATION_STATE_NOT_REGISTERED);
+ if (info != null) {
+ mLastDisconnectCause = info;
+ } else {
+ Log.w(LOG_TAG, "updateToDisconnectedState: no ImsReasonInfo provided.");
+ mLastDisconnectCause = new ImsReasonInfo();
+ }
+ }
+ }
+
+ private @ImsRegistrationTech int getConnectionType() {
+ synchronized (mLock) {
+ return mConnectionType;
+ }
+ }
+
+ /**
+ * @param c the newly registered callback that will be updated with the current registration
+ * state.
+ */
+ private void updateNewCallbackWithState(IImsRegistrationCallback c) throws RemoteException {
+ int state;
+ ImsReasonInfo disconnectInfo;
+ synchronized (mLock) {
+ state = mRegistrationState;
+ disconnectInfo = mLastDisconnectCause;
+ }
+ switch (state) {
+ case REGISTRATION_STATE_NOT_REGISTERED: {
+ c.onDeregistered(disconnectInfo);
+ break;
+ }
+ case REGISTRATION_STATE_REGISTERING: {
+ c.onRegistering(getConnectionType());
+ break;
+ }
+ case REGISTRATION_STATE_REGISTERED: {
+ c.onRegistered(getConnectionType());
+ break;
+ }
+ }
+ }
+}
diff --git a/android/telephony/ims/stub/ImsConfigImplBase.java b/android/telephony/ims/stub/ImsConfigImplBase.java
index 5a4db99e..1670e6b9 100644
--- a/android/telephony/ims/stub/ImsConfigImplBase.java
+++ b/android/telephony/ims/stub/ImsConfigImplBase.java
@@ -16,15 +16,23 @@
package android.telephony.ims.stub;
+import android.content.Context;
+import android.content.Intent;
import android.os.RemoteException;
+import android.util.Log;
import com.android.ims.ImsConfig;
import com.android.ims.ImsConfigListener;
import com.android.ims.internal.IImsConfig;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+
/**
- * Base implementation of ImsConfig, which implements stub versions of the methods
- * in the IImsConfig AIDL. Override the methods that your implementation of ImsConfig supports.
+ * Base implementation of ImsConfig.
+ * Override the methods that your implementation of ImsConfig supports.
*
* DO NOT remove or change the existing APIs, only add new ones to this Base implementation or you
* will break other implementations of ImsConfig maintained by other ImsServices.
@@ -34,10 +42,25 @@ import com.android.ims.internal.IImsConfig;
* 1) Items provisioned by the operator.
* 2) Items configured by user. Mainly service feature class.
*
+ * The inner class {@link ImsConfigStub} implements methods of IImsConfig AIDL interface.
+ * The IImsConfig AIDL interface is called by ImsConfig, which may exist in many other processes.
+ * ImsConfigImpl access to the configuration parameters may be arbitrarily slow, especially in
+ * during initialization, or times when a lot of configuration parameters are being set/get
+ * (such as during boot up or SIM card change). By providing a cache in ImsConfigStub, we can speed
+ * up access to these configuration parameters, so a query to the ImsConfigImpl does not have to be
+ * performed every time.
* @hide
*/
-public class ImsConfigImplBase extends IImsConfig.Stub {
+public class ImsConfigImplBase {
+
+ static final private String TAG = "ImsConfigImplBase";
+
+ ImsConfigStub mImsConfigStub;
+
+ public ImsConfigImplBase(Context context) {
+ mImsConfigStub = new ImsConfigStub(this, context);
+ }
/**
* Gets the value for ims service/capabilities parameters from the provisioned
@@ -46,7 +69,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub {
* @param item, as defined in com.android.ims.ImsConfig#ConfigConstants.
* @return value in Integer format.
*/
- @Override
public int getProvisionedValue(int item) throws RemoteException {
return -1;
}
@@ -58,7 +80,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub {
* @param item, as defined in com.android.ims.ImsConfig#ConfigConstants.
* @return value in String format.
*/
- @Override
public String getProvisionedStringValue(int item) throws RemoteException {
return null;
}
@@ -72,7 +93,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub {
* @param value in Integer format.
* @return as defined in com.android.ims.ImsConfig#OperationStatusConstants.
*/
- @Override
public int setProvisionedValue(int item, int value) throws RemoteException {
return ImsConfig.OperationStatusConstants.FAILED;
}
@@ -86,7 +106,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub {
* @param value in String format.
* @return as defined in com.android.ims.ImsConfig#OperationStatusConstants.
*/
- @Override
public int setProvisionedStringValue(int item, String value) throws RemoteException {
return ImsConfig.OperationStatusConstants.FAILED;
}
@@ -100,7 +119,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub {
* @param network as defined in android.telephony.TelephonyManager#NETWORK_TYPE_XXX.
* @param listener feature value returned asynchronously through listener.
*/
- @Override
public void getFeatureValue(int feature, int network, ImsConfigListener listener)
throws RemoteException {
}
@@ -115,7 +133,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub {
* @param value as defined in com.android.ims.ImsConfig#FeatureValueConstants.
* @param listener, provided if caller needs to be notified for set result.
*/
- @Override
public void setFeatureValue(int feature, int network, int value, ImsConfigListener listener)
throws RemoteException {
}
@@ -124,7 +141,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub {
* Gets the value for IMS VoLTE provisioned.
* This should be the same as the operator provisioned value if applies.
*/
- @Override
public boolean getVolteProvisioned() throws RemoteException {
return false;
}
@@ -134,7 +150,6 @@ public class ImsConfigImplBase extends IImsConfig.Stub {
*
* @param listener Video quality value returned asynchronously through listener.
*/
- @Override
public void getVideoQuality(ImsConfigListener listener) throws RemoteException {
}
@@ -144,7 +159,233 @@ public class ImsConfigImplBase extends IImsConfig.Stub {
* @param quality, defines the value of video quality.
* @param listener, provided if caller needs to be notified for set result.
*/
- @Override
public void setVideoQuality(int quality, ImsConfigListener listener) throws RemoteException {
}
+
+ public IImsConfig getIImsConfig() { return mImsConfigStub; }
+
+ /**
+ * Updates provisioning value and notifies the framework of the change.
+ * Doesn't call #setProvisionedValue and assumes the result succeeded.
+ * This should only be used by modem when they implicitly changed provisioned values.
+ *
+ * @param item, as defined in com.android.ims.ImsConfig#ConfigConstants.
+ * @param value in Integer format.
+ */
+ public final void notifyProvisionedValueChanged(int item, int value) {
+ mImsConfigStub.updateCachedValue(item, value, true);
+ }
+
+ /**
+ * Updates provisioning value and notifies the framework of the change.
+ * Doesn't call #setProvisionedValue and assumes the result succeeded.
+ * This should only be used by modem when they implicitly changed provisioned values.
+ *
+ * @param item, as defined in com.android.ims.ImsConfig#ConfigConstants.
+ * @param value in String format.
+ */
+ public final void notifyProvisionedValueChanged(int item, String value) {
+ mImsConfigStub.updateCachedValue(item, value, true);
+ }
+
+ /**
+ * Implements the IImsConfig AIDL interface, which is called by potentially many processes
+ * in order to get/set configuration parameters.
+ *
+ * It holds an object of ImsConfigImplBase class which is usually extended by ImsConfigImpl
+ * with actual implementations from vendors. This class caches provisioned values from
+ * ImsConfigImpl layer because queries through ImsConfigImpl can be slow. When query goes in,
+ * it first checks cache layer. If missed, it will call the vendor implementation of
+ * ImsConfigImplBase API.
+ * and cache the return value if the set succeeds.
+ *
+ * Provides APIs to get/set the IMS service feature/capability/parameters.
+ * The config items include:
+ * 1) Items provisioned by the operator.
+ * 2) Items configured by user. Mainly service feature class.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ static public class ImsConfigStub extends IImsConfig.Stub {
+ Context mContext;
+ WeakReference<ImsConfigImplBase> mImsConfigImplBaseWeakReference;
+ private HashMap<Integer, Integer> mProvisionedIntValue = new HashMap<>();
+ private HashMap<Integer, String> mProvisionedStringValue = new HashMap<>();
+
+ @VisibleForTesting
+ public ImsConfigStub(ImsConfigImplBase imsConfigImplBase, Context context) {
+ mContext = context;
+ mImsConfigImplBaseWeakReference =
+ new WeakReference<ImsConfigImplBase>(imsConfigImplBase);
+ }
+
+ /**
+ * Gets the value for ims service/capabilities parameters. It first checks its local cache,
+ * if missed, it will call ImsConfigImplBase.getProvisionedValue.
+ * Synchronous blocking call.
+ *
+ * @param item, as defined in com.android.ims.ImsConfig#ConfigConstants.
+ * @return value in Integer format.
+ */
+ @Override
+ public synchronized int getProvisionedValue(int item) throws RemoteException {
+ if (mProvisionedIntValue.containsKey(item)) {
+ return mProvisionedIntValue.get(item);
+ } else {
+ int retVal = getImsConfigImpl().getProvisionedValue(item);
+ if (retVal != ImsConfig.OperationStatusConstants.UNKNOWN) {
+ updateCachedValue(item, retVal, false);
+ }
+ return retVal;
+ }
+ }
+
+ /**
+ * Gets the value for ims service/capabilities parameters. It first checks its local cache,
+ * if missed, it will call #ImsConfigImplBase.getProvisionedValue.
+ * Synchronous blocking call.
+ *
+ * @param item, as defined in com.android.ims.ImsConfig#ConfigConstants.
+ * @return value in String format.
+ */
+ @Override
+ public synchronized String getProvisionedStringValue(int item) throws RemoteException {
+ if (mProvisionedIntValue.containsKey(item)) {
+ return mProvisionedStringValue.get(item);
+ } else {
+ String retVal = getImsConfigImpl().getProvisionedStringValue(item);
+ if (retVal != null) {
+ updateCachedValue(item, retVal, false);
+ }
+ return retVal;
+ }
+ }
+
+ /**
+ * Sets the value for IMS service/capabilities parameters by the operator device
+ * management entity. It sets the config item value in the provisioned storage
+ * from which the master value is derived, and write it into local cache.
+ * Synchronous blocking call.
+ *
+ * @param item, as defined in com.android.ims.ImsConfig#ConfigConstants.
+ * @param value in Integer format.
+ * @return as defined in com.android.ims.ImsConfig#OperationStatusConstants.
+ */
+ @Override
+ public synchronized int setProvisionedValue(int item, int value) throws RemoteException {
+ mProvisionedIntValue.remove(item);
+ int retVal = getImsConfigImpl().setProvisionedValue(item, value);
+ if (retVal == ImsConfig.OperationStatusConstants.SUCCESS) {
+ updateCachedValue(item, retVal, true);
+ } else {
+ Log.d(TAG, "Set provision value of " + item +
+ " to " + value + " failed with error code " + retVal);
+ }
+
+ return retVal;
+ }
+
+ /**
+ * Sets the value for IMS service/capabilities parameters by the operator device
+ * management entity. It sets the config item value in the provisioned storage
+ * from which the master value is derived, and write it into local cache.
+ * Synchronous blocking call.
+ *
+ * @param item as defined in com.android.ims.ImsConfig#ConfigConstants.
+ * @param value in String format.
+ * @return as defined in com.android.ims.ImsConfig#OperationStatusConstants.
+ */
+ @Override
+ public synchronized int setProvisionedStringValue(int item, String value)
+ throws RemoteException {
+ mProvisionedStringValue.remove(item);
+ int retVal = getImsConfigImpl().setProvisionedStringValue(item, value);
+ if (retVal == ImsConfig.OperationStatusConstants.SUCCESS) {
+ updateCachedValue(item, retVal, true);
+ }
+
+ return retVal;
+ }
+
+ /**
+ * Wrapper function to call ImsConfigImplBase.getFeatureValue.
+ */
+ @Override
+ public void getFeatureValue(int feature, int network, ImsConfigListener listener)
+ throws RemoteException {
+ getImsConfigImpl().getFeatureValue(feature, network, listener);
+ }
+
+ /**
+ * Wrapper function to call ImsConfigImplBase.setFeatureValue.
+ */
+ @Override
+ public void setFeatureValue(int feature, int network, int value, ImsConfigListener listener)
+ throws RemoteException {
+ getImsConfigImpl().setFeatureValue(feature, network, value, listener);
+ }
+
+ /**
+ * Wrapper function to call ImsConfigImplBase.getVolteProvisioned.
+ */
+ @Override
+ public boolean getVolteProvisioned() throws RemoteException {
+ return getImsConfigImpl().getVolteProvisioned();
+ }
+
+ /**
+ * Wrapper function to call ImsConfigImplBase.getVideoQuality.
+ */
+ @Override
+ public void getVideoQuality(ImsConfigListener listener) throws RemoteException {
+ getImsConfigImpl().getVideoQuality(listener);
+ }
+
+ /**
+ * Wrapper function to call ImsConfigImplBase.setVideoQuality.
+ */
+ @Override
+ public void setVideoQuality(int quality, ImsConfigListener listener)
+ throws RemoteException {
+ getImsConfigImpl().setVideoQuality(quality, listener);
+ }
+
+ private ImsConfigImplBase getImsConfigImpl() throws RemoteException {
+ ImsConfigImplBase ref = mImsConfigImplBaseWeakReference.get();
+ if (ref == null) {
+ throw new RemoteException("Fail to get ImsConfigImpl");
+ } else {
+ return ref;
+ }
+ }
+
+ private void sendImsConfigChangedIntent(int item, int value) {
+ sendImsConfigChangedIntent(item, Integer.toString(value));
+ }
+
+ private void sendImsConfigChangedIntent(int item, String value) {
+ Intent configChangedIntent = new Intent(ImsConfig.ACTION_IMS_CONFIG_CHANGED);
+ configChangedIntent.putExtra(ImsConfig.EXTRA_CHANGED_ITEM, item);
+ configChangedIntent.putExtra(ImsConfig.EXTRA_NEW_VALUE, value);
+ if (mContext != null) {
+ mContext.sendBroadcast(configChangedIntent);
+ }
+ }
+
+ protected synchronized void updateCachedValue(int item, int value, boolean notifyChange) {
+ mProvisionedIntValue.put(item, value);
+ if (notifyChange) {
+ sendImsConfigChangedIntent(item, value);
+ }
+ }
+
+ protected synchronized void updateCachedValue(
+ int item, String value, boolean notifyChange) {
+ mProvisionedStringValue.put(item, value);
+ if (notifyChange) {
+ sendImsConfigChangedIntent(item, value);
+ }
+ }
+ }
}
diff --git a/android/telephony/ims/stub/ImsUtImplBase.java b/android/telephony/ims/stub/ImsUtImplBase.java
index dc74094d..054a8b22 100644
--- a/android/telephony/ims/stub/ImsUtImplBase.java
+++ b/android/telephony/ims/stub/ImsUtImplBase.java
@@ -53,6 +53,15 @@ public class ImsUtImplBase extends IImsUt.Stub {
}
/**
+ * Retrieves the configuration of the call barring for specified service class.
+ */
+ @Override
+ public int queryCallBarringForServiceClass(int cbType, int serviceClass)
+ throws RemoteException {
+ return -1;
+ }
+
+ /**
* Retrieves the configuration of the call forward.
*/
@Override
@@ -117,6 +126,15 @@ public class ImsUtImplBase extends IImsUt.Stub {
}
/**
+ * Updates the configuration of the call barring for specified service class.
+ */
+ @Override
+ public int updateCallBarringForServiceClass(int cbType, int action, String[] barrList,
+ int serviceClass) throws RemoteException {
+ return -1;
+ }
+
+ /**
* Updates the configuration of the call forward.
*/
@Override
diff --git a/android/telephony/ims/stub/ImsUtListenerImplBase.java b/android/telephony/ims/stub/ImsUtListenerImplBase.java
index b371efb6..daa74c8f 100644
--- a/android/telephony/ims/stub/ImsUtListenerImplBase.java
+++ b/android/telephony/ims/stub/ImsUtListenerImplBase.java
@@ -21,6 +21,7 @@ import android.os.RemoteException;
import com.android.ims.ImsCallForwardInfo;
import com.android.ims.ImsReasonInfo;
+import com.android.ims.ImsSsData;
import com.android.ims.ImsSsInfo;
import com.android.ims.internal.IImsUt;
import com.android.ims.internal.IImsUtListener;
@@ -85,4 +86,10 @@ public class ImsUtListenerImplBase extends IImsUtListener.Stub {
public void utConfigurationCallWaitingQueried(IImsUt ut, int id, ImsSsInfo[] cwInfo)
throws RemoteException {
}
+
+ /**
+ * Notifies client when Supplementary Service indication is received
+ */
+ @Override
+ public void onSupplementaryServiceIndication(ImsSsData ssData) {}
}
diff --git a/android/telephony/mbms/ServiceInfo.java b/android/telephony/mbms/ServiceInfo.java
index 8529f525..f78e7a6e 100644
--- a/android/telephony/mbms/ServiceInfo.java
+++ b/android/telephony/mbms/ServiceInfo.java
@@ -51,8 +51,8 @@ public class ServiceInfo {
/** @hide */
public ServiceInfo(Map<Locale, String> newNames, String newClassName, List<Locale> newLocales,
String newServiceId, Date start, Date end) {
- if (newNames == null || newNames.isEmpty() || TextUtils.isEmpty(newClassName)
- || newLocales == null || newLocales.isEmpty() || TextUtils.isEmpty(newServiceId)
+ if (newNames == null || newClassName == null
+ || newLocales == null || newServiceId == null
|| start == null || end == null) {
throw new IllegalArgumentException("Bad ServiceInfo construction");
}
diff --git a/android/test/mock/MockContext.java b/android/test/mock/MockContext.java
index 5e5ba462..4dfd0507 100644
--- a/android/test/mock/MockContext.java
+++ b/android/test/mock/MockContext.java
@@ -19,13 +19,12 @@ package android.test.mock;
import android.annotation.SystemApi;
import android.app.IApplicationThread;
import android.app.IServiceConnection;
-import android.app.Notification;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.BroadcastReceiver;
import android.content.IntentSender;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
@@ -44,8 +43,8 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.UserHandle;
-import android.view.DisplayAdjustments;
import android.view.Display;
+import android.view.DisplayAdjustments;
import java.io.File;
import java.io.FileInputStream;
@@ -53,6 +52,7 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.util.concurrent.Executor;
/**
* A mock {@link android.content.Context} class. All methods are non-functional and throw
@@ -87,6 +87,11 @@ public class MockContext extends Context {
}
@Override
+ public Executor getMainExecutor() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
public Context getApplicationContext() {
throw new UnsupportedOperationException();
}
diff --git a/android/test/mock/MockPackageManager.java b/android/test/mock/MockPackageManager.java
index 0c562e65..ce8019f8 100644
--- a/android/test/mock/MockPackageManager.java
+++ b/android/test/mock/MockPackageManager.java
@@ -46,6 +46,7 @@ import android.content.pm.ServiceInfo;
import android.content.pm.SharedLibraryInfo;
import android.content.pm.VerifierDeviceIdentity;
import android.content.pm.VersionedPackage;
+import android.content.pm.dex.ArtManager;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.Rect;
@@ -1174,4 +1175,12 @@ public class MockPackageManager extends PackageManager {
@Nullable DexModuleRegisterCallback callback) {
throw new UnsupportedOperationException();
}
+
+ /**
+ * @hide
+ */
+ @Override
+ public ArtManager getArtManager() {
+ throw new UnsupportedOperationException();
+ }
}
diff --git a/android/text/AutoGrowArray.java b/android/text/AutoGrowArray.java
new file mode 100644
index 00000000..e428377a
--- /dev/null
+++ b/android/text/AutoGrowArray.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+
+import com.android.internal.util.ArrayUtils;
+
+import libcore.util.EmptyArray;
+
+/**
+ * Implements a growing array of int primitives.
+ *
+ * These arrays are NOT thread safe.
+ *
+ * @hide
+ */
+public final class AutoGrowArray {
+ private static final int MIN_CAPACITY_INCREMENT = 12;
+ private static final int MAX_CAPACITY_TO_BE_KEPT = 10000;
+
+ /**
+ * Returns next capacity size.
+ *
+ * The returned capacity is larger than requested capacity.
+ */
+ private static int computeNewCapacity(int currentSize, int requested) {
+ final int targetCapacity = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2)
+ ? MIN_CAPACITY_INCREMENT : currentSize >> 1);
+ return targetCapacity > requested ? targetCapacity : requested;
+ }
+
+ /**
+ * An auto growing byte array.
+ */
+ public static class ByteArray {
+
+ private @NonNull byte[] mValues;
+ private @IntRange(from = 0) int mSize;
+
+ /**
+ * Creates an empty ByteArray with the default initial capacity.
+ */
+ public ByteArray() {
+ this(10);
+ }
+
+ /**
+ * Creates an empty ByteArray with the specified initial capacity.
+ */
+ public ByteArray(@IntRange(from = 0) int initialCapacity) {
+ if (initialCapacity == 0) {
+ mValues = EmptyArray.BYTE;
+ } else {
+ mValues = ArrayUtils.newUnpaddedByteArray(initialCapacity);
+ }
+ mSize = 0;
+ }
+
+ /**
+ * Changes the size of this ByteArray. If this ByteArray is shrinked, the backing array
+ * capacity is unchanged.
+ */
+ public void resize(@IntRange(from = 0) int newSize) {
+ if (newSize > mValues.length) {
+ ensureCapacity(newSize - mSize);
+ }
+ mSize = newSize;
+ }
+
+ /**
+ * Appends the specified value to the end of this array.
+ */
+ public void append(byte value) {
+ ensureCapacity(1);
+ mValues[mSize++] = value;
+ }
+
+ /**
+ * Ensures capacity to append at least <code>count</code> values.
+ */
+ private void ensureCapacity(@IntRange int count) {
+ final int requestedSize = mSize + count;
+ if (requestedSize >= mValues.length) {
+ final int newCapacity = computeNewCapacity(mSize, requestedSize);
+ final byte[] newValues = ArrayUtils.newUnpaddedByteArray(newCapacity);
+ System.arraycopy(mValues, 0, newValues, 0, mSize);
+ mValues = newValues;
+ }
+ }
+
+ /**
+ * Removes all values from this array.
+ */
+ public void clear() {
+ mSize = 0;
+ }
+
+ /**
+ * Removes all values from this array and release the internal array object if it is too
+ * large.
+ */
+ public void clearWithReleasingLargeArray() {
+ clear();
+ if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) {
+ mValues = EmptyArray.BYTE;
+ }
+ }
+
+ /**
+ * Returns the value at the specified position in this array.
+ */
+ public byte get(@IntRange(from = 0) int index) {
+ return mValues[index];
+ }
+
+ /**
+ * Sets the value at the specified position in this array.
+ */
+ public void set(@IntRange(from = 0) int index, byte value) {
+ mValues[index] = value;
+ }
+
+ /**
+ * Returns the number of values in this array.
+ */
+ public @IntRange(from = 0) int size() {
+ return mSize;
+ }
+
+ /**
+ * Returns internal raw array.
+ *
+ * Note that this array may have larger size than you requested.
+ * Use size() instead for getting the actual array size.
+ */
+ public @NonNull byte[] getRawArray() {
+ return mValues;
+ }
+ }
+
+ /**
+ * An auto growing int array.
+ */
+ public static class IntArray {
+
+ private @NonNull int[] mValues;
+ private @IntRange(from = 0) int mSize;
+
+ /**
+ * Creates an empty IntArray with the default initial capacity.
+ */
+ public IntArray() {
+ this(10);
+ }
+
+ /**
+ * Creates an empty IntArray with the specified initial capacity.
+ */
+ public IntArray(@IntRange(from = 0) int initialCapacity) {
+ if (initialCapacity == 0) {
+ mValues = EmptyArray.INT;
+ } else {
+ mValues = ArrayUtils.newUnpaddedIntArray(initialCapacity);
+ }
+ mSize = 0;
+ }
+
+ /**
+ * Changes the size of this IntArray. If this IntArray is shrinked, the backing array
+ * capacity is unchanged.
+ */
+ public void resize(@IntRange(from = 0) int newSize) {
+ if (newSize > mValues.length) {
+ ensureCapacity(newSize - mSize);
+ }
+ mSize = newSize;
+ }
+
+ /**
+ * Appends the specified value to the end of this array.
+ */
+ public void append(int value) {
+ ensureCapacity(1);
+ mValues[mSize++] = value;
+ }
+
+ /**
+ * Ensures capacity to append at least <code>count</code> values.
+ */
+ private void ensureCapacity(@IntRange(from = 0) int count) {
+ final int requestedSize = mSize + count;
+ if (requestedSize >= mValues.length) {
+ final int newCapacity = computeNewCapacity(mSize, requestedSize);
+ final int[] newValues = ArrayUtils.newUnpaddedIntArray(newCapacity);
+ System.arraycopy(mValues, 0, newValues, 0, mSize);
+ mValues = newValues;
+ }
+ }
+
+ /**
+ * Removes all values from this array.
+ */
+ public void clear() {
+ mSize = 0;
+ }
+
+ /**
+ * Removes all values from this array and release the internal array object if it is too
+ * large.
+ */
+ public void clearWithReleasingLargeArray() {
+ clear();
+ if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) {
+ mValues = EmptyArray.INT;
+ }
+ }
+
+ /**
+ * Returns the value at the specified position in this array.
+ */
+ public int get(@IntRange(from = 0) int index) {
+ return mValues[index];
+ }
+
+ /**
+ * Sets the value at the specified position in this array.
+ */
+ public void set(@IntRange(from = 0) int index, int value) {
+ mValues[index] = value;
+ }
+
+ /**
+ * Returns the number of values in this array.
+ */
+ public @IntRange(from = 0) int size() {
+ return mSize;
+ }
+
+ /**
+ * Returns internal raw array.
+ *
+ * Note that this array may have larger size than you requested.
+ * Use size() instead for getting the actual array size.
+ */
+ public @NonNull int[] getRawArray() {
+ return mValues;
+ }
+ }
+
+ /**
+ * An auto growing float array.
+ */
+ public static class FloatArray {
+
+ private @NonNull float[] mValues;
+ private @IntRange(from = 0) int mSize;
+
+ /**
+ * Creates an empty FloatArray with the default initial capacity.
+ */
+ public FloatArray() {
+ this(10);
+ }
+
+ /**
+ * Creates an empty FloatArray with the specified initial capacity.
+ */
+ public FloatArray(@IntRange(from = 0) int initialCapacity) {
+ if (initialCapacity == 0) {
+ mValues = EmptyArray.FLOAT;
+ } else {
+ mValues = ArrayUtils.newUnpaddedFloatArray(initialCapacity);
+ }
+ mSize = 0;
+ }
+
+ /**
+ * Changes the size of this FloatArray. If this FloatArray is shrinked, the backing array
+ * capacity is unchanged.
+ */
+ public void resize(@IntRange(from = 0) int newSize) {
+ if (newSize > mValues.length) {
+ ensureCapacity(newSize - mSize);
+ }
+ mSize = newSize;
+ }
+
+ /**
+ * Appends the specified value to the end of this array.
+ */
+ public void append(float value) {
+ ensureCapacity(1);
+ mValues[mSize++] = value;
+ }
+
+ /**
+ * Ensures capacity to append at least <code>count</code> values.
+ */
+ private void ensureCapacity(int count) {
+ final int requestedSize = mSize + count;
+ if (requestedSize >= mValues.length) {
+ final int newCapacity = computeNewCapacity(mSize, requestedSize);
+ final float[] newValues = ArrayUtils.newUnpaddedFloatArray(newCapacity);
+ System.arraycopy(mValues, 0, newValues, 0, mSize);
+ mValues = newValues;
+ }
+ }
+
+ /**
+ * Removes all values from this array.
+ */
+ public void clear() {
+ mSize = 0;
+ }
+
+ /**
+ * Removes all values from this array and release the internal array object if it is too
+ * large.
+ */
+ public void clearWithReleasingLargeArray() {
+ clear();
+ if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) {
+ mValues = EmptyArray.FLOAT;
+ }
+ }
+
+ /**
+ * Returns the value at the specified position in this array.
+ */
+ public float get(@IntRange(from = 0) int index) {
+ return mValues[index];
+ }
+
+ /**
+ * Sets the value at the specified position in this array.
+ */
+ public void set(@IntRange(from = 0) int index, float value) {
+ mValues[index] = value;
+ }
+
+ /**
+ * Returns the number of values in this array.
+ */
+ public @IntRange(from = 0) int size() {
+ return mSize;
+ }
+
+ /**
+ * Returns internal raw array.
+ *
+ * Note that this array may have larger size than you requested.
+ * Use size() instead for getting the actual array size.
+ */
+ public @NonNull float[] getRawArray() {
+ return mValues;
+ }
+ }
+}
diff --git a/android/text/DynamicLayout.java b/android/text/DynamicLayout.java
index fba358cf..6bca37af 100644
--- a/android/text/DynamicLayout.java
+++ b/android/text/DynamicLayout.java
@@ -42,8 +42,7 @@ import java.lang.ref.WeakReference;
* {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
* Canvas.drawText()} directly.</p>
*/
-public class DynamicLayout extends Layout
-{
+public class DynamicLayout extends Layout {
private static final int PRIORITY = 128;
private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400;
@@ -303,8 +302,9 @@ public class DynamicLayout extends Layout
}
/**
- * Make a layout for the specified text that will be updated as the text is changed.
+ * @deprecated Use {@link Builder} instead.
*/
+ @Deprecated
public DynamicLayout(@NonNull CharSequence base,
@NonNull TextPaint paint,
@IntRange(from = 0) int width, @NonNull Alignment align,
@@ -315,9 +315,9 @@ public class DynamicLayout extends Layout
}
/**
- * Make a layout for the transformed text (password transformation being the primary example of
- * a transformation) that will be updated as the base text is changed.
+ * @deprecated Use {@link Builder} instead.
*/
+ @Deprecated
public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display,
@NonNull TextPaint paint,
@IntRange(from = 0) int width, @NonNull Alignment align,
@@ -328,10 +328,9 @@ public class DynamicLayout extends Layout
}
/**
- * Make a layout for the transformed text (password transformation being the primary example of
- * a transformation) that will be updated as the base text is changed. If ellipsize is non-null,
- * the Layout will ellipsize the text down to ellipsizedWidth.
+ * @deprecated Use {@link Builder} instead.
*/
+ @Deprecated
public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display,
@NonNull TextPaint paint,
@IntRange(from = 0) int width, @NonNull Alignment align,
@@ -351,7 +350,9 @@ public class DynamicLayout extends Layout
* the Layout will ellipsize the text down to ellipsizedWidth.
*
* @hide
+ * @deprecated Use {@link Builder} instead.
*/
+ @Deprecated
public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display,
@NonNull TextPaint paint,
@IntRange(from = 0) int width,
@@ -492,7 +493,9 @@ public class DynamicLayout extends Layout
}
}
- private void reflow(CharSequence s, int where, int before, int after) {
+ /** @hide */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void reflow(CharSequence s, int where, int before, int after) {
if (s != mBase)
return;
@@ -805,8 +808,8 @@ public class DynamicLayout extends Layout
return;
}
- int firstBlock = -1;
- int lastBlock = -1;
+ /*final*/ int firstBlock = -1;
+ /*final*/ int lastBlock = -1;
for (int i = 0; i < mNumberOfBlocks; i++) {
if (mBlockEndLines[i] >= startLine) {
firstBlock = i;
@@ -821,10 +824,10 @@ public class DynamicLayout extends Layout
}
final int lastBlockEndLine = mBlockEndLines[lastBlock];
- boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
+ final boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
mBlockEndLines[firstBlock - 1] + 1);
- boolean createBlock = newLineCount > 0;
- boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
+ final boolean createBlock = newLineCount > 0;
+ final boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
int numAddedBlocks = 0;
if (createBlockBefore) numAddedBlocks++;
@@ -863,12 +866,18 @@ public class DynamicLayout extends Layout
if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) {
final ArraySet<Integer> set = new ArraySet<>();
+ final int changedBlockCount = numAddedBlocks - numRemovedBlocks;
for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) {
Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i);
- if (block > firstBlock) {
- block += numAddedBlocks - numRemovedBlocks;
+ if (block < firstBlock) {
+ // block index is before firstBlock add it since it did not change
+ set.add(block);
+ }
+ if (block > lastBlock) {
+ // block index is after lastBlock, the index reduced to += changedBlockCount
+ block += changedBlockCount;
+ set.add(block);
}
- set.add(block);
}
mBlocksAlwaysNeedToBeRedrawn = set;
}
diff --git a/android/text/FontConfig.java b/android/text/FontConfig.java
index 4654e83c..7386e3e8 100644
--- a/android/text/FontConfig.java
+++ b/android/text/FontConfig.java
@@ -179,7 +179,11 @@ public final class FontConfig {
/** @hide */
@Retention(SOURCE)
- @IntDef({VARIANT_DEFAULT, VARIANT_COMPACT, VARIANT_ELEGANT})
+ @IntDef(prefix = { "VARIANT_" }, value = {
+ VARIANT_DEFAULT,
+ VARIANT_COMPACT,
+ VARIANT_ELEGANT
+ })
public @interface Variant {}
/**
diff --git a/android/text/Layout.java b/android/text/Layout.java
index 4d2a9629..bf4b6ac5 100644
--- a/android/text/Layout.java
+++ b/android/text/Layout.java
@@ -48,7 +48,11 @@ import java.util.Arrays;
*/
public abstract class Layout {
/** @hide */
- @IntDef({BREAK_STRATEGY_SIMPLE, BREAK_STRATEGY_HIGH_QUALITY, BREAK_STRATEGY_BALANCED})
+ @IntDef(prefix = { "BREAK_STRATEGY_" }, value = {
+ BREAK_STRATEGY_SIMPLE,
+ BREAK_STRATEGY_HIGH_QUALITY,
+ BREAK_STRATEGY_BALANCED
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface BreakStrategy {}
@@ -73,8 +77,11 @@ public abstract class Layout {
public static final int BREAK_STRATEGY_BALANCED = 2;
/** @hide */
- @IntDef({HYPHENATION_FREQUENCY_NORMAL, HYPHENATION_FREQUENCY_FULL,
- HYPHENATION_FREQUENCY_NONE})
+ @IntDef(prefix = { "HYPHENATION_FREQUENCY_" }, value = {
+ HYPHENATION_FREQUENCY_NORMAL,
+ HYPHENATION_FREQUENCY_FULL,
+ HYPHENATION_FREQUENCY_NONE
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface HyphenationFrequency {}
@@ -105,7 +112,10 @@ public abstract class Layout {
ArrayUtils.emptyArray(ParagraphStyle.class);
/** @hide */
- @IntDef({JUSTIFICATION_MODE_NONE, JUSTIFICATION_MODE_INTER_WORD})
+ @IntDef(prefix = { "JUSTIFICATION_MODE_" }, value = {
+ JUSTIFICATION_MODE_NONE,
+ JUSTIFICATION_MODE_INTER_WORD
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface JustificationMode {}
@@ -1907,22 +1917,14 @@ public abstract class Layout {
private static float measurePara(TextPaint paint, CharSequence text, int start, int end,
TextDirectionHeuristic textDir) {
- MeasuredText mt = MeasuredText.obtain();
+ MeasuredText mt = null;
TextLine tl = TextLine.obtain();
try {
- mt.setPara(text, start, end, textDir);
- Directions directions;
- int dir;
- if (mt.mEasy) {
- directions = DIRS_ALL_LEFT_TO_RIGHT;
- dir = Layout.DIR_LEFT_TO_RIGHT;
- } else {
- directions = AndroidBidi.directions(mt.mDir, mt.mLevels,
- 0, mt.mChars, 0, mt.mLen);
- dir = mt.mDir;
- }
- char[] chars = mt.mChars;
- int len = mt.mLen;
+ mt = MeasuredText.buildForBidi(text, start, end, textDir, mt);
+ final char[] chars = mt.getChars();
+ final int len = chars.length;
+ final Directions directions = mt.getDirections(0, len);
+ final int dir = mt.getParagraphDir();
boolean hasTabs = false;
TabStops tabStops = null;
// leading margins should be taken into account when measuring a paragraph
@@ -1955,7 +1957,9 @@ public abstract class Layout {
return margin + Math.abs(tl.metrics(null));
} finally {
TextLine.recycle(tl);
- MeasuredText.recycle(mt);
+ if (mt != null) {
+ mt.recycle();
+ }
}
}
@@ -2272,6 +2276,14 @@ public abstract class Layout {
private SpanSet<LineBackgroundSpan> mLineBackgroundSpans;
private int mJustificationMode;
+ /** @hide */
+ @IntDef(prefix = { "DIR_" }, value = {
+ DIR_LEFT_TO_RIGHT,
+ DIR_RIGHT_TO_LEFT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Direction {}
+
public static final int DIR_LEFT_TO_RIGHT = 1;
public static final int DIR_RIGHT_TO_LEFT = -1;
@@ -2309,7 +2321,10 @@ public abstract class Layout {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT, TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT})
+ @IntDef(prefix = { "TEXT_SELECTION_LAYOUT_" }, value = {
+ TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT,
+ TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT
+ })
public @interface TextSelectionLayout {}
/** @hide */
diff --git a/android/text/MeasuredText.java b/android/text/MeasuredText.java
index 3d9fba71..14d6f9e8 100644
--- a/android/text/MeasuredText.java
+++ b/android/text/MeasuredText.java
@@ -16,125 +16,436 @@
package android.text;
+import android.annotation.FloatRange;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.graphics.Paint;
+import android.text.AutoGrowArray.ByteArray;
+import android.text.AutoGrowArray.FloatArray;
+import android.text.AutoGrowArray.IntArray;
+import android.text.Layout.Directions;
import android.text.style.MetricAffectingSpan;
import android.text.style.ReplacementSpan;
-import android.util.Log;
+import android.util.Pools.SynchronizedPool;
-import com.android.internal.util.ArrayUtils;
+import dalvik.annotation.optimization.CriticalNative;
+
+import libcore.util.NativeAllocationRegistry;
+
+import java.util.Arrays;
/**
+ * MeasuredText provides text information for rendering purpose.
+ *
+ * The first motivation of this class is identify the text directions and retrieving individual
+ * character widths. However retrieving character widths is slower than identifying text directions.
+ * Thus, this class provides several builder methods for specific purposes.
+ *
+ * - buildForBidi:
+ * Compute only text directions.
+ * - buildForMeasurement:
+ * Compute text direction and all character widths.
+ * - buildForStaticLayout:
+ * This is bit special. StaticLayout also needs to know text direction and character widths for
+ * line breaking, but all things are done in native code. Similarly, text measurement is done
+ * in native code. So instead of storing result to Java array, this keeps the result in native
+ * code since there is no good reason to move the results to Java layer.
+ *
+ * In addition to the character widths, some additional information is computed for each purposes,
+ * e.g. whole text length for measurement or font metrics for static layout.
+ *
+ * MeasuredText is NOT a thread safe object.
* @hide
*/
-class MeasuredText {
- private static final boolean localLOGV = false;
- CharSequence mText;
- int mTextStart;
- float[] mWidths;
- char[] mChars;
- byte[] mLevels;
- int mDir;
- boolean mEasy;
- int mLen;
-
- private int mPos;
- private TextPaint mWorkPaint;
-
- private MeasuredText() {
- mWorkPaint = new TextPaint();
+public class MeasuredText {
+ private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
+
+ private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
+ MeasuredText.class.getClassLoader(), nGetReleaseFunc(), 1024);
+
+ private MeasuredText() {} // Use build static functions instead.
+
+ private static final SynchronizedPool<MeasuredText> sPool = new SynchronizedPool<>(1);
+
+ private static @NonNull MeasuredText obtain() { // Use build static functions instead.
+ final MeasuredText mt = sPool.acquire();
+ return mt != null ? mt : new MeasuredText();
}
- private static final Object[] sLock = new Object[0];
- private static final MeasuredText[] sCached = new MeasuredText[3];
-
- static MeasuredText obtain() {
- MeasuredText mt;
- synchronized (sLock) {
- for (int i = sCached.length; --i >= 0;) {
- if (sCached[i] != null) {
- mt = sCached[i];
- sCached[i] = null;
- return mt;
- }
- }
+ /**
+ * Recycle the MeasuredText.
+ *
+ * Do not call any methods after you call this method.
+ */
+ public void recycle() {
+ release();
+ sPool.release(this);
+ }
+
+ // The casted original text.
+ //
+ // This may be null if the passed text is not a Spanned.
+ private @Nullable Spanned mSpanned;
+
+ // The start offset of the target range in the original text (mSpanned);
+ private @IntRange(from = 0) int mTextStart;
+
+ // The length of the target range in the original text.
+ private @IntRange(from = 0) int mTextLength;
+
+ // The copied character buffer for measuring text.
+ //
+ // The length of this array is mTextLength.
+ private @Nullable char[] mCopiedBuffer;
+
+ // The whole paragraph direction.
+ private @Layout.Direction int mParaDir;
+
+ // True if the text is LTR direction and doesn't contain any bidi characters.
+ private boolean mLtrWithoutBidi;
+
+ // The bidi level for individual characters.
+ //
+ // This is empty if mLtrWithoutBidi is true.
+ private @NonNull ByteArray mLevels = new ByteArray();
+
+ // The whole width of the text.
+ // See getWholeWidth comments.
+ private @FloatRange(from = 0.0f) float mWholeWidth;
+
+ // Individual characters' widths.
+ // See getWidths comments.
+ private @Nullable FloatArray mWidths = new FloatArray();
+
+ // The span end positions.
+ // See getSpanEndCache comments.
+ private @Nullable IntArray mSpanEndCache = new IntArray(4);
+
+ // The font metrics.
+ // See getFontMetrics comments.
+ private @Nullable IntArray mFontMetrics = new IntArray(4 * 4);
+
+ // The native MeasuredText.
+ // See getNativePtr comments.
+ // Do not modify these members directly. Use bindNativeObject/unbindNativeObject instead.
+ private /* Maybe Zero */ long mNativePtr = 0;
+ private @Nullable Runnable mNativeObjectCleaner;
+
+ // Associate the native object to this Java object.
+ private void bindNativeObject(/* Non Zero*/ long nativePtr) {
+ mNativePtr = nativePtr;
+ mNativeObjectCleaner = sRegistry.registerNativeAllocation(this, nativePtr);
+ }
+
+ // Decouple the native object from this Java object and release the native object.
+ private void unbindNativeObject() {
+ if (mNativePtr != 0) {
+ mNativeObjectCleaner.run();
+ mNativePtr = 0;
}
- mt = new MeasuredText();
- if (localLOGV) {
- Log.v("MEAS", "new: " + mt);
+ }
+
+ // Following two objects are for avoiding object allocation.
+ private @NonNull TextPaint mCachedPaint = new TextPaint();
+ private @Nullable Paint.FontMetricsInt mCachedFm;
+
+ /**
+ * Releases internal buffers.
+ */
+ public void release() {
+ reset();
+ mLevels.clearWithReleasingLargeArray();
+ mWidths.clearWithReleasingLargeArray();
+ mFontMetrics.clearWithReleasingLargeArray();
+ mSpanEndCache.clearWithReleasingLargeArray();
+ }
+
+ /**
+ * Resets the internal state for starting new text.
+ */
+ private void reset() {
+ mSpanned = null;
+ mCopiedBuffer = null;
+ mWholeWidth = 0;
+ mLevels.clear();
+ mWidths.clear();
+ mFontMetrics.clear();
+ mSpanEndCache.clear();
+ unbindNativeObject();
+ }
+
+ /**
+ * Returns the characters to be measured.
+ *
+ * This is always available.
+ */
+ public @NonNull char[] getChars() {
+ return mCopiedBuffer;
+ }
+
+ /**
+ * Returns the paragraph direction.
+ *
+ * This is always available.
+ */
+ public @Layout.Direction int getParagraphDir() {
+ return mParaDir;
+ }
+
+ /**
+ * Returns the directions.
+ *
+ * This is always available.
+ */
+ public Directions getDirections(@IntRange(from = 0) int start, // inclusive
+ @IntRange(from = 0) int end) { // exclusive
+ if (mLtrWithoutBidi) {
+ return Layout.DIRS_ALL_LEFT_TO_RIGHT;
}
+
+ final int length = end - start;
+ return AndroidBidi.directions(mParaDir, mLevels.getRawArray(), start, mCopiedBuffer, start,
+ length);
+ }
+
+ /**
+ * Returns the whole text width.
+ *
+ * This is available only if the MeasureText is computed with computeForMeasurement.
+ * Returns 0 in other cases.
+ */
+ public @FloatRange(from = 0.0f) float getWholeWidth() {
+ return mWholeWidth;
+ }
+
+ /**
+ * Returns the individual character's width.
+ *
+ * This is available only if the MeasureText is computed with computeForMeasurement.
+ * Returns empty array in other cases.
+ */
+ public @NonNull FloatArray getWidths() {
+ return mWidths;
+ }
+
+ /**
+ * Returns the MetricsAffectingSpan end indices.
+ *
+ * If the input text is not a spanned string, this has one value that is the length of the text.
+ *
+ * This is available only if the MeasureText is computed with computeForStaticLayout.
+ * Returns empty array in other cases.
+ */
+ public @NonNull IntArray getSpanEndCache() {
+ return mSpanEndCache;
+ }
+
+ /**
+ * Returns the int array which holds FontMetrics.
+ *
+ * This array holds the repeat of top, bottom, ascent, descent of font metrics value.
+ *
+ * This is available only if the MeasureText is computed with computeForStaticLayout.
+ * Returns empty array in other cases.
+ */
+ public @NonNull IntArray getFontMetrics() {
+ return mFontMetrics;
+ }
+
+ /**
+ * Returns the native ptr of the MeasuredText.
+ *
+ * This is available only if the MeasureText is computed with computeForStaticLayout.
+ * Returns 0 in other cases.
+ */
+ public /* Maybe Zero */ long getNativePtr() {
+ return mNativePtr;
+ }
+
+ /**
+ * Generates new MeasuredText for Bidi computation.
+ *
+ * If recycle is null, this returns new instance. If recycle is not null, this fills computed
+ * result to recycle and returns recycle.
+ *
+ * @param text the character sequence to be measured
+ * @param start the inclusive start offset of the target region in the text
+ * @param end the exclusive end offset of the target region in the text
+ * @param textDir the text direction
+ * @param recycle pass existing MeasuredText if you want to recycle it.
+ *
+ * @return measured text
+ */
+ public static @NonNull MeasuredText buildForBidi(@NonNull CharSequence text,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @NonNull TextDirectionHeuristic textDir,
+ @Nullable MeasuredText recycle) {
+ final MeasuredText mt = recycle == null ? obtain() : recycle;
+ mt.resetAndAnalyzeBidi(text, start, end, textDir);
return mt;
}
- static MeasuredText recycle(MeasuredText mt) {
- mt.finish();
- synchronized(sLock) {
- for (int i = 0; i < sCached.length; ++i) {
- if (sCached[i] == null) {
- sCached[i] = mt;
- mt.mText = null;
- break;
- }
+ /**
+ * Generates new MeasuredText for measuring texts.
+ *
+ * If recycle is null, this returns new instance. If recycle is not null, this fills computed
+ * result to recycle and returns recycle.
+ *
+ * @param paint the paint to be used for rendering the text.
+ * @param text the character sequence to be measured
+ * @param start the inclusive start offset of the target region in the text
+ * @param end the exclusive end offset of the target region in the text
+ * @param textDir the text direction
+ * @param recycle pass existing MeasuredText if you want to recycle it.
+ *
+ * @return measured text
+ */
+ public static @NonNull MeasuredText buildForMeasurement(@NonNull TextPaint paint,
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @NonNull TextDirectionHeuristic textDir,
+ @Nullable MeasuredText recycle) {
+ final MeasuredText mt = recycle == null ? obtain() : recycle;
+ mt.resetAndAnalyzeBidi(text, start, end, textDir);
+
+ mt.mWidths.resize(mt.mTextLength);
+ if (mt.mTextLength == 0) {
+ return mt;
+ }
+
+ if (mt.mSpanned == null) {
+ // No style change by MetricsAffectingSpan. Just measure all text.
+ mt.applyMetricsAffectingSpan(
+ paint, null /* spans */, start, end, 0 /* native static layout ptr */);
+ } else {
+ // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
+ int spanEnd;
+ for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
+ spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
+ MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
+ MetricAffectingSpan.class);
+ spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
+ mt.applyMetricsAffectingSpan(
+ paint, spans, spanStart, spanEnd, 0 /* native static layout ptr */);
}
}
- return null;
+ return mt;
}
- void finish() {
- mText = null;
- if (mLen > 1000) {
- mWidths = null;
- mChars = null;
- mLevels = null;
+ /**
+ * Generates new MeasuredText for StaticLayout.
+ *
+ * If recycle is null, this returns new instance. If recycle is not null, this fills computed
+ * result to recycle and returns recycle.
+ *
+ * @param paint the paint to be used for rendering the text.
+ * @param text the character sequence to be measured
+ * @param start the inclusive start offset of the target region in the text
+ * @param end the exclusive end offset of the target region in the text
+ * @param textDir the text direction
+ * @param recycle pass existing MeasuredText if you want to recycle it.
+ *
+ * @return measured text
+ */
+ public static @NonNull MeasuredText buildForStaticLayout(
+ @NonNull TextPaint paint,
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @NonNull TextDirectionHeuristic textDir,
+ @Nullable MeasuredText recycle) {
+ final MeasuredText mt = recycle == null ? obtain() : recycle;
+ mt.resetAndAnalyzeBidi(text, start, end, textDir);
+ if (mt.mTextLength == 0) {
+ // Need to build empty native measured text for StaticLayout.
+ // TODO: Stop creating empty measured text for empty lines.
+ long nativeBuilderPtr = nInitBuilder();
+ try {
+ mt.bindNativeObject(nBuildNativeMeasuredText(nativeBuilderPtr, mt.mCopiedBuffer));
+ } finally {
+ nFreeBuilder(nativeBuilderPtr);
+ }
+ return mt;
+ }
+
+ long nativeBuilderPtr = nInitBuilder();
+ try {
+ if (mt.mSpanned == null) {
+ // No style change by MetricsAffectingSpan. Just measure all text.
+ mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, nativeBuilderPtr);
+ mt.mSpanEndCache.append(end);
+ } else {
+ // There may be a MetricsAffectingSpan. Split into span transitions and apply
+ // styles.
+ int spanEnd;
+ for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
+ spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
+ MetricAffectingSpan.class);
+ MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
+ MetricAffectingSpan.class);
+ spans = TextUtils.removeEmptySpans(spans, mt.mSpanned,
+ MetricAffectingSpan.class);
+ mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd,
+ nativeBuilderPtr);
+ mt.mSpanEndCache.append(spanEnd);
+ }
+ }
+ mt.bindNativeObject(nBuildNativeMeasuredText(nativeBuilderPtr, mt.mCopiedBuffer));
+ } finally {
+ nFreeBuilder(nativeBuilderPtr);
}
+
+ return mt;
}
/**
- * Analyzes text for bidirectional runs. Allocates working buffers.
+ * Reset internal state and analyzes text for bidirectional runs.
+ *
+ * @param text the character sequence to be measured
+ * @param start the inclusive start offset of the target region in the text
+ * @param end the exclusive end offset of the target region in the text
+ * @param textDir the text direction
*/
- void setPara(CharSequence text, int start, int end, TextDirectionHeuristic textDir) {
- mText = text;
+ private void resetAndAnalyzeBidi(@NonNull CharSequence text,
+ @IntRange(from = 0) int start, // inclusive
+ @IntRange(from = 0) int end, // exclusive
+ @NonNull TextDirectionHeuristic textDir) {
+ reset();
+ mSpanned = text instanceof Spanned ? (Spanned) text : null;
mTextStart = start;
+ mTextLength = end - start;
- int len = end - start;
- mLen = len;
- mPos = 0;
-
- if (mWidths == null || mWidths.length < len) {
- mWidths = ArrayUtils.newUnpaddedFloatArray(len);
- }
- if (mChars == null || mChars.length != len) {
- mChars = new char[len];
+ if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) {
+ mCopiedBuffer = new char[mTextLength];
}
- TextUtils.getChars(text, start, end, mChars, 0);
+ TextUtils.getChars(text, start, end, mCopiedBuffer, 0);
- if (text instanceof Spanned) {
- Spanned spanned = (Spanned) text;
- ReplacementSpan[] spans = spanned.getSpans(start, end,
- ReplacementSpan.class);
+ // Replace characters associated with ReplacementSpan to U+FFFC.
+ if (mSpanned != null) {
+ ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class);
for (int i = 0; i < spans.length; i++) {
- int startInPara = spanned.getSpanStart(spans[i]) - start;
- int endInPara = spanned.getSpanEnd(spans[i]) - start;
- // The span interval may be larger and must be restricted to [start, end[
+ int startInPara = mSpanned.getSpanStart(spans[i]) - start;
+ int endInPara = mSpanned.getSpanEnd(spans[i]) - start;
+ // The span interval may be larger and must be restricted to [start, end)
if (startInPara < 0) startInPara = 0;
- if (endInPara > len) endInPara = len;
- for (int j = startInPara; j < endInPara; j++) {
- mChars[j] = '\uFFFC'; // object replacement character
- }
+ if (endInPara > mTextLength) endInPara = mTextLength;
+ Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER);
}
}
if ((textDir == TextDirectionHeuristics.LTR ||
textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR ||
textDir == TextDirectionHeuristics.ANYRTL_LTR) &&
- TextUtils.doesNotNeedBidi(mChars, 0, len)) {
- mDir = Layout.DIR_LEFT_TO_RIGHT;
- mEasy = true;
+ TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
+ mLevels.clear();
+ mParaDir = Layout.DIR_LEFT_TO_RIGHT;
+ mLtrWithoutBidi = true;
} else {
- if (mLevels == null || mLevels.length < len) {
- mLevels = ArrayUtils.newUnpaddedByteArray(len);
- }
- int bidiRequest;
+ final int bidiRequest;
if (textDir == TextDirectionHeuristics.LTR) {
bidiRequest = Layout.DIR_REQUEST_LTR;
} else if (textDir == TextDirectionHeuristics.RTL) {
@@ -144,122 +455,147 @@ class MeasuredText {
} else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL;
} else {
- boolean isRtl = textDir.isRtl(mChars, 0, len);
+ final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR;
}
- mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels);
- mEasy = false;
+ mLevels.resize(mTextLength);
+ mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray());
+ mLtrWithoutBidi = false;
}
}
- /**
- * Apply the style.
- *
- * If nativeStaticLayoutPtr is 0, this method measures the styled text width.
- * If nativeStaticLayoutPtr is not 0, this method just passes the style information to native
- * code by calling StaticLayout.addstyleRun() and returns 0.
- */
- float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm,
- long nativeStaticLayoutPtr) {
- if (fm != null) {
- paint.getFontMetricsInt(fm);
+ private void applyReplacementRun(@NonNull ReplacementSpan replacement,
+ @IntRange(from = 0) int start, // inclusive, in copied buffer
+ @IntRange(from = 0) int end, // exclusive, in copied buffer
+ /* Maybe Zero */ long nativeBuilderPtr) {
+ // Use original text. Shouldn't matter.
+ // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for
+ // backward compatibility? or Should we initialize them for getFontMetricsInt?
+ final float width = replacement.getSize(
+ mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
+ if (nativeBuilderPtr == 0) {
+ // Assigns all width to the first character. This is the same behavior as minikin.
+ mWidths.set(start, width);
+ if (end > start + 1) {
+ Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f);
+ }
+ mWholeWidth += width;
+ } else {
+ nAddReplacementRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
+ width);
}
+ }
- final int p = mPos;
- mPos = p + len;
+ private void applyStyleRun(@IntRange(from = 0) int start, // inclusive, in copied buffer
+ @IntRange(from = 0) int end, // exclusive, in copied buffer
+ /* Maybe Zero */ long nativeBuilderPtr) {
+ if (nativeBuilderPtr != 0) {
+ mCachedPaint.getFontMetricsInt(mCachedFm);
+ }
- if (mEasy) {
- final boolean isRtl = mDir != Layout.DIR_LEFT_TO_RIGHT;
- if (nativeStaticLayoutPtr == 0) {
- return paint.getTextRunAdvances(mChars, p, len, p, len, isRtl, mWidths, p);
+ if (mLtrWithoutBidi) {
+ // If the whole text is LTR direction, just apply whole region.
+ if (nativeBuilderPtr == 0) {
+ mWholeWidth += mCachedPaint.getTextRunAdvances(
+ mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */,
+ mWidths.getRawArray(), start);
} else {
- StaticLayout.addStyleRun(nativeStaticLayoutPtr, paint, p, p + len, isRtl);
- return 0.0f; // Builder.addStyleRun doesn't return the width.
+ nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
+ false /* isRtl */);
}
- }
-
- float totalAdvance = 0;
- int level = mLevels[p];
- for (int q = p, i = p + 1, e = p + len;; ++i) {
- if (i == e || mLevels[i] != level) {
- final boolean isRtl = (level & 0x1) != 0;
- if (nativeStaticLayoutPtr == 0) {
- totalAdvance +=
- paint.getTextRunAdvances(mChars, q, i - q, q, i - q, isRtl, mWidths, q);
- } else {
- // Builder.addStyleRun doesn't return the width.
- StaticLayout.addStyleRun(nativeStaticLayoutPtr, paint, q, i, isRtl);
- }
- if (i == e) {
- break;
+ } else {
+ // If there is multiple bidi levels, split into individual bidi level and apply style.
+ byte level = mLevels.get(start);
+ // Note that the empty text or empty range won't reach this method.
+ // Safe to search from start + 1.
+ for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) {
+ if (levelEnd == end || mLevels.get(levelEnd) != level) { // transition point
+ final boolean isRtl = (level & 0x1) != 0;
+ if (nativeBuilderPtr == 0) {
+ final int levelLength = levelEnd - levelStart;
+ mWholeWidth += mCachedPaint.getTextRunAdvances(
+ mCopiedBuffer, levelStart, levelLength, levelStart, levelLength,
+ isRtl, mWidths.getRawArray(), levelStart);
+ } else {
+ nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), levelStart,
+ levelEnd, isRtl);
+ }
+ if (levelEnd == end) {
+ break;
+ }
+ levelStart = levelEnd;
+ level = mLevels.get(levelEnd);
}
- q = i;
- level = mLevels[i];
}
}
- return totalAdvance; // If nativeStaticLayoutPtr is 0, the result is zero.
}
- float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) {
- return addStyleRun(paint, len, fm, 0 /* native ptr */);
- }
+ private void applyMetricsAffectingSpan(
+ @NonNull TextPaint paint,
+ @Nullable MetricAffectingSpan[] spans,
+ @IntRange(from = 0) int start, // inclusive, in original text buffer
+ @IntRange(from = 0) int end, // exclusive, in original text buffer
+ /* Maybe Zero */ long nativeBuilderPtr) {
+ mCachedPaint.set(paint);
+ // XXX paint should not have a baseline shift, but...
+ mCachedPaint.baselineShift = 0;
- float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len,
- Paint.FontMetricsInt fm, long nativeStaticLayoutPtr) {
+ final boolean needFontMetrics = nativeBuilderPtr != 0;
- TextPaint workPaint = mWorkPaint;
- workPaint.set(paint);
- // XXX paint should not have a baseline shift, but...
- workPaint.baselineShift = 0;
+ if (needFontMetrics && mCachedFm == null) {
+ mCachedFm = new Paint.FontMetricsInt();
+ }
ReplacementSpan replacement = null;
- for (int i = 0; i < spans.length; i++) {
- MetricAffectingSpan span = spans[i];
- if (span instanceof ReplacementSpan) {
- replacement = (ReplacementSpan)span;
- } else {
- span.updateMeasureState(workPaint);
+ if (spans != null) {
+ for (int i = 0; i < spans.length; i++) {
+ MetricAffectingSpan span = spans[i];
+ if (span instanceof ReplacementSpan) {
+ // The last ReplacementSpan is effective for backward compatibility reasons.
+ replacement = (ReplacementSpan) span;
+ } else {
+ // TODO: No need to call updateMeasureState for ReplacementSpan as well?
+ span.updateMeasureState(mCachedPaint);
+ }
}
}
- float wid;
- if (replacement == null) {
- wid = addStyleRun(workPaint, len, fm, nativeStaticLayoutPtr);
+ final int startInCopiedBuffer = start - mTextStart;
+ final int endInCopiedBuffer = end - mTextStart;
+
+ if (replacement != null) {
+ applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer,
+ nativeBuilderPtr);
} else {
- // Use original text. Shouldn't matter.
- wid = replacement.getSize(workPaint, mText, mTextStart + mPos,
- mTextStart + mPos + len, fm);
- if (nativeStaticLayoutPtr == 0) {
- float[] w = mWidths;
- w[mPos] = wid;
- for (int i = mPos + 1, e = mPos + len; i < e; i++)
- w[i] = 0;
- } else {
- StaticLayout.addReplacementRun(nativeStaticLayoutPtr, paint, mPos, mPos + len, wid);
- }
- mPos += len;
+ applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeBuilderPtr);
}
- if (fm != null) {
- if (workPaint.baselineShift < 0) {
- fm.ascent += workPaint.baselineShift;
- fm.top += workPaint.baselineShift;
+ if (needFontMetrics) {
+ if (mCachedPaint.baselineShift < 0) {
+ mCachedFm.ascent += mCachedPaint.baselineShift;
+ mCachedFm.top += mCachedPaint.baselineShift;
} else {
- fm.descent += workPaint.baselineShift;
- fm.bottom += workPaint.baselineShift;
+ mCachedFm.descent += mCachedPaint.baselineShift;
+ mCachedFm.bottom += mCachedPaint.baselineShift;
}
- }
-
- return wid;
- }
- float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len,
- Paint.FontMetricsInt fm) {
- return addStyleRun(paint, spans, len, fm, 0 /* native ptr */);
+ mFontMetrics.append(mCachedFm.top);
+ mFontMetrics.append(mCachedFm.bottom);
+ mFontMetrics.append(mCachedFm.ascent);
+ mFontMetrics.append(mCachedFm.descent);
+ }
}
- int breakText(int limit, boolean forwards, float width) {
- float[] w = mWidths;
+ /**
+ * Returns the maximum index that the accumulated width not exceeds the width.
+ *
+ * If forward=false is passed, returns the minimum index from the end instead.
+ *
+ * This only works if the MeasuredText is computed with computeForMeasurement.
+ * Undefined behavior in other case.
+ */
+ @IntRange(from = 0) int breakText(int limit, boolean forwards, float width) {
+ float[] w = mWidths.getRawArray();
if (forwards) {
int i = 0;
while (i < limit) {
@@ -267,7 +603,7 @@ class MeasuredText {
if (width < 0.0f) break;
i++;
}
- while (i > 0 && mChars[i - 1] == ' ') i--;
+ while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--;
return i;
} else {
int i = limit - 1;
@@ -276,19 +612,65 @@ class MeasuredText {
if (width < 0.0f) break;
i--;
}
- while (i < limit - 1 && (mChars[i + 1] == ' ' || w[i + 1] == 0.0f)) {
+ while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) {
i++;
}
return limit - i - 1;
}
}
- float measure(int start, int limit) {
+ /**
+ * Returns the length of the substring.
+ *
+ * This only works if the MeasuredText is computed with computeForMeasurement.
+ * Undefined behavior in other case.
+ */
+ @FloatRange(from = 0.0f) float measure(int start, int limit) {
float width = 0;
- float[] w = mWidths;
+ float[] w = mWidths.getRawArray();
for (int i = start; i < limit; ++i) {
width += w[i];
}
return width;
}
+
+ private static native /* Non Zero */ long nInitBuilder();
+
+ /**
+ * Apply style to make native measured text.
+ *
+ * @param nativeBuilderPtr The native MeasuredText builder pointer.
+ * @param paintPtr The native paint pointer to be applied.
+ * @param start The start offset in the copied buffer.
+ * @param end The end offset in the copied buffer.
+ * @param isRtl True if the text is RTL.
+ */
+ private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr,
+ /* Non Zero */ long paintPtr,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ boolean isRtl);
+
+ /**
+ * Apply ReplacementRun to make native measured text.
+ *
+ * @param nativeBuilderPtr The native MeasuredText builder pointer.
+ * @param paintPtr The native paint pointer to be applied.
+ * @param start The start offset in the copied buffer.
+ * @param end The end offset in the copied buffer.
+ * @param width The width of the replacement.
+ */
+ private static native void nAddReplacementRun(/* Non Zero */ long nativeBuilderPtr,
+ /* Non Zero */ long paintPtr,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @FloatRange(from = 0) float width);
+
+ private static native long nBuildNativeMeasuredText(/* Non Zero */ long nativeBuilderPtr,
+ @NonNull char[] text);
+
+ private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
+
+ @CriticalNative
+ private static native /* Non Zero */ long nGetReleaseFunc();
}
diff --git a/android/text/MeasuredText_Delegate.java b/android/text/MeasuredText_Delegate.java
new file mode 100644
index 00000000..adcc774c
--- /dev/null
+++ b/android/text/MeasuredText_Delegate.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.annotation.NonNull;
+import android.graphics.BidiRenderer;
+import android.graphics.Paint;
+import android.graphics.Paint_Delegate;
+import android.graphics.RectF;
+import android.text.StaticLayout_Delegate.Builder;
+import android.text.StaticLayout_Delegate.Run;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import libcore.util.NativeAllocationRegistry_Delegate;
+
+/**
+ * Delegate that provides implementation for native methods in {@link android.text.MeasuredText}
+ * <p/>
+ * Through the layoutlib_create tool, selected methods of StaticLayout have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ */
+public class MeasuredText_Delegate {
+
+ // ---- Builder delegate manager ----
+ private static final DelegateManager<MeasuredTextBuilder> sBuilderManager =
+ new DelegateManager<>(MeasuredTextBuilder.class);
+ private static final DelegateManager<MeasuredText_Delegate> sManager =
+ new DelegateManager<>(MeasuredText_Delegate.class);
+ private static long sFinalizer = -1;
+
+ private long mNativeBuilderPtr;
+
+ @LayoutlibDelegate
+ /*package*/ static long nInitBuilder() {
+ return sBuilderManager.addNewDelegate(new MeasuredTextBuilder());
+ }
+
+ /**
+ * Apply style to make native measured text.
+ *
+ * @param nativeBuilderPtr The native MeasuredText builder pointer.
+ * @param paintPtr The native paint pointer to be applied.
+ * @param start The start offset in the copied buffer.
+ * @param end The end offset in the copied buffer.
+ * @param isRtl True if the text is RTL.
+ */
+ @LayoutlibDelegate
+ /*package*/ static void nAddStyleRun(long nativeBuilderPtr, long paintPtr, int start,
+ int end, boolean isRtl) {
+ MeasuredTextBuilder builder = sBuilderManager.getDelegate(nativeBuilderPtr);
+ if (builder == null) {
+ return;
+ }
+ builder.mRuns.add(new StyleRun(paintPtr, start, end, isRtl));
+ }
+
+ /**
+ * Apply ReplacementRun to make native measured text.
+ *
+ * @param nativeBuilderPtr The native MeasuredText builder pointer.
+ * @param paintPtr The native paint pointer to be applied.
+ * @param start The start offset in the copied buffer.
+ * @param end The end offset in the copied buffer.
+ * @param width The width of the replacement.
+ */
+ @LayoutlibDelegate
+ /*package*/ static void nAddReplacementRun(long nativeBuilderPtr, long paintPtr, int start,
+ int end, float width) {
+ MeasuredTextBuilder builder = sBuilderManager.getDelegate(nativeBuilderPtr);
+ if (builder == null) {
+ return;
+ }
+ builder.mRuns.add(new ReplacementRun(start, end, width));
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nBuildNativeMeasuredText(long nativeBuilderPtr, @NonNull char[] text) {
+ MeasuredText_Delegate delegate = new MeasuredText_Delegate();
+ delegate.mNativeBuilderPtr = nativeBuilderPtr;
+ return sManager.addNewDelegate(delegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nFreeBuilder(long nativeBuilderPtr) {
+ sBuilderManager.removeJavaReferenceFor(nativeBuilderPtr);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nGetReleaseFunc() {
+ synchronized (MeasuredText_Delegate.class) {
+ if (sFinalizer == -1) {
+ sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(
+ sManager::removeJavaReferenceFor);
+ }
+ }
+ return sFinalizer;
+ }
+
+ private static float measureText(long nativePaint, char[] text, int index, int count,
+ float[] widths, int bidiFlags) {
+ Paint_Delegate paint = Paint_Delegate.getDelegate(nativePaint);
+ RectF bounds =
+ new BidiRenderer(null, paint, text).renderText(index, index + count, bidiFlags,
+ widths, 0, false);
+ return bounds.right - bounds.left;
+ }
+
+ public static void computeRuns(long measuredTextPtr, Builder staticLayoutBuilder) {
+ MeasuredText_Delegate delegate = sManager.getDelegate(measuredTextPtr);
+ if (delegate == null) {
+ return;
+ }
+ MeasuredTextBuilder builder = sBuilderManager.getDelegate(delegate.mNativeBuilderPtr);
+ if (builder == null) {
+ return;
+ }
+ for (Run run: builder.mRuns) {
+ run.addTo(staticLayoutBuilder);
+ }
+ }
+
+ private static class StyleRun extends Run {
+ private final long mNativePaint;
+ private final boolean mIsRtl;
+
+ private StyleRun(long nativePaint, int start, int end, boolean isRtl) {
+ super(start, end);
+ mNativePaint = nativePaint;
+ mIsRtl = isRtl;
+ }
+
+ @Override
+ void addTo(Builder builder) {
+ int bidiFlags = mIsRtl ? Paint.BIDI_FORCE_RTL : Paint.BIDI_FORCE_LTR;
+ measureText(mNativePaint, builder.mText, mStart, mEnd - mStart, builder.mWidths,
+ bidiFlags);
+ }
+ }
+
+ private static class ReplacementRun extends Run {
+ private final float mWidth;
+
+ private ReplacementRun(int start, int end, float width) {
+ super(start, end);
+ mWidth = width;
+ }
+
+ @Override
+ void addTo(Builder builder) {
+ builder.mWidths[mStart] = mWidth;
+ Arrays.fill(builder.mWidths, mStart + 1, mEnd, 0.0f);
+ }
+ }
+
+ private static class MeasuredTextBuilder {
+ private final ArrayList<Run> mRuns = new ArrayList<>();
+ }
+}
diff --git a/android/text/PremeasuredText.java b/android/text/PremeasuredText.java
new file mode 100644
index 00000000..465314dd
--- /dev/null
+++ b/android/text/PremeasuredText.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.util.IntArray;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+
+/**
+ * A text which has already been measured.
+ *
+ * TODO: Rename to better name? e.g. MeasuredText, FrozenText etc.
+ */
+public class PremeasuredText implements Spanned {
+ private static final char LINE_FEED = '\n';
+
+ // The original text.
+ private final @NonNull CharSequence mText;
+
+ // The inclusive start offset of the measuring target.
+ private final @IntRange(from = 0) int mStart;
+
+ // The exclusive end offset of the measuring target.
+ private final @IntRange(from = 0) int mEnd;
+
+ // The TextPaint used for measurement.
+ private final @NonNull TextPaint mPaint;
+
+ // The requested text direction.
+ private final @NonNull TextDirectionHeuristic mTextDir;
+
+ // The measured paragraph texts.
+ private final @NonNull MeasuredText[] mMeasuredTexts;
+
+ // The sorted paragraph end offsets.
+ private final @NonNull int[] mParagraphBreakPoints;
+
+ /**
+ * Build PremeasuredText from the text.
+ *
+ * @param text The text to be measured.
+ * @param paint The paint to be used for drawing.
+ * @param textDir The text direction.
+ * @return The measured text.
+ */
+ public static @NonNull PremeasuredText build(@NonNull CharSequence text,
+ @NonNull TextPaint paint,
+ @NonNull TextDirectionHeuristic textDir) {
+ return PremeasuredText.build(text, paint, textDir, 0, text.length());
+ }
+
+ /**
+ * Build PremeasuredText from the specific range of the text..
+ *
+ * @param text The text to be measured.
+ * @param paint The paint to be used for drawing.
+ * @param textDir The text direction.
+ * @param start The inclusive start offset of the text.
+ * @param end The exclusive start offset of the text.
+ * @return The measured text.
+ */
+ public static @NonNull PremeasuredText build(@NonNull CharSequence text,
+ @NonNull TextPaint paint,
+ @NonNull TextDirectionHeuristic textDir,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end) {
+ Preconditions.checkNotNull(text);
+ Preconditions.checkNotNull(paint);
+ Preconditions.checkNotNull(textDir);
+ Preconditions.checkArgumentInRange(start, 0, text.length(), "start");
+ Preconditions.checkArgumentInRange(end, 0, text.length(), "end");
+
+ final IntArray paragraphEnds = new IntArray();
+ final ArrayList<MeasuredText> measuredTexts = new ArrayList<>();
+
+ int paraEnd = 0;
+ for (int paraStart = start; paraStart < end; paraStart = paraEnd) {
+ paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end);
+ if (paraEnd < 0) {
+ // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph end.
+ paraEnd = end;
+ } else {
+ paraEnd++; // Includes LINE_FEED(U+000A) to the prev paragraph.
+ }
+
+ paragraphEnds.add(paraEnd);
+ measuredTexts.add(MeasuredText.buildForStaticLayout(
+ paint, text, paraStart, paraEnd, textDir, null /* no recycle */));
+ }
+
+ return new PremeasuredText(text, start, end, paint, textDir,
+ measuredTexts.toArray(new MeasuredText[measuredTexts.size()]),
+ paragraphEnds.toArray());
+ }
+
+ // Use PremeasuredText.build instead.
+ private PremeasuredText(@NonNull CharSequence text,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @NonNull TextPaint paint,
+ @NonNull TextDirectionHeuristic textDir,
+ @NonNull MeasuredText[] measuredTexts,
+ @NonNull int[] paragraphBreakPoints) {
+ mText = text;
+ mStart = start;
+ mEnd = end;
+ mPaint = paint;
+ mMeasuredTexts = measuredTexts;
+ mParagraphBreakPoints = paragraphBreakPoints;
+ mTextDir = textDir;
+ }
+
+ /**
+ * Return the underlying text.
+ */
+ public @NonNull CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Returns the inclusive start offset of measured region.
+ */
+ public @IntRange(from = 0) int getStart() {
+ return mStart;
+ }
+
+ /**
+ * Returns the exclusive end offset of measured region.
+ */
+ public @IntRange(from = 0) int getEnd() {
+ return mEnd;
+ }
+
+ /**
+ * Returns the text direction associated with char sequence.
+ */
+ public @NonNull TextDirectionHeuristic getTextDir() {
+ return mTextDir;
+ }
+
+ /**
+ * Returns the paint used to measure this text.
+ */
+ public @NonNull TextPaint getPaint() {
+ return mPaint;
+ }
+
+ /**
+ * Returns the length of the paragraph of this text.
+ */
+ public @IntRange(from = 0) int getParagraphCount() {
+ return mParagraphBreakPoints.length;
+ }
+
+ /**
+ * Returns the paragraph start offset of the text.
+ */
+ public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) {
+ Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
+ return paraIndex == 0 ? mStart : mParagraphBreakPoints[paraIndex - 1];
+ }
+
+ /**
+ * Returns the paragraph end offset of the text.
+ */
+ public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) {
+ Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
+ return mParagraphBreakPoints[paraIndex];
+ }
+
+ /** @hide */
+ public @NonNull MeasuredText getMeasuredText(@IntRange(from = 0) int paraIndex) {
+ return mMeasuredTexts[paraIndex];
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Spanned overrides
+ //
+ // Just proxy for underlying mText if appropriate.
+
+ @Override
+ public <T> T[] getSpans(int start, int end, Class<T> type) {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).getSpans(start, end, type);
+ } else {
+ return ArrayUtils.emptyArray(type);
+ }
+ }
+
+ @Override
+ public int getSpanStart(Object tag) {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).getSpanStart(tag);
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public int getSpanEnd(Object tag) {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).getSpanEnd(tag);
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public int getSpanFlags(Object tag) {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).getSpanFlags(tag);
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public int nextSpanTransition(int start, int limit, Class type) {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).nextSpanTransition(start, limit, type);
+ } else {
+ return mText.length();
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // CharSequence overrides.
+ //
+ // Just proxy for underlying mText.
+
+ @Override
+ public int length() {
+ return mText.length();
+ }
+
+ @Override
+ public char charAt(int index) {
+ // TODO: Should this be index + mStart ?
+ return mText.charAt(index);
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ // TODO: return PremeasuredText.
+ // TODO: Should this be index + mStart, end + mStart ?
+ return mText.subSequence(start, end);
+ }
+
+ @Override
+ public String toString() {
+ return mText.toString();
+ }
+}
diff --git a/android/text/StaticLayout.java b/android/text/StaticLayout.java
index c0fc44fd..d69b1190 100644
--- a/android/text/StaticLayout.java
+++ b/android/text/StaticLayout.java
@@ -21,10 +21,10 @@ import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Paint;
+import android.text.AutoGrowArray.FloatArray;
import android.text.style.LeadingMarginSpan;
import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
import android.text.style.LineHeightSpan;
-import android.text.style.MetricAffectingSpan;
import android.text.style.TabStopSpan;
import android.util.Log;
import android.util.Pools.SynchronizedPool;
@@ -48,6 +48,18 @@ import java.util.Arrays;
* Canvas.drawText()} directly.</p>
*/
public class StaticLayout extends Layout {
+ /*
+ * The break iteration is done in native code. The protocol for using the native code is as
+ * follows.
+ *
+ * First, call nInit to setup native line breaker object. Then, for each paragraph, do the
+ * following:
+ *
+ * - Create MeasuredText by MeasuredText.buildForStaticLayout which measures in native.
+ * - Run nComputeLineBreaks() to obtain line breaks for the paragraph.
+ *
+ * After all paragraphs, call finish() to release expensive buffers.
+ */
static final String TAG = "StaticLayout";
@@ -99,8 +111,6 @@ public class StaticLayout extends Layout {
b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
-
- b.mMeasuredText = MeasuredText.obtain();
return b;
}
@@ -111,8 +121,6 @@ public class StaticLayout extends Layout {
private static void recycle(@NonNull Builder b) {
b.mPaint = null;
b.mText = null;
- MeasuredText.recycle(b.mMeasuredText);
- b.mMeasuredText = null;
b.mLeftIndents = null;
b.mRightIndents = null;
b.mLeftPaddings = null;
@@ -128,7 +136,6 @@ public class StaticLayout extends Layout {
mRightIndents = null;
mLeftPaddings = null;
mRightPaddings = null;
- mMeasuredText.finish();
}
public Builder setText(CharSequence source) {
@@ -444,12 +451,13 @@ public class StaticLayout extends Layout {
private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
- // This will go away and be subsumed by native builder code
- private MeasuredText mMeasuredText;
-
private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3);
}
+ /**
+ * @deprecated Use {@link Builder} instead.
+ */
+ @Deprecated
public StaticLayout(CharSequence source, TextPaint paint,
int width,
Alignment align, float spacingmult, float spacingadd,
@@ -459,16 +467,9 @@ public class StaticLayout extends Layout {
}
/**
- * @hide
+ * @deprecated Use {@link Builder} instead.
*/
- public StaticLayout(CharSequence source, TextPaint paint,
- int width, Alignment align, TextDirectionHeuristic textDir,
- float spacingmult, float spacingadd,
- boolean includepad) {
- this(source, 0, source.length(), paint, width, align, textDir,
- spacingmult, spacingadd, includepad);
- }
-
+ @Deprecated
public StaticLayout(CharSequence source, int bufstart, int bufend,
TextPaint paint, int outerwidth,
Alignment align,
@@ -479,17 +480,9 @@ public class StaticLayout extends Layout {
}
/**
- * @hide
+ * @deprecated Use {@link Builder} instead.
*/
- public StaticLayout(CharSequence source, int bufstart, int bufend,
- TextPaint paint, int outerwidth,
- Alignment align, TextDirectionHeuristic textDir,
- float spacingmult, float spacingadd,
- boolean includepad) {
- this(source, bufstart, bufend, paint, outerwidth, align, textDir,
- spacingmult, spacingadd, includepad, null, 0, Integer.MAX_VALUE);
-}
-
+ @Deprecated
public StaticLayout(CharSequence source, int bufstart, int bufend,
TextPaint paint, int outerwidth,
Alignment align,
@@ -503,7 +496,9 @@ public class StaticLayout extends Layout {
/**
* @hide
+ * @deprecated Use {@link Builder} instead.
*/
+ @Deprecated
public StaticLayout(CharSequence source, int bufstart, int bufend,
TextPaint paint, int outerwidth,
Alignment align, TextDirectionHeuristic textDir,
@@ -561,6 +556,9 @@ public class StaticLayout extends Layout {
Builder.recycle(b);
}
+ /**
+ * Used by DynamicLayout.
+ */
/* package */ StaticLayout(@Nullable CharSequence text) {
super(text, null, 0, null, 0, 0);
@@ -606,8 +604,8 @@ public class StaticLayout extends Layout {
/* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
final CharSequence source = b.mText;
- int bufStart = b.mStart;
- int bufEnd = b.mEnd;
+ final int bufStart = b.mStart;
+ final int bufEnd = b.mEnd;
TextPaint paint = b.mPaint;
int outerWidth = b.mWidth;
TextDirectionHeuristic textDir = b.mTextDir;
@@ -618,11 +616,7 @@ public class StaticLayout extends Layout {
TextUtils.TruncateAt ellipsize = b.mEllipsize;
final boolean addLastLineSpacing = b.mAddLastLineLineSpacing;
LineBreaks lineBreaks = new LineBreaks(); // TODO: move to builder to avoid allocation costs
- // store span end locations
- int[] spanEndCache = new int[4];
- // store fontMetrics per span range
- // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range)
- int[] fmCache = new int[4 * 4];
+ FloatArray widths = new FloatArray();
mLineCount = 0;
mEllipsized = false;
@@ -634,12 +628,6 @@ public class StaticLayout extends Layout {
Paint.FontMetricsInt fm = b.mFontMetricsInt;
int[] chooseHtv = null;
- MeasuredText measured = b.mMeasuredText;
-
- Spanned spanned = null;
- if (source instanceof Spanned)
- spanned = (Spanned) source;
-
final int[] indents;
if (mLeftIndents != null || mRightIndents != null) {
final int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length;
@@ -662,15 +650,34 @@ public class StaticLayout extends Layout {
b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE,
indents, mLeftPaddings, mRightPaddings);
+ PremeasuredText premeasured = null;
+ final Spanned spanned;
+ if (source instanceof PremeasuredText) {
+ premeasured = (PremeasuredText) source;
+
+ final CharSequence original = premeasured.getText();
+ spanned = (original instanceof Spanned) ? (Spanned) original : null;
+
+ if (bufStart != premeasured.getStart() || bufEnd != premeasured.getEnd()) {
+ // The buffer position has changed. Re-measure here.
+ premeasured = PremeasuredText.build(original, paint, textDir, bufStart, bufEnd);
+ } else {
+ // We can use premeasured information.
+
+ // Overwrite with the one when premeasured.
+ // TODO: Give an option for developer not to overwrite and measure again here?
+ textDir = premeasured.getTextDir();
+ paint = premeasured.getPaint();
+ }
+ } else {
+ premeasured = PremeasuredText.build(source, paint, textDir, bufStart, bufEnd);
+ spanned = (source instanceof Spanned) ? (Spanned) source : null;
+ }
+
try {
- int paraEnd;
- for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
- paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
- if (paraEnd < 0) {
- paraEnd = bufEnd;
- } else {
- paraEnd++;
- }
+ for (int paraIndex = 0; paraIndex < premeasured.getParagraphCount(); paraIndex++) {
+ final int paraStart = premeasured.getParagraphStart(paraIndex);
+ final int paraEnd = premeasured.getParagraphEnd(paraIndex);
int firstWidthLineCount = 1;
int firstWidth = outerWidth;
@@ -721,13 +728,6 @@ public class StaticLayout extends Layout {
}
}
- measured.setPara(source, paraStart, paraEnd, textDir);
- char[] chs = measured.mChars;
- float[] widths = measured.mWidths;
- byte[] chdirs = measured.mLevels;
- int dir = measured.mDir;
- boolean easy = measured.mEasy;
-
// tab stop locations
int[] variableTabStops = null;
if (spanned != null) {
@@ -743,56 +743,23 @@ public class StaticLayout extends Layout {
}
}
+ final MeasuredText measured = premeasured.getMeasuredText(paraIndex);
+ final char[] chs = measured.getChars();
+ final int[] spanEndCache = measured.getSpanEndCache().getRawArray();
+ final int[] fmCache = measured.getFontMetrics().getRawArray();
+ // TODO: Stop keeping duplicated width copy in native and Java.
+ widths.resize(chs.length);
+
// measurement has to be done before performing line breaking
// but we don't want to recompute fontmetrics or span ranges the
// second time, so we cache those and then use those stored values
- int fmCacheCount = 0;
- int spanEndCacheCount = 0;
- for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
- if (fmCacheCount * 4 >= fmCache.length) {
- int[] grow = new int[fmCacheCount * 4 * 2];
- System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4);
- fmCache = grow;
- }
-
- if (spanEndCacheCount >= spanEndCache.length) {
- int[] grow = new int[spanEndCacheCount * 2];
- System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount);
- spanEndCache = grow;
- }
-
- if (spanned == null) {
- spanEnd = paraEnd;
- int spanLen = spanEnd - spanStart;
- measured.addStyleRun(paint, spanLen, fm, nativePtr);
- } else {
- spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
- MetricAffectingSpan.class);
- int spanLen = spanEnd - spanStart;
- MetricAffectingSpan[] spans =
- spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
- spans = TextUtils.removeEmptySpans(spans, spanned,
- MetricAffectingSpan.class);
- measured.addStyleRun(paint, spans, spanLen, fm, nativePtr);
- }
-
- // the order of storage here (top, bottom, ascent, descent) has to match the
- // code below where these values are retrieved
- fmCache[fmCacheCount * 4 + 0] = fm.top;
- fmCache[fmCacheCount * 4 + 1] = fm.bottom;
- fmCache[fmCacheCount * 4 + 2] = fm.ascent;
- fmCache[fmCacheCount * 4 + 3] = fm.descent;
- fmCacheCount++;
-
- spanEndCache[spanEndCacheCount] = spanEnd;
- spanEndCacheCount++;
- }
int breakCount = nComputeLineBreaks(
nativePtr,
// Inputs
chs,
+ measured.getNativePtr(),
paraEnd - paraStart,
firstWidth,
firstWidthLineCount,
@@ -809,7 +776,7 @@ public class StaticLayout extends Layout {
lineBreaks.ascents,
lineBreaks.descents,
lineBreaks.flags,
- widths);
+ widths.getRawArray());
final int[] breaks = lineBreaks.breaks;
final float[] lineWidths = lineBreaks.widths;
@@ -832,7 +799,7 @@ public class StaticLayout extends Layout {
width += lineWidths[i];
} else {
for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) {
- width += widths[j];
+ width += widths.get(j);
}
}
flag |= flags[i] & TAB_MASK;
@@ -896,10 +863,10 @@ public class StaticLayout extends Layout {
v = out(source, here, endPos,
ascent, descent, fmTop, fmBottom,
v, spacingmult, spacingadd, chooseHt, chooseHtv, fm,
- flags[breakIndex], needMultiply, chdirs, dir, easy, bufEnd,
- includepad, trackpad, addLastLineSpacing, chs, widths, paraStart,
- ellipsize, ellipsizedWidth, lineWidths[breakIndex], paint,
- moreChars);
+ flags[breakIndex], needMultiply, measured, bufEnd,
+ includepad, trackpad, addLastLineSpacing, chs, widths.getRawArray(),
+ paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex],
+ paint, moreChars);
if (endPos < spanEnd) {
// preserve metrics for current span
@@ -927,17 +894,16 @@ public class StaticLayout extends Layout {
if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
&& mLineCount < mMaximumVisibleLineCount) {
- measured.setPara(source, bufEnd, bufEnd, textDir);
-
+ final MeasuredText measured =
+ MeasuredText.buildForBidi(source, bufEnd, bufEnd, textDir, null);
paint.getFontMetricsInt(fm);
-
v = out(source,
bufEnd, bufEnd, fm.ascent, fm.descent,
fm.top, fm.bottom,
v,
spacingmult, spacingadd, null,
null, fm, 0,
- needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
+ needMultiply, measured, bufEnd,
includepad, trackpad, addLastLineSpacing, null,
null, bufStart, ellipsize,
ellipsizedWidth, 0, paint, false);
@@ -952,8 +918,8 @@ public class StaticLayout extends Layout {
private int out(final CharSequence text, final int start, final int end, int above, int below,
int top, int bottom, int v, final float spacingmult, final float spacingadd,
final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
- final int flags, final boolean needMultiply, final byte[] chdirs, final int dir,
- final boolean easy, final int bufEnd, final boolean includePad, final boolean trackPad,
+ final int flags, final boolean needMultiply, @NonNull final MeasuredText measured,
+ final int bufEnd, final boolean includePad, final boolean trackPad,
final boolean addLastLineLineSpacing, final char[] chs, final float[] widths,
final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,
final float textWidth, final TextPaint paint, final boolean moreChars) {
@@ -961,6 +927,7 @@ public class StaticLayout extends Layout {
final int off = j * mColumns;
final int want = off + mColumns + TOP;
int[] lines = mLines;
+ final int dir = measured.getParagraphDir();
if (want >= lines.length) {
final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want));
@@ -986,17 +953,8 @@ public class StaticLayout extends Layout {
// one bit for start field
lines[off + TAB] |= flags & TAB_MASK;
lines[off + HYPHEN] = flags;
-
lines[off + DIR] |= dir << DIR_SHIFT;
- // easy means all chars < the first RTL, so no emoji, no nothing
- // XXX a run with no text or all spaces is easy but might be an empty
- // RTL paragraph. Make sure easy is false if this is the case.
- if (easy) {
- mLineDirections[j] = DIRS_ALL_LEFT_TO_RIGHT;
- } else {
- mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
- start - widthStart, end - start);
- }
+ mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
final boolean firstLine = (j == 0);
final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
@@ -1473,33 +1431,6 @@ public class StaticLayout extends Layout {
mMaxLineHeight : super.getHeight();
}
- /**
- * Measurement and break iteration is done in native code. The protocol for using
- * the native code is as follows.
- *
- * First, call nInit to setup native line breaker object. Then, for each paragraph, do the
- * following:
- *
- * - Call one of the following methods for each run within the paragraph depending on the type
- * of run:
- * + addStyleRun (a text run, to be measured in native code)
- * + addReplacementRun (a replacement run, width is given)
- *
- * - Run nComputeLineBreaks() to obtain line breaks for the paragraph.
- *
- * After all paragraphs, call finish() to release expensive buffers.
- */
-
- /* package */ static void addStyleRun(long nativePtr, TextPaint paint, int start, int end,
- boolean isRtl) {
- nAddStyleRun(nativePtr, paint.getNativeInstance(), start, end, isRtl);
- }
-
- /* package */ static void addReplacementRun(long nativePtr, TextPaint paint, int start, int end,
- float width) {
- nAddReplacementRun(nativePtr, paint.getNativeInstance(), start, end, width);
- }
-
@FastNative
private static native long nInit(
@BreakStrategy int breakStrategy,
@@ -1512,17 +1443,6 @@ public class StaticLayout extends Layout {
@CriticalNative
private static native void nFinish(long nativePtr);
- @CriticalNative
- private static native void nAddStyleRun(
- /* non-zero */ long nativePtr, /* non-zero */ long nativePaint,
- @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean isRtl);
-
- @CriticalNative
- private static native void nAddReplacementRun(
- /* non-zero */ long nativePtr, /* non-zero */ long nativePaint,
- @IntRange(from = 0) int start, @IntRange(from = 0) int end,
- @FloatRange(from = 0.0f) float width);
-
// populates LineBreaks and returns the number of breaks found
//
// the arrays inside the LineBreaks objects are passed in as well
@@ -1535,6 +1455,7 @@ public class StaticLayout extends Layout {
// Inputs
@NonNull char[] text,
+ /* Non Zero */ long measuredTextPtr,
@IntRange(from = 0) int length,
@FloatRange(from = 0.0f) float firstWidth,
@IntRange(from = 0) int firstWidthLineCount,
diff --git a/android/text/StaticLayoutPerfTest.java b/android/text/StaticLayoutPerfTest.java
index 57a61ec8..5653a039 100644
--- a/android/text/StaticLayoutPerfTest.java
+++ b/android/text/StaticLayoutPerfTest.java
@@ -16,12 +16,19 @@
package android.text;
+import static android.text.TextDirectionHeuristics.LTR;
+
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;
import android.support.test.filters.LargeTest;
import android.support.test.runner.AndroidJUnit4;
+import android.content.res.ColorStateList;
+import android.graphics.Typeface;
+import android.text.Layout;
+import android.text.style.TextAppearanceSpan;
+
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -33,40 +40,35 @@ import java.util.Random;
@RunWith(AndroidJUnit4.class)
public class StaticLayoutPerfTest {
- public StaticLayoutPerfTest() {
- }
+ public StaticLayoutPerfTest() {}
@Rule
public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
- private static final String FIXED_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing "
- + "elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad "
- + "minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
- + "commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse "
- + "cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non "
- + "proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
- private static final int FIXED_TEXT_LENGTH = FIXED_TEXT.length();
+ private static final int WORD_LENGTH = 9; // Random word has 9 characters.
+ private static final int WORDS_IN_LINE = 8; // Roughly, 8 words in a line.
+ private static final int PARA_LENGTH = 500; // Number of characters in a paragraph.
- private static TextPaint PAINT = new TextPaint();
- private static final int TEXT_WIDTH = 20 * (int) PAINT.getTextSize();
+ private static final boolean NO_STYLE_TEXT = false;
+ private static final boolean STYLE_TEXT = true;
- @Test
- public void testCreate() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- while (state.keepRunning()) {
- StaticLayout.Builder.obtain(FIXED_TEXT, 0, FIXED_TEXT_LENGTH, PAINT, TEXT_WIDTH)
- .build();
- }
- }
+ private final Random mRandom = new Random(31415926535L);
private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private static final int ALPHABET_LENGTH = ALPHABET.length();
- private static final int PARA_LENGTH = 500;
+ private static final ColorStateList TEXT_COLOR = ColorStateList.valueOf(0x00000000);
+ private static final String[] FAMILIES = { "sans-serif", "serif", "monospace" };
+ private static final int[] STYLES = {
+ Typeface.NORMAL, Typeface.BOLD, Typeface.ITALIC, Typeface.BOLD_ITALIC
+ };
+
private final char[] mBuffer = new char[PARA_LENGTH];
- private final Random mRandom = new Random(31415926535L);
- private CharSequence generateRandomParagraph(int wordLen) {
+ private static TextPaint PAINT = new TextPaint();
+ private static final int TEXT_WIDTH = WORDS_IN_LINE * WORD_LENGTH * (int) PAINT.getTextSize();
+
+ private CharSequence generateRandomParagraph(int wordLen, boolean applyRandomStyle) {
for (int i = 0; i < PARA_LENGTH; i++) {
if (i % (wordLen + 1) == wordLen) {
mBuffer[i] = ' ';
@@ -74,29 +76,192 @@ public class StaticLayoutPerfTest {
mBuffer[i] = ALPHABET.charAt(mRandom.nextInt(ALPHABET_LENGTH));
}
}
- return CharBuffer.wrap(mBuffer);
+
+ CharSequence cs = CharBuffer.wrap(mBuffer);
+ if (!applyRandomStyle) {
+ return cs;
+ }
+
+ SpannableStringBuilder ssb = new SpannableStringBuilder(cs);
+ for (int i = 0; i < ssb.length(); i += WORD_LENGTH) {
+ final int spanStart = i;
+ final int spanEnd = (i + WORD_LENGTH) > ssb.length() ? ssb.length() : i + WORD_LENGTH;
+
+ final TextAppearanceSpan span = new TextAppearanceSpan(
+ FAMILIES[mRandom.nextInt(FAMILIES.length)],
+ STYLES[mRandom.nextInt(STYLES.length)],
+ 24 + mRandom.nextInt(32), // text size. min 24 max 56
+ TEXT_COLOR, TEXT_COLOR);
+
+ ssb.setSpan(span, spanStart, spanEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ }
+ return ssb;
+ }
+
+ @Test
+ public void testCreate_FixedText_NoStyle_Greedy_NoHyphenation() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT);
+ while (state.keepRunning()) {
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .build();
+ }
+ }
+
+ @Test
+ public void testCreate_RandomText_NoStyled_Greedy_NoHyphenation() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT);
+ state.resumeTiming();
+
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .build();
+ }
+ }
+
+ @Test
+ public void testCreate_RandomText_NoStyled_Greedy_Hyphenation() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT);
+ state.resumeTiming();
+
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .build();
+ }
+ }
+
+ @Test
+ public void testCreate_RandomText_NoStyled_Balanced_NoHyphenation() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT);
+ state.resumeTiming();
+
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED)
+ .build();
+ }
}
- // This tries to simulate the case where the cache hit rate is low, and most of the text is
- // new text.
@Test
- public void testCreateRandom() {
+ public void testCreate_RandomText_NoStyled_Balanced_Hyphenation() {
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
- final CharSequence text = generateRandomParagraph(9);
+ state.pauseTiming();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT);
+ state.resumeTiming();
+
StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED)
.build();
}
}
@Test
- public void testCreateRandom_breakBalanced() {
+ public void testCreate_RandomText_Styled_Greedy_NoHyphenation() {
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
- final CharSequence text = generateRandomParagraph(9);
+ state.pauseTiming();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, STYLE_TEXT);
+ state.resumeTiming();
+
StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .build();
+ }
+ }
+
+ @Test
+ public void testCreate_MeasuredText_NoStyled_Greedy_NoHyphenation() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final PremeasuredText text = PremeasuredText.build(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
+ state.resumeTiming();
+
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .build();
+ }
+ }
+
+ @Test
+ public void testCreate_MeasuredText_NoStyled_Greedy_Hyphenation() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final PremeasuredText text = PremeasuredText.build(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
+ state.resumeTiming();
+
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .build();
+ }
+ }
+
+ @Test
+ public void testCreate_MeasuredText_NoStyled_Balanced_NoHyphenation() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final PremeasuredText text = PremeasuredText.build(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
+ state.resumeTiming();
+
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED)
+ .build();
+ }
+ }
+
+ @Test
+ public void testCreate_MeasuredText_NoStyled_Balanced_Hyphenation() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final PremeasuredText text = PremeasuredText.build(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
+ state.resumeTiming();
+
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
.setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED)
.build();
}
}
+
+ @Test
+ public void testCreate_MeasuredText_Styled_Greedy_NoHyphenation() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final PremeasuredText text = PremeasuredText.build(
+ generateRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT, LTR);
+ state.resumeTiming();
+
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .build();
+ }
+ }
}
diff --git a/android/text/StaticLayout_Delegate.java b/android/text/StaticLayout_Delegate.java
index ca8743c7..d524954e 100644
--- a/android/text/StaticLayout_Delegate.java
+++ b/android/text/StaticLayout_Delegate.java
@@ -5,10 +5,6 @@ import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.graphics.BidiRenderer;
-import android.graphics.Paint;
-import android.graphics.Paint_Delegate;
-import android.graphics.RectF;
import android.icu.text.BreakIterator;
import android.text.Layout.BreakStrategy;
import android.text.Layout.HyphenationFrequency;
@@ -17,7 +13,6 @@ import android.text.StaticLayout.LineBreaks;
import java.text.CharacterIterator;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import javax.swing.text.Segment;
@@ -59,31 +54,12 @@ public class StaticLayout_Delegate {
}
@LayoutlibDelegate
- /*package*/ static void nAddStyleRun(long nativeBuilder, long nativePaint, int start,
- int end, boolean isRtl) {
- Builder builder = sBuilderManager.getDelegate(nativeBuilder);
- if (builder == null) {
- return;
- }
- builder.mRuns.add(new StyleRun(nativePaint, start, end, isRtl));
- }
-
- @LayoutlibDelegate
- /*package*/ static void nAddReplacementRun(long nativeBuilder, long nativePaint, int start,
- int end, float width) {
- Builder builder = sBuilderManager.getDelegate(nativeBuilder);
- if (builder == null) {
- return;
- }
- builder.mRuns.add(new ReplacementRun(start, end, width));
- }
-
- @LayoutlibDelegate
/*package*/ static int nComputeLineBreaks(
/* non zero */ long nativePtr,
// Inputs
@NonNull char[] text,
+ long measuredTextPtr,
int length,
float firstWidth,
int firstWidthLineCount,
@@ -111,9 +87,7 @@ public class StaticLayout_Delegate {
builder.mLineWidth = new LineWidth(firstWidth, firstWidthLineCount, restWidth);
builder.mTabStopCalculator = new TabStops(variableTabStops, defaultTabStop);
- for (Run run: builder.mRuns) {
- run.addTo(builder);
- }
+ MeasuredText_Delegate.computeRuns(measuredTextPtr, builder);
// compute all possible breakpoints.
BreakIterator it = BreakIterator.getLineInstance();
@@ -192,29 +166,20 @@ public class StaticLayout_Delegate {
return primitives;
}
- private static float measureText(long nativePaint, char []text, int index, int count,
- float[] widths, int bidiFlags) {
- Paint_Delegate paint = Paint_Delegate.getDelegate(nativePaint);
- RectF bounds = new BidiRenderer(null, paint, text)
- .renderText(index, index + count, bidiFlags, widths, 0, false);
- return bounds.right - bounds.left;
- }
-
// TODO: Rename to LineBreakerRef and move everything other than LineBreaker to LineBreaker.
/**
* Java representation of the native Builder class.
*/
- private static class Builder {
+ public static class Builder {
char[] mText;
float[] mWidths;
private LineBreaker mLineBreaker;
private int mBreakStrategy;
private LineWidth mLineWidth;
private TabStops mTabStopCalculator;
- private ArrayList<Run> mRuns = new ArrayList<>();
}
- private abstract static class Run {
+ public abstract static class Run {
int mStart;
int mEnd;
@@ -225,37 +190,4 @@ public class StaticLayout_Delegate {
abstract void addTo(Builder builder);
}
-
- private static class StyleRun extends Run {
- private long mNativePaint;
- private boolean mIsRtl;
-
- private StyleRun(long nativePaint, int start, int end, boolean isRtl) {
- super(start, end);
- mNativePaint = nativePaint;
- mIsRtl = isRtl;
- }
-
- @Override
- void addTo(Builder builder) {
- int bidiFlags = mIsRtl ? Paint.BIDI_FORCE_RTL : Paint.BIDI_FORCE_LTR;
- measureText(mNativePaint, builder.mText, mStart, mEnd - mStart, builder.mWidths,
- bidiFlags);
- }
- }
-
- private static class ReplacementRun extends Run {
- private final float mWidth;
-
- private ReplacementRun(int start, int end, float width) {
- super(start, end);
- mWidth = width;
- }
-
- @Override
- void addTo(Builder builder) {
- builder.mWidths[mStart] = mWidth;
- Arrays.fill(builder.mWidths, mStart + 1, mEnd, 0.0f);
- }
- }
}
diff --git a/android/text/TextUtils.java b/android/text/TextUtils.java
index cbdaa69b..9c9fbf23 100644
--- a/android/text/TextUtils.java
+++ b/android/text/TextUtils.java
@@ -42,7 +42,6 @@ import android.text.style.EasyEditSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.LeadingMarginSpan;
import android.text.style.LocaleSpan;
-import android.text.style.MetricAffectingSpan;
import android.text.style.ParagraphStyle;
import android.text.style.QuoteSpan;
import android.text.style.RelativeSizeSpan;
@@ -1251,10 +1250,11 @@ public class TextUtils {
@NonNull String ellipsis) {
final int len = text.length();
- final MeasuredText mt = MeasuredText.obtain();
+ MeasuredText mt = null;
MeasuredText resultMt = null;
try {
- float width = setPara(mt, paint, text, 0, text.length(), textDir);
+ mt = MeasuredText.buildForMeasurement(paint, text, 0, text.length(), textDir, mt);
+ float width = mt.getWholeWidth();
if (width <= avail) {
if (callback != null) {
@@ -1263,7 +1263,6 @@ public class TextUtils {
return text;
}
- resultMt = MeasuredText.obtain();
// First estimate of effective width of ellipsis.
float ellipsisWidth = paint.measureText(ellipsis);
int numberOfTries = 0;
@@ -1290,7 +1289,7 @@ public class TextUtils {
}
}
- final char[] buf = mt.mChars;
+ final char[] buf = mt.getChars();
final Spanned sp = text instanceof Spanned ? (Spanned) text : null;
final int removed = end - start;
@@ -1333,7 +1332,9 @@ public class TextUtils {
if (remaining == 0) { // All text is gone.
textFits = true;
} else {
- width = setPara(resultMt, paint, result, 0, result.length(), textDir);
+ resultMt = MeasuredText.buildForMeasurement(
+ paint, result, 0, result.length(), textDir, resultMt);
+ width = resultMt.getWholeWidth();
if (width <= avail) {
textFits = true;
} else {
@@ -1357,9 +1358,11 @@ public class TextUtils {
}
return result;
} finally {
- MeasuredText.recycle(mt);
+ if (mt != null) {
+ mt.recycle();
+ }
if (resultMt != null) {
- MeasuredText.recycle(resultMt);
+ resultMt.recycle();
}
}
}
@@ -1476,15 +1479,17 @@ public class TextUtils {
public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
- MeasuredText mt = MeasuredText.obtain();
+ MeasuredText mt = null;
+ MeasuredText tempMt = null;
try {
int len = text.length();
- float width = setPara(mt, p, text, 0, len, textDir);
+ mt = MeasuredText.buildForMeasurement(p, text, 0, len, textDir, mt);
+ final float width = mt.getWholeWidth();
if (width <= avail) {
return text;
}
- char[] buf = mt.mChars;
+ char[] buf = mt.getChars();
int commaCount = 0;
for (int i = 0; i < len; i++) {
@@ -1500,9 +1505,8 @@ public class TextUtils {
int w = 0;
int count = 0;
- float[] widths = mt.mWidths;
+ float[] widths = mt.getWidths().getRawArray();
- MeasuredText tempMt = MeasuredText.obtain();
for (int i = 0; i < len; i++) {
w += widths[i];
@@ -1519,8 +1523,9 @@ public class TextUtils {
}
// XXX this is probably ok, but need to look at it more
- tempMt.setPara(format, 0, format.length(), textDir);
- float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null);
+ tempMt = MeasuredText.buildForMeasurement(
+ p, format, 0, format.length(), textDir, tempMt);
+ float moreWid = tempMt.getWholeWidth();
if (w + moreWid <= avail) {
ok = i + 1;
@@ -1528,40 +1533,18 @@ public class TextUtils {
}
}
}
- MeasuredText.recycle(tempMt);
SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
out.insert(0, text, 0, ok);
return out;
} finally {
- MeasuredText.recycle(mt);
- }
- }
-
- private static float setPara(MeasuredText mt, TextPaint paint,
- CharSequence text, int start, int end, TextDirectionHeuristic textDir) {
-
- mt.setPara(text, start, end, textDir);
-
- float width;
- Spanned sp = text instanceof Spanned ? (Spanned) text : null;
- int len = end - start;
- if (sp == null) {
- width = mt.addStyleRun(paint, len, null);
- } else {
- width = 0;
- int spanEnd;
- for (int spanStart = 0; spanStart < len; spanStart = spanEnd) {
- spanEnd = sp.nextSpanTransition(spanStart, len,
- MetricAffectingSpan.class);
- MetricAffectingSpan[] spans = sp.getSpans(
- spanStart, spanEnd, MetricAffectingSpan.class);
- spans = TextUtils.removeEmptySpans(spans, sp, MetricAffectingSpan.class);
- width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null);
+ if (mt != null) {
+ mt.recycle();
+ }
+ if (tempMt != null) {
+ tempMt.recycle();
}
}
-
- return width;
}
// Returns true if the character's presence could affect RTL layout.
diff --git a/android/transition/Visibility.java b/android/transition/Visibility.java
index 5b851caa..f0838a16 100644
--- a/android/transition/Visibility.java
+++ b/android/transition/Visibility.java
@@ -52,7 +52,10 @@ public abstract class Visibility extends Transition {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag=true, value={MODE_IN, MODE_OUT})
+ @IntDef(flag = true, prefix = { "MODE_" }, value = {
+ MODE_IN,
+ MODE_OUT
+ })
@interface VisibilityMode {}
/**
diff --git a/android/util/FeatureFlagUtils.java b/android/util/FeatureFlagUtils.java
index 29baea17..bfb51309 100644
--- a/android/util/FeatureFlagUtils.java
+++ b/android/util/FeatureFlagUtils.java
@@ -21,6 +21,7 @@ import android.os.SystemProperties;
import android.provider.Settings;
import android.text.TextUtils;
+import java.util.HashMap;
import java.util.Map;
/**
@@ -33,6 +34,19 @@ public class FeatureFlagUtils {
public static final String FFLAG_PREFIX = "sys.fflag.";
public static final String FFLAG_OVERRIDE_PREFIX = FFLAG_PREFIX + "override.";
+ private static final Map<String, String> DEFAULT_FLAGS;
+ static {
+ DEFAULT_FLAGS = new HashMap<>();
+ DEFAULT_FLAGS.put("device_info_v2", "true");
+ DEFAULT_FLAGS.put("new_settings_suggestion", "true");
+ DEFAULT_FLAGS.put("settings_search_v2", "true");
+ DEFAULT_FLAGS.put("settings_app_info_v2", "false");
+ DEFAULT_FLAGS.put("settings_connected_device_v2", "true");
+ DEFAULT_FLAGS.put("settings_battery_v2", "false");
+ DEFAULT_FLAGS.put("settings_battery_display_app_list", "false");
+ DEFAULT_FLAGS.put("settings_security_settings_v2", "false");
+ }
+
/**
* Whether or not a flag is enabled.
*
@@ -41,7 +55,7 @@ public class FeatureFlagUtils {
*/
public static boolean isEnabled(Context context, String feature) {
// Override precedence:
- // Settings.Global -> sys.fflag.override.* -> sys.fflag.*
+ // Settings.Global -> sys.fflag.override.* -> static list
// Step 1: check if feature flag is set in Settings.Global.
String value;
@@ -57,8 +71,8 @@ public class FeatureFlagUtils {
if (!TextUtils.isEmpty(value)) {
return Boolean.parseBoolean(value);
}
- // Step 3: check if feature flag has any default value. Flag name: sys.fflag.<feature>
- value = SystemProperties.get(FFLAG_PREFIX + feature);
+ // Step 3: check if feature flag has any default value.
+ value = getAllFeatureFlags().get(feature);
return Boolean.parseBoolean(value);
}
@@ -73,6 +87,6 @@ public class FeatureFlagUtils {
* Returns all feature flags in their raw form.
*/
public static Map<String, String> getAllFeatureFlags() {
- return null;
+ return DEFAULT_FLAGS;
}
}
diff --git a/android/util/KeyValueListParser.java b/android/util/KeyValueListParser.java
index d50395e2..0a00794a 100644
--- a/android/util/KeyValueListParser.java
+++ b/android/util/KeyValueListParser.java
@@ -149,6 +149,34 @@ public class KeyValueListParser {
}
/**
+ * Get the value for key as an integer array.
+ *
+ * The value should be encoded as "0:1:2:3:4"
+ *
+ * @param key The key to lookup.
+ * @param def The value to return if the key was not found.
+ * @return the int[] value associated with the key.
+ */
+ public int[] getIntArray(String key, int[] def) {
+ String value = mValues.get(key);
+ if (value != null) {
+ try {
+ String[] parts = value.split(":");
+ if (parts.length > 0) {
+ int[] ret = new int[parts.length];
+ for (int i = 0; i < parts.length; i++) {
+ ret[i] = Integer.parseInt(parts[i]);
+ }
+ return ret;
+ }
+ } catch (NumberFormatException e) {
+ // fallthrough
+ }
+ }
+ return def;
+ }
+
+ /**
* @return the number of keys.
*/
public int size() {
diff --git a/android/util/Log.java b/android/util/Log.java
index 02998653..b94e48b3 100644
--- a/android/util/Log.java
+++ b/android/util/Log.java
@@ -16,45 +16,12 @@
package android.util;
-import android.os.DeadSystemException;
-
-import com.android.internal.os.RuntimeInit;
-import com.android.internal.util.FastPrintWriter;
-import com.android.internal.util.LineBreakBufferedWriter;
-
import java.io.PrintWriter;
import java.io.StringWriter;
-import java.io.Writer;
import java.net.UnknownHostException;
/**
- * API for sending log output.
- *
- * <p>Generally, you should use the {@link #v Log.v()}, {@link #d Log.d()},
- * {@link #i Log.i()}, {@link #w Log.w()}, and {@link #e Log.e()} methods to write logs.
- * You can then <a href="{@docRoot}studio/debug/am-logcat.html">view the logs in logcat</a>.
- *
- * <p>The order in terms of verbosity, from least to most is
- * ERROR, WARN, INFO, DEBUG, VERBOSE. Verbose should never be compiled
- * into an application except during development. Debug logs are compiled
- * in but stripped at runtime. Error, warning and info logs are always kept.
- *
- * <p><b>Tip:</b> A good convention is to declare a <code>TAG</code> constant
- * in your class:
- *
- * <pre>private static final String TAG = "MyActivity";</pre>
- *
- * and use that in subsequent calls to the log methods.
- * </p>
- *
- * <p><b>Tip:</b> Don't forget that when you make a call like
- * <pre>Log.v(TAG, "index=" + i);</pre>
- * that when you're building the string to pass into Log.d, the compiler uses a
- * StringBuilder and at least three allocations occur: the StringBuilder
- * itself, the buffer, and the String object. Realistically, there is also
- * another buffer allocation and copy, and even more pressure on the gc.
- * That means that if your log message is filtered out, you might be doing
- * significant work and incurring significant overhead.
+ * Mock Log implementation for testing on non android host.
*/
public final class Log {
@@ -88,29 +55,6 @@ public final class Log {
*/
public static final int ASSERT = 7;
- /**
- * Exception class used to capture a stack trace in {@link #wtf}.
- * @hide
- */
- public static class TerribleFailure extends Exception {
- TerribleFailure(String msg, Throwable cause) { super(msg, cause); }
- }
-
- /**
- * Interface to handle terrible failures from {@link #wtf}.
- *
- * @hide
- */
- public interface TerribleFailureHandler {
- void onTerribleFailure(String tag, TerribleFailure what, boolean system);
- }
-
- private static TerribleFailureHandler sWtfHandler = new TerribleFailureHandler() {
- public void onTerribleFailure(String tag, TerribleFailure what, boolean system) {
- RuntimeInit.wtf(tag, what, system);
- }
- };
-
private Log() {
}
@@ -121,7 +65,7 @@ public final class Log {
* @param msg The message you would like logged.
*/
public static int v(String tag, String msg) {
- return println_native(LOG_ID_MAIN, VERBOSE, tag, msg);
+ return println(LOG_ID_MAIN, VERBOSE, tag, msg);
}
/**
@@ -132,7 +76,7 @@ public final class Log {
* @param tr An exception to log
*/
public static int v(String tag, String msg, Throwable tr) {
- return printlns(LOG_ID_MAIN, VERBOSE, tag, msg, tr);
+ return println(LOG_ID_MAIN, VERBOSE, tag, msg + '\n' + getStackTraceString(tr));
}
/**
@@ -142,7 +86,7 @@ public final class Log {
* @param msg The message you would like logged.
*/
public static int d(String tag, String msg) {
- return println_native(LOG_ID_MAIN, DEBUG, tag, msg);
+ return println(LOG_ID_MAIN, DEBUG, tag, msg);
}
/**
@@ -153,7 +97,7 @@ public final class Log {
* @param tr An exception to log
*/
public static int d(String tag, String msg, Throwable tr) {
- return printlns(LOG_ID_MAIN, DEBUG, tag, msg, tr);
+ return println(LOG_ID_MAIN, DEBUG, tag, msg + '\n' + getStackTraceString(tr));
}
/**
@@ -163,7 +107,7 @@ public final class Log {
* @param msg The message you would like logged.
*/
public static int i(String tag, String msg) {
- return println_native(LOG_ID_MAIN, INFO, tag, msg);
+ return println(LOG_ID_MAIN, INFO, tag, msg);
}
/**
@@ -174,7 +118,7 @@ public final class Log {
* @param tr An exception to log
*/
public static int i(String tag, String msg, Throwable tr) {
- return printlns(LOG_ID_MAIN, INFO, tag, msg, tr);
+ return println(LOG_ID_MAIN, INFO, tag, msg + '\n' + getStackTraceString(tr));
}
/**
@@ -184,7 +128,7 @@ public final class Log {
* @param msg The message you would like logged.
*/
public static int w(String tag, String msg) {
- return println_native(LOG_ID_MAIN, WARN, tag, msg);
+ return println(LOG_ID_MAIN, WARN, tag, msg);
}
/**
@@ -195,31 +139,9 @@ public final class Log {
* @param tr An exception to log
*/
public static int w(String tag, String msg, Throwable tr) {
- return printlns(LOG_ID_MAIN, WARN, tag, msg, tr);
+ return println(LOG_ID_MAIN, WARN, tag, msg + '\n' + getStackTraceString(tr));
}
- /**
- * Checks to see whether or not a log for the specified tag is loggable at the specified level.
- *
- * The default level of any tag is set to INFO. This means that any level above and including
- * INFO will be logged. Before you make any calls to a logging method you should check to see
- * if your tag should be logged. You can change the default level by setting a system property:
- * 'setprop log.tag.&lt;YOUR_LOG_TAG> &lt;LEVEL>'
- * Where level is either VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT, or SUPPRESS. SUPPRESS will
- * turn off all logging for your tag. You can also create a local.prop file that with the
- * following in it:
- * 'log.tag.&lt;YOUR_LOG_TAG>=&lt;LEVEL>'
- * and place that in /data/local.prop.
- *
- * @param tag The tag to check.
- * @param level The level to check.
- * @return Whether or not that this is allowed to be logged.
- * @throws IllegalArgumentException is thrown if the tag.length() > 23
- * for Nougat (7.0) releases (API <= 23) and prior, there is no
- * tag limit of concern after this API level.
- */
- public static native boolean isLoggable(String tag, int level);
-
/*
* Send a {@link #WARN} log message and log the exception.
* @param tag Used to identify the source of a log message. It usually identifies
@@ -227,7 +149,7 @@ public final class Log {
* @param tr An exception to log
*/
public static int w(String tag, Throwable tr) {
- return printlns(LOG_ID_MAIN, WARN, tag, "", tr);
+ return println(LOG_ID_MAIN, WARN, tag, getStackTraceString(tr));
}
/**
@@ -237,7 +159,7 @@ public final class Log {
* @param msg The message you would like logged.
*/
public static int e(String tag, String msg) {
- return println_native(LOG_ID_MAIN, ERROR, tag, msg);
+ return println(LOG_ID_MAIN, ERROR, tag, msg);
}
/**
@@ -248,82 +170,7 @@ public final class Log {
* @param tr An exception to log
*/
public static int e(String tag, String msg, Throwable tr) {
- return printlns(LOG_ID_MAIN, ERROR, tag, msg, tr);
- }
-
- /**
- * What a Terrible Failure: Report a condition that should never happen.
- * The error will always be logged at level ASSERT with the call stack.
- * Depending on system configuration, a report may be added to the
- * {@link android.os.DropBoxManager} and/or the process may be terminated
- * immediately with an error dialog.
- * @param tag Used to identify the source of a log message.
- * @param msg The message you would like logged.
- */
- public static int wtf(String tag, String msg) {
- return wtf(LOG_ID_MAIN, tag, msg, null, false, false);
- }
-
- /**
- * Like {@link #wtf(String, String)}, but also writes to the log the full
- * call stack.
- * @hide
- */
- public static int wtfStack(String tag, String msg) {
- return wtf(LOG_ID_MAIN, tag, msg, null, true, false);
- }
-
- /**
- * What a Terrible Failure: Report an exception that should never happen.
- * Similar to {@link #wtf(String, String)}, with an exception to log.
- * @param tag Used to identify the source of a log message.
- * @param tr An exception to log.
- */
- public static int wtf(String tag, Throwable tr) {
- return wtf(LOG_ID_MAIN, tag, tr.getMessage(), tr, false, false);
- }
-
- /**
- * What a Terrible Failure: Report an exception that should never happen.
- * Similar to {@link #wtf(String, Throwable)}, with a message as well.
- * @param tag Used to identify the source of a log message.
- * @param msg The message you would like logged.
- * @param tr An exception to log. May be null.
- */
- public static int wtf(String tag, String msg, Throwable tr) {
- return wtf(LOG_ID_MAIN, tag, msg, tr, false, false);
- }
-
- static int wtf(int logId, String tag, String msg, Throwable tr, boolean localStack,
- boolean system) {
- TerribleFailure what = new TerribleFailure(msg, tr);
- // Only mark this as ERROR, do not use ASSERT since that should be
- // reserved for cases where the system is guaranteed to abort.
- // The onTerribleFailure call does not always cause a crash.
- int bytes = printlns(logId, ERROR, tag, msg, localStack ? what : tr);
- sWtfHandler.onTerribleFailure(tag, what, system);
- return bytes;
- }
-
- static void wtfQuiet(int logId, String tag, String msg, boolean system) {
- TerribleFailure what = new TerribleFailure(msg, null);
- sWtfHandler.onTerribleFailure(tag, what, system);
- }
-
- /**
- * Sets the terrible failure handler, for testing.
- *
- * @return the old handler
- *
- * @hide
- */
- public static TerribleFailureHandler setWtfHandler(TerribleFailureHandler handler) {
- if (handler == null) {
- throw new NullPointerException("handler == null");
- }
- TerribleFailureHandler oldHandler = sWtfHandler;
- sWtfHandler = handler;
- return oldHandler;
+ return println(LOG_ID_MAIN, ERROR, tag, msg + '\n' + getStackTraceString(tr));
}
/**
@@ -346,7 +193,7 @@ public final class Log {
}
StringWriter sw = new StringWriter();
- PrintWriter pw = new FastPrintWriter(sw, false, 256);
+ PrintWriter pw = new PrintWriter(sw);
tr.printStackTrace(pw);
pw.flush();
return sw.toString();
@@ -361,7 +208,7 @@ public final class Log {
* @return The number of bytes written.
*/
public static int println(int priority, String tag, String msg) {
- return println_native(LOG_ID_MAIN, priority, tag, msg);
+ return println(LOG_ID_MAIN, priority, tag, msg);
}
/** @hide */ public static final int LOG_ID_MAIN = 0;
@@ -370,115 +217,9 @@ public final class Log {
/** @hide */ public static final int LOG_ID_SYSTEM = 3;
/** @hide */ public static final int LOG_ID_CRASH = 4;
- /** @hide */ public static native int println_native(int bufID,
- int priority, String tag, String msg);
-
- /**
- * Return the maximum payload the log daemon accepts without truncation.
- * @return LOGGER_ENTRY_MAX_PAYLOAD.
- */
- private static native int logger_entry_max_payload_native();
-
- /**
- * Helper function for long messages. Uses the LineBreakBufferedWriter to break
- * up long messages and stacktraces along newlines, but tries to write in large
- * chunks. This is to avoid truncation.
- * @hide
- */
- public static int printlns(int bufID, int priority, String tag, String msg,
- Throwable tr) {
- ImmediateLogWriter logWriter = new ImmediateLogWriter(bufID, priority, tag);
- // Acceptable buffer size. Get the native buffer size, subtract two zero terminators,
- // and the length of the tag.
- // Note: we implicitly accept possible truncation for Modified-UTF8 differences. It
- // is too expensive to compute that ahead of time.
- int bufferSize = PreloadHolder.LOGGER_ENTRY_MAX_PAYLOAD // Base.
- - 2 // Two terminators.
- - (tag != null ? tag.length() : 0) // Tag length.
- - 32; // Some slack.
- // At least assume you can print *some* characters (tag is not too large).
- bufferSize = Math.max(bufferSize, 100);
-
- LineBreakBufferedWriter lbbw = new LineBreakBufferedWriter(logWriter, bufferSize);
-
- lbbw.println(msg);
-
- if (tr != null) {
- // This is to reduce the amount of log spew that apps do in the non-error
- // condition of the network being unavailable.
- Throwable t = tr;
- while (t != null) {
- if (t instanceof UnknownHostException) {
- break;
- }
- if (t instanceof DeadSystemException) {
- lbbw.println("DeadSystemException: The system died; "
- + "earlier logs will point to the root cause");
- break;
- }
- t = t.getCause();
- }
- if (t == null) {
- tr.printStackTrace(lbbw);
- }
- }
-
- lbbw.flush();
-
- return logWriter.getWritten();
- }
-
- /**
- * PreloadHelper class. Caches the LOGGER_ENTRY_MAX_PAYLOAD value to avoid
- * a JNI call during logging.
- */
- static class PreloadHolder {
- public final static int LOGGER_ENTRY_MAX_PAYLOAD =
- logger_entry_max_payload_native();
- }
-
- /**
- * Helper class to write to the logcat. Different from LogWriter, this writes
- * the whole given buffer and does not break along newlines.
- */
- private static class ImmediateLogWriter extends Writer {
-
- private int bufID;
- private int priority;
- private String tag;
-
- private int written = 0;
-
- /**
- * Create a writer that immediately writes to the log, using the given
- * parameters.
- */
- public ImmediateLogWriter(int bufID, int priority, String tag) {
- this.bufID = bufID;
- this.priority = priority;
- this.tag = tag;
- }
-
- public int getWritten() {
- return written;
- }
-
- @Override
- public void write(char[] cbuf, int off, int len) {
- // Note: using String here has a bit of overhead as a Java object is created,
- // but using the char[] directly is not easier, as it needs to be translated
- // to a C char[] for logging.
- written += println_native(bufID, priority, tag, new String(cbuf, off, len));
- }
-
- @Override
- public void flush() {
- // Ignored.
- }
-
- @Override
- public void close() {
- // Ignored.
- }
+ /** @hide */ @SuppressWarnings("unused")
+ public static int println(int bufID,
+ int priority, String tag, String msg) {
+ return 0;
}
}
diff --git a/android/util/LruCache.java b/android/util/LruCache.java
index 40154880..52086065 100644
--- a/android/util/LruCache.java
+++ b/android/util/LruCache.java
@@ -20,6 +20,10 @@ import java.util.LinkedHashMap;
import java.util.Map;
/**
+ * BEGIN LAYOUTLIB CHANGE
+ * This is a custom version that doesn't use the non standard LinkedHashMap#eldest.
+ * END LAYOUTLIB CHANGE
+ *
* A cache that holds strong references to a limited number of values. Each time
* a value is accessed, it is moved to the head of a queue. When a value is
* added to a full cache, the value at the end of that queue is evicted and may
@@ -87,8 +91,9 @@ public class LruCache<K, V> {
/**
* Sets the size of the cache.
- *
* @param maxSize The new maximum size.
+ *
+ * @hide
*/
public void resize(int maxSize) {
if (maxSize <= 0) {
@@ -185,13 +190,10 @@ public class LruCache<K, V> {
}
/**
- * Remove the eldest entries until the total of remaining entries is at or
- * below the requested size.
- *
* @param maxSize the maximum size of the cache before returning. May be -1
- * to evict even 0-sized elements.
+ * to evict even 0-sized elements.
*/
- public void trimToSize(int maxSize) {
+ private void trimToSize(int maxSize) {
while (true) {
K key;
V value;
@@ -205,7 +207,16 @@ public class LruCache<K, V> {
break;
}
- Map.Entry<K, V> toEvict = map.eldest();
+ // BEGIN LAYOUTLIB CHANGE
+ // get the last item in the linked list.
+ // This is not efficient, the goal here is to minimize the changes
+ // compared to the platform version.
+ Map.Entry<K, V> toEvict = null;
+ for (Map.Entry<K, V> entry : map.entrySet()) {
+ toEvict = entry;
+ }
+ // END LAYOUTLIB CHANGE
+
if (toEvict == null) {
break;
}
diff --git a/android/util/Pools.java b/android/util/Pools.java
index 70581be8..f0b7e01d 100644
--- a/android/util/Pools.java
+++ b/android/util/Pools.java
@@ -130,22 +130,29 @@ public final class Pools {
}
/**
- * Synchronized) pool of objects.
+ * Synchronized pool of objects.
*
* @param <T> The pooled type.
*/
public static class SynchronizedPool<T> extends SimplePool<T> {
- private final Object mLock = new Object();
+ private final Object mLock;
/**
* Creates a new instance.
*
* @param maxPoolSize The max pool size.
+ * @param lock an optional custom object to synchronize on
*
* @throws IllegalArgumentException If the max pool size is less than zero.
*/
- public SynchronizedPool(int maxPoolSize) {
+ public SynchronizedPool(int maxPoolSize, Object lock) {
super(maxPoolSize);
+ mLock = lock;
+ }
+
+ /** @see #SynchronizedPool(int, Object) */
+ public SynchronizedPool(int maxPoolSize) {
+ this(maxPoolSize, new Object());
}
@Override
diff --git a/android/util/SparseBooleanArray.java b/android/util/SparseBooleanArray.java
index 4f76463a..68d347c9 100644
--- a/android/util/SparseBooleanArray.java
+++ b/android/util/SparseBooleanArray.java
@@ -117,7 +117,11 @@ public class SparseBooleanArray implements Cloneable {
}
}
- /** @hide */
+ /**
+ * Removes the mapping at the specified index.
+ * <p>
+ * For indices outside of the range {@code 0...size()-1}, the behavior is undefined.
+ */
public void removeAt(int index) {
System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1));
diff --git a/android/util/StatsLog.java b/android/util/StatsLog.java
new file mode 100644
index 00000000..48053183
--- /dev/null
+++ b/android/util/StatsLog.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * StatsLog provides an API for developers to send events to statsd. The events can be used to
+ * define custom metrics inside statsd. We will rate-limit how often the calls can be made inside
+ * statsd.
+ */
+public final class StatsLog extends StatsLogInternal {
+ private static final String TAG = "StatsManager";
+
+ private StatsLog() {}
+
+ /**
+ * Logs a start event.
+ *
+ * @param label developer-chosen label that is from [0, 16).
+ * @return True if the log request was sent to statsd.
+ */
+ public static boolean logStart(int label) {
+ if (label >= 0 && label < 16) {
+ StatsLog.write(APP_HOOK, label, APP_HOOK__STATE__START);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Logs a stop event.
+ *
+ * @param label developer-chosen label that is from [0, 16).
+ * @return True if the log request was sent to statsd.
+ */
+ public static boolean logStop(int label) {
+ if (label >= 0 && label < 16) {
+ StatsLog.write(APP_HOOK, label, APP_HOOK__STATE__STOP);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Logs an event that does not represent a start or stop boundary.
+ *
+ * @param label developer-chosen label that is from [0, 16).
+ * @return True if the log request was sent to statsd.
+ */
+ public static boolean logEvent(int label) {
+ if (label >= 0 && label < 16) {
+ StatsLog.write(APP_HOOK, label, APP_HOOK__STATE__UNSPECIFIED);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/android/util/StatsManager.java b/android/util/StatsManager.java
index 2bcd863c..26a3c361 100644
--- a/android/util/StatsManager.java
+++ b/android/util/StatsManager.java
@@ -93,10 +93,11 @@ public final class StatsManager {
}
/**
- * Clients can request data with a binder call.
+ * Clients can request data with a binder call. This getter is destructive and also clears
+ * the retrieved metrics from statsd memory.
*
* @param configKey Configuration key to retrieve data from.
- * @return Serialized ConfigMetricsReport proto. Returns null on failure.
+ * @return Serialized ConfigMetricsReportList proto. Returns null on failure.
*/
@RequiresPermission(Manifest.permission.DUMP)
public byte[] getData(String configKey) {
@@ -115,6 +116,30 @@ public final class StatsManager {
}
}
+ /**
+ * Clients can request metadata for statsd. Will contain stats across all configurations but not
+ * the actual metrics themselves (metrics must be collected via {@link #getData(String)}.
+ * This getter is not destructive and will not reset any metrics/counters.
+ *
+ * @return Serialized StatsdStatsReport proto. Returns null on failure.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public byte[] getMetadata() {
+ synchronized (this) {
+ try {
+ IStatsManager service = getIStatsManagerLocked();
+ if (service == null) {
+ Slog.d(TAG, "Failed to find statsd when getting metadata");
+ return null;
+ }
+ return service.getMetadata();
+ } catch (RemoteException e) {
+ Slog.d(TAG, "Failed to connecto statsd when getting metadata");
+ return null;
+ }
+ }
+ }
+
private class StatsdDeathRecipient implements IBinder.DeathRecipient {
@Override
public void binderDied() {
diff --git a/android/util/apk/ApkSignatureSchemeV2Verifier.java b/android/util/apk/ApkSignatureSchemeV2Verifier.java
index 18081234..530937e7 100644
--- a/android/util/apk/ApkSignatureSchemeV2Verifier.java
+++ b/android/util/apk/ApkSignatureSchemeV2Verifier.java
@@ -16,6 +16,21 @@
package android.util.apk;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_DSA_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_ECDSA_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_ECDSA_WITH_SHA512;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PSS_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PSS_WITH_SHA512;
+import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getContentDigestAlgorithmJcaDigestAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaKeyAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
+
import android.util.ArrayMap;
import android.util.Pair;
@@ -23,56 +38,47 @@ import java.io.ByteArrayInputStream;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.RandomAccessFile;
-import java.math.BigInteger;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.security.DigestException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.Principal;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
-import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
-import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
-import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidKeySpecException;
-import java.security.spec.MGF1ParameterSpec;
-import java.security.spec.PSSParameterSpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Date;
import java.util.List;
import java.util.Map;
-import java.util.Set;
/**
* APK Signature Scheme v2 verifier.
*
+ * <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
+ * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
+ * uncompressed contents of ZIP entries.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
+ *
* @hide for internal use only.
*/
public class ApkSignatureSchemeV2Verifier {
/**
- * {@code .SF} file header section attribute indicating that the APK is signed not just with
- * JAR signature scheme but also with APK Signature Scheme v2 or newer. This attribute
- * facilitates v2 signature stripping detection.
- *
- * <p>The attribute contains a comma-separated set of signature scheme IDs.
+ * ID of this signature scheme as used in X-Android-APK-Signed header used in JAR signing.
*/
- public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed";
public static final int SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID = 2;
+ private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
+
/**
* Returns {@code true} if the provided APK contains an APK Signature Scheme V2 signature.
*
@@ -97,8 +103,27 @@ public class ApkSignatureSchemeV2Verifier {
*/
public static X509Certificate[][] verify(String apkFile)
throws SignatureNotFoundException, SecurityException, IOException {
+ return verify(apkFile, true);
+ }
+
+ /**
+ * Returns the certificates associated with each signer for the given APK without verification.
+ * This method is dangerous and should not be used, unless the caller is absolutely certain the
+ * APK is trusted. Specifically, verification is only done for the APK Signature Scheme v2
+ * Block while gathering signer information. The APK contents are not verified.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
+ * @throws IOException if an I/O error occurs while reading the APK file.
+ */
+ public static X509Certificate[][] plsCertsNoVerifyOnlyCerts(String apkFile)
+ throws SignatureNotFoundException, SecurityException, IOException {
+ return verify(apkFile, false);
+ }
+
+ private static X509Certificate[][] verify(String apkFile, boolean verifyIntegrity)
+ throws SignatureNotFoundException, SecurityException, IOException {
try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
- return verify(apk);
+ return verify(apk, verifyIntegrity);
}
}
@@ -111,10 +136,10 @@ public class ApkSignatureSchemeV2Verifier {
* verify.
* @throws IOException if an I/O error occurs while reading the APK file.
*/
- private static X509Certificate[][] verify(RandomAccessFile apk)
+ private static X509Certificate[][] verify(RandomAccessFile apk, boolean verifyIntegrity)
throws SignatureNotFoundException, SecurityException, IOException {
SignatureInfo signatureInfo = findSignature(apk);
- return verify(apk.getFD(), signatureInfo);
+ return verify(apk.getFD(), signatureInfo, verifyIntegrity);
}
/**
@@ -126,30 +151,7 @@ public class ApkSignatureSchemeV2Verifier {
*/
private static SignatureInfo findSignature(RandomAccessFile apk)
throws IOException, SignatureNotFoundException {
- // Find the ZIP End of Central Directory (EoCD) record.
- Pair<ByteBuffer, Long> eocdAndOffsetInFile = getEocd(apk);
- ByteBuffer eocd = eocdAndOffsetInFile.first;
- long eocdOffset = eocdAndOffsetInFile.second;
- if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
- throw new SignatureNotFoundException("ZIP64 APK not supported");
- }
-
- // Find the APK Signing Block. The block immediately precedes the Central Directory.
- long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
- Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile =
- findApkSigningBlock(apk, centralDirOffset);
- ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first;
- long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second;
-
- // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
- ByteBuffer apkSignatureSchemeV2Block = findApkSignatureSchemeV2Block(apkSigningBlock);
-
- return new SignatureInfo(
- apkSignatureSchemeV2Block,
- apkSigningBlockOffset,
- centralDirOffset,
- eocdOffset,
- eocd);
+ return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
}
/**
@@ -161,7 +163,8 @@ public class ApkSignatureSchemeV2Verifier {
*/
private static X509Certificate[][] verify(
FileDescriptor apkFileDescriptor,
- SignatureInfo signatureInfo) throws SecurityException {
+ SignatureInfo signatureInfo,
+ boolean doVerifyIntegrity) throws SecurityException {
int signerCount = 0;
Map<Integer, byte[]> contentDigests = new ArrayMap<>();
List<X509Certificate[]> signerCerts = new ArrayList<>();
@@ -198,13 +201,15 @@ public class ApkSignatureSchemeV2Verifier {
throw new SecurityException("No content digests found");
}
- verifyIntegrity(
- contentDigests,
- apkFileDescriptor,
- signatureInfo.apkSigningBlockOffset,
- signatureInfo.centralDirOffset,
- signatureInfo.eocdOffset,
- signatureInfo.eocd);
+ if (doVerifyIntegrity) {
+ ApkSigningBlockUtils.verifyIntegrity(
+ contentDigests,
+ apkFileDescriptor,
+ signatureInfo.apkSigningBlockOffset,
+ signatureInfo.centralDirOffset,
+ signatureInfo.eocdOffset,
+ signatureInfo.eocd);
+ }
return signerCerts.toArray(new X509Certificate[signerCerts.size()][]);
}
@@ -328,7 +333,8 @@ public class ApkSignatureSchemeV2Verifier {
} catch (CertificateException e) {
throw new SecurityException("Failed to decode certificate #" + certificateCount, e);
}
- certificate = new VerbatimX509Certificate(certificate, encodedCert);
+ certificate = new VerbatimX509Certificate(
+ certificate, encodedCert);
certs.add(certificate);
}
@@ -342,235 +348,44 @@ public class ApkSignatureSchemeV2Verifier {
"Public key mismatch between certificate and signature record");
}
- return certs.toArray(new X509Certificate[certs.size()]);
- }
-
- private static void verifyIntegrity(
- Map<Integer, byte[]> expectedDigests,
- FileDescriptor apkFileDescriptor,
- long apkSigningBlockOffset,
- long centralDirOffset,
- long eocdOffset,
- ByteBuffer eocdBuf) throws SecurityException {
-
- if (expectedDigests.isEmpty()) {
- throw new SecurityException("No digests provided");
- }
-
- // We need to verify the integrity of the following three sections of the file:
- // 1. Everything up to the start of the APK Signing Block.
- // 2. ZIP Central Directory.
- // 3. ZIP End of Central Directory (EoCD).
- // Each of these sections is represented as a separate DataSource instance below.
-
- // To handle large APKs, these sections are read in 1 MB chunks using memory-mapped I/O to
- // avoid wasting physical memory. In most APK verification scenarios, the contents of the
- // APK are already there in the OS's page cache and thus mmap does not use additional
- // physical memory.
- DataSource beforeApkSigningBlock =
- new MemoryMappedFileDataSource(apkFileDescriptor, 0, apkSigningBlockOffset);
- DataSource centralDir =
- new MemoryMappedFileDataSource(
- apkFileDescriptor, centralDirOffset, eocdOffset - centralDirOffset);
+ ByteBuffer additionalAttrs = getLengthPrefixedSlice(signedData);
+ verifyAdditionalAttributes(additionalAttrs);
- // For the purposes of integrity verification, ZIP End of Central Directory's field Start of
- // Central Directory must be considered to point to the offset of the APK Signing Block.
- eocdBuf = eocdBuf.duplicate();
- eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
- ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, apkSigningBlockOffset);
- DataSource eocd = new ByteBufferDataSource(eocdBuf);
-
- int[] digestAlgorithms = new int[expectedDigests.size()];
- int digestAlgorithmCount = 0;
- for (int digestAlgorithm : expectedDigests.keySet()) {
- digestAlgorithms[digestAlgorithmCount] = digestAlgorithm;
- digestAlgorithmCount++;
- }
- byte[][] actualDigests;
- try {
- actualDigests =
- computeContentDigests(
- digestAlgorithms,
- new DataSource[] {beforeApkSigningBlock, centralDir, eocd});
- } catch (DigestException e) {
- throw new SecurityException("Failed to compute digest(s) of contents", e);
- }
- for (int i = 0; i < digestAlgorithms.length; i++) {
- int digestAlgorithm = digestAlgorithms[i];
- byte[] expectedDigest = expectedDigests.get(digestAlgorithm);
- byte[] actualDigest = actualDigests[i];
- if (!MessageDigest.isEqual(expectedDigest, actualDigest)) {
- throw new SecurityException(
- getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
- + " digest of contents did not verify");
- }
- }
+ return certs.toArray(new X509Certificate[certs.size()]);
}
- private static byte[][] computeContentDigests(
- int[] digestAlgorithms,
- DataSource[] contents) throws DigestException {
- // For each digest algorithm the result is computed as follows:
- // 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
- // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
- // No chunks are produced for empty (zero length) segments.
- // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's
- // length in bytes (uint32 little-endian) and the chunk's contents.
- // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of
- // chunks (uint32 little-endian) and the concatenation of digests of chunks of all
- // segments in-order.
-
- long totalChunkCountLong = 0;
- for (DataSource input : contents) {
- totalChunkCountLong += getChunkCount(input.size());
- }
- if (totalChunkCountLong >= Integer.MAX_VALUE / 1024) {
- throw new DigestException("Too many chunks: " + totalChunkCountLong);
- }
- int totalChunkCount = (int) totalChunkCountLong;
-
- byte[][] digestsOfChunks = new byte[digestAlgorithms.length][];
- for (int i = 0; i < digestAlgorithms.length; i++) {
- int digestAlgorithm = digestAlgorithms[i];
- int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
- byte[] concatenationOfChunkCountAndChunkDigests =
- new byte[5 + totalChunkCount * digestOutputSizeBytes];
- concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
- setUnsignedInt32LittleEndian(
- totalChunkCount,
- concatenationOfChunkCountAndChunkDigests,
- 1);
- digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests;
- }
+ // Attribute to check whether a newer APK Signature Scheme signature was stripped
+ private static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
- byte[] chunkContentPrefix = new byte[5];
- chunkContentPrefix[0] = (byte) 0xa5;
- int chunkIndex = 0;
- MessageDigest[] mds = new MessageDigest[digestAlgorithms.length];
- for (int i = 0; i < digestAlgorithms.length; i++) {
- String jcaAlgorithmName =
- getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithms[i]);
- try {
- mds[i] = MessageDigest.getInstance(jcaAlgorithmName);
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(jcaAlgorithmName + " digest not supported", e);
+ private static void verifyAdditionalAttributes(ByteBuffer attrs)
+ throws SecurityException, IOException {
+ while (attrs.hasRemaining()) {
+ ByteBuffer attr = getLengthPrefixedSlice(attrs);
+ if (attr.remaining() < 4) {
+ throw new IOException("Remaining buffer too short to contain additional attribute "
+ + "ID. Remaining: " + attr.remaining());
}
- }
- // TODO: Compute digests of chunks in parallel when beneficial. This requires some research
- // into how to parallelize (if at all) based on the capabilities of the hardware on which
- // this code is running and based on the size of input.
- DataDigester digester = new MultipleDigestDataDigester(mds);
- int dataSourceIndex = 0;
- for (DataSource input : contents) {
- long inputOffset = 0;
- long inputRemaining = input.size();
- while (inputRemaining > 0) {
- int chunkSize = (int) Math.min(inputRemaining, CHUNK_SIZE_BYTES);
- setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1);
- for (int i = 0; i < mds.length; i++) {
- mds[i].update(chunkContentPrefix);
- }
- try {
- input.feedIntoDataDigester(digester, inputOffset, chunkSize);
- } catch (IOException e) {
- throw new DigestException(
- "Failed to digest chunk #" + chunkIndex + " of section #"
- + dataSourceIndex,
- e);
- }
- for (int i = 0; i < digestAlgorithms.length; i++) {
- int digestAlgorithm = digestAlgorithms[i];
- byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
- int expectedDigestSizeBytes =
- getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
- MessageDigest md = mds[i];
- int actualDigestSizeBytes =
- md.digest(
- concatenationOfChunkCountAndChunkDigests,
- 5 + chunkIndex * expectedDigestSizeBytes,
- expectedDigestSizeBytes);
- if (actualDigestSizeBytes != expectedDigestSizeBytes) {
- throw new RuntimeException(
- "Unexpected output size of " + md.getAlgorithm() + " digest: "
- + actualDigestSizeBytes);
+ int id = attr.getInt();
+ switch (id) {
+ case STRIPPING_PROTECTION_ATTR_ID:
+ if (attr.remaining() < 4) {
+ throw new IOException("V2 Signature Scheme Stripping Protection Attribute "
+ + " value too small. Expected 4 bytes, but found "
+ + attr.remaining());
}
- }
- inputOffset += chunkSize;
- inputRemaining -= chunkSize;
- chunkIndex++;
- }
- dataSourceIndex++;
- }
-
- byte[][] result = new byte[digestAlgorithms.length][];
- for (int i = 0; i < digestAlgorithms.length; i++) {
- int digestAlgorithm = digestAlgorithms[i];
- byte[] input = digestsOfChunks[i];
- String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
- MessageDigest md;
- try {
- md = MessageDigest.getInstance(jcaAlgorithmName);
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(jcaAlgorithmName + " digest not supported", e);
+ int vers = attr.getInt();
+ if (vers == ApkSignatureSchemeV3Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID) {
+ throw new SecurityException("V2 signature indicates APK is signed using APK"
+ + " Signature Scheme v3, but none was found. Signature stripped?");
+ }
+ break;
+ default:
+ // not the droid we're looking for, move along, move along.
+ break;
}
- byte[] output = md.digest(input);
- result[i] = output;
- }
- return result;
- }
-
- /**
- * Returns the ZIP End of Central Directory (EoCD) and its offset in the file.
- *
- * @throws IOException if an I/O error occurs while reading the file.
- * @throws SignatureNotFoundException if the EoCD could not be found.
- */
- private static Pair<ByteBuffer, Long> getEocd(RandomAccessFile apk)
- throws IOException, SignatureNotFoundException {
- Pair<ByteBuffer, Long> eocdAndOffsetInFile =
- ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
- if (eocdAndOffsetInFile == null) {
- throw new SignatureNotFoundException(
- "Not an APK file: ZIP End of Central Directory record not found");
- }
- return eocdAndOffsetInFile;
- }
-
- private static long getCentralDirOffset(ByteBuffer eocd, long eocdOffset)
- throws SignatureNotFoundException {
- // Look up the offset of ZIP Central Directory.
- long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd);
- if (centralDirOffset > eocdOffset) {
- throw new SignatureNotFoundException(
- "ZIP Central Directory offset out of range: " + centralDirOffset
- + ". ZIP End of Central Directory offset: " + eocdOffset);
}
- long centralDirSize = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocd);
- if (centralDirOffset + centralDirSize != eocdOffset) {
- throw new SignatureNotFoundException(
- "ZIP Central Directory is not immediately followed by End of Central"
- + " Directory");
- }
- return centralDirOffset;
- }
-
- private static final long getChunkCount(long inputSizeBytes) {
- return (inputSizeBytes + CHUNK_SIZE_BYTES - 1) / CHUNK_SIZE_BYTES;
+ return;
}
-
- private static final int CHUNK_SIZE_BYTES = 1024 * 1024;
-
- private static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101;
- private static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102;
- private static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103;
- private static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104;
- private static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201;
- private static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202;
- private static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301;
-
- private static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1;
- private static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2;
-
private static boolean isSupportedSignatureAlgorithm(int sigAlgorithm) {
switch (sigAlgorithm) {
case SIGNATURE_RSA_PSS_WITH_SHA256:
@@ -585,531 +400,4 @@ public class ApkSignatureSchemeV2Verifier {
return false;
}
}
-
- private static int compareSignatureAlgorithm(int sigAlgorithm1, int sigAlgorithm2) {
- int digestAlgorithm1 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm1);
- int digestAlgorithm2 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm2);
- return compareContentDigestAlgorithm(digestAlgorithm1, digestAlgorithm2);
- }
-
- private static int compareContentDigestAlgorithm(int digestAlgorithm1, int digestAlgorithm2) {
- switch (digestAlgorithm1) {
- case CONTENT_DIGEST_CHUNKED_SHA256:
- switch (digestAlgorithm2) {
- case CONTENT_DIGEST_CHUNKED_SHA256:
- return 0;
- case CONTENT_DIGEST_CHUNKED_SHA512:
- return -1;
- default:
- throw new IllegalArgumentException(
- "Unknown digestAlgorithm2: " + digestAlgorithm2);
- }
- case CONTENT_DIGEST_CHUNKED_SHA512:
- switch (digestAlgorithm2) {
- case CONTENT_DIGEST_CHUNKED_SHA256:
- return 1;
- case CONTENT_DIGEST_CHUNKED_SHA512:
- return 0;
- default:
- throw new IllegalArgumentException(
- "Unknown digestAlgorithm2: " + digestAlgorithm2);
- }
- default:
- throw new IllegalArgumentException("Unknown digestAlgorithm1: " + digestAlgorithm1);
- }
- }
-
- private static int getSignatureAlgorithmContentDigestAlgorithm(int sigAlgorithm) {
- switch (sigAlgorithm) {
- case SIGNATURE_RSA_PSS_WITH_SHA256:
- case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
- case SIGNATURE_ECDSA_WITH_SHA256:
- case SIGNATURE_DSA_WITH_SHA256:
- return CONTENT_DIGEST_CHUNKED_SHA256;
- case SIGNATURE_RSA_PSS_WITH_SHA512:
- case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
- case SIGNATURE_ECDSA_WITH_SHA512:
- return CONTENT_DIGEST_CHUNKED_SHA512;
- default:
- throw new IllegalArgumentException(
- "Unknown signature algorithm: 0x"
- + Long.toHexString(sigAlgorithm & 0xffffffff));
- }
- }
-
- private static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) {
- switch (digestAlgorithm) {
- case CONTENT_DIGEST_CHUNKED_SHA256:
- return "SHA-256";
- case CONTENT_DIGEST_CHUNKED_SHA512:
- return "SHA-512";
- default:
- throw new IllegalArgumentException(
- "Unknown content digest algorthm: " + digestAlgorithm);
- }
- }
-
- private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) {
- switch (digestAlgorithm) {
- case CONTENT_DIGEST_CHUNKED_SHA256:
- return 256 / 8;
- case CONTENT_DIGEST_CHUNKED_SHA512:
- return 512 / 8;
- default:
- throw new IllegalArgumentException(
- "Unknown content digest algorthm: " + digestAlgorithm);
- }
- }
-
- private static String getSignatureAlgorithmJcaKeyAlgorithm(int sigAlgorithm) {
- switch (sigAlgorithm) {
- case SIGNATURE_RSA_PSS_WITH_SHA256:
- case SIGNATURE_RSA_PSS_WITH_SHA512:
- case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
- case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
- return "RSA";
- case SIGNATURE_ECDSA_WITH_SHA256:
- case SIGNATURE_ECDSA_WITH_SHA512:
- return "EC";
- case SIGNATURE_DSA_WITH_SHA256:
- return "DSA";
- default:
- throw new IllegalArgumentException(
- "Unknown signature algorithm: 0x"
- + Long.toHexString(sigAlgorithm & 0xffffffff));
- }
- }
-
- private static Pair<String, ? extends AlgorithmParameterSpec>
- getSignatureAlgorithmJcaSignatureAlgorithm(int sigAlgorithm) {
- switch (sigAlgorithm) {
- case SIGNATURE_RSA_PSS_WITH_SHA256:
- return Pair.create(
- "SHA256withRSA/PSS",
- new PSSParameterSpec(
- "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1));
- case SIGNATURE_RSA_PSS_WITH_SHA512:
- return Pair.create(
- "SHA512withRSA/PSS",
- new PSSParameterSpec(
- "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1));
- case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
- return Pair.create("SHA256withRSA", null);
- case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
- return Pair.create("SHA512withRSA", null);
- case SIGNATURE_ECDSA_WITH_SHA256:
- return Pair.create("SHA256withECDSA", null);
- case SIGNATURE_ECDSA_WITH_SHA512:
- return Pair.create("SHA512withECDSA", null);
- case SIGNATURE_DSA_WITH_SHA256:
- return Pair.create("SHA256withDSA", null);
- default:
- throw new IllegalArgumentException(
- "Unknown signature algorithm: 0x"
- + Long.toHexString(sigAlgorithm & 0xffffffff));
- }
- }
-
- /**
- * Returns new byte buffer whose content is a shared subsequence of this buffer's content
- * between the specified start (inclusive) and end (exclusive) positions. As opposed to
- * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
- * buffer's byte order.
- */
- private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
- if (start < 0) {
- throw new IllegalArgumentException("start: " + start);
- }
- if (end < start) {
- throw new IllegalArgumentException("end < start: " + end + " < " + start);
- }
- int capacity = source.capacity();
- if (end > source.capacity()) {
- throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
- }
- int originalLimit = source.limit();
- int originalPosition = source.position();
- try {
- source.position(0);
- source.limit(end);
- source.position(start);
- ByteBuffer result = source.slice();
- result.order(source.order());
- return result;
- } finally {
- source.position(0);
- source.limit(originalLimit);
- source.position(originalPosition);
- }
- }
-
- /**
- * Relative <em>get</em> method for reading {@code size} number of bytes from the current
- * position of this buffer.
- *
- * <p>This method reads the next {@code size} bytes at this buffer's current position,
- * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
- * {@code size}, byte order set to this buffer's byte order; and then increments the position by
- * {@code size}.
- */
- private static ByteBuffer getByteBuffer(ByteBuffer source, int size)
- throws BufferUnderflowException {
- if (size < 0) {
- throw new IllegalArgumentException("size: " + size);
- }
- int originalLimit = source.limit();
- int position = source.position();
- int limit = position + size;
- if ((limit < position) || (limit > originalLimit)) {
- throw new BufferUnderflowException();
- }
- source.limit(limit);
- try {
- ByteBuffer result = source.slice();
- result.order(source.order());
- source.position(limit);
- return result;
- } finally {
- source.limit(originalLimit);
- }
- }
-
- private static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws IOException {
- if (source.remaining() < 4) {
- throw new IOException(
- "Remaining buffer too short to contain length of length-prefixed field."
- + " Remaining: " + source.remaining());
- }
- int len = source.getInt();
- if (len < 0) {
- throw new IllegalArgumentException("Negative length");
- } else if (len > source.remaining()) {
- throw new IOException("Length-prefixed field longer than remaining buffer."
- + " Field length: " + len + ", remaining: " + source.remaining());
- }
- return getByteBuffer(source, len);
- }
-
- private static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws IOException {
- int len = buf.getInt();
- if (len < 0) {
- throw new IOException("Negative length");
- } else if (len > buf.remaining()) {
- throw new IOException("Underflow while reading length-prefixed value. Length: " + len
- + ", available: " + buf.remaining());
- }
- byte[] result = new byte[len];
- buf.get(result);
- return result;
- }
-
- private static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) {
- result[offset] = (byte) (value & 0xff);
- result[offset + 1] = (byte) ((value >>> 8) & 0xff);
- result[offset + 2] = (byte) ((value >>> 16) & 0xff);
- result[offset + 3] = (byte) ((value >>> 24) & 0xff);
- }
-
- private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
- private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
- private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
-
- private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
-
- private static Pair<ByteBuffer, Long> findApkSigningBlock(
- RandomAccessFile apk, long centralDirOffset)
- throws IOException, SignatureNotFoundException {
- // FORMAT:
- // OFFSET DATA TYPE DESCRIPTION
- // * @+0 bytes uint64: size in bytes (excluding this field)
- // * @+8 bytes payload
- // * @-24 bytes uint64: size in bytes (same as the one above)
- // * @-16 bytes uint128: magic
-
- if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) {
- throw new SignatureNotFoundException(
- "APK too small for APK Signing Block. ZIP Central Directory offset: "
- + centralDirOffset);
- }
- // Read the magic and offset in file from the footer section of the block:
- // * uint64: size of block
- // * 16 bytes: magic
- ByteBuffer footer = ByteBuffer.allocate(24);
- footer.order(ByteOrder.LITTLE_ENDIAN);
- apk.seek(centralDirOffset - footer.capacity());
- apk.readFully(footer.array(), footer.arrayOffset(), footer.capacity());
- if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
- || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
- throw new SignatureNotFoundException(
- "No APK Signing Block before ZIP Central Directory");
- }
- // Read and compare size fields
- long apkSigBlockSizeInFooter = footer.getLong(0);
- if ((apkSigBlockSizeInFooter < footer.capacity())
- || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
- throw new SignatureNotFoundException(
- "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
- }
- int totalSize = (int) (apkSigBlockSizeInFooter + 8);
- long apkSigBlockOffset = centralDirOffset - totalSize;
- if (apkSigBlockOffset < 0) {
- throw new SignatureNotFoundException(
- "APK Signing Block offset out of range: " + apkSigBlockOffset);
- }
- ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize);
- apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
- apk.seek(apkSigBlockOffset);
- apk.readFully(apkSigBlock.array(), apkSigBlock.arrayOffset(), apkSigBlock.capacity());
- long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
- if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
- throw new SignatureNotFoundException(
- "APK Signing Block sizes in header and footer do not match: "
- + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
- }
- return Pair.create(apkSigBlock, apkSigBlockOffset);
- }
-
- private static ByteBuffer findApkSignatureSchemeV2Block(ByteBuffer apkSigningBlock)
- throws SignatureNotFoundException {
- checkByteOrderLittleEndian(apkSigningBlock);
- // FORMAT:
- // OFFSET DATA TYPE DESCRIPTION
- // * @+0 bytes uint64: size in bytes (excluding this field)
- // * @+8 bytes pairs
- // * @-24 bytes uint64: size in bytes (same as the one above)
- // * @-16 bytes uint128: magic
- ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
-
- int entryCount = 0;
- while (pairs.hasRemaining()) {
- entryCount++;
- if (pairs.remaining() < 8) {
- throw new SignatureNotFoundException(
- "Insufficient data to read size of APK Signing Block entry #" + entryCount);
- }
- long lenLong = pairs.getLong();
- if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
- throw new SignatureNotFoundException(
- "APK Signing Block entry #" + entryCount
- + " size out of range: " + lenLong);
- }
- int len = (int) lenLong;
- int nextEntryPos = pairs.position() + len;
- if (len > pairs.remaining()) {
- throw new SignatureNotFoundException(
- "APK Signing Block entry #" + entryCount + " size out of range: " + len
- + ", available: " + pairs.remaining());
- }
- int id = pairs.getInt();
- if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
- return getByteBuffer(pairs, len - 4);
- }
- pairs.position(nextEntryPos);
- }
-
- throw new SignatureNotFoundException(
- "No APK Signature Scheme v2 block in APK Signing Block");
- }
-
- private static void checkByteOrderLittleEndian(ByteBuffer buffer) {
- if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
- throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
- }
- }
-
- public static class SignatureNotFoundException extends Exception {
- private static final long serialVersionUID = 1L;
-
- public SignatureNotFoundException(String message) {
- super(message);
- }
-
- public SignatureNotFoundException(String message, Throwable cause) {
- super(message, cause);
- }
- }
-
- /**
- * {@link DataDigester} that updates multiple {@link MessageDigest}s whenever data is feeded.
- */
- private static class MultipleDigestDataDigester implements DataDigester {
- private final MessageDigest[] mMds;
-
- MultipleDigestDataDigester(MessageDigest[] mds) {
- mMds = mds;
- }
-
- @Override
- public void consume(ByteBuffer buffer) {
- buffer = buffer.slice();
- for (MessageDigest md : mMds) {
- buffer.position(0);
- md.update(buffer);
- }
- }
-
- @Override
- public void finish() {}
- }
-
- /**
- * For legacy reasons we need to return exactly the original encoded certificate bytes, instead
- * of letting the underlying implementation have a shot at re-encoding the data.
- */
- private static class VerbatimX509Certificate extends WrappedX509Certificate {
- private byte[] encodedVerbatim;
-
- public VerbatimX509Certificate(X509Certificate wrapped, byte[] encodedVerbatim) {
- super(wrapped);
- this.encodedVerbatim = encodedVerbatim;
- }
-
- @Override
- public byte[] getEncoded() throws CertificateEncodingException {
- return encodedVerbatim;
- }
- }
-
- private static class WrappedX509Certificate extends X509Certificate {
- private final X509Certificate wrapped;
-
- public WrappedX509Certificate(X509Certificate wrapped) {
- this.wrapped = wrapped;
- }
-
- @Override
- public Set<String> getCriticalExtensionOIDs() {
- return wrapped.getCriticalExtensionOIDs();
- }
-
- @Override
- public byte[] getExtensionValue(String oid) {
- return wrapped.getExtensionValue(oid);
- }
-
- @Override
- public Set<String> getNonCriticalExtensionOIDs() {
- return wrapped.getNonCriticalExtensionOIDs();
- }
-
- @Override
- public boolean hasUnsupportedCriticalExtension() {
- return wrapped.hasUnsupportedCriticalExtension();
- }
-
- @Override
- public void checkValidity()
- throws CertificateExpiredException, CertificateNotYetValidException {
- wrapped.checkValidity();
- }
-
- @Override
- public void checkValidity(Date date)
- throws CertificateExpiredException, CertificateNotYetValidException {
- wrapped.checkValidity(date);
- }
-
- @Override
- public int getVersion() {
- return wrapped.getVersion();
- }
-
- @Override
- public BigInteger getSerialNumber() {
- return wrapped.getSerialNumber();
- }
-
- @Override
- public Principal getIssuerDN() {
- return wrapped.getIssuerDN();
- }
-
- @Override
- public Principal getSubjectDN() {
- return wrapped.getSubjectDN();
- }
-
- @Override
- public Date getNotBefore() {
- return wrapped.getNotBefore();
- }
-
- @Override
- public Date getNotAfter() {
- return wrapped.getNotAfter();
- }
-
- @Override
- public byte[] getTBSCertificate() throws CertificateEncodingException {
- return wrapped.getTBSCertificate();
- }
-
- @Override
- public byte[] getSignature() {
- return wrapped.getSignature();
- }
-
- @Override
- public String getSigAlgName() {
- return wrapped.getSigAlgName();
- }
-
- @Override
- public String getSigAlgOID() {
- return wrapped.getSigAlgOID();
- }
-
- @Override
- public byte[] getSigAlgParams() {
- return wrapped.getSigAlgParams();
- }
-
- @Override
- public boolean[] getIssuerUniqueID() {
- return wrapped.getIssuerUniqueID();
- }
-
- @Override
- public boolean[] getSubjectUniqueID() {
- return wrapped.getSubjectUniqueID();
- }
-
- @Override
- public boolean[] getKeyUsage() {
- return wrapped.getKeyUsage();
- }
-
- @Override
- public int getBasicConstraints() {
- return wrapped.getBasicConstraints();
- }
-
- @Override
- public byte[] getEncoded() throws CertificateEncodingException {
- return wrapped.getEncoded();
- }
-
- @Override
- public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException,
- InvalidKeyException, NoSuchProviderException, SignatureException {
- wrapped.verify(key);
- }
-
- @Override
- public void verify(PublicKey key, String sigProvider)
- throws CertificateException, NoSuchAlgorithmException, InvalidKeyException,
- NoSuchProviderException, SignatureException {
- wrapped.verify(key, sigProvider);
- }
-
- @Override
- public String toString() {
- return wrapped.toString();
- }
-
- @Override
- public PublicKey getPublicKey() {
- return wrapped.getPublicKey();
- }
- }
}
diff --git a/android/util/apk/ApkSignatureSchemeV3Verifier.java b/android/util/apk/ApkSignatureSchemeV3Verifier.java
new file mode 100644
index 00000000..e43dee35
--- /dev/null
+++ b/android/util/apk/ApkSignatureSchemeV3Verifier.java
@@ -0,0 +1,558 @@
+/*
+ * 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.util.apk;
+
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_DSA_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_ECDSA_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_ECDSA_WITH_SHA512;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PSS_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PSS_WITH_SHA512;
+import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getContentDigestAlgorithmJcaDigestAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaKeyAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
+
+import android.os.Build;
+import android.util.ArrayMap;
+import android.util.Pair;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * APK Signature Scheme v3 verifier.
+ *
+ * @hide for internal use only.
+ */
+public class ApkSignatureSchemeV3Verifier {
+
+ /**
+ * ID of this signature scheme as used in X-Android-APK-Signed header used in JAR signing.
+ */
+ public static final int SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID = 3;
+
+ private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
+
+ /**
+ * Returns {@code true} if the provided APK contains an APK Signature Scheme V3 signature.
+ *
+ * <p><b>NOTE: This method does not verify the signature.</b>
+ */
+ public static boolean hasSignature(String apkFile) throws IOException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
+ findSignature(apk);
+ return true;
+ } catch (SignatureNotFoundException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Verifies APK Signature Scheme v3 signatures of the provided APK and returns the certificates
+ * associated with each signer.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3.
+ * @throws SecurityException if the APK Signature Scheme v3 signature of this APK does not
+ * verify.
+ * @throws IOException if an I/O error occurs while reading the APK file.
+ */
+ public static VerifiedSigner verify(String apkFile)
+ throws SignatureNotFoundException, SecurityException, IOException {
+ return verify(apkFile, true);
+ }
+
+ /**
+ * Returns the certificates associated with each signer for the given APK without verification.
+ * This method is dangerous and should not be used, unless the caller is absolutely certain the
+ * APK is trusted. Specifically, verification is only done for the APK Signature Scheme v3
+ * Block while gathering signer information. The APK contents are not verified.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3.
+ * @throws IOException if an I/O error occurs while reading the APK file.
+ */
+ public static VerifiedSigner plsCertsNoVerifyOnlyCerts(String apkFile)
+ throws SignatureNotFoundException, SecurityException, IOException {
+ return verify(apkFile, false);
+ }
+
+ private static VerifiedSigner verify(String apkFile, boolean verifyIntegrity)
+ throws SignatureNotFoundException, SecurityException, IOException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
+ return verify(apk, verifyIntegrity);
+ }
+ }
+
+ /**
+ * Verifies APK Signature Scheme v3 signatures of the provided APK and returns the certificates
+ * associated with each signer.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3.
+ * @throws SecurityException if an APK Signature Scheme v3 signature of this APK does not
+ * verify.
+ * @throws IOException if an I/O error occurs while reading the APK file.
+ */
+ private static VerifiedSigner verify(RandomAccessFile apk, boolean verifyIntegrity)
+ throws SignatureNotFoundException, SecurityException, IOException {
+ SignatureInfo signatureInfo = findSignature(apk);
+ return verify(apk.getFD(), signatureInfo, verifyIntegrity);
+ }
+
+ /**
+ * Returns the APK Signature Scheme v3 block contained in the provided APK file and the
+ * additional information relevant for verifying the block against the file.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3.
+ * @throws IOException if an I/O error occurs while reading the APK file.
+ */
+ private static SignatureInfo findSignature(RandomAccessFile apk)
+ throws IOException, SignatureNotFoundException {
+ return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ }
+
+ /**
+ * Verifies the contents of the provided APK file against the provided APK Signature Scheme v3
+ * Block.
+ *
+ * @param signatureInfo APK Signature Scheme v3 Block and information relevant for verifying it
+ * against the APK file.
+ */
+ private static VerifiedSigner verify(
+ FileDescriptor apkFileDescriptor,
+ SignatureInfo signatureInfo,
+ boolean doVerifyIntegrity) throws SecurityException {
+ int signerCount = 0;
+ Map<Integer, byte[]> contentDigests = new ArrayMap<>();
+ VerifiedSigner result = null;
+ CertificateFactory certFactory;
+ try {
+ certFactory = CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+ }
+ ByteBuffer signers;
+ try {
+ signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
+ } catch (IOException e) {
+ throw new SecurityException("Failed to read list of signers", e);
+ }
+ while (signers.hasRemaining()) {
+ try {
+ ByteBuffer signer = getLengthPrefixedSlice(signers);
+ result = verifySigner(signer, contentDigests, certFactory);
+ signerCount++;
+ } catch (PlatformNotSupportedException e) {
+ // this signer is for a different platform, ignore it.
+ continue;
+ } catch (IOException | BufferUnderflowException | SecurityException e) {
+ throw new SecurityException(
+ "Failed to parse/verify signer #" + signerCount + " block",
+ e);
+ }
+ }
+
+ if (signerCount < 1 || result == null) {
+ throw new SecurityException("No signers found");
+ }
+
+ if (signerCount != 1) {
+ throw new SecurityException("APK Signature Scheme V3 only supports one signer: "
+ + "multiple signers found.");
+ }
+
+ if (contentDigests.isEmpty()) {
+ throw new SecurityException("No content digests found");
+ }
+
+ if (doVerifyIntegrity) {
+ ApkSigningBlockUtils.verifyIntegrity(
+ contentDigests,
+ apkFileDescriptor,
+ signatureInfo.apkSigningBlockOffset,
+ signatureInfo.centralDirOffset,
+ signatureInfo.eocdOffset,
+ signatureInfo.eocd);
+ }
+
+ return result;
+ }
+
+ private static VerifiedSigner verifySigner(
+ ByteBuffer signerBlock,
+ Map<Integer, byte[]> contentDigests,
+ CertificateFactory certFactory)
+ throws SecurityException, IOException, PlatformNotSupportedException {
+ ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
+ int minSdkVersion = signerBlock.getInt();
+ int maxSdkVersion = signerBlock.getInt();
+
+ if (Build.VERSION.SDK_INT < minSdkVersion || Build.VERSION.SDK_INT > maxSdkVersion) {
+ // this signature isn't meant to be used with this platform, skip it.
+ throw new PlatformNotSupportedException(
+ "Signer not supported by this platform "
+ + "version. This platform: " + Build.VERSION.SDK_INT
+ + ", signer minSdkVersion: " + minSdkVersion
+ + ", maxSdkVersion: " + maxSdkVersion);
+ }
+
+ ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
+ byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
+
+ int signatureCount = 0;
+ int bestSigAlgorithm = -1;
+ byte[] bestSigAlgorithmSignatureBytes = null;
+ List<Integer> signaturesSigAlgorithms = new ArrayList<>();
+ while (signatures.hasRemaining()) {
+ signatureCount++;
+ try {
+ ByteBuffer signature = getLengthPrefixedSlice(signatures);
+ if (signature.remaining() < 8) {
+ throw new SecurityException("Signature record too short");
+ }
+ int sigAlgorithm = signature.getInt();
+ signaturesSigAlgorithms.add(sigAlgorithm);
+ if (!isSupportedSignatureAlgorithm(sigAlgorithm)) {
+ continue;
+ }
+ if ((bestSigAlgorithm == -1)
+ || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) {
+ bestSigAlgorithm = sigAlgorithm;
+ bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature);
+ }
+ } catch (IOException | BufferUnderflowException e) {
+ throw new SecurityException(
+ "Failed to parse signature record #" + signatureCount,
+ e);
+ }
+ }
+ if (bestSigAlgorithm == -1) {
+ if (signatureCount == 0) {
+ throw new SecurityException("No signatures found");
+ } else {
+ throw new SecurityException("No supported signatures found");
+ }
+ }
+
+ String keyAlgorithm = getSignatureAlgorithmJcaKeyAlgorithm(bestSigAlgorithm);
+ Pair<String, ? extends AlgorithmParameterSpec> signatureAlgorithmParams =
+ getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm);
+ String jcaSignatureAlgorithm = signatureAlgorithmParams.first;
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second;
+ boolean sigVerified;
+ try {
+ PublicKey publicKey =
+ KeyFactory.getInstance(keyAlgorithm)
+ .generatePublic(new X509EncodedKeySpec(publicKeyBytes));
+ Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+ sig.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ sig.setParameter(jcaSignatureAlgorithmParams);
+ }
+ sig.update(signedData);
+ sigVerified = sig.verify(bestSigAlgorithmSignatureBytes);
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException
+ | InvalidAlgorithmParameterException | SignatureException e) {
+ throw new SecurityException(
+ "Failed to verify " + jcaSignatureAlgorithm + " signature", e);
+ }
+ if (!sigVerified) {
+ throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify");
+ }
+
+ // Signature over signedData has verified.
+
+ byte[] contentDigest = null;
+ signedData.clear();
+ ByteBuffer digests = getLengthPrefixedSlice(signedData);
+ List<Integer> digestsSigAlgorithms = new ArrayList<>();
+ int digestCount = 0;
+ while (digests.hasRemaining()) {
+ digestCount++;
+ try {
+ ByteBuffer digest = getLengthPrefixedSlice(digests);
+ if (digest.remaining() < 8) {
+ throw new IOException("Record too short");
+ }
+ int sigAlgorithm = digest.getInt();
+ digestsSigAlgorithms.add(sigAlgorithm);
+ if (sigAlgorithm == bestSigAlgorithm) {
+ contentDigest = readLengthPrefixedByteArray(digest);
+ }
+ } catch (IOException | BufferUnderflowException e) {
+ throw new IOException("Failed to parse digest record #" + digestCount, e);
+ }
+ }
+
+ if (!signaturesSigAlgorithms.equals(digestsSigAlgorithms)) {
+ throw new SecurityException(
+ "Signature algorithms don't match between digests and signatures records");
+ }
+ int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(bestSigAlgorithm);
+ byte[] previousSignerDigest = contentDigests.put(digestAlgorithm, contentDigest);
+ if ((previousSignerDigest != null)
+ && (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) {
+ throw new SecurityException(
+ getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
+ + " contents digest does not match the digest specified by a preceding signer");
+ }
+
+ ByteBuffer certificates = getLengthPrefixedSlice(signedData);
+ List<X509Certificate> certs = new ArrayList<>();
+ int certificateCount = 0;
+ while (certificates.hasRemaining()) {
+ certificateCount++;
+ byte[] encodedCert = readLengthPrefixedByteArray(certificates);
+ X509Certificate certificate;
+ try {
+ certificate = (X509Certificate)
+ certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
+ } catch (CertificateException e) {
+ throw new SecurityException("Failed to decode certificate #" + certificateCount, e);
+ }
+ certificate = new VerbatimX509Certificate(
+ certificate, encodedCert);
+ certs.add(certificate);
+ }
+
+ if (certs.isEmpty()) {
+ throw new SecurityException("No certificates listed");
+ }
+ X509Certificate mainCertificate = certs.get(0);
+ byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
+ if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
+ throw new SecurityException(
+ "Public key mismatch between certificate and signature record");
+ }
+
+ int signedMinSDK = signedData.getInt();
+ if (signedMinSDK != minSdkVersion) {
+ throw new SecurityException(
+ "minSdkVersion mismatch between signed and unsigned in v3 signer block.");
+ }
+
+ int signedMaxSDK = signedData.getInt();
+ if (signedMaxSDK != maxSdkVersion) {
+ throw new SecurityException(
+ "maxSdkVersion mismatch between signed and unsigned in v3 signer block.");
+ }
+
+ ByteBuffer additionalAttrs = getLengthPrefixedSlice(signedData);
+ return verifyAdditionalAttributes(additionalAttrs, certs, certFactory);
+ }
+
+ private static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
+
+ private static VerifiedSigner verifyAdditionalAttributes(ByteBuffer attrs,
+ List<X509Certificate> certs, CertificateFactory certFactory) throws IOException {
+ X509Certificate[] certChain = certs.toArray(new X509Certificate[certs.size()]);
+ VerifiedProofOfRotation por = null;
+
+ while (attrs.hasRemaining()) {
+ ByteBuffer attr = getLengthPrefixedSlice(attrs);
+ if (attr.remaining() < 4) {
+ throw new IOException("Remaining buffer too short to contain additional attribute "
+ + "ID. Remaining: " + attr.remaining());
+ }
+ int id = attr.getInt();
+ switch(id) {
+ case PROOF_OF_ROTATION_ATTR_ID:
+ if (por != null) {
+ throw new SecurityException("Encountered multiple Proof-of-rotation records"
+ + " when verifying APK Signature Scheme v3 signature");
+ }
+ por = verifyProofOfRotationStruct(attr, certFactory);
+ // make sure that the last certificate in the Proof-of-rotation record matches
+ // the one used to sign this APK.
+ try {
+ if (por.certs.size() > 0
+ && !Arrays.equals(por.certs.get(por.certs.size() - 1).getEncoded(),
+ certChain[0].getEncoded())) {
+ throw new SecurityException("Terminal certificate in Proof-of-rotation"
+ + " record does not match APK signing certificate");
+ }
+ } catch (CertificateEncodingException e) {
+ throw new SecurityException("Failed to encode certificate when comparing"
+ + " Proof-of-rotation record and signing certificate", e);
+ }
+
+ break;
+ default:
+ // not the droid we're looking for, move along, move along.
+ break;
+ }
+ }
+ return new VerifiedSigner(certChain, por);
+ }
+
+ private static VerifiedProofOfRotation verifyProofOfRotationStruct(
+ ByteBuffer porBuf,
+ CertificateFactory certFactory)
+ throws SecurityException, IOException {
+ int levelCount = 0;
+ int lastSigAlgorithm = -1;
+ X509Certificate lastCert = null;
+ List<X509Certificate> certs = new ArrayList<>();
+ List<Integer> flagsList = new ArrayList<>();
+
+ // Proof-of-rotation struct:
+ // is basically a singly linked list of nodes, called levels here, each of which have the
+ // following structure:
+ // * length-prefix for the entire level
+ // - length-prefixed signed data (if previous level exists)
+ // * length-prefixed X509 Certificate
+ // * uint32 signature algorithm ID describing how this signed data was signed
+ // - uint32 flags describing how to treat the cert contained in this level
+ // - uint32 signature algorithm ID to use to verify the signature of the next level. The
+ // algorithm here must match the one in the signed data section of the next level.
+ // - length-prefixed signature over the signed data in this level. The signature here
+ // is verified using the certificate from the previous level.
+ // The linking is provided by the certificate of each level signing the one of the next.
+ while (porBuf.hasRemaining()) {
+ levelCount++;
+ try {
+ ByteBuffer level = getLengthPrefixedSlice(porBuf);
+ ByteBuffer signedData = getLengthPrefixedSlice(level);
+ int flags = level.getInt();
+ int sigAlgorithm = level.getInt();
+ byte[] signature = readLengthPrefixedByteArray(level);
+
+ if (lastCert != null) {
+ // Use previous level cert to verify current level
+ Pair<String, ? extends AlgorithmParameterSpec> sigAlgParams =
+ getSignatureAlgorithmJcaSignatureAlgorithm(lastSigAlgorithm);
+ PublicKey publicKey = lastCert.getPublicKey();
+ Signature sig = Signature.getInstance(sigAlgParams.first);
+ sig.initVerify(publicKey);
+ if (sigAlgParams.second != null) {
+ sig.setParameter(sigAlgParams.second);
+ }
+ sig.update(signedData);
+ if (!sig.verify(signature)) {
+ throw new SecurityException("Unable to verify signature of certificate #"
+ + levelCount + " using " + sigAlgParams.first + " when verifying"
+ + " Proof-of-rotation record");
+ }
+ }
+
+ byte[] encodedCert = readLengthPrefixedByteArray(signedData);
+ int signedSigAlgorithm = signedData.getInt();
+ if (lastCert != null && lastSigAlgorithm != signedSigAlgorithm) {
+ throw new SecurityException("Signing algorithm ID mismatch for certificate #"
+ + levelCount + " when verifying Proof-of-rotation record");
+ }
+ lastCert = (X509Certificate)
+ certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
+ lastCert = new VerbatimX509Certificate(lastCert, encodedCert);
+
+ lastSigAlgorithm = sigAlgorithm;
+ certs.add(lastCert);
+ flagsList.add(flags);
+ } catch (IOException | BufferUnderflowException e) {
+ throw new IOException("Failed to parse Proof-of-rotation record", e);
+ } catch (NoSuchAlgorithmException | InvalidKeyException
+ | InvalidAlgorithmParameterException | SignatureException e) {
+ throw new SecurityException(
+ "Failed to verify signature over signed data for certificate #"
+ + levelCount + " when verifying Proof-of-rotation record", e);
+ } catch (CertificateException e) {
+ throw new SecurityException("Failed to decode certificate #" + levelCount
+ + " when verifying Proof-of-rotation record", e);
+ }
+ }
+ return new VerifiedProofOfRotation(certs, flagsList);
+ }
+
+ private static boolean isSupportedSignatureAlgorithm(int sigAlgorithm) {
+ switch (sigAlgorithm) {
+ case SIGNATURE_RSA_PSS_WITH_SHA256:
+ case SIGNATURE_RSA_PSS_WITH_SHA512:
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
+ case SIGNATURE_ECDSA_WITH_SHA256:
+ case SIGNATURE_ECDSA_WITH_SHA512:
+ case SIGNATURE_DSA_WITH_SHA256:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Verified processed proof of rotation.
+ *
+ * @hide for internal use only.
+ */
+ public static class VerifiedProofOfRotation {
+ public final List<X509Certificate> certs;
+ public final List<Integer> flagsList;
+
+ public VerifiedProofOfRotation(List<X509Certificate> certs, List<Integer> flagsList) {
+ this.certs = certs;
+ this.flagsList = flagsList;
+ }
+ }
+
+ /**
+ * Verified APK Signature Scheme v3 signer, including the proof of rotation structure.
+ *
+ * @hide for internal use only.
+ */
+ public static class VerifiedSigner {
+ public final X509Certificate[] certs;
+ public final VerifiedProofOfRotation por;
+
+ public VerifiedSigner(X509Certificate[] certs, VerifiedProofOfRotation por) {
+ this.certs = certs;
+ this.por = por;
+ }
+
+ }
+
+ private static class PlatformNotSupportedException extends Exception {
+
+ PlatformNotSupportedException(String s) {
+ super(s);
+ }
+ }
+}
diff --git a/android/util/apk/ApkSignatureVerifier.java b/android/util/apk/ApkSignatureVerifier.java
new file mode 100644
index 00000000..81467292
--- /dev/null
+++ b/android/util/apk/ApkSignatureVerifier.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST;
+import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING;
+import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES;
+import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
+import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION;
+import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
+
+import android.content.pm.PackageParser;
+import android.content.pm.PackageParser.PackageParserException;
+import android.content.pm.Signature;
+import android.os.Trace;
+import android.util.jar.StrictJarFile;
+
+import com.android.internal.util.ArrayUtils;
+
+import libcore.io.IoUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.zip.ZipEntry;
+
+/**
+ * Facade class that takes care of the details of APK verification on
+ * behalf of PackageParser.
+ *
+ * @hide for internal use only.
+ */
+public class ApkSignatureVerifier {
+
+ public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
+ public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
+
+ private static final AtomicReference<byte[]> sBuffer = new AtomicReference<>();
+
+ /**
+ * Verifies the provided APK and returns the certificates associated with each signer.
+ *
+ * @throws PackageParserException if the APK's signature failed to verify.
+ */
+ public static Result verify(String apkPath, int minSignatureSchemeVersion)
+ throws PackageParserException {
+
+ if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V3) {
+ // V3 and before are older than the requested minimum signing version
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No signature found in package of version " + minSignatureSchemeVersion
+ + " or newer for package " + apkPath);
+ }
+
+ // first try v3
+ Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV3");
+ try {
+ ApkSignatureSchemeV3Verifier.VerifiedSigner vSigner =
+ ApkSignatureSchemeV3Verifier.verify(apkPath);
+ Certificate[][] signerCerts = new Certificate[][] { vSigner.certs };
+ Signature[] signerSigs = convertToSignatures(signerCerts);
+ return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V3);
+ } catch (SignatureNotFoundException e) {
+ // not signed with v2, try older if allowed
+ if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V3) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No APK Signature Scheme v3 signature in package " + apkPath, e);
+ }
+ } catch (Exception e) {
+ // APK Signature Scheme v2 signature found but did not verify
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "Failed to collect certificates from " + apkPath
+ + " using APK Signature Scheme v2", e);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+ }
+
+ // redundant, protective version check
+ if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V2) {
+ // V2 and before are older than the requested minimum signing version
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No signature found in package of version " + minSignatureSchemeVersion
+ + " or newer for package " + apkPath);
+ }
+
+ // try v2
+ Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV2");
+ try {
+ Certificate[][] signerCerts = ApkSignatureSchemeV2Verifier.verify(apkPath);
+ Signature[] signerSigs = convertToSignatures(signerCerts);
+
+ return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V2);
+ } catch (SignatureNotFoundException e) {
+ // not signed with v2, try older if allowed
+ if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V2) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No APK Signature Scheme v2 signature in package " + apkPath, e);
+ }
+ } catch (Exception e) {
+ // APK Signature Scheme v2 signature found but did not verify
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "Failed to collect certificates from " + apkPath
+ + " using APK Signature Scheme v2", e);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+ }
+
+ // redundant, protective version check
+ if (minSignatureSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME) {
+ // V1 and is older than the requested minimum signing version
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No signature found in package of version " + minSignatureSchemeVersion
+ + " or newer for package " + apkPath);
+ }
+
+ // v2 didn't work, try jarsigner
+ return verifyV1Signature(apkPath, true);
+ }
+
+ /**
+ * Verifies the provided APK and returns the certificates associated with each signer.
+ *
+ * @param verifyFull whether to verify all contents of this APK or just collect certificates.
+ *
+ * @throws PackageParserException if there was a problem collecting certificates
+ */
+ private static Result verifyV1Signature(String apkPath, boolean verifyFull)
+ throws PackageParserException {
+ StrictJarFile jarFile = null;
+
+ try {
+ final Certificate[][] lastCerts;
+ final Signature[] lastSigs;
+
+ Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "strictJarFileCtor");
+
+ // we still pass verify = true to ctor to collect certs, even though we're not checking
+ // the whole jar.
+ jarFile = new StrictJarFile(
+ apkPath,
+ true, // collect certs
+ verifyFull); // whether to reject APK with stripped v2 signatures (b/27887819)
+ final List<ZipEntry> toVerify = new ArrayList<>();
+
+ // Gather certs from AndroidManifest.xml, which every APK must have, as an optimization
+ // to not need to verify the whole APK when verifyFUll == false.
+ final ZipEntry manifestEntry = jarFile.findEntry(
+ PackageParser.ANDROID_MANIFEST_FILENAME);
+ if (manifestEntry == null) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
+ "Package " + apkPath + " has no manifest");
+ }
+ lastCerts = loadCertificates(jarFile, manifestEntry);
+ if (ArrayUtils.isEmpty(lastCerts)) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, "Package "
+ + apkPath + " has no certificates at entry "
+ + PackageParser.ANDROID_MANIFEST_FILENAME);
+ }
+ lastSigs = convertToSignatures(lastCerts);
+
+ // fully verify all contents, except for AndroidManifest.xml and the META-INF/ files.
+ if (verifyFull) {
+ final Iterator<ZipEntry> i = jarFile.iterator();
+ while (i.hasNext()) {
+ final ZipEntry entry = i.next();
+ if (entry.isDirectory()) continue;
+
+ final String entryName = entry.getName();
+ if (entryName.startsWith("META-INF/")) continue;
+ if (entryName.equals(PackageParser.ANDROID_MANIFEST_FILENAME)) continue;
+
+ toVerify.add(entry);
+ }
+
+ for (ZipEntry entry : toVerify) {
+ final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
+ if (ArrayUtils.isEmpty(entryCerts)) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "Package " + apkPath + " has no certificates at entry "
+ + entry.getName());
+ }
+
+ // make sure all entries use the same signing certs
+ final Signature[] entrySigs = convertToSignatures(entryCerts);
+ if (!Signature.areExactMatch(lastSigs, entrySigs)) {
+ throw new PackageParserException(
+ INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
+ "Package " + apkPath + " has mismatched certificates at entry "
+ + entry.getName());
+ }
+ }
+ }
+ return new Result(lastCerts, lastSigs, VERSION_JAR_SIGNATURE_SCHEME);
+ } catch (GeneralSecurityException e) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
+ "Failed to collect certificates from " + apkPath, e);
+ } catch (IOException | RuntimeException e) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "Failed to collect certificates from " + apkPath, e);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+ closeQuietly(jarFile);
+ }
+ }
+
+ private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry)
+ throws PackageParserException {
+ InputStream is = null;
+ try {
+ // We must read the stream for the JarEntry to retrieve
+ // its certificates.
+ is = jarFile.getInputStream(entry);
+ readFullyIgnoringContents(is);
+ return jarFile.getCertificateChains(entry);
+ } catch (IOException | RuntimeException e) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
+ "Failed reading " + entry.getName() + " in " + jarFile, e);
+ } finally {
+ IoUtils.closeQuietly(is);
+ }
+ }
+
+ private static void readFullyIgnoringContents(InputStream in) throws IOException {
+ byte[] buffer = sBuffer.getAndSet(null);
+ if (buffer == null) {
+ buffer = new byte[4096];
+ }
+
+ int n = 0;
+ int count = 0;
+ while ((n = in.read(buffer, 0, buffer.length)) != -1) {
+ count += n;
+ }
+
+ sBuffer.set(buffer);
+ return;
+ }
+
+ /**
+ * Converts an array of certificate chains into the {@code Signature} equivalent used by the
+ * PackageManager.
+ *
+ * @throws CertificateEncodingException if it is unable to create a Signature object.
+ */
+ public static Signature[] convertToSignatures(Certificate[][] certs)
+ throws CertificateEncodingException {
+ final Signature[] res = new Signature[certs.length];
+ for (int i = 0; i < certs.length; i++) {
+ res[i] = new Signature(certs[i]);
+ }
+ return res;
+ }
+
+ private static void closeQuietly(StrictJarFile jarFile) {
+ if (jarFile != null) {
+ try {
+ jarFile.close();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ /**
+ * Returns the certificates associated with each signer for the given APK without verification.
+ * This method is dangerous and should not be used, unless the caller is absolutely certain the
+ * APK is trusted.
+ *
+ * @throws PackageParserException if the APK's signature failed to verify.
+ * or greater is not found, except in the case of no JAR signature.
+ */
+ public static Result plsCertsNoVerifyOnlyCerts(String apkPath, int minSignatureSchemeVersion)
+ throws PackageParserException {
+
+ if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V3) {
+ // V3 and before are older than the requested minimum signing version
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No signature found in package of version " + minSignatureSchemeVersion
+ + " or newer for package " + apkPath);
+ }
+
+ // first try v3
+ Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV3");
+ try {
+ ApkSignatureSchemeV3Verifier.VerifiedSigner vSigner =
+ ApkSignatureSchemeV3Verifier.plsCertsNoVerifyOnlyCerts(apkPath);
+ Certificate[][] signerCerts = new Certificate[][] { vSigner.certs };
+ Signature[] signerSigs = convertToSignatures(signerCerts);
+ return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V3);
+ } catch (SignatureNotFoundException e) {
+ // not signed with v2, try older if allowed
+ if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V3) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No APK Signature Scheme v3 signature in package " + apkPath, e);
+ }
+ } catch (Exception e) {
+ // APK Signature Scheme v2 signature found but did not verify
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "Failed to collect certificates from " + apkPath
+ + " using APK Signature Scheme v2", e);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+ }
+
+ // redundant, protective version check
+ if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V2) {
+ // V2 and before are older than the requested minimum signing version
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No signature found in package of version " + minSignatureSchemeVersion
+ + " or newer for package " + apkPath);
+ }
+
+ // first try v2
+ Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "certsOnlyV2");
+ try {
+ Certificate[][] signerCerts =
+ ApkSignatureSchemeV2Verifier.plsCertsNoVerifyOnlyCerts(apkPath);
+ Signature[] signerSigs = convertToSignatures(signerCerts);
+ return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V2);
+ } catch (SignatureNotFoundException e) {
+ // not signed with v2, try older if allowed
+ if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V2) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No APK Signature Scheme v2 signature in package " + apkPath, e);
+ }
+ } catch (Exception e) {
+ // APK Signature Scheme v2 signature found but did not verify
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "Failed to collect certificates from " + apkPath
+ + " using APK Signature Scheme v2", e);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+ }
+
+ // redundant, protective version check
+ if (minSignatureSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME) {
+ // V1 and is older than the requested minimum signing version
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No signature found in package of version " + minSignatureSchemeVersion
+ + " or newer for package " + apkPath);
+ }
+
+ // v2 didn't work, try jarsigner
+ return verifyV1Signature(apkPath, false);
+ }
+
+ /**
+ * Result of a successful APK verification operation.
+ */
+ public static class Result {
+ public final Certificate[][] certs;
+ public final Signature[] sigs;
+ public final int signatureSchemeVersion;
+
+ public Result(Certificate[][] certs, Signature[] sigs, int signingVersion) {
+ this.certs = certs;
+ this.sigs = sigs;
+ this.signatureSchemeVersion = signingVersion;
+ }
+ }
+}
diff --git a/android/util/apk/ApkSigningBlockUtils.java b/android/util/apk/ApkSigningBlockUtils.java
new file mode 100644
index 00000000..9279510a
--- /dev/null
+++ b/android/util/apk/ApkSigningBlockUtils.java
@@ -0,0 +1,663 @@
+/*
+ * 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.util.apk;
+
+import android.util.Pair;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.DigestException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.MGF1ParameterSpec;
+import java.security.spec.PSSParameterSpec;
+import java.util.Map;
+
+/**
+ * Utility class for an APK Signature Scheme using the APK Signing Block.
+ *
+ * @hide for internal use only.
+ */
+final class ApkSigningBlockUtils {
+
+ private ApkSigningBlockUtils() {
+ }
+
+ /**
+ * Returns the APK Signature Scheme block contained in the provided APK file and the
+ * additional information relevant for verifying the block against the file.
+ *
+ * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
+ * identifying the appropriate block to find, e.g. the APK Signature Scheme v2
+ * block ID.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using this scheme.
+ * @throws IOException if an I/O error occurs while reading the APK file.
+ */
+ static SignatureInfo findSignature(RandomAccessFile apk, int blockId)
+ throws IOException, SignatureNotFoundException {
+ // Find the ZIP End of Central Directory (EoCD) record.
+ Pair<ByteBuffer, Long> eocdAndOffsetInFile = getEocd(apk);
+ ByteBuffer eocd = eocdAndOffsetInFile.first;
+ long eocdOffset = eocdAndOffsetInFile.second;
+ if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
+ throw new SignatureNotFoundException("ZIP64 APK not supported");
+ }
+
+ // Find the APK Signing Block. The block immediately precedes the Central Directory.
+ long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
+ Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile =
+ findApkSigningBlock(apk, centralDirOffset);
+ ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first;
+ long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second;
+
+ // Find the APK Signature Scheme Block inside the APK Signing Block.
+ ByteBuffer apkSignatureSchemeBlock = findApkSignatureSchemeBlock(apkSigningBlock,
+ blockId);
+
+ return new SignatureInfo(
+ apkSignatureSchemeBlock,
+ apkSigningBlockOffset,
+ centralDirOffset,
+ eocdOffset,
+ eocd);
+ }
+
+ static void verifyIntegrity(
+ Map<Integer, byte[]> expectedDigests,
+ FileDescriptor apkFileDescriptor,
+ long apkSigningBlockOffset,
+ long centralDirOffset,
+ long eocdOffset,
+ ByteBuffer eocdBuf) throws SecurityException {
+
+ if (expectedDigests.isEmpty()) {
+ throw new SecurityException("No digests provided");
+ }
+
+ // We need to verify the integrity of the following three sections of the file:
+ // 1. Everything up to the start of the APK Signing Block.
+ // 2. ZIP Central Directory.
+ // 3. ZIP End of Central Directory (EoCD).
+ // Each of these sections is represented as a separate DataSource instance below.
+
+ // To handle large APKs, these sections are read in 1 MB chunks using memory-mapped I/O to
+ // avoid wasting physical memory. In most APK verification scenarios, the contents of the
+ // APK are already there in the OS's page cache and thus mmap does not use additional
+ // physical memory.
+ DataSource beforeApkSigningBlock =
+ new MemoryMappedFileDataSource(apkFileDescriptor, 0, apkSigningBlockOffset);
+ DataSource centralDir =
+ new MemoryMappedFileDataSource(
+ apkFileDescriptor, centralDirOffset, eocdOffset - centralDirOffset);
+
+ // For the purposes of integrity verification, ZIP End of Central Directory's field Start of
+ // Central Directory must be considered to point to the offset of the APK Signing Block.
+ eocdBuf = eocdBuf.duplicate();
+ eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
+ ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, apkSigningBlockOffset);
+ DataSource eocd = new ByteBufferDataSource(eocdBuf);
+
+ int[] digestAlgorithms = new int[expectedDigests.size()];
+ int digestAlgorithmCount = 0;
+ for (int digestAlgorithm : expectedDigests.keySet()) {
+ digestAlgorithms[digestAlgorithmCount] = digestAlgorithm;
+ digestAlgorithmCount++;
+ }
+ byte[][] actualDigests;
+ try {
+ actualDigests =
+ computeContentDigests(
+ digestAlgorithms,
+ new DataSource[] {beforeApkSigningBlock, centralDir, eocd});
+ } catch (DigestException e) {
+ throw new SecurityException("Failed to compute digest(s) of contents", e);
+ }
+ for (int i = 0; i < digestAlgorithms.length; i++) {
+ int digestAlgorithm = digestAlgorithms[i];
+ byte[] expectedDigest = expectedDigests.get(digestAlgorithm);
+ byte[] actualDigest = actualDigests[i];
+ if (!MessageDigest.isEqual(expectedDigest, actualDigest)) {
+ throw new SecurityException(
+ getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
+ + " digest of contents did not verify");
+ }
+ }
+ }
+
+ private static byte[][] computeContentDigests(
+ int[] digestAlgorithms,
+ DataSource[] contents) throws DigestException {
+ // For each digest algorithm the result is computed as follows:
+ // 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
+ // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
+ // No chunks are produced for empty (zero length) segments.
+ // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's
+ // length in bytes (uint32 little-endian) and the chunk's contents.
+ // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of
+ // chunks (uint32 little-endian) and the concatenation of digests of chunks of all
+ // segments in-order.
+
+ long totalChunkCountLong = 0;
+ for (DataSource input : contents) {
+ totalChunkCountLong += getChunkCount(input.size());
+ }
+ if (totalChunkCountLong >= Integer.MAX_VALUE / 1024) {
+ throw new DigestException("Too many chunks: " + totalChunkCountLong);
+ }
+ int totalChunkCount = (int) totalChunkCountLong;
+
+ byte[][] digestsOfChunks = new byte[digestAlgorithms.length][];
+ for (int i = 0; i < digestAlgorithms.length; i++) {
+ int digestAlgorithm = digestAlgorithms[i];
+ int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
+ byte[] concatenationOfChunkCountAndChunkDigests =
+ new byte[5 + totalChunkCount * digestOutputSizeBytes];
+ concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
+ setUnsignedInt32LittleEndian(
+ totalChunkCount,
+ concatenationOfChunkCountAndChunkDigests,
+ 1);
+ digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests;
+ }
+
+ byte[] chunkContentPrefix = new byte[5];
+ chunkContentPrefix[0] = (byte) 0xa5;
+ int chunkIndex = 0;
+ MessageDigest[] mds = new MessageDigest[digestAlgorithms.length];
+ for (int i = 0; i < digestAlgorithms.length; i++) {
+ String jcaAlgorithmName =
+ getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithms[i]);
+ try {
+ mds[i] = MessageDigest.getInstance(jcaAlgorithmName);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(jcaAlgorithmName + " digest not supported", e);
+ }
+ }
+ // TODO: Compute digests of chunks in parallel when beneficial. This requires some research
+ // into how to parallelize (if at all) based on the capabilities of the hardware on which
+ // this code is running and based on the size of input.
+ DataDigester digester = new MultipleDigestDataDigester(mds);
+ int dataSourceIndex = 0;
+ for (DataSource input : contents) {
+ long inputOffset = 0;
+ long inputRemaining = input.size();
+ while (inputRemaining > 0) {
+ int chunkSize = (int) Math.min(inputRemaining, CHUNK_SIZE_BYTES);
+ setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1);
+ for (int i = 0; i < mds.length; i++) {
+ mds[i].update(chunkContentPrefix);
+ }
+ try {
+ input.feedIntoDataDigester(digester, inputOffset, chunkSize);
+ } catch (IOException e) {
+ throw new DigestException(
+ "Failed to digest chunk #" + chunkIndex + " of section #"
+ + dataSourceIndex,
+ e);
+ }
+ for (int i = 0; i < digestAlgorithms.length; i++) {
+ int digestAlgorithm = digestAlgorithms[i];
+ byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
+ int expectedDigestSizeBytes =
+ getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
+ MessageDigest md = mds[i];
+ int actualDigestSizeBytes =
+ md.digest(
+ concatenationOfChunkCountAndChunkDigests,
+ 5 + chunkIndex * expectedDigestSizeBytes,
+ expectedDigestSizeBytes);
+ if (actualDigestSizeBytes != expectedDigestSizeBytes) {
+ throw new RuntimeException(
+ "Unexpected output size of " + md.getAlgorithm() + " digest: "
+ + actualDigestSizeBytes);
+ }
+ }
+ inputOffset += chunkSize;
+ inputRemaining -= chunkSize;
+ chunkIndex++;
+ }
+ dataSourceIndex++;
+ }
+
+ byte[][] result = new byte[digestAlgorithms.length][];
+ for (int i = 0; i < digestAlgorithms.length; i++) {
+ int digestAlgorithm = digestAlgorithms[i];
+ byte[] input = digestsOfChunks[i];
+ String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance(jcaAlgorithmName);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(jcaAlgorithmName + " digest not supported", e);
+ }
+ byte[] output = md.digest(input);
+ result[i] = output;
+ }
+ return result;
+ }
+
+ /**
+ * Returns the ZIP End of Central Directory (EoCD) and its offset in the file.
+ *
+ * @throws IOException if an I/O error occurs while reading the file.
+ * @throws SignatureNotFoundException if the EoCD could not be found.
+ */
+ static Pair<ByteBuffer, Long> getEocd(RandomAccessFile apk)
+ throws IOException, SignatureNotFoundException {
+ Pair<ByteBuffer, Long> eocdAndOffsetInFile =
+ ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
+ if (eocdAndOffsetInFile == null) {
+ throw new SignatureNotFoundException(
+ "Not an APK file: ZIP End of Central Directory record not found");
+ }
+ return eocdAndOffsetInFile;
+ }
+
+ static long getCentralDirOffset(ByteBuffer eocd, long eocdOffset)
+ throws SignatureNotFoundException {
+ // Look up the offset of ZIP Central Directory.
+ long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd);
+ if (centralDirOffset > eocdOffset) {
+ throw new SignatureNotFoundException(
+ "ZIP Central Directory offset out of range: " + centralDirOffset
+ + ". ZIP End of Central Directory offset: " + eocdOffset);
+ }
+ long centralDirSize = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocd);
+ if (centralDirOffset + centralDirSize != eocdOffset) {
+ throw new SignatureNotFoundException(
+ "ZIP Central Directory is not immediately followed by End of Central"
+ + " Directory");
+ }
+ return centralDirOffset;
+ }
+
+ private static long getChunkCount(long inputSizeBytes) {
+ return (inputSizeBytes + CHUNK_SIZE_BYTES - 1) / CHUNK_SIZE_BYTES;
+ }
+
+ private static final int CHUNK_SIZE_BYTES = 1024 * 1024;
+
+ static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101;
+ static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102;
+ static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103;
+ static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104;
+ static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201;
+ static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202;
+ static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301;
+
+ static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1;
+ static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2;
+
+ static int compareSignatureAlgorithm(int sigAlgorithm1, int sigAlgorithm2) {
+ int digestAlgorithm1 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm1);
+ int digestAlgorithm2 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm2);
+ return compareContentDigestAlgorithm(digestAlgorithm1, digestAlgorithm2);
+ }
+
+ private static int compareContentDigestAlgorithm(int digestAlgorithm1, int digestAlgorithm2) {
+ switch (digestAlgorithm1) {
+ case CONTENT_DIGEST_CHUNKED_SHA256:
+ switch (digestAlgorithm2) {
+ case CONTENT_DIGEST_CHUNKED_SHA256:
+ return 0;
+ case CONTENT_DIGEST_CHUNKED_SHA512:
+ return -1;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown digestAlgorithm2: " + digestAlgorithm2);
+ }
+ case CONTENT_DIGEST_CHUNKED_SHA512:
+ switch (digestAlgorithm2) {
+ case CONTENT_DIGEST_CHUNKED_SHA256:
+ return 1;
+ case CONTENT_DIGEST_CHUNKED_SHA512:
+ return 0;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown digestAlgorithm2: " + digestAlgorithm2);
+ }
+ default:
+ throw new IllegalArgumentException("Unknown digestAlgorithm1: " + digestAlgorithm1);
+ }
+ }
+
+ static int getSignatureAlgorithmContentDigestAlgorithm(int sigAlgorithm) {
+ switch (sigAlgorithm) {
+ case SIGNATURE_RSA_PSS_WITH_SHA256:
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_ECDSA_WITH_SHA256:
+ case SIGNATURE_DSA_WITH_SHA256:
+ return CONTENT_DIGEST_CHUNKED_SHA256;
+ case SIGNATURE_RSA_PSS_WITH_SHA512:
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
+ case SIGNATURE_ECDSA_WITH_SHA512:
+ return CONTENT_DIGEST_CHUNKED_SHA512;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown signature algorithm: 0x"
+ + Long.toHexString(sigAlgorithm & 0xffffffff));
+ }
+ }
+
+ static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) {
+ switch (digestAlgorithm) {
+ case CONTENT_DIGEST_CHUNKED_SHA256:
+ return "SHA-256";
+ case CONTENT_DIGEST_CHUNKED_SHA512:
+ return "SHA-512";
+ default:
+ throw new IllegalArgumentException(
+ "Unknown content digest algorthm: " + digestAlgorithm);
+ }
+ }
+
+ private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) {
+ switch (digestAlgorithm) {
+ case CONTENT_DIGEST_CHUNKED_SHA256:
+ return 256 / 8;
+ case CONTENT_DIGEST_CHUNKED_SHA512:
+ return 512 / 8;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown content digest algorthm: " + digestAlgorithm);
+ }
+ }
+
+ static String getSignatureAlgorithmJcaKeyAlgorithm(int sigAlgorithm) {
+ switch (sigAlgorithm) {
+ case SIGNATURE_RSA_PSS_WITH_SHA256:
+ case SIGNATURE_RSA_PSS_WITH_SHA512:
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
+ return "RSA";
+ case SIGNATURE_ECDSA_WITH_SHA256:
+ case SIGNATURE_ECDSA_WITH_SHA512:
+ return "EC";
+ case SIGNATURE_DSA_WITH_SHA256:
+ return "DSA";
+ default:
+ throw new IllegalArgumentException(
+ "Unknown signature algorithm: 0x"
+ + Long.toHexString(sigAlgorithm & 0xffffffff));
+ }
+ }
+
+ static Pair<String, ? extends AlgorithmParameterSpec>
+ getSignatureAlgorithmJcaSignatureAlgorithm(int sigAlgorithm) {
+ switch (sigAlgorithm) {
+ case SIGNATURE_RSA_PSS_WITH_SHA256:
+ return Pair.create(
+ "SHA256withRSA/PSS",
+ new PSSParameterSpec(
+ "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1));
+ case SIGNATURE_RSA_PSS_WITH_SHA512:
+ return Pair.create(
+ "SHA512withRSA/PSS",
+ new PSSParameterSpec(
+ "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1));
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
+ return Pair.create("SHA256withRSA", null);
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
+ return Pair.create("SHA512withRSA", null);
+ case SIGNATURE_ECDSA_WITH_SHA256:
+ return Pair.create("SHA256withECDSA", null);
+ case SIGNATURE_ECDSA_WITH_SHA512:
+ return Pair.create("SHA512withECDSA", null);
+ case SIGNATURE_DSA_WITH_SHA256:
+ return Pair.create("SHA256withDSA", null);
+ default:
+ throw new IllegalArgumentException(
+ "Unknown signature algorithm: 0x"
+ + Long.toHexString(sigAlgorithm & 0xffffffff));
+ }
+ }
+
+ /**
+ * Returns new byte buffer whose content is a shared subsequence of this buffer's content
+ * between the specified start (inclusive) and end (exclusive) positions. As opposed to
+ * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
+ * buffer's byte order.
+ */
+ static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
+ if (start < 0) {
+ throw new IllegalArgumentException("start: " + start);
+ }
+ if (end < start) {
+ throw new IllegalArgumentException("end < start: " + end + " < " + start);
+ }
+ int capacity = source.capacity();
+ if (end > source.capacity()) {
+ throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
+ }
+ int originalLimit = source.limit();
+ int originalPosition = source.position();
+ try {
+ source.position(0);
+ source.limit(end);
+ source.position(start);
+ ByteBuffer result = source.slice();
+ result.order(source.order());
+ return result;
+ } finally {
+ source.position(0);
+ source.limit(originalLimit);
+ source.position(originalPosition);
+ }
+ }
+
+ /**
+ * Relative <em>get</em> method for reading {@code size} number of bytes from the current
+ * position of this buffer.
+ *
+ * <p>This method reads the next {@code size} bytes at this buffer's current position,
+ * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
+ * {@code size}, byte order set to this buffer's byte order; and then increments the position by
+ * {@code size}.
+ */
+ static ByteBuffer getByteBuffer(ByteBuffer source, int size)
+ throws BufferUnderflowException {
+ if (size < 0) {
+ throw new IllegalArgumentException("size: " + size);
+ }
+ int originalLimit = source.limit();
+ int position = source.position();
+ int limit = position + size;
+ if ((limit < position) || (limit > originalLimit)) {
+ throw new BufferUnderflowException();
+ }
+ source.limit(limit);
+ try {
+ ByteBuffer result = source.slice();
+ result.order(source.order());
+ source.position(limit);
+ return result;
+ } finally {
+ source.limit(originalLimit);
+ }
+ }
+
+ static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws IOException {
+ if (source.remaining() < 4) {
+ throw new IOException(
+ "Remaining buffer too short to contain length of length-prefixed field."
+ + " Remaining: " + source.remaining());
+ }
+ int len = source.getInt();
+ if (len < 0) {
+ throw new IllegalArgumentException("Negative length");
+ } else if (len > source.remaining()) {
+ throw new IOException("Length-prefixed field longer than remaining buffer."
+ + " Field length: " + len + ", remaining: " + source.remaining());
+ }
+ return getByteBuffer(source, len);
+ }
+
+ static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws IOException {
+ int len = buf.getInt();
+ if (len < 0) {
+ throw new IOException("Negative length");
+ } else if (len > buf.remaining()) {
+ throw new IOException("Underflow while reading length-prefixed value. Length: " + len
+ + ", available: " + buf.remaining());
+ }
+ byte[] result = new byte[len];
+ buf.get(result);
+ return result;
+ }
+
+ static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) {
+ result[offset] = (byte) (value & 0xff);
+ result[offset + 1] = (byte) ((value >>> 8) & 0xff);
+ result[offset + 2] = (byte) ((value >>> 16) & 0xff);
+ result[offset + 3] = (byte) ((value >>> 24) & 0xff);
+ }
+
+ private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
+ private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
+ private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
+
+ static Pair<ByteBuffer, Long> findApkSigningBlock(
+ RandomAccessFile apk, long centralDirOffset)
+ throws IOException, SignatureNotFoundException {
+ // FORMAT:
+ // OFFSET DATA TYPE DESCRIPTION
+ // * @+0 bytes uint64: size in bytes (excluding this field)
+ // * @+8 bytes payload
+ // * @-24 bytes uint64: size in bytes (same as the one above)
+ // * @-16 bytes uint128: magic
+
+ if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) {
+ throw new SignatureNotFoundException(
+ "APK too small for APK Signing Block. ZIP Central Directory offset: "
+ + centralDirOffset);
+ }
+ // Read the magic and offset in file from the footer section of the block:
+ // * uint64: size of block
+ // * 16 bytes: magic
+ ByteBuffer footer = ByteBuffer.allocate(24);
+ footer.order(ByteOrder.LITTLE_ENDIAN);
+ apk.seek(centralDirOffset - footer.capacity());
+ apk.readFully(footer.array(), footer.arrayOffset(), footer.capacity());
+ if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
+ || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
+ throw new SignatureNotFoundException(
+ "No APK Signing Block before ZIP Central Directory");
+ }
+ // Read and compare size fields
+ long apkSigBlockSizeInFooter = footer.getLong(0);
+ if ((apkSigBlockSizeInFooter < footer.capacity())
+ || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
+ }
+ int totalSize = (int) (apkSigBlockSizeInFooter + 8);
+ long apkSigBlockOffset = centralDirOffset - totalSize;
+ if (apkSigBlockOffset < 0) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block offset out of range: " + apkSigBlockOffset);
+ }
+ ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize);
+ apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
+ apk.seek(apkSigBlockOffset);
+ apk.readFully(apkSigBlock.array(), apkSigBlock.arrayOffset(), apkSigBlock.capacity());
+ long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
+ if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block sizes in header and footer do not match: "
+ + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
+ }
+ return Pair.create(apkSigBlock, apkSigBlockOffset);
+ }
+
+ static ByteBuffer findApkSignatureSchemeBlock(ByteBuffer apkSigningBlock, int blockId)
+ throws SignatureNotFoundException {
+ checkByteOrderLittleEndian(apkSigningBlock);
+ // FORMAT:
+ // OFFSET DATA TYPE DESCRIPTION
+ // * @+0 bytes uint64: size in bytes (excluding this field)
+ // * @+8 bytes pairs
+ // * @-24 bytes uint64: size in bytes (same as the one above)
+ // * @-16 bytes uint128: magic
+ ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
+
+ int entryCount = 0;
+ while (pairs.hasRemaining()) {
+ entryCount++;
+ if (pairs.remaining() < 8) {
+ throw new SignatureNotFoundException(
+ "Insufficient data to read size of APK Signing Block entry #" + entryCount);
+ }
+ long lenLong = pairs.getLong();
+ if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block entry #" + entryCount
+ + " size out of range: " + lenLong);
+ }
+ int len = (int) lenLong;
+ int nextEntryPos = pairs.position() + len;
+ if (len > pairs.remaining()) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block entry #" + entryCount + " size out of range: " + len
+ + ", available: " + pairs.remaining());
+ }
+ int id = pairs.getInt();
+ if (id == blockId) {
+ return getByteBuffer(pairs, len - 4);
+ }
+ pairs.position(nextEntryPos);
+ }
+
+ throw new SignatureNotFoundException(
+ "No block with ID " + blockId + " in APK Signing Block.");
+ }
+
+ private static void checkByteOrderLittleEndian(ByteBuffer buffer) {
+ if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
+ throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
+ }
+ }
+
+ /**
+ * {@link DataDigester} that updates multiple {@link MessageDigest}s whenever data is fed.
+ */
+ private static class MultipleDigestDataDigester implements DataDigester {
+ private final MessageDigest[] mMds;
+
+ MultipleDigestDataDigester(MessageDigest[] mds) {
+ mMds = mds;
+ }
+
+ @Override
+ public void consume(ByteBuffer buffer) {
+ buffer = buffer.slice();
+ for (MessageDigest md : mMds) {
+ buffer.position(0);
+ md.update(buffer);
+ }
+ }
+
+ @Override
+ public void finish() {}
+ }
+
+}
diff --git a/android/util/apk/SignatureNotFoundException.java b/android/util/apk/SignatureNotFoundException.java
new file mode 100644
index 00000000..9c7c7600
--- /dev/null
+++ b/android/util/apk/SignatureNotFoundException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+/**
+ * Indicates that the APK is missing a signature.
+ *
+ * @hide
+ */
+public class SignatureNotFoundException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public SignatureNotFoundException(String message) {
+ super(message);
+ }
+
+ public SignatureNotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/android/util/apk/VerbatimX509Certificate.java b/android/util/apk/VerbatimX509Certificate.java
new file mode 100644
index 00000000..9984c6d2
--- /dev/null
+++ b/android/util/apk/VerbatimX509Certificate.java
@@ -0,0 +1,38 @@
+/*
+ * 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.util.apk;
+
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+
+/**
+ * For legacy reasons we need to return exactly the original encoded certificate bytes, instead
+ * of letting the underlying implementation have a shot at re-encoding the data.
+ */
+class VerbatimX509Certificate extends WrappedX509Certificate {
+ private final byte[] mEncodedVerbatim;
+
+ VerbatimX509Certificate(X509Certificate wrapped, byte[] encodedVerbatim) {
+ super(wrapped);
+ this.mEncodedVerbatim = encodedVerbatim;
+ }
+
+ @Override
+ public byte[] getEncoded() throws CertificateEncodingException {
+ return mEncodedVerbatim;
+ }
+}
diff --git a/android/util/apk/WrappedX509Certificate.java b/android/util/apk/WrappedX509Certificate.java
new file mode 100644
index 00000000..fdaa4202
--- /dev/null
+++ b/android/util/apk/WrappedX509Certificate.java
@@ -0,0 +1,175 @@
+/*
+ * 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.util.apk;
+
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.Principal;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import java.util.Set;
+
+class WrappedX509Certificate extends X509Certificate {
+ private final X509Certificate mWrapped;
+
+ WrappedX509Certificate(X509Certificate wrapped) {
+ this.mWrapped = wrapped;
+ }
+
+ @Override
+ public Set<String> getCriticalExtensionOIDs() {
+ return mWrapped.getCriticalExtensionOIDs();
+ }
+
+ @Override
+ public byte[] getExtensionValue(String oid) {
+ return mWrapped.getExtensionValue(oid);
+ }
+
+ @Override
+ public Set<String> getNonCriticalExtensionOIDs() {
+ return mWrapped.getNonCriticalExtensionOIDs();
+ }
+
+ @Override
+ public boolean hasUnsupportedCriticalExtension() {
+ return mWrapped.hasUnsupportedCriticalExtension();
+ }
+
+ @Override
+ public void checkValidity()
+ throws CertificateExpiredException, CertificateNotYetValidException {
+ mWrapped.checkValidity();
+ }
+
+ @Override
+ public void checkValidity(Date date)
+ throws CertificateExpiredException, CertificateNotYetValidException {
+ mWrapped.checkValidity(date);
+ }
+
+ @Override
+ public int getVersion() {
+ return mWrapped.getVersion();
+ }
+
+ @Override
+ public BigInteger getSerialNumber() {
+ return mWrapped.getSerialNumber();
+ }
+
+ @Override
+ public Principal getIssuerDN() {
+ return mWrapped.getIssuerDN();
+ }
+
+ @Override
+ public Principal getSubjectDN() {
+ return mWrapped.getSubjectDN();
+ }
+
+ @Override
+ public Date getNotBefore() {
+ return mWrapped.getNotBefore();
+ }
+
+ @Override
+ public Date getNotAfter() {
+ return mWrapped.getNotAfter();
+ }
+
+ @Override
+ public byte[] getTBSCertificate() throws CertificateEncodingException {
+ return mWrapped.getTBSCertificate();
+ }
+
+ @Override
+ public byte[] getSignature() {
+ return mWrapped.getSignature();
+ }
+
+ @Override
+ public String getSigAlgName() {
+ return mWrapped.getSigAlgName();
+ }
+
+ @Override
+ public String getSigAlgOID() {
+ return mWrapped.getSigAlgOID();
+ }
+
+ @Override
+ public byte[] getSigAlgParams() {
+ return mWrapped.getSigAlgParams();
+ }
+
+ @Override
+ public boolean[] getIssuerUniqueID() {
+ return mWrapped.getIssuerUniqueID();
+ }
+
+ @Override
+ public boolean[] getSubjectUniqueID() {
+ return mWrapped.getSubjectUniqueID();
+ }
+
+ @Override
+ public boolean[] getKeyUsage() {
+ return mWrapped.getKeyUsage();
+ }
+
+ @Override
+ public int getBasicConstraints() {
+ return mWrapped.getBasicConstraints();
+ }
+
+ @Override
+ public byte[] getEncoded() throws CertificateEncodingException {
+ return mWrapped.getEncoded();
+ }
+
+ @Override
+ public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException,
+ InvalidKeyException, NoSuchProviderException, SignatureException {
+ mWrapped.verify(key);
+ }
+
+ @Override
+ public void verify(PublicKey key, String sigProvider)
+ throws CertificateException, NoSuchAlgorithmException, InvalidKeyException,
+ NoSuchProviderException, SignatureException {
+ mWrapped.verify(key, sigProvider);
+ }
+
+ @Override
+ public String toString() {
+ return mWrapped.toString();
+ }
+
+ @Override
+ public PublicKey getPublicKey() {
+ return mWrapped.getPublicKey();
+ }
+}
diff --git a/android/util/jar/StrictJarVerifier.java b/android/util/jar/StrictJarVerifier.java
index debc170f..45254908 100644
--- a/android/util/jar/StrictJarVerifier.java
+++ b/android/util/jar/StrictJarVerifier.java
@@ -18,6 +18,8 @@
package android.util.jar;
import android.util.apk.ApkSignatureSchemeV2Verifier;
+import android.util.apk.ApkSignatureSchemeV3Verifier;
+
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
@@ -36,6 +38,7 @@ import java.util.Map;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
+
import sun.security.jca.Providers;
import sun.security.pkcs.PKCS7;
import sun.security.pkcs.SignerInfo;
@@ -56,6 +59,15 @@ import sun.security.pkcs.SignerInfo;
*/
class StrictJarVerifier {
/**
+ * {@code .SF} file header section attribute indicating that the APK is signed not just with
+ * JAR signature scheme but also with APK Signature Scheme v2 or newer. This attribute
+ * facilitates v2 signature stripping detection.
+ *
+ * <p>The attribute contains a comma-separated set of signature scheme IDs.
+ */
+ private static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed";
+
+ /**
* List of accepted digest algorithms. This list is in order from most
* preferred to least preferred.
*/
@@ -373,17 +385,17 @@ class StrictJarVerifier {
return;
}
- // If requested, check whether APK Signature Scheme v2 signature was stripped.
+ // If requested, check whether a newer APK Signature Scheme signature was stripped.
if (signatureSchemeRollbackProtectionsEnforced) {
String apkSignatureSchemeIdList =
- attributes.getValue(
- ApkSignatureSchemeV2Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME);
+ attributes.getValue(SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME);
if (apkSignatureSchemeIdList != null) {
// This field contains a comma-separated list of APK signature scheme IDs which
// were used to sign this APK. If an ID is known to us, it means signatures of that
// scheme were stripped from the APK because otherwise we wouldn't have fallen back
// to verifying the APK using the JAR signature scheme.
boolean v2SignatureGenerated = false;
+ boolean v3SignatureGenerated = false;
StringTokenizer tokenizer = new StringTokenizer(apkSignatureSchemeIdList, ",");
while (tokenizer.hasMoreTokens()) {
String idText = tokenizer.nextToken().trim();
@@ -402,6 +414,12 @@ class StrictJarVerifier {
v2SignatureGenerated = true;
break;
}
+ if (id == ApkSignatureSchemeV3Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID) {
+ // This APK was supposed to be signed with APK Signature Scheme v3 but no
+ // such signature was found.
+ v3SignatureGenerated = true;
+ break;
+ }
}
if (v2SignatureGenerated) {
@@ -409,6 +427,11 @@ class StrictJarVerifier {
+ " is signed using APK Signature Scheme v2, but no such signature was"
+ " found. Signature stripped?");
}
+ if (v3SignatureGenerated) {
+ throw new SecurityException(signatureFile + " indicates " + jarName
+ + " is signed using APK Signature Scheme v3, but no such signature was"
+ + " found. Signature stripped?");
+ }
}
}
diff --git a/android/view/Choreographer.java b/android/view/Choreographer.java
index 2ffa1c5e..ba6b6cf6 100644
--- a/android/view/Choreographer.java
+++ b/android/view/Choreographer.java
@@ -164,6 +164,7 @@ public final class Choreographer {
private long mLastFrameTimeNanos;
private long mFrameIntervalNanos;
private boolean mDebugPrintNextFrameTimeDelta;
+ private int mFPSDivisor = 1;
/**
* Contains information about the current frame for jank-tracking,
@@ -601,6 +602,11 @@ public final class Choreographer {
}
}
+ void setFPSDivisor(int divisor) {
+ if (divisor <= 0) divisor = 1;
+ mFPSDivisor = divisor;
+ }
+
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
@@ -643,6 +649,14 @@ public final class Choreographer {
return;
}
+ if (mFPSDivisor > 1) {
+ long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos;
+ if (timeSinceVsync < (mFrameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) {
+ scheduleVsyncLocked();
+ return;
+ }
+ }
+
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
diff --git a/android/view/Display.java b/android/view/Display.java
index 9673be01..5bd7446d 100644
--- a/android/view/Display.java
+++ b/android/view/Display.java
@@ -1323,10 +1323,10 @@ public final class Display {
public static final int HDR_TYPE_HLG = 3;
/** @hide */
- @IntDef({
- HDR_TYPE_DOLBY_VISION,
- HDR_TYPE_HDR10,
- HDR_TYPE_HLG,
+ @IntDef(prefix = { "HDR_TYPE_" }, value = {
+ HDR_TYPE_DOLBY_VISION,
+ HDR_TYPE_HDR10,
+ HDR_TYPE_HLG,
})
@Retention(RetentionPolicy.SOURCE)
public @interface HdrType {}
diff --git a/android/view/DisplayCutout.java b/android/view/DisplayCutout.java
index 19cd42e1..e448f14c 100644
--- a/android/view/DisplayCutout.java
+++ b/android/view/DisplayCutout.java
@@ -21,40 +21,37 @@ import static android.view.Surface.ROTATION_180;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
-import android.annotation.NonNull;
+import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.internal.annotations.VisibleForTesting;
-import java.util.ArrayList;
import java.util.List;
/**
* Represents a part of the display that is not functional for displaying content.
*
* <p>{@code DisplayCutout} is immutable.
- *
- * @hide will become API
*/
public final class DisplayCutout {
- private static final Rect ZERO_RECT = new Rect(0, 0, 0, 0);
- private static final ArrayList<Point> EMPTY_LIST = new ArrayList<>();
+ private static final Rect ZERO_RECT = new Rect();
+ private static final Region EMPTY_REGION = new Region();
/**
- * An instance where {@link #hasCutout()} returns {@code false}.
+ * An instance where {@link #isEmpty()} returns {@code true}.
*
* @hide
*/
- public static final DisplayCutout NO_CUTOUT =
- new DisplayCutout(ZERO_RECT, ZERO_RECT, EMPTY_LIST);
+ public static final DisplayCutout NO_CUTOUT = new DisplayCutout(ZERO_RECT, EMPTY_REGION);
private final Rect mSafeInsets;
- private final Rect mBoundingRect;
- private final List<Point> mBoundingPolygon;
+ private final Region mBounds;
/**
* Creates a DisplayCutout instance.
@@ -64,22 +61,18 @@ public final class DisplayCutout {
* @hide
*/
@VisibleForTesting
- public DisplayCutout(Rect safeInsets, Rect boundingRect, List<Point> boundingPolygon) {
+ public DisplayCutout(Rect safeInsets, Region bounds) {
mSafeInsets = safeInsets != null ? safeInsets : ZERO_RECT;
- mBoundingRect = boundingRect != null ? boundingRect : ZERO_RECT;
- mBoundingPolygon = boundingPolygon != null ? boundingPolygon : EMPTY_LIST;
+ mBounds = bounds != null ? bounds : Region.obtain();
}
/**
- * Returns whether there is a cutout.
- *
- * If false, the safe insets will all return zero, and the bounding box or polygon will be
- * empty or outside the content view.
+ * Returns true if there is no cutout or it is outside of the content view.
*
- * @return {@code true} if there is a cutout, {@code false} otherwise
+ * @hide
*/
- public boolean hasCutout() {
- return !mSafeInsets.equals(ZERO_RECT);
+ public boolean isEmpty() {
+ return mSafeInsets.equals(ZERO_RECT);
}
/** Returns the inset from the top which avoids the display cutout. */
@@ -103,44 +96,41 @@ public final class DisplayCutout {
}
/**
- * Obtains the safe insets in a rect.
+ * Returns the safe insets in a rect.
*
- * @param out a rect which is set to the safe insets.
+ * @return a rect which is set to the safe insets.
* @hide
*/
- public void getSafeInsets(@NonNull Rect out) {
- out.set(mSafeInsets);
+ public Rect getSafeInsets() {
+ return new Rect(mSafeInsets);
}
/**
- * Obtains the bounding rect of the cutout.
+ * Returns the bounding region of the cutout.
*
- * @param outRect is filled with the bounding rect of the cutout. Coordinates are relative
+ * @return the bounding region of the cutout. Coordinates are relative
* to the top-left corner of the content view.
*/
- public void getBoundingRect(@NonNull Rect outRect) {
- outRect.set(mBoundingRect);
+ public Region getBounds() {
+ return Region.obtain(mBounds);
}
/**
- * Obtains the bounding polygon of the cutout.
+ * Returns the bounding rect of the cutout.
*
- * @param outPolygon is filled with a list of points representing the corners of a convex
- * polygon which covers the cutout. Coordinates are relative to the
- * top-left corner of the content view.
+ * @return the bounding rect of the cutout. Coordinates are relative
+ * to the top-left corner of the content view.
+ * @hide
*/
- public void getBoundingPolygon(List<Point> outPolygon) {
- outPolygon.clear();
- for (int i = 0; i < mBoundingPolygon.size(); i++) {
- outPolygon.add(new Point(mBoundingPolygon.get(i)));
- }
+ public Rect getBoundingRect() {
+ // TODO(roosa): Inline.
+ return mBounds.getBounds();
}
@Override
public int hashCode() {
int result = mSafeInsets.hashCode();
- result = result * 31 + mBoundingRect.hashCode();
- result = result * 31 + mBoundingPolygon.hashCode();
+ result = result * 31 + mBounds.getBounds().hashCode();
return result;
}
@@ -152,8 +142,7 @@ public final class DisplayCutout {
if (o instanceof DisplayCutout) {
DisplayCutout c = (DisplayCutout) o;
return mSafeInsets.equals(c.mSafeInsets)
- && mBoundingRect.equals(c.mBoundingRect)
- && mBoundingPolygon.equals(c.mBoundingPolygon);
+ && mBounds.equals(c.mBounds);
}
return false;
}
@@ -161,7 +150,7 @@ public final class DisplayCutout {
@Override
public String toString() {
return "DisplayCutout{insets=" + mSafeInsets
- + " bounding=" + mBoundingRect
+ + " bounds=" + mBounds
+ "}";
}
@@ -172,15 +161,13 @@ public final class DisplayCutout {
* @hide
*/
public DisplayCutout inset(int insetLeft, int insetTop, int insetRight, int insetBottom) {
- if (mBoundingRect.isEmpty()
+ if (mBounds.isEmpty()
|| insetLeft == 0 && insetTop == 0 && insetRight == 0 && insetBottom == 0) {
return this;
}
Rect safeInsets = new Rect(mSafeInsets);
- Rect boundingRect = new Rect(mBoundingRect);
- ArrayList<Point> boundingPolygon = new ArrayList<>();
- getBoundingPolygon(boundingPolygon);
+ Region bounds = Region.obtain(mBounds);
// Note: it's not really well defined what happens when the inset is negative, because we
// don't know if the safe inset needs to expand in general.
@@ -197,10 +184,9 @@ public final class DisplayCutout {
safeInsets.right = atLeastZero(safeInsets.right - insetRight);
}
- boundingRect.offset(-insetLeft, -insetTop);
- offset(boundingPolygon, -insetLeft, -insetTop);
+ bounds.translate(-insetLeft, -insetTop);
- return new DisplayCutout(safeInsets, boundingRect, boundingPolygon);
+ return new DisplayCutout(safeInsets, bounds);
}
/**
@@ -210,20 +196,17 @@ public final class DisplayCutout {
* @hide
*/
public DisplayCutout calculateRelativeTo(Rect frame) {
- if (mBoundingRect.isEmpty() || !Rect.intersects(frame, mBoundingRect)) {
+ if (mBounds.isEmpty() || !Rect.intersects(frame, mBounds.getBounds())) {
return NO_CUTOUT;
}
- Rect boundingRect = new Rect(mBoundingRect);
- ArrayList<Point> boundingPolygon = new ArrayList<>();
- getBoundingPolygon(boundingPolygon);
-
- return DisplayCutout.calculateRelativeTo(frame, boundingRect, boundingPolygon);
+ return DisplayCutout.calculateRelativeTo(frame, Region.obtain(mBounds));
}
- private static DisplayCutout calculateRelativeTo(Rect frame, Rect boundingRect,
- ArrayList<Point> boundingPolygon) {
+ private static DisplayCutout calculateRelativeTo(Rect frame, Region bounds) {
+ Rect boundingRect = bounds.getBounds();
Rect safeRect = new Rect();
+
int bestArea = 0;
int bestVariant = 0;
for (int variant = ROTATION_0; variant <= ROTATION_270; variant++) {
@@ -247,10 +230,9 @@ public final class DisplayCutout {
Math.max(0, frame.bottom - safeRect.bottom));
}
- boundingRect.offset(-frame.left, -frame.top);
- offset(boundingPolygon, -frame.left, -frame.top);
+ bounds.translate(-frame.left, -frame.top);
- return new DisplayCutout(safeRect, boundingRect, boundingPolygon);
+ return new DisplayCutout(safeRect, bounds);
}
private static int calculateInsetVariantArea(Rect frame, Rect boundingRect, int variant,
@@ -277,11 +259,6 @@ public final class DisplayCutout {
return value < 0 ? 0 : value;
}
- private static void offset(ArrayList<Point> points, int dx, int dy) {
- for (int i = 0; i < points.size(); i++) {
- points.get(i).offset(dx, dy);
- }
- }
/**
* Creates an instance from a bounding polygon.
@@ -289,20 +266,28 @@ public final class DisplayCutout {
* @hide
*/
public static DisplayCutout fromBoundingPolygon(List<Point> points) {
- Rect boundingRect = new Rect(Integer.MAX_VALUE, Integer.MAX_VALUE,
- Integer.MIN_VALUE, Integer.MIN_VALUE);
- ArrayList<Point> boundingPolygon = new ArrayList<>();
+ Region bounds = Region.obtain();
+ Path path = new Path();
+ path.reset();
for (int i = 0; i < points.size(); i++) {
Point point = points.get(i);
- boundingRect.left = Math.min(boundingRect.left, point.x);
- boundingRect.right = Math.max(boundingRect.right, point.x);
- boundingRect.top = Math.min(boundingRect.top, point.y);
- boundingRect.bottom = Math.max(boundingRect.bottom, point.y);
- boundingPolygon.add(new Point(point));
+ if (i == 0) {
+ path.moveTo(point.x, point.y);
+ } else {
+ path.lineTo(point.x, point.y);
+ }
}
+ path.close();
+
+ RectF clipRect = new RectF();
+ path.computeBounds(clipRect, false /* unused */);
+ Region clipRegion = Region.obtain();
+ clipRegion.set((int) clipRect.left, (int) clipRect.top,
+ (int) clipRect.right, (int) clipRect.bottom);
- return new DisplayCutout(ZERO_RECT, boundingRect, boundingPolygon);
+ bounds.setPath(path, clipRegion);
+ return new DisplayCutout(ZERO_RECT, bounds);
}
/**
@@ -336,8 +321,7 @@ public final class DisplayCutout {
} else {
out.writeInt(1);
out.writeTypedObject(mInner.mSafeInsets, flags);
- out.writeTypedObject(mInner.mBoundingRect, flags);
- out.writeTypedList(mInner.mBoundingPolygon, flags);
+ out.writeTypedObject(mInner.mBounds, flags);
}
}
@@ -368,13 +352,10 @@ public final class DisplayCutout {
return NO_CUTOUT;
}
- ArrayList<Point> boundingPolygon = new ArrayList<>();
-
Rect safeInsets = in.readTypedObject(Rect.CREATOR);
- Rect boundingRect = in.readTypedObject(Rect.CREATOR);
- in.readTypedList(boundingPolygon, Point.CREATOR);
+ Region bounds = in.readTypedObject(Region.CREATOR);
- return new DisplayCutout(safeInsets, boundingRect, boundingPolygon);
+ return new DisplayCutout(safeInsets, bounds);
}
public DisplayCutout get() {
diff --git a/android/view/FrameInfo.java b/android/view/FrameInfo.java
index c79547c8..6c5e048f 100644
--- a/android/view/FrameInfo.java
+++ b/android/view/FrameInfo.java
@@ -16,7 +16,7 @@
package android.view;
-import android.annotation.IntDef;
+import android.annotation.LongDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -48,7 +48,7 @@ final class FrameInfo {
// Is this the first-draw following a window layout?
public static final long FLAG_WINDOW_LAYOUT_CHANGED = 1;
- @IntDef(flag = true, value = {
+ @LongDef(flag = true, value = {
FLAG_WINDOW_LAYOUT_CHANGED })
@Retention(RetentionPolicy.SOURCE)
public @interface FrameInfoFlags {}
diff --git a/android/view/GestureDetector.java b/android/view/GestureDetector.java
index 52e53b07..bc2953e0 100644
--- a/android/view/GestureDetector.java
+++ b/android/view/GestureDetector.java
@@ -520,162 +520,163 @@ public class GestureDetector {
boolean handled = false;
switch (action & MotionEvent.ACTION_MASK) {
- case MotionEvent.ACTION_POINTER_DOWN:
- mDownFocusX = mLastFocusX = focusX;
- mDownFocusY = mLastFocusY = focusY;
- // Cancel long press and taps
- cancelTaps();
- break;
-
- case MotionEvent.ACTION_POINTER_UP:
- mDownFocusX = mLastFocusX = focusX;
- mDownFocusY = mLastFocusY = focusY;
-
- // Check the dot product of current velocities.
- // If the pointer that left was opposing another velocity vector, clear.
- mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
- final int upIndex = ev.getActionIndex();
- final int id1 = ev.getPointerId(upIndex);
- final float x1 = mVelocityTracker.getXVelocity(id1);
- final float y1 = mVelocityTracker.getYVelocity(id1);
- for (int i = 0; i < count; i++) {
- if (i == upIndex) continue;
-
- final int id2 = ev.getPointerId(i);
- final float x = x1 * mVelocityTracker.getXVelocity(id2);
- final float y = y1 * mVelocityTracker.getYVelocity(id2);
-
- final float dot = x + y;
- if (dot < 0) {
- mVelocityTracker.clear();
- break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ mDownFocusX = mLastFocusX = focusX;
+ mDownFocusY = mLastFocusY = focusY;
+ // Cancel long press and taps
+ cancelTaps();
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ mDownFocusX = mLastFocusX = focusX;
+ mDownFocusY = mLastFocusY = focusY;
+
+ // Check the dot product of current velocities.
+ // If the pointer that left was opposing another velocity vector, clear.
+ mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
+ final int upIndex = ev.getActionIndex();
+ final int id1 = ev.getPointerId(upIndex);
+ final float x1 = mVelocityTracker.getXVelocity(id1);
+ final float y1 = mVelocityTracker.getYVelocity(id1);
+ for (int i = 0; i < count; i++) {
+ if (i == upIndex) continue;
+
+ final int id2 = ev.getPointerId(i);
+ final float x = x1 * mVelocityTracker.getXVelocity(id2);
+ final float y = y1 * mVelocityTracker.getYVelocity(id2);
+
+ final float dot = x + y;
+ if (dot < 0) {
+ mVelocityTracker.clear();
+ break;
+ }
}
- }
- break;
-
- case MotionEvent.ACTION_DOWN:
- if (mDoubleTapListener != null) {
- boolean hadTapMessage = mHandler.hasMessages(TAP);
- if (hadTapMessage) mHandler.removeMessages(TAP);
- if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
- isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
- // This is a second tap
- mIsDoubleTapping = true;
- // Give a callback with the first tap of the double-tap
- handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
- // Give a callback with down event of the double-tap
- handled |= mDoubleTapListener.onDoubleTapEvent(ev);
- } else {
- // This is a first tap
- mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
+ break;
+
+ case MotionEvent.ACTION_DOWN:
+ if (mDoubleTapListener != null) {
+ boolean hadTapMessage = mHandler.hasMessages(TAP);
+ if (hadTapMessage) mHandler.removeMessages(TAP);
+ if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null)
+ && hadTapMessage
+ && isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
+ // This is a second tap
+ mIsDoubleTapping = true;
+ // Give a callback with the first tap of the double-tap
+ handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
+ // Give a callback with down event of the double-tap
+ handled |= mDoubleTapListener.onDoubleTapEvent(ev);
+ } else {
+ // This is a first tap
+ mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
+ }
}
- }
- mDownFocusX = mLastFocusX = focusX;
- mDownFocusY = mLastFocusY = focusY;
- if (mCurrentDownEvent != null) {
- mCurrentDownEvent.recycle();
- }
- mCurrentDownEvent = MotionEvent.obtain(ev);
- mAlwaysInTapRegion = true;
- mAlwaysInBiggerTapRegion = true;
- mStillDown = true;
- mInLongPress = false;
- mDeferConfirmSingleTap = false;
-
- if (mIsLongpressEnabled) {
- mHandler.removeMessages(LONG_PRESS);
- mHandler.sendEmptyMessageAtTime(LONG_PRESS,
- mCurrentDownEvent.getDownTime() + LONGPRESS_TIMEOUT);
- }
- mHandler.sendEmptyMessageAtTime(SHOW_PRESS,
- mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
- handled |= mListener.onDown(ev);
- break;
+ mDownFocusX = mLastFocusX = focusX;
+ mDownFocusY = mLastFocusY = focusY;
+ if (mCurrentDownEvent != null) {
+ mCurrentDownEvent.recycle();
+ }
+ mCurrentDownEvent = MotionEvent.obtain(ev);
+ mAlwaysInTapRegion = true;
+ mAlwaysInBiggerTapRegion = true;
+ mStillDown = true;
+ mInLongPress = false;
+ mDeferConfirmSingleTap = false;
- case MotionEvent.ACTION_MOVE:
- if (mInLongPress || mInContextClick) {
+ if (mIsLongpressEnabled) {
+ mHandler.removeMessages(LONG_PRESS);
+ mHandler.sendEmptyMessageAtTime(LONG_PRESS,
+ mCurrentDownEvent.getDownTime() + LONGPRESS_TIMEOUT);
+ }
+ mHandler.sendEmptyMessageAtTime(SHOW_PRESS,
+ mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
+ handled |= mListener.onDown(ev);
break;
- }
- final float scrollX = mLastFocusX - focusX;
- final float scrollY = mLastFocusY - focusY;
- if (mIsDoubleTapping) {
- // Give the move events of the double-tap
- handled |= mDoubleTapListener.onDoubleTapEvent(ev);
- } else if (mAlwaysInTapRegion) {
- final int deltaX = (int) (focusX - mDownFocusX);
- final int deltaY = (int) (focusY - mDownFocusY);
- int distance = (deltaX * deltaX) + (deltaY * deltaY);
- int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
- if (distance > slopSquare) {
+
+ case MotionEvent.ACTION_MOVE:
+ if (mInLongPress || mInContextClick) {
+ break;
+ }
+ final float scrollX = mLastFocusX - focusX;
+ final float scrollY = mLastFocusY - focusY;
+ if (mIsDoubleTapping) {
+ // Give the move events of the double-tap
+ handled |= mDoubleTapListener.onDoubleTapEvent(ev);
+ } else if (mAlwaysInTapRegion) {
+ final int deltaX = (int) (focusX - mDownFocusX);
+ final int deltaY = (int) (focusY - mDownFocusY);
+ int distance = (deltaX * deltaX) + (deltaY * deltaY);
+ int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
+ if (distance > slopSquare) {
+ handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
+ mLastFocusX = focusX;
+ mLastFocusY = focusY;
+ mAlwaysInTapRegion = false;
+ mHandler.removeMessages(TAP);
+ mHandler.removeMessages(SHOW_PRESS);
+ mHandler.removeMessages(LONG_PRESS);
+ }
+ int doubleTapSlopSquare = isGeneratedGesture ? 0 : mDoubleTapTouchSlopSquare;
+ if (distance > doubleTapSlopSquare) {
+ mAlwaysInBiggerTapRegion = false;
+ }
+ } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
mLastFocusX = focusX;
mLastFocusY = focusY;
- mAlwaysInTapRegion = false;
- mHandler.removeMessages(TAP);
- mHandler.removeMessages(SHOW_PRESS);
- mHandler.removeMessages(LONG_PRESS);
}
- int doubleTapSlopSquare = isGeneratedGesture ? 0 : mDoubleTapTouchSlopSquare;
- if (distance > doubleTapSlopSquare) {
- mAlwaysInBiggerTapRegion = false;
+ break;
+
+ case MotionEvent.ACTION_UP:
+ mStillDown = false;
+ MotionEvent currentUpEvent = MotionEvent.obtain(ev);
+ if (mIsDoubleTapping) {
+ // Finally, give the up event of the double-tap
+ handled |= mDoubleTapListener.onDoubleTapEvent(ev);
+ } else if (mInLongPress) {
+ mHandler.removeMessages(TAP);
+ mInLongPress = false;
+ } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
+ handled = mListener.onSingleTapUp(ev);
+ if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
+ mDoubleTapListener.onSingleTapConfirmed(ev);
+ }
+ } else if (!mIgnoreNextUpEvent) {
+
+ // A fling must travel the minimum tap distance
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ final int pointerId = ev.getPointerId(0);
+ velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
+ final float velocityY = velocityTracker.getYVelocity(pointerId);
+ final float velocityX = velocityTracker.getXVelocity(pointerId);
+
+ if ((Math.abs(velocityY) > mMinimumFlingVelocity)
+ || (Math.abs(velocityX) > mMinimumFlingVelocity)) {
+ handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
+ }
}
- } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
- handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
- mLastFocusX = focusX;
- mLastFocusY = focusY;
- }
- break;
-
- case MotionEvent.ACTION_UP:
- mStillDown = false;
- MotionEvent currentUpEvent = MotionEvent.obtain(ev);
- if (mIsDoubleTapping) {
- // Finally, give the up event of the double-tap
- handled |= mDoubleTapListener.onDoubleTapEvent(ev);
- } else if (mInLongPress) {
- mHandler.removeMessages(TAP);
- mInLongPress = false;
- } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
- handled = mListener.onSingleTapUp(ev);
- if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
- mDoubleTapListener.onSingleTapConfirmed(ev);
+ if (mPreviousUpEvent != null) {
+ mPreviousUpEvent.recycle();
}
- } else if (!mIgnoreNextUpEvent) {
-
- // A fling must travel the minimum tap distance
- final VelocityTracker velocityTracker = mVelocityTracker;
- final int pointerId = ev.getPointerId(0);
- velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
- final float velocityY = velocityTracker.getYVelocity(pointerId);
- final float velocityX = velocityTracker.getXVelocity(pointerId);
-
- if ((Math.abs(velocityY) > mMinimumFlingVelocity)
- || (Math.abs(velocityX) > mMinimumFlingVelocity)){
- handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
+ // Hold the event we obtained above - listeners may have changed the original.
+ mPreviousUpEvent = currentUpEvent;
+ if (mVelocityTracker != null) {
+ // This may have been cleared when we called out to the
+ // application above.
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
}
- }
- if (mPreviousUpEvent != null) {
- mPreviousUpEvent.recycle();
- }
- // Hold the event we obtained above - listeners may have changed the original.
- mPreviousUpEvent = currentUpEvent;
- if (mVelocityTracker != null) {
- // This may have been cleared when we called out to the
- // application above.
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
- mIsDoubleTapping = false;
- mDeferConfirmSingleTap = false;
- mIgnoreNextUpEvent = false;
- mHandler.removeMessages(SHOW_PRESS);
- mHandler.removeMessages(LONG_PRESS);
- break;
-
- case MotionEvent.ACTION_CANCEL:
- cancel();
- break;
+ mIsDoubleTapping = false;
+ mDeferConfirmSingleTap = false;
+ mIgnoreNextUpEvent = false;
+ mHandler.removeMessages(SHOW_PRESS);
+ mHandler.removeMessages(LONG_PRESS);
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ cancel();
+ break;
}
if (!handled && mInputEventConsistencyVerifier != null) {
diff --git a/android/view/IWindowManagerImpl.java b/android/view/IWindowManagerImpl.java
index 6c006cae..4d804c55 100644
--- a/android/view/IWindowManagerImpl.java
+++ b/android/view/IWindowManagerImpl.java
@@ -156,12 +156,6 @@ public class IWindowManagerImpl implements IWindowManager {
}
@Override
- public boolean inKeyguardRestrictedInputMode() throws RemoteException {
- // TODO Auto-generated method stub
- return false;
- }
-
- @Override
public boolean inputMethodClientHasFocus(IInputMethodClient arg0) throws RemoteException {
// TODO Auto-generated method stub
return false;
diff --git a/android/view/Surface.java b/android/view/Surface.java
index ddced6cd..a417a4a0 100644
--- a/android/view/Surface.java
+++ b/android/view/Surface.java
@@ -121,8 +121,12 @@ public class Surface implements Parcelable {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({SCALING_MODE_FREEZE, SCALING_MODE_SCALE_TO_WINDOW,
- SCALING_MODE_SCALE_CROP, SCALING_MODE_NO_SCALE_CROP})
+ @IntDef(prefix = { "SCALING_MODE_" }, value = {
+ SCALING_MODE_FREEZE,
+ SCALING_MODE_SCALE_TO_WINDOW,
+ SCALING_MODE_SCALE_CROP,
+ SCALING_MODE_NO_SCALE_CROP
+ })
public @interface ScalingMode {}
// From system/window.h
/** @hide */
@@ -135,7 +139,12 @@ public class Surface implements Parcelable {
public static final int SCALING_MODE_NO_SCALE_CROP = 3;
/** @hide */
- @IntDef({ROTATION_0, ROTATION_90, ROTATION_180, ROTATION_270})
+ @IntDef(prefix = { "ROTATION_" }, value = {
+ ROTATION_0,
+ ROTATION_90,
+ ROTATION_180,
+ ROTATION_270
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface Rotation {}
diff --git a/android/view/SurfaceControl.java b/android/view/SurfaceControl.java
index 3d01ec23..268e460d 100644
--- a/android/view/SurfaceControl.java
+++ b/android/view/SurfaceControl.java
@@ -16,17 +16,34 @@
package android.view;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+import static android.graphics.Matrix.MSCALE_X;
+import static android.graphics.Matrix.MSCALE_Y;
+import static android.graphics.Matrix.MSKEW_X;
+import static android.graphics.Matrix.MSKEW_Y;
+import static android.graphics.Matrix.MTRANS_X;
+import static android.graphics.Matrix.MTRANS_Y;
+
import android.annotation.Size;
import android.graphics.Bitmap;
import android.graphics.GraphicBuffer;
import android.graphics.PixelFormat;
+import android.graphics.Matrix;
+import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.os.Process;
import android.os.UserHandle;
+import android.util.ArrayMap;
import android.util.Log;
import android.view.Surface.OutOfResourcesException;
+
+import com.android.internal.annotations.GuardedBy;
+
import dalvik.system.CloseGuard;
import libcore.util.NativeAllocationRegistry;
@@ -36,12 +53,14 @@ import java.io.Closeable;
* SurfaceControl
* @hide
*/
-public class SurfaceControl {
+public class SurfaceControl implements Parcelable {
private static final String TAG = "SurfaceControl";
private static native long nativeCreate(SurfaceSession session, String name,
int w, int h, int format, int flags, long parentObject, int windowType, int ownerUid)
throws OutOfResourcesException;
+ private static native long nativeReadFromParcel(Parcel in);
+ private static native void nativeWriteToParcel(long nativeObject, Parcel out);
private static native void nativeRelease(long nativeObject);
private static native void nativeDestroy(long nativeObject);
private static native void nativeDisconnect(long nativeObject);
@@ -55,8 +74,6 @@ public class SurfaceControl {
private static native void nativeScreenshot(IBinder displayToken, Surface consumer,
Rect sourceCrop, int width, int height, int minLayer, int maxLayer,
boolean allLayers, boolean useIdentityTransform);
- private static native void nativeCaptureLayers(IBinder layerHandleToken, Surface consumer,
- Rect sourceCrop, float frameScale);
private static native GraphicBuffer nativeCaptureLayers(IBinder layerHandleToken,
Rect sourceCrop, float frameScale);
@@ -141,6 +158,13 @@ public class SurfaceControl {
private final String mName;
long mNativeObject; // package visibility only for Surface.java access
+ // TODO: Move this to native.
+ private final Object mSizeLock = new Object();
+ @GuardedBy("mSizeLock")
+ private int mWidth;
+ @GuardedBy("mSizeLock")
+ private int mHeight;
+
static Transaction sGlobalTransaction;
static long sTransactionNestCount = 0;
@@ -555,6 +579,8 @@ public class SurfaceControl {
}
mName = name;
+ mWidth = w;
+ mHeight = h;
mNativeObject = nativeCreate(session, name, w, h, format, flags,
parent != null ? parent.mNativeObject : 0, windowType, ownerUid);
if (mNativeObject == 0) {
@@ -570,12 +596,49 @@ public class SurfaceControl {
// event logging.
public SurfaceControl(SurfaceControl other) {
mName = other.mName;
+ mWidth = other.mWidth;
+ mHeight = other.mHeight;
mNativeObject = other.mNativeObject;
other.mCloseGuard.close();
other.mNativeObject = 0;
mCloseGuard.open("release");
}
+ private SurfaceControl(Parcel in) {
+ mName = in.readString();
+ mWidth = in.readInt();
+ mHeight = in.readInt();
+ mNativeObject = nativeReadFromParcel(in);
+ if (mNativeObject == 0) {
+ throw new IllegalArgumentException("Couldn't read SurfaceControl from parcel=" + in);
+ }
+ mCloseGuard.open("release");
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mName);
+ dest.writeInt(mWidth);
+ dest.writeInt(mHeight);
+ nativeWriteToParcel(mNativeObject, dest);
+ }
+
+ public static final Creator<SurfaceControl> CREATOR
+ = new Creator<SurfaceControl>() {
+ public SurfaceControl createFromParcel(Parcel in) {
+ return new SurfaceControl(in);
+ }
+
+ public SurfaceControl[] newArray(int size) {
+ return new SurfaceControl[size];
+ }
+ };
+
@Override
protected void finalize() throws Throwable {
try {
@@ -666,7 +729,7 @@ public class SurfaceControl {
*/
@Deprecated
public static void mergeToGlobalTransaction(Transaction t) {
- synchronized(sGlobalTransaction) {
+ synchronized(SurfaceControl.class) {
sGlobalTransaction.merge(t);
}
}
@@ -826,6 +889,22 @@ public class SurfaceControl {
}
}
+ /**
+ * Sets the transform and position of a {@link SurfaceControl} from a 3x3 transformation matrix.
+ *
+ * @param matrix The matrix to apply.
+ * @param float9 An array of 9 floats to be used to extract the values from the matrix.
+ */
+ public void setMatrix(Matrix matrix, float[] float9) {
+ checkNotReleased();
+ matrix.getValues(float9);
+ synchronized (SurfaceControl.class) {
+ sGlobalTransaction.setMatrix(this, float9[MSCALE_X], float9[MSKEW_Y],
+ float9[MSKEW_X], float9[MSCALE_Y]);
+ sGlobalTransaction.setPosition(this, float9[MTRANS_X], float9[MTRANS_Y]);
+ }
+ }
+
public void setWindowCrop(Rect crop) {
checkNotReleased();
synchronized (SurfaceControl.class) {
@@ -863,6 +942,18 @@ public class SurfaceControl {
}
}
+ public int getWidth() {
+ synchronized (mSizeLock) {
+ return mWidth;
+ }
+ }
+
+ public int getHeight() {
+ synchronized (mSizeLock) {
+ return mHeight;
+ }
+ }
+
@Override
public String toString() {
return "Surface(name=" + mName + ")/@0x" +
@@ -1090,7 +1181,9 @@ public class SurfaceControl {
}
/**
- * Copy the current screen contents into a bitmap and return it.
+ * Copy the current screen contents into a hardware bitmap and return it.
+ * Note: If you want to modify the Bitmap in software, you will need to copy the Bitmap into
+ * a software Bitmap using {@link Bitmap#copy(Bitmap.Config, boolean)}
*
* CAVEAT: Versions of screenshot that return a {@link Bitmap} can
* be extremely slow; avoid use unless absolutely necessary; prefer
@@ -1115,7 +1208,7 @@ public class SurfaceControl {
* screenshots in its native portrait orientation by default, so this is
* useful for returning screenshots that are independent of device
* orientation.
- * @return Returns a Bitmap containing the screen contents, or null
+ * @return Returns a hardware Bitmap containing the screen contents, or null
* if an error occurs. Make sure to call Bitmap.recycle() as soon as
* possible, once its content is not needed anymore.
*/
@@ -1143,23 +1236,36 @@ public class SurfaceControl {
}
/**
- * Like {@link SurfaceControl#screenshot(int, int, int, int, boolean)} but
- * includes all Surfaces in the screenshot.
+ * Like {@link SurfaceControl#screenshot(Rect, int, int, int, int, boolean, int)} but
+ * includes all Surfaces in the screenshot. This will also update the orientation so it
+ * sends the correct coordinates to SF based on the rotation value.
*
+ * @param sourceCrop The portion of the screen to capture into the Bitmap;
+ * caller may pass in 'new Rect()' if no cropping is desired.
* @param width The desired width of the returned bitmap; the raw
* screen will be scaled down to this size.
* @param height The desired height of the returned bitmap; the raw
* screen will be scaled down to this size.
+ * @param rotation Apply a custom clockwise rotation to the screenshot, i.e.
+ * Surface.ROTATION_0,90,180,270. Surfaceflinger will always take
+ * screenshots in its native portrait orientation by default, so this is
+ * useful for returning screenshots that are independent of device
+ * orientation.
* @return Returns a Bitmap containing the screen contents, or null
* if an error occurs. Make sure to call Bitmap.recycle() as soon as
* possible, once its content is not needed anymore.
*/
- public static Bitmap screenshot(int width, int height) {
+ public static Bitmap screenshot(Rect sourceCrop, int width, int height, int rotation) {
// TODO: should take the display as a parameter
IBinder displayToken = SurfaceControl.getBuiltInDisplay(
SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN);
- return nativeScreenshot(displayToken, new Rect(), width, height, 0, 0, true,
- false, Surface.ROTATION_0);
+ if (rotation == ROTATION_90 || rotation == ROTATION_270) {
+ rotation = (rotation == ROTATION_90) ? ROTATION_270 : ROTATION_90;
+ }
+
+ SurfaceControl.rotateCropForSF(sourceCrop, rotation);
+ return nativeScreenshot(displayToken, sourceCrop, width, height, 0, 0, true,
+ false, rotation);
}
private static void screenshot(IBinder display, Surface consumer, Rect sourceCrop,
@@ -1175,26 +1281,29 @@ public class SurfaceControl {
minLayer, maxLayer, allLayers, useIdentityTransform);
}
+ private static void rotateCropForSF(Rect crop, int rot) {
+ if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
+ int tmp = crop.top;
+ crop.top = crop.left;
+ crop.left = tmp;
+ tmp = crop.right;
+ crop.right = crop.bottom;
+ crop.bottom = tmp;
+ }
+ }
+
/**
- * Captures a layer and its children into the provided {@link Surface}.
+ * Captures a layer and its children and returns a {@link GraphicBuffer} with the content.
*
* @param layerHandleToken The root layer to capture.
- * @param consumer The {@link Surface} to capture the layer into.
* @param sourceCrop The portion of the root surface to capture; caller may pass in 'new
* Rect()' or null if no cropping is desired.
* @param frameScale The desired scale of the returned buffer; the raw
* screen will be scaled up/down.
+ *
+ * @return Returns a GraphicBuffer that contains the layer capture.
*/
- public static void captureLayers(IBinder layerHandleToken, Surface consumer, Rect sourceCrop,
- float frameScale) {
- nativeCaptureLayers(layerHandleToken, consumer, sourceCrop, frameScale);
- }
-
- /**
- * Same as {@link #captureLayers(IBinder, Surface, Rect, float)} except this
- * captures to a {@link GraphicBuffer} instead of a {@link Surface}.
- */
- public static GraphicBuffer captureLayersToBuffer(IBinder layerHandleToken, Rect sourceCrop,
+ public static GraphicBuffer captureLayers(IBinder layerHandleToken, Rect sourceCrop,
float frameScale) {
return nativeCaptureLayers(layerHandleToken, sourceCrop, frameScale);
}
@@ -1205,6 +1314,7 @@ public class SurfaceControl {
nativeGetNativeTransactionFinalizer(), 512);
private long mNativeObject;
+ private final ArrayMap<SurfaceControl, Point> mResizedSurfaces = new ArrayMap<>();
Runnable mFreeNativeResources;
public Transaction() {
@@ -1235,9 +1345,22 @@ public class SurfaceControl {
* Jankier version of apply. Avoid use (b/28068298).
*/
public void apply(boolean sync) {
+ applyResizedSurfaces();
nativeApplyTransaction(mNativeObject, sync);
}
+ private void applyResizedSurfaces() {
+ for (int i = mResizedSurfaces.size() - 1; i >= 0; i--) {
+ final Point size = mResizedSurfaces.valueAt(i);
+ final SurfaceControl surfaceControl = mResizedSurfaces.keyAt(i);
+ synchronized (surfaceControl.mSizeLock) {
+ surfaceControl.mWidth = size.x;
+ surfaceControl.mHeight = size.y;
+ }
+ }
+ mResizedSurfaces.clear();
+ }
+
public Transaction show(SurfaceControl sc) {
sc.checkNotReleased();
nativeSetFlags(mNativeObject, sc.mNativeObject, 0, SURFACE_HIDDEN);
@@ -1258,6 +1381,7 @@ public class SurfaceControl {
public Transaction setSize(SurfaceControl sc, int w, int h) {
sc.checkNotReleased();
+ mResizedSurfaces.put(sc, new Point(w, h));
nativeSetSize(mNativeObject, sc.mNativeObject, w, h);
return this;
}
@@ -1296,6 +1420,14 @@ public class SurfaceControl {
return this;
}
+ public Transaction setMatrix(SurfaceControl sc, Matrix matrix, float[] float9) {
+ matrix.getValues(float9);
+ setMatrix(sc, float9[MSCALE_X], float9[MSKEW_Y],
+ float9[MSKEW_X], float9[MSCALE_Y]);
+ setPosition(sc, float9[MTRANS_X], float9[MTRANS_Y]);
+ return this;
+ }
+
public Transaction setWindowCrop(SurfaceControl sc, Rect crop) {
sc.checkNotReleased();
if (crop != null) {
@@ -1482,6 +1614,8 @@ public class SurfaceControl {
* other transaction as if it had been applied.
*/
public Transaction merge(Transaction other) {
+ mResizedSurfaces.putAll(other.mResizedSurfaces);
+ other.mResizedSurfaces.clear();
nativeMergeTransaction(mNativeObject, other.mNativeObject);
return this;
}
diff --git a/android/view/SurfaceView.java b/android/view/SurfaceView.java
index 578679b1..ebb2af45 100644
--- a/android/view/SurfaceView.java
+++ b/android/view/SurfaceView.java
@@ -16,1215 +16,115 @@
package android.view;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
-import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_OVERLAY_SUBLAYER;
-import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_SUBLAYER;
-import static android.view.WindowManagerPolicyConstants.APPLICATION_PANEL_SUBLAYER;
+import com.android.layoutlib.bridge.MockView;
import android.content.Context;
-import android.content.res.CompatibilityInfo.Translator;
-import android.content.res.Configuration;
import android.graphics.Canvas;
-import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Region;
-import android.os.Build;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.SystemClock;
import android.util.AttributeSet;
-import android.util.Log;
-
-import com.android.internal.view.SurfaceCallbackHelper;
-
-import java.util.ArrayList;
-import java.util.concurrent.locks.ReentrantLock;
/**
- * Provides a dedicated drawing surface embedded inside of a view hierarchy.
- * You can control the format of this surface and, if you like, its size; the
- * SurfaceView takes care of placing the surface at the correct location on the
- * screen
- *
- * <p>The surface is Z ordered so that it is behind the window holding its
- * SurfaceView; the SurfaceView punches a hole in its window to allow its
- * surface to be displayed. The view hierarchy will take care of correctly
- * compositing with the Surface any siblings of the SurfaceView that would
- * normally appear on top of it. This can be used to place overlays such as
- * buttons on top of the Surface, though note however that it can have an
- * impact on performance since a full alpha-blended composite will be performed
- * each time the Surface changes.
- *
- * <p> The transparent region that makes the surface visible is based on the
- * layout positions in the view hierarchy. If the post-layout transform
- * properties are used to draw a sibling view on top of the SurfaceView, the
- * view may not be properly composited with the surface.
+ * Mock version of the SurfaceView.
+ * Only non override public methods from the real SurfaceView have been added in there.
+ * Methods that take an unknown class as parameter or as return object, have been removed for now.
*
- * <p>Access to the underlying surface is provided via the SurfaceHolder interface,
- * which can be retrieved by calling {@link #getHolder}.
+ * TODO: generate automatically.
*
- * <p>The Surface will be created for you while the SurfaceView's window is
- * visible; you should implement {@link SurfaceHolder.Callback#surfaceCreated}
- * and {@link SurfaceHolder.Callback#surfaceDestroyed} to discover when the
- * Surface is created and destroyed as the window is shown and hidden.
- *
- * <p>One of the purposes of this class is to provide a surface in which a
- * secondary thread can render into the screen. If you are going to use it
- * this way, you need to be aware of some threading semantics:
- *
- * <ul>
- * <li> All SurfaceView and
- * {@link SurfaceHolder.Callback SurfaceHolder.Callback} methods will be called
- * from the thread running the SurfaceView's window (typically the main thread
- * of the application). They thus need to correctly synchronize with any
- * state that is also touched by the drawing thread.
- * <li> You must ensure that the drawing thread only touches the underlying
- * Surface while it is valid -- between
- * {@link SurfaceHolder.Callback#surfaceCreated SurfaceHolder.Callback.surfaceCreated()}
- * and
- * {@link SurfaceHolder.Callback#surfaceDestroyed SurfaceHolder.Callback.surfaceDestroyed()}.
- * </ul>
- *
- * <p class="note"><strong>Note:</strong> Starting in platform version
- * {@link android.os.Build.VERSION_CODES#N}, SurfaceView's window position is
- * updated synchronously with other View rendering. This means that translating
- * and scaling a SurfaceView on screen will not cause rendering artifacts. Such
- * artifacts may occur on previous versions of the platform when its window is
- * positioned asynchronously.</p>
*/
-public class SurfaceView extends View implements ViewRootImpl.WindowStoppedCallback {
- private static final String TAG = "SurfaceView";
- private static final boolean DEBUG = false;
-
- final ArrayList<SurfaceHolder.Callback> mCallbacks
- = new ArrayList<SurfaceHolder.Callback>();
-
- final int[] mLocation = new int[2];
-
- final ReentrantLock mSurfaceLock = new ReentrantLock();
- final Surface mSurface = new Surface(); // Current surface in use
- boolean mDrawingStopped = true;
- // We use this to track if the application has produced a frame
- // in to the Surface. Up until that point, we should be careful not to punch
- // holes.
- boolean mDrawFinished = false;
-
- final Rect mScreenRect = new Rect();
- SurfaceSession mSurfaceSession;
-
- SurfaceControl mSurfaceControl;
- // In the case of format changes we switch out the surface in-place
- // we need to preserve the old one until the new one has drawn.
- SurfaceControl mDeferredDestroySurfaceControl;
- final Rect mTmpRect = new Rect();
- final Configuration mConfiguration = new Configuration();
-
- int mSubLayer = APPLICATION_MEDIA_SUBLAYER;
-
- boolean mIsCreating = false;
- private volatile boolean mRtHandlingPositionUpdates = false;
-
- private final ViewTreeObserver.OnScrollChangedListener mScrollChangedListener
- = new ViewTreeObserver.OnScrollChangedListener() {
- @Override
- public void onScrollChanged() {
- updateSurface();
- }
- };
-
- private final ViewTreeObserver.OnPreDrawListener mDrawListener =
- new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- // reposition ourselves where the surface is
- mHaveFrame = getWidth() > 0 && getHeight() > 0;
- updateSurface();
- return true;
- }
- };
-
- boolean mRequestedVisible = false;
- boolean mWindowVisibility = false;
- boolean mLastWindowVisibility = false;
- boolean mViewVisibility = false;
- boolean mWindowStopped = false;
-
- int mRequestedWidth = -1;
- int mRequestedHeight = -1;
- /* Set SurfaceView's format to 565 by default to maintain backward
- * compatibility with applications assuming this format.
- */
- int mRequestedFormat = PixelFormat.RGB_565;
-
- boolean mHaveFrame = false;
- boolean mSurfaceCreated = false;
- long mLastLockTime = 0;
-
- boolean mVisible = false;
- int mWindowSpaceLeft = -1;
- int mWindowSpaceTop = -1;
- int mSurfaceWidth = -1;
- int mSurfaceHeight = -1;
- int mFormat = -1;
- final Rect mSurfaceFrame = new Rect();
- int mLastSurfaceWidth = -1, mLastSurfaceHeight = -1;
- private Translator mTranslator;
-
- private boolean mGlobalListenersAdded;
- private boolean mAttachedToWindow;
-
- private int mSurfaceFlags = SurfaceControl.HIDDEN;
-
- private int mPendingReportDraws;
+public class SurfaceView extends MockView {
public SurfaceView(Context context) {
this(context, null);
}
public SurfaceView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
+ this(context, attrs , 0);
}
- public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
+ public SurfaceView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
}
public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- mRenderNode.requestPositionUpdates(this);
-
- setWillNotDraw(true);
- }
-
- /**
- * Return the SurfaceHolder providing access and control over this
- * SurfaceView's underlying surface.
- *
- * @return SurfaceHolder The holder of the surface.
- */
- public SurfaceHolder getHolder() {
- return mSurfaceHolder;
- }
-
- private void updateRequestedVisibility() {
- mRequestedVisible = mViewVisibility && mWindowVisibility && !mWindowStopped;
- }
-
- /** @hide */
- @Override
- public void windowStopped(boolean stopped) {
- mWindowStopped = stopped;
- updateRequestedVisibility();
- updateSurface();
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- getViewRootImpl().addWindowStoppedCallback(this);
- mWindowStopped = false;
-
- mViewVisibility = getVisibility() == VISIBLE;
- updateRequestedVisibility();
-
- mAttachedToWindow = true;
- mParent.requestTransparentRegion(SurfaceView.this);
- if (!mGlobalListenersAdded) {
- ViewTreeObserver observer = getViewTreeObserver();
- observer.addOnScrollChangedListener(mScrollChangedListener);
- observer.addOnPreDrawListener(mDrawListener);
- mGlobalListenersAdded = true;
- }
- }
-
- @Override
- protected void onWindowVisibilityChanged(int visibility) {
- super.onWindowVisibilityChanged(visibility);
- mWindowVisibility = visibility == VISIBLE;
- updateRequestedVisibility();
- updateSurface();
- }
-
- @Override
- public void setVisibility(int visibility) {
- super.setVisibility(visibility);
- mViewVisibility = visibility == VISIBLE;
- boolean newRequestedVisible = mWindowVisibility && mViewVisibility && !mWindowStopped;
- if (newRequestedVisible != mRequestedVisible) {
- // our base class (View) invalidates the layout only when
- // we go from/to the GONE state. However, SurfaceView needs
- // to request a re-layout when the visibility changes at all.
- // This is needed because the transparent region is computed
- // as part of the layout phase, and it changes (obviously) when
- // the visibility changes.
- requestLayout();
- }
- mRequestedVisible = newRequestedVisible;
- updateSurface();
- }
-
- private void performDrawFinished() {
- if (mPendingReportDraws > 0) {
- mDrawFinished = true;
- if (mAttachedToWindow) {
- notifyDrawFinished();
- invalidate();
- }
- } else {
- Log.e(TAG, System.identityHashCode(this) + "finished drawing"
- + " but no pending report draw (extra call"
- + " to draw completion runnable?)");
- }
- }
-
- void notifyDrawFinished() {
- ViewRootImpl viewRoot = getViewRootImpl();
- if (viewRoot != null) {
- viewRoot.pendingDrawFinished();
- }
- mPendingReportDraws--;
- }
-
- @Override
- protected void onDetachedFromWindow() {
- ViewRootImpl viewRoot = getViewRootImpl();
- // It's possible to create a SurfaceView using the default constructor and never
- // attach it to a view hierarchy, this is a common use case when dealing with
- // OpenGL. A developer will probably create a new GLSurfaceView, and let it manage
- // the lifecycle. Instead of attaching it to a view, he/she can just pass
- // the SurfaceHolder forward, most live wallpapers do it.
- if (viewRoot != null) {
- viewRoot.removeWindowStoppedCallback(this);
- }
-
- mAttachedToWindow = false;
- if (mGlobalListenersAdded) {
- ViewTreeObserver observer = getViewTreeObserver();
- observer.removeOnScrollChangedListener(mScrollChangedListener);
- observer.removeOnPreDrawListener(mDrawListener);
- mGlobalListenersAdded = false;
- }
-
- while (mPendingReportDraws > 0) {
- notifyDrawFinished();
- }
-
- mRequestedVisible = false;
-
- updateSurface();
- if (mSurfaceControl != null) {
- mSurfaceControl.destroy();
- }
- mSurfaceControl = null;
-
- mHaveFrame = false;
-
- super.onDetachedFromWindow();
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int width = mRequestedWidth >= 0
- ? resolveSizeAndState(mRequestedWidth, widthMeasureSpec, 0)
- : getDefaultSize(0, widthMeasureSpec);
- int height = mRequestedHeight >= 0
- ? resolveSizeAndState(mRequestedHeight, heightMeasureSpec, 0)
- : getDefaultSize(0, heightMeasureSpec);
- setMeasuredDimension(width, height);
- }
-
- /** @hide */
- @Override
- protected boolean setFrame(int left, int top, int right, int bottom) {
- boolean result = super.setFrame(left, top, right, bottom);
- updateSurface();
- return result;
}
- @Override
public boolean gatherTransparentRegion(Region region) {
- if (isAboveParent() || !mDrawFinished) {
- return super.gatherTransparentRegion(region);
- }
-
- boolean opaque = true;
- if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
- // this view draws, remove it from the transparent region
- opaque = super.gatherTransparentRegion(region);
- } else if (region != null) {
- int w = getWidth();
- int h = getHeight();
- if (w>0 && h>0) {
- getLocationInWindow(mLocation);
- // otherwise, punch a hole in the whole hierarchy
- int l = mLocation[0];
- int t = mLocation[1];
- region.op(l, t, l+w, t+h, Region.Op.UNION);
- }
- }
- if (PixelFormat.formatHasAlpha(mRequestedFormat)) {
- opaque = false;
- }
- return opaque;
+ return false;
}
- @Override
- public void draw(Canvas canvas) {
- if (mDrawFinished && !isAboveParent()) {
- // draw() is not called when SKIP_DRAW is set
- if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
- // punch a whole in the view-hierarchy below us
- canvas.drawColor(0, PorterDuff.Mode.CLEAR);
- }
- }
- super.draw(canvas);
- }
-
- @Override
- protected void dispatchDraw(Canvas canvas) {
- if (mDrawFinished && !isAboveParent()) {
- // draw() is not called when SKIP_DRAW is set
- if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
- // punch a whole in the view-hierarchy below us
- canvas.drawColor(0, PorterDuff.Mode.CLEAR);
- }
- }
- super.dispatchDraw(canvas);
- }
-
- /**
- * Control whether the surface view's surface is placed on top of another
- * regular surface view in the window (but still behind the window itself).
- * This is typically used to place overlays on top of an underlying media
- * surface view.
- *
- * <p>Note that this must be set before the surface view's containing
- * window is attached to the window manager.
- *
- * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}.
- */
public void setZOrderMediaOverlay(boolean isMediaOverlay) {
- mSubLayer = isMediaOverlay
- ? APPLICATION_MEDIA_OVERLAY_SUBLAYER : APPLICATION_MEDIA_SUBLAYER;
}
- /**
- * Control whether the surface view's surface is placed on top of its
- * window. Normally it is placed behind the window, to allow it to
- * (for the most part) appear to composite with the views in the
- * hierarchy. By setting this, you cause it to be placed above the
- * window. This means that none of the contents of the window this
- * SurfaceView is in will be visible on top of its surface.
- *
- * <p>Note that this must be set before the surface view's containing
- * window is attached to the window manager.
- *
- * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}.
- */
public void setZOrderOnTop(boolean onTop) {
- if (onTop) {
- mSubLayer = APPLICATION_PANEL_SUBLAYER;
- } else {
- mSubLayer = APPLICATION_MEDIA_SUBLAYER;
- }
}
- /**
- * Control whether the surface view's content should be treated as secure,
- * preventing it from appearing in screenshots or from being viewed on
- * non-secure displays.
- *
- * <p>Note that this must be set before the surface view's containing
- * window is attached to the window manager.
- *
- * <p>See {@link android.view.Display#FLAG_SECURE} for details.
- *
- * @param isSecure True if the surface view is secure.
- */
public void setSecure(boolean isSecure) {
- if (isSecure) {
- mSurfaceFlags |= SurfaceControl.SECURE;
- } else {
- mSurfaceFlags &= ~SurfaceControl.SECURE;
- }
- }
-
- private void updateOpaqueFlag() {
- if (!PixelFormat.formatHasAlpha(mRequestedFormat)) {
- mSurfaceFlags |= SurfaceControl.OPAQUE;
- } else {
- mSurfaceFlags &= ~SurfaceControl.OPAQUE;
- }
}
- private Rect getParentSurfaceInsets() {
- final ViewRootImpl root = getViewRootImpl();
- if (root == null) {
- return null;
- } else {
- return root.mWindowAttributes.surfaceInsets;
- }
- }
-
- /** @hide */
- protected void updateSurface() {
- if (!mHaveFrame) {
- return;
- }
- ViewRootImpl viewRoot = getViewRootImpl();
- if (viewRoot == null || viewRoot.mSurface == null || !viewRoot.mSurface.isValid()) {
- return;
- }
-
- mTranslator = viewRoot.mTranslator;
- if (mTranslator != null) {
- mSurface.setCompatibilityTranslator(mTranslator);
- }
-
- int myWidth = mRequestedWidth;
- if (myWidth <= 0) myWidth = getWidth();
- int myHeight = mRequestedHeight;
- if (myHeight <= 0) myHeight = getHeight();
-
- final boolean formatChanged = mFormat != mRequestedFormat;
- final boolean visibleChanged = mVisible != mRequestedVisible;
- final boolean creating = (mSurfaceControl == null || formatChanged || visibleChanged)
- && mRequestedVisible;
- final boolean sizeChanged = mSurfaceWidth != myWidth || mSurfaceHeight != myHeight;
- final boolean windowVisibleChanged = mWindowVisibility != mLastWindowVisibility;
- boolean redrawNeeded = false;
-
- if (creating || formatChanged || sizeChanged || visibleChanged || windowVisibleChanged) {
- getLocationInWindow(mLocation);
-
- if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
- + "Changes: creating=" + creating
- + " format=" + formatChanged + " size=" + sizeChanged
- + " visible=" + visibleChanged
- + " left=" + (mWindowSpaceLeft != mLocation[0])
- + " top=" + (mWindowSpaceTop != mLocation[1]));
-
- try {
- final boolean visible = mVisible = mRequestedVisible;
- mWindowSpaceLeft = mLocation[0];
- mWindowSpaceTop = mLocation[1];
- mSurfaceWidth = myWidth;
- mSurfaceHeight = myHeight;
- mFormat = mRequestedFormat;
- mLastWindowVisibility = mWindowVisibility;
-
- mScreenRect.left = mWindowSpaceLeft;
- mScreenRect.top = mWindowSpaceTop;
- mScreenRect.right = mWindowSpaceLeft + getWidth();
- mScreenRect.bottom = mWindowSpaceTop + getHeight();
- if (mTranslator != null) {
- mTranslator.translateRectInAppWindowToScreen(mScreenRect);
- }
-
- final Rect surfaceInsets = getParentSurfaceInsets();
- mScreenRect.offset(surfaceInsets.left, surfaceInsets.top);
-
- if (creating) {
- mSurfaceSession = new SurfaceSession(viewRoot.mSurface);
- mDeferredDestroySurfaceControl = mSurfaceControl;
-
- updateOpaqueFlag();
- final String name = "SurfaceView - " + viewRoot.getTitle().toString();
-
- mSurfaceControl = new SurfaceControlWithBackground(
- name,
- (mSurfaceFlags & SurfaceControl.OPAQUE) != 0,
- new SurfaceControl.Builder(mSurfaceSession)
- .setSize(mSurfaceWidth, mSurfaceHeight)
- .setFormat(mFormat)
- .setFlags(mSurfaceFlags));
- } else if (mSurfaceControl == null) {
- return;
- }
-
- boolean realSizeChanged = false;
-
- mSurfaceLock.lock();
- try {
- mDrawingStopped = !visible;
-
- if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
- + "Cur surface: " + mSurface);
-
- SurfaceControl.openTransaction();
- try {
- mSurfaceControl.setLayer(mSubLayer);
- if (mViewVisibility) {
- mSurfaceControl.show();
- } else {
- mSurfaceControl.hide();
- }
-
- // While creating the surface, we will set it's initial
- // geometry. Outside of that though, we should generally
- // leave it to the RenderThread.
- //
- // There is one more case when the buffer size changes we aren't yet
- // prepared to sync (as even following the transaction applying
- // we still need to latch a buffer).
- // b/28866173
- if (sizeChanged || creating || !mRtHandlingPositionUpdates) {
- mSurfaceControl.setPosition(mScreenRect.left, mScreenRect.top);
- mSurfaceControl.setMatrix(mScreenRect.width() / (float) mSurfaceWidth,
- 0.0f, 0.0f,
- mScreenRect.height() / (float) mSurfaceHeight);
- }
- if (sizeChanged) {
- mSurfaceControl.setSize(mSurfaceWidth, mSurfaceHeight);
- }
- } finally {
- SurfaceControl.closeTransaction();
- }
-
- if (sizeChanged || creating) {
- redrawNeeded = true;
- }
-
- mSurfaceFrame.left = 0;
- mSurfaceFrame.top = 0;
- if (mTranslator == null) {
- mSurfaceFrame.right = mSurfaceWidth;
- mSurfaceFrame.bottom = mSurfaceHeight;
- } else {
- float appInvertedScale = mTranslator.applicationInvertedScale;
- mSurfaceFrame.right = (int) (mSurfaceWidth * appInvertedScale + 0.5f);
- mSurfaceFrame.bottom = (int) (mSurfaceHeight * appInvertedScale + 0.5f);
- }
-
- final int surfaceWidth = mSurfaceFrame.right;
- final int surfaceHeight = mSurfaceFrame.bottom;
- realSizeChanged = mLastSurfaceWidth != surfaceWidth
- || mLastSurfaceHeight != surfaceHeight;
- mLastSurfaceWidth = surfaceWidth;
- mLastSurfaceHeight = surfaceHeight;
- } finally {
- mSurfaceLock.unlock();
- }
-
- try {
- redrawNeeded |= visible && !mDrawFinished;
-
- SurfaceHolder.Callback callbacks[] = null;
-
- final boolean surfaceChanged = creating;
- if (mSurfaceCreated && (surfaceChanged || (!visible && visibleChanged))) {
- mSurfaceCreated = false;
- if (mSurface.isValid()) {
- if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
- + "visibleChanged -- surfaceDestroyed");
- callbacks = getSurfaceCallbacks();
- for (SurfaceHolder.Callback c : callbacks) {
- c.surfaceDestroyed(mSurfaceHolder);
- }
- // Since Android N the same surface may be reused and given to us
- // again by the system server at a later point. However
- // as we didn't do this in previous releases, clients weren't
- // necessarily required to clean up properly in
- // surfaceDestroyed. This leads to problems for example when
- // clients don't destroy their EGL context, and try
- // and create a new one on the same surface following reuse.
- // Since there is no valid use of the surface in-between
- // surfaceDestroyed and surfaceCreated, we force a disconnect,
- // so the next connect will always work if we end up reusing
- // the surface.
- if (mSurface.isValid()) {
- mSurface.forceScopedDisconnect();
- }
- }
- }
-
- if (creating) {
- mSurface.copyFrom(mSurfaceControl);
- }
-
- if (sizeChanged && getContext().getApplicationInfo().targetSdkVersion
- < Build.VERSION_CODES.O) {
- // Some legacy applications use the underlying native {@link Surface} object
- // as a key to whether anything has changed. In these cases, updates to the
- // existing {@link Surface} will be ignored when the size changes.
- // Therefore, we must explicitly recreate the {@link Surface} in these
- // cases.
- mSurface.createFrom(mSurfaceControl);
- }
-
- if (visible && mSurface.isValid()) {
- if (!mSurfaceCreated && (surfaceChanged || visibleChanged)) {
- mSurfaceCreated = true;
- mIsCreating = true;
- if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
- + "visibleChanged -- surfaceCreated");
- if (callbacks == null) {
- callbacks = getSurfaceCallbacks();
- }
- for (SurfaceHolder.Callback c : callbacks) {
- c.surfaceCreated(mSurfaceHolder);
- }
- }
- if (creating || formatChanged || sizeChanged
- || visibleChanged || realSizeChanged) {
- if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
- + "surfaceChanged -- format=" + mFormat
- + " w=" + myWidth + " h=" + myHeight);
- if (callbacks == null) {
- callbacks = getSurfaceCallbacks();
- }
- for (SurfaceHolder.Callback c : callbacks) {
- c.surfaceChanged(mSurfaceHolder, mFormat, myWidth, myHeight);
- }
- }
- if (redrawNeeded) {
- if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
- + "surfaceRedrawNeeded");
- if (callbacks == null) {
- callbacks = getSurfaceCallbacks();
- }
-
- mPendingReportDraws++;
- viewRoot.drawPending();
- SurfaceCallbackHelper sch =
- new SurfaceCallbackHelper(this::onDrawFinished);
- sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks);
- }
- }
- } finally {
- mIsCreating = false;
- if (mSurfaceControl != null && !mSurfaceCreated) {
- mSurface.release();
- // If we are not in the stopped state, then the destruction of the Surface
- // represents a visual change we need to display, and we should go ahead
- // and destroy the SurfaceControl. However if we are in the stopped state,
- // we can just leave the Surface around so it can be a part of animations,
- // and we let the life-time be tied to the parent surface.
- if (!mWindowStopped) {
- mSurfaceControl.destroy();
- mSurfaceControl = null;
- }
- }
- }
- } catch (Exception ex) {
- Log.e(TAG, "Exception configuring surface", ex);
- }
- if (DEBUG) Log.v(
- TAG, "Layout: x=" + mScreenRect.left + " y=" + mScreenRect.top
- + " w=" + mScreenRect.width() + " h=" + mScreenRect.height()
- + ", frame=" + mSurfaceFrame);
- } else {
- // Calculate the window position in case RT loses the window
- // and we need to fallback to a UI-thread driven position update
- getLocationInSurface(mLocation);
- final boolean positionChanged = mWindowSpaceLeft != mLocation[0]
- || mWindowSpaceTop != mLocation[1];
- final boolean layoutSizeChanged = getWidth() != mScreenRect.width()
- || getHeight() != mScreenRect.height();
- if (positionChanged || layoutSizeChanged) { // Only the position has changed
- mWindowSpaceLeft = mLocation[0];
- mWindowSpaceTop = mLocation[1];
- // For our size changed check, we keep mScreenRect.width() and mScreenRect.height()
- // in view local space.
- mLocation[0] = getWidth();
- mLocation[1] = getHeight();
-
- mScreenRect.set(mWindowSpaceLeft, mWindowSpaceTop,
- mWindowSpaceLeft + mLocation[0], mWindowSpaceTop + mLocation[1]);
-
- if (mTranslator != null) {
- mTranslator.translateRectInAppWindowToScreen(mScreenRect);
- }
-
- if (mSurfaceControl == null) {
- return;
- }
-
- if (!isHardwareAccelerated() || !mRtHandlingPositionUpdates) {
- try {
- if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition UI, " +
- "postion = [%d, %d, %d, %d]", System.identityHashCode(this),
- mScreenRect.left, mScreenRect.top,
- mScreenRect.right, mScreenRect.bottom));
- setParentSpaceRectangle(mScreenRect, -1);
- } catch (Exception ex) {
- Log.e(TAG, "Exception configuring surface", ex);
- }
- }
- }
- }
- }
-
- private void onDrawFinished() {
- if (DEBUG) {
- Log.i(TAG, System.identityHashCode(this) + " "
- + "finishedDrawing");
- }
-
- if (mDeferredDestroySurfaceControl != null) {
- mDeferredDestroySurfaceControl.destroy();
- mDeferredDestroySurfaceControl = null;
- }
-
- runOnUiThread(() -> {
- performDrawFinished();
- });
- }
-
- private void setParentSpaceRectangle(Rect position, long frameNumber) {
- ViewRootImpl viewRoot = getViewRootImpl();
-
- SurfaceControl.openTransaction();
- try {
- if (frameNumber > 0) {
- mSurfaceControl.deferTransactionUntil(viewRoot.mSurface, frameNumber);
- }
- mSurfaceControl.setPosition(position.left, position.top);
- mSurfaceControl.setMatrix(position.width() / (float) mSurfaceWidth,
- 0.0f, 0.0f,
- position.height() / (float) mSurfaceHeight);
- } finally {
- SurfaceControl.closeTransaction();
- }
- }
-
- private Rect mRTLastReportedPosition = new Rect();
-
- /**
- * Called by native by a Rendering Worker thread to update the window position
- * @hide
- */
- public final void updateSurfacePosition_renderWorker(long frameNumber,
- int left, int top, int right, int bottom) {
- if (mSurfaceControl == null) {
- return;
- }
-
- // TODO: This is teensy bit racey in that a brand new SurfaceView moving on
- // its 2nd frame if RenderThread is running slowly could potentially see
- // this as false, enter the branch, get pre-empted, then this comes along
- // and reports a new position, then the UI thread resumes and reports
- // its position. This could therefore be de-sync'd in that interval, but
- // the synchronization would violate the rule that RT must never block
- // on the UI thread which would open up potential deadlocks. The risk of
- // a single-frame desync is therefore preferable for now.
- mRtHandlingPositionUpdates = true;
- if (mRTLastReportedPosition.left == left
- && mRTLastReportedPosition.top == top
- && mRTLastReportedPosition.right == right
- && mRTLastReportedPosition.bottom == bottom) {
- return;
- }
- try {
- if (DEBUG) {
- Log.d(TAG, String.format("%d updateSurfacePosition RenderWorker, frameNr = %d, " +
- "postion = [%d, %d, %d, %d]", System.identityHashCode(this),
- frameNumber, left, top, right, bottom));
- }
- mRTLastReportedPosition.set(left, top, right, bottom);
- setParentSpaceRectangle(mRTLastReportedPosition, frameNumber);
- // Now overwrite mRTLastReportedPosition with our values
- } catch (Exception ex) {
- Log.e(TAG, "Exception from repositionChild", ex);
- }
- }
-
- /**
- * Called by native on RenderThread to notify that the view is no longer in the
- * draw tree. UI thread is blocked at this point.
- * @hide
- */
- public final void surfacePositionLost_uiRtSync(long frameNumber) {
- if (DEBUG) {
- Log.d(TAG, String.format("%d windowPositionLost, frameNr = %d",
- System.identityHashCode(this), frameNumber));
- }
- mRTLastReportedPosition.setEmpty();
-
- if (mSurfaceControl == null) {
- return;
- }
- if (mRtHandlingPositionUpdates) {
- mRtHandlingPositionUpdates = false;
- // This callback will happen while the UI thread is blocked, so we can
- // safely access other member variables at this time.
- // So do what the UI thread would have done if RT wasn't handling position
- // updates.
- if (!mScreenRect.isEmpty() && !mScreenRect.equals(mRTLastReportedPosition)) {
- try {
- if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition, " +
- "postion = [%d, %d, %d, %d]", System.identityHashCode(this),
- mScreenRect.left, mScreenRect.top,
- mScreenRect.right, mScreenRect.bottom));
- setParentSpaceRectangle(mScreenRect, frameNumber);
- } catch (Exception ex) {
- Log.e(TAG, "Exception configuring surface", ex);
- }
- }
- }
- }
-
- private SurfaceHolder.Callback[] getSurfaceCallbacks() {
- SurfaceHolder.Callback callbacks[];
- synchronized (mCallbacks) {
- callbacks = new SurfaceHolder.Callback[mCallbacks.size()];
- mCallbacks.toArray(callbacks);
- }
- return callbacks;
- }
-
- /**
- * This method still exists only for compatibility reasons because some applications have relied
- * on this method via reflection. See Issue 36345857 for details.
- *
- * @deprecated No platform code is using this method anymore.
- * @hide
- */
- @Deprecated
- public void setWindowType(int type) {
- if (getContext().getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.O) {
- throw new UnsupportedOperationException(
- "SurfaceView#setWindowType() has never been a public API.");
- }
-
- if (type == TYPE_APPLICATION_PANEL) {
- Log.e(TAG, "If you are calling SurfaceView#setWindowType(TYPE_APPLICATION_PANEL) "
- + "just to make the SurfaceView to be placed on top of its window, you must "
- + "call setZOrderOnTop(true) instead.", new Throwable());
- setZOrderOnTop(true);
- return;
- }
- Log.e(TAG, "SurfaceView#setWindowType(int) is deprecated and now does nothing. "
- + "type=" + type, new Throwable());
- }
-
- private void runOnUiThread(Runnable runnable) {
- Handler handler = getHandler();
- if (handler != null && handler.getLooper() != Looper.myLooper()) {
- handler.post(runnable);
- } else {
- runnable.run();
- }
- }
-
- /**
- * Check to see if the surface has fixed size dimensions or if the surface's
- * dimensions are dimensions are dependent on its current layout.
- *
- * @return true if the surface has dimensions that are fixed in size
- * @hide
- */
- public boolean isFixedSize() {
- return (mRequestedWidth != -1 || mRequestedHeight != -1);
- }
-
- private boolean isAboveParent() {
- return mSubLayer >= 0;
+ public SurfaceHolder getHolder() {
+ return mSurfaceHolder;
}
- private final SurfaceHolder mSurfaceHolder = new SurfaceHolder() {
- private static final String LOG_TAG = "SurfaceHolder";
+ private SurfaceHolder mSurfaceHolder = new SurfaceHolder() {
@Override
public boolean isCreating() {
- return mIsCreating;
+ return false;
}
@Override
public void addCallback(Callback callback) {
- synchronized (mCallbacks) {
- // This is a linear search, but in practice we'll
- // have only a couple callbacks, so it doesn't matter.
- if (mCallbacks.contains(callback) == false) {
- mCallbacks.add(callback);
- }
- }
}
@Override
public void removeCallback(Callback callback) {
- synchronized (mCallbacks) {
- mCallbacks.remove(callback);
- }
}
@Override
public void setFixedSize(int width, int height) {
- if (mRequestedWidth != width || mRequestedHeight != height) {
- mRequestedWidth = width;
- mRequestedHeight = height;
- requestLayout();
- }
}
@Override
public void setSizeFromLayout() {
- if (mRequestedWidth != -1 || mRequestedHeight != -1) {
- mRequestedWidth = mRequestedHeight = -1;
- requestLayout();
- }
}
@Override
public void setFormat(int format) {
- // for backward compatibility reason, OPAQUE always
- // means 565 for SurfaceView
- if (format == PixelFormat.OPAQUE)
- format = PixelFormat.RGB_565;
-
- mRequestedFormat = format;
- if (mSurfaceControl != null) {
- updateSurface();
- }
}
- /**
- * @deprecated setType is now ignored.
- */
@Override
- @Deprecated
- public void setType(int type) { }
+ public void setType(int type) {
+ }
@Override
public void setKeepScreenOn(boolean screenOn) {
- runOnUiThread(() -> SurfaceView.this.setKeepScreenOn(screenOn));
}
- /**
- * Gets a {@link Canvas} for drawing into the SurfaceView's Surface
- *
- * After drawing into the provided {@link Canvas}, the caller must
- * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface.
- *
- * The caller must redraw the entire surface.
- * @return A canvas for drawing into the surface.
- */
@Override
public Canvas lockCanvas() {
- return internalLockCanvas(null, false);
- }
-
- /**
- * Gets a {@link Canvas} for drawing into the SurfaceView's Surface
- *
- * After drawing into the provided {@link Canvas}, the caller must
- * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface.
- *
- * @param inOutDirty A rectangle that represents the dirty region that the caller wants
- * to redraw. This function may choose to expand the dirty rectangle if for example
- * the surface has been resized or if the previous contents of the surface were
- * not available. The caller must redraw the entire dirty region as represented
- * by the contents of the inOutDirty rectangle upon return from this function.
- * The caller may also pass <code>null</code> instead, in the case where the
- * entire surface should be redrawn.
- * @return A canvas for drawing into the surface.
- */
- @Override
- public Canvas lockCanvas(Rect inOutDirty) {
- return internalLockCanvas(inOutDirty, false);
+ return null;
}
@Override
- public Canvas lockHardwareCanvas() {
- return internalLockCanvas(null, true);
- }
-
- private Canvas internalLockCanvas(Rect dirty, boolean hardware) {
- mSurfaceLock.lock();
-
- if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Locking canvas... stopped="
- + mDrawingStopped + ", surfaceControl=" + mSurfaceControl);
-
- Canvas c = null;
- if (!mDrawingStopped && mSurfaceControl != null) {
- try {
- if (hardware) {
- c = mSurface.lockHardwareCanvas();
- } else {
- c = mSurface.lockCanvas(dirty);
- }
- } catch (Exception e) {
- Log.e(LOG_TAG, "Exception locking surface", e);
- }
- }
-
- if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Returned canvas: " + c);
- if (c != null) {
- mLastLockTime = SystemClock.uptimeMillis();
- return c;
- }
-
- // If the Surface is not ready to be drawn, then return null,
- // but throttle calls to this function so it isn't called more
- // than every 100ms.
- long now = SystemClock.uptimeMillis();
- long nextTime = mLastLockTime + 100;
- if (nextTime > now) {
- try {
- Thread.sleep(nextTime-now);
- } catch (InterruptedException e) {
- }
- now = SystemClock.uptimeMillis();
- }
- mLastLockTime = now;
- mSurfaceLock.unlock();
-
+ public Canvas lockCanvas(Rect dirty) {
return null;
}
- /**
- * Posts the new contents of the {@link Canvas} to the surface and
- * releases the {@link Canvas}.
- *
- * @param canvas The canvas previously obtained from {@link #lockCanvas}.
- */
@Override
public void unlockCanvasAndPost(Canvas canvas) {
- mSurface.unlockCanvasAndPost(canvas);
- mSurfaceLock.unlock();
}
@Override
public Surface getSurface() {
- return mSurface;
+ return null;
}
@Override
public Rect getSurfaceFrame() {
- return mSurfaceFrame;
+ return null;
}
};
-
- class SurfaceControlWithBackground extends SurfaceControl {
- private SurfaceControl mBackgroundControl;
- private boolean mOpaque = true;
- public boolean mVisible = false;
-
- public SurfaceControlWithBackground(String name, boolean opaque, SurfaceControl.Builder b)
- throws Exception {
- super(b.setName(name).build());
-
- mBackgroundControl = b.setName("Background for -" + name)
- .setFormat(OPAQUE)
- .setColorLayer(true)
- .build();
- mOpaque = opaque;
- }
-
- @Override
- public void setAlpha(float alpha) {
- super.setAlpha(alpha);
- mBackgroundControl.setAlpha(alpha);
- }
-
- @Override
- public void setLayer(int zorder) {
- super.setLayer(zorder);
- // -3 is below all other child layers as SurfaceView never goes below -2
- mBackgroundControl.setLayer(-3);
- }
-
- @Override
- public void setPosition(float x, float y) {
- super.setPosition(x, y);
- mBackgroundControl.setPosition(x, y);
- }
-
- @Override
- public void setSize(int w, int h) {
- super.setSize(w, h);
- mBackgroundControl.setSize(w, h);
- }
-
- @Override
- public void setWindowCrop(Rect crop) {
- super.setWindowCrop(crop);
- mBackgroundControl.setWindowCrop(crop);
- }
-
- @Override
- public void setFinalCrop(Rect crop) {
- super.setFinalCrop(crop);
- mBackgroundControl.setFinalCrop(crop);
- }
-
- @Override
- public void setLayerStack(int layerStack) {
- super.setLayerStack(layerStack);
- mBackgroundControl.setLayerStack(layerStack);
- }
-
- @Override
- public void setOpaque(boolean isOpaque) {
- super.setOpaque(isOpaque);
- mOpaque = isOpaque;
- updateBackgroundVisibility();
- }
-
- @Override
- public void setSecure(boolean isSecure) {
- super.setSecure(isSecure);
- }
-
- @Override
- public void setMatrix(float dsdx, float dtdx, float dsdy, float dtdy) {
- super.setMatrix(dsdx, dtdx, dsdy, dtdy);
- mBackgroundControl.setMatrix(dsdx, dtdx, dsdy, dtdy);
- }
-
- @Override
- public void hide() {
- super.hide();
- mVisible = false;
- updateBackgroundVisibility();
- }
-
- @Override
- public void show() {
- super.show();
- mVisible = true;
- updateBackgroundVisibility();
- }
-
- @Override
- public void destroy() {
- super.destroy();
- mBackgroundControl.destroy();
- }
-
- @Override
- public void release() {
- super.release();
- mBackgroundControl.release();
- }
-
- @Override
- public void setTransparentRegionHint(Region region) {
- super.setTransparentRegionHint(region);
- mBackgroundControl.setTransparentRegionHint(region);
- }
-
- @Override
- public void deferTransactionUntil(IBinder handle, long frame) {
- super.deferTransactionUntil(handle, frame);
- mBackgroundControl.deferTransactionUntil(handle, frame);
- }
-
- @Override
- public void deferTransactionUntil(Surface barrier, long frame) {
- super.deferTransactionUntil(barrier, frame);
- mBackgroundControl.deferTransactionUntil(barrier, frame);
- }
-
- void updateBackgroundVisibility() {
- if (mOpaque && mVisible) {
- mBackgroundControl.show();
- } else {
- mBackgroundControl.hide();
- }
- }
- }
}
+
diff --git a/android/view/ThreadedRenderer.java b/android/view/ThreadedRenderer.java
index 7c76bab2..6a8f8b12 100644
--- a/android/view/ThreadedRenderer.java
+++ b/android/view/ThreadedRenderer.java
@@ -190,6 +190,17 @@ public final class ThreadedRenderer {
public static final String DEBUG_SHOW_NON_RECTANGULAR_CLIP_PROPERTY =
"debug.hwui.show_non_rect_clip";
+ /**
+ * Sets the FPS devisor to lower the FPS.
+ *
+ * Sets a positive integer as a divisor. 1 (the default value) menas the full FPS, and 2
+ * means half the full FPS.
+ *
+ *
+ * @hide
+ */
+ public static final String DEBUG_FPS_DIVISOR = "debug.hwui.fps_divisor";
+
static {
// Try to check OpenGL support early if possible.
isAvailable();
@@ -333,8 +344,10 @@ public final class ThreadedRenderer {
private static final int FLAG_DUMP_FRAMESTATS = 1 << 0;
private static final int FLAG_DUMP_RESET = 1 << 1;
- @IntDef(flag = true, value = {
- FLAG_DUMP_FRAMESTATS, FLAG_DUMP_RESET })
+ @IntDef(flag = true, prefix = { "FLAG_DUMP_" }, value = {
+ FLAG_DUMP_FRAMESTATS,
+ FLAG_DUMP_RESET
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface DumpFlags {}
@@ -955,6 +968,9 @@ public final class ThreadedRenderer {
if (mInitialized) return;
mInitialized = true;
mAppContext = context.getApplicationContext();
+
+ // b/68769804: For low FPS experiments.
+ setFPSDivisor(SystemProperties.getInt(DEBUG_FPS_DIVISOR, 1));
initSched(renderProxy);
initGraphicsStats();
}
@@ -1007,6 +1023,13 @@ public final class ThreadedRenderer {
observer.mNative = null;
}
+ /** b/68769804: For low FPS experiments. */
+ public static void setFPSDivisor(int divisor) {
+ if (divisor <= 0) divisor = 1;
+ Choreographer.getInstance().setFPSDivisor(divisor);
+ nHackySetRTAnimationsEnabled(divisor == 1);
+ }
+
/** Not actually public - internal use only. This doc to make lint happy */
public static native void disableVsync();
@@ -1075,4 +1098,6 @@ public final class ThreadedRenderer {
private static native Bitmap nCreateHardwareBitmap(long renderNode, int width, int height);
private static native void nSetHighContrastText(boolean enabled);
+ // For temporary experimentation b/66945974
+ private static native void nHackySetRTAnimationsEnabled(boolean enabled);
}
diff --git a/android/view/View.java b/android/view/View.java
index 0525ab16..cc63a62c 100644
--- a/android/view/View.java
+++ b/android/view/View.java
@@ -893,6 +893,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
private static boolean sAutoFocusableOffUIThreadWontNotifyParents;
+ /**
+ * Prior to P things like setScaleX() allowed passing float values that were bogus such as
+ * Float.NaN. If the app is targetting P or later then passing these values will result in an
+ * exception being thrown. If the app is targetting an earlier SDK version, then we will
+ * silently clamp these values to avoid crashes elsewhere when the rendering code hits
+ * these bogus values.
+ */
+ private static boolean sThrowOnInvalidFloatProperties;
+
/** @hide */
@IntDef({NOT_FOCUSABLE, FOCUSABLE, FOCUSABLE_AUTO})
@Retention(RetentionPolicy.SOURCE)
@@ -1169,7 +1178,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
private AutofillId mAutofillId;
/** @hide */
- @IntDef({
+ @IntDef(prefix = { "AUTOFILL_TYPE_" }, value = {
AUTOFILL_TYPE_NONE,
AUTOFILL_TYPE_TEXT,
AUTOFILL_TYPE_TOGGLE,
@@ -1240,7 +1249,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
public static final int AUTOFILL_TYPE_DATE = 4;
/** @hide */
- @IntDef({
+ @IntDef(prefix = { "IMPORTANT_FOR_AUTOFILL_" }, value = {
IMPORTANT_FOR_AUTOFILL_AUTO,
IMPORTANT_FOR_AUTOFILL_YES,
IMPORTANT_FOR_AUTOFILL_NO,
@@ -1291,9 +1300,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
public static final int IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS = 0x8;
/** @hide */
- @IntDef(
- flag = true,
- value = {AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS})
+ @IntDef(flag = true, prefix = { "AUTOFILL_FLAG_" }, value = {
+ AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface AutofillFlags {}
@@ -1443,7 +1452,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({DRAWING_CACHE_QUALITY_LOW, DRAWING_CACHE_QUALITY_HIGH, DRAWING_CACHE_QUALITY_AUTO})
+ @IntDef(prefix = { "DRAWING_CACHE_QUALITY_" }, value = {
+ DRAWING_CACHE_QUALITY_LOW,
+ DRAWING_CACHE_QUALITY_HIGH,
+ DRAWING_CACHE_QUALITY_AUTO
+ })
public @interface DrawingCacheQuality {}
/**
@@ -1542,13 +1555,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
static final int CONTEXT_CLICKABLE = 0x00800000;
-
/** @hide */
- @IntDef({
- SCROLLBARS_INSIDE_OVERLAY,
- SCROLLBARS_INSIDE_INSET,
- SCROLLBARS_OUTSIDE_OVERLAY,
- SCROLLBARS_OUTSIDE_INSET
+ @IntDef(prefix = { "SCROLLBARS_" }, value = {
+ SCROLLBARS_INSIDE_OVERLAY,
+ SCROLLBARS_INSIDE_INSET,
+ SCROLLBARS_OUTSIDE_OVERLAY,
+ SCROLLBARS_OUTSIDE_INSET
})
@Retention(RetentionPolicy.SOURCE)
public @interface ScrollBarStyle {}
@@ -1642,11 +1654,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
static final int TOOLTIP = 0x40000000;
/** @hide */
- @IntDef(flag = true,
- value = {
- FOCUSABLES_ALL,
- FOCUSABLES_TOUCH_MODE
- })
+ @IntDef(flag = true, prefix = { "FOCUSABLES_" }, value = {
+ FOCUSABLES_ALL,
+ FOCUSABLES_TOUCH_MODE
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface FocusableMode {}
@@ -1663,7 +1674,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
public static final int FOCUSABLES_TOUCH_MODE = 0x00000001;
/** @hide */
- @IntDef({
+ @IntDef(prefix = { "FOCUS_" }, value = {
FOCUS_BACKWARD,
FOCUS_FORWARD,
FOCUS_LEFT,
@@ -1675,7 +1686,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
public @interface FocusDirection {}
/** @hide */
- @IntDef({
+ @IntDef(prefix = { "FOCUS_" }, value = {
FOCUS_LEFT,
FOCUS_UP,
FOCUS_RIGHT,
@@ -2417,20 +2428,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
static final int PFLAG2_DRAG_HOVERED = 0x00000002;
/** @hide */
- @IntDef({
- LAYOUT_DIRECTION_LTR,
- LAYOUT_DIRECTION_RTL,
- LAYOUT_DIRECTION_INHERIT,
- LAYOUT_DIRECTION_LOCALE
+ @IntDef(prefix = { "LAYOUT_DIRECTION_" }, value = {
+ LAYOUT_DIRECTION_LTR,
+ LAYOUT_DIRECTION_RTL,
+ LAYOUT_DIRECTION_INHERIT,
+ LAYOUT_DIRECTION_LOCALE
})
@Retention(RetentionPolicy.SOURCE)
// Not called LayoutDirection to avoid conflict with android.util.LayoutDirection
public @interface LayoutDir {}
/** @hide */
- @IntDef({
- LAYOUT_DIRECTION_LTR,
- LAYOUT_DIRECTION_RTL
+ @IntDef(prefix = { "LAYOUT_DIRECTION_" }, value = {
+ LAYOUT_DIRECTION_LTR,
+ LAYOUT_DIRECTION_RTL
})
@Retention(RetentionPolicy.SOURCE)
public @interface ResolvedLayoutDir {}
@@ -2636,14 +2647,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
TEXT_DIRECTION_RESOLVED_DEFAULT << PFLAG2_TEXT_DIRECTION_RESOLVED_MASK_SHIFT;
/** @hide */
- @IntDef({
- TEXT_ALIGNMENT_INHERIT,
- TEXT_ALIGNMENT_GRAVITY,
- TEXT_ALIGNMENT_CENTER,
- TEXT_ALIGNMENT_TEXT_START,
- TEXT_ALIGNMENT_TEXT_END,
- TEXT_ALIGNMENT_VIEW_START,
- TEXT_ALIGNMENT_VIEW_END
+ @IntDef(prefix = { "TEXT_ALIGNMENT_" }, value = {
+ TEXT_ALIGNMENT_INHERIT,
+ TEXT_ALIGNMENT_GRAVITY,
+ TEXT_ALIGNMENT_CENTER,
+ TEXT_ALIGNMENT_TEXT_START,
+ TEXT_ALIGNMENT_TEXT_END,
+ TEXT_ALIGNMENT_VIEW_START,
+ TEXT_ALIGNMENT_VIEW_END
})
@Retention(RetentionPolicy.SOURCE)
public @interface TextAlignment {}
@@ -3040,15 +3051,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true,
- value = {
- SCROLL_INDICATOR_TOP,
- SCROLL_INDICATOR_BOTTOM,
- SCROLL_INDICATOR_LEFT,
- SCROLL_INDICATOR_RIGHT,
- SCROLL_INDICATOR_START,
- SCROLL_INDICATOR_END,
- })
+ @IntDef(flag = true, prefix = { "SCROLL_INDICATOR_" }, value = {
+ SCROLL_INDICATOR_TOP,
+ SCROLL_INDICATOR_BOTTOM,
+ SCROLL_INDICATOR_LEFT,
+ SCROLL_INDICATOR_RIGHT,
+ SCROLL_INDICATOR_START,
+ SCROLL_INDICATOR_END,
+ })
public @interface ScrollIndicators {}
/**
@@ -3674,8 +3684,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
| SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
/** @hide */
- @IntDef(flag = true,
- value = { FIND_VIEWS_WITH_TEXT, FIND_VIEWS_WITH_CONTENT_DESCRIPTION })
+ @IntDef(flag = true, prefix = { "FIND_VIEWS_" }, value = {
+ FIND_VIEWS_WITH_TEXT,
+ FIND_VIEWS_WITH_CONTENT_DESCRIPTION
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface FindViewFlags {}
@@ -4287,6 +4299,38 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
Runnable mShowTooltipRunnable;
Runnable mHideTooltipRunnable;
+
+ /**
+ * Hover move is ignored if it is within this distance in pixels from the previous one.
+ */
+ int mHoverSlop;
+
+ /**
+ * Update the anchor position if it significantly (that is by at least mHoverSlop)
+ * different from the previously stored position. Ignoring insignificant changes
+ * filters out the jitter which is typical for such input sources as stylus.
+ *
+ * @return True if the position has been updated.
+ */
+ private boolean updateAnchorPos(MotionEvent event) {
+ final int newAnchorX = (int) event.getX();
+ final int newAnchorY = (int) event.getY();
+ if (Math.abs(newAnchorX - mAnchorX) <= mHoverSlop
+ && Math.abs(newAnchorY - mAnchorY) <= mHoverSlop) {
+ return false;
+ }
+ mAnchorX = newAnchorX;
+ mAnchorY = newAnchorY;
+ return true;
+ }
+
+ /**
+ * Clear the anchor position to ensure that the next change is considered significant.
+ */
+ private void clearAnchorPos() {
+ mAnchorX = Integer.MAX_VALUE;
+ mAnchorY = Integer.MAX_VALUE;
+ }
}
TooltipInfo mTooltipInfo;
@@ -4752,6 +4796,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
sUseDefaultFocusHighlight = context.getResources().getBoolean(
com.android.internal.R.bool.config_useDefaultFocusHighlight);
+ sThrowOnInvalidFloatProperties = targetSdkVersion >= Build.VERSION_CODES.P;
+
sCompatibilityDone = true;
}
}
@@ -7208,8 +7254,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* @param text The announcement text.
*/
public void announceForAccessibility(CharSequence text) {
- if (AccessibilityManager.getInstance(mContext).isObservedEventType(
- AccessibilityEvent.TYPE_ANNOUNCEMENT) && mParent != null) {
+ if (AccessibilityManager.getInstance(mContext).isEnabled() && mParent != null) {
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_ANNOUNCEMENT);
onInitializeAccessibilityEvent(event);
@@ -10968,8 +11013,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
if ((mPrivateFlags2 & PFLAG2_ACCESSIBILITY_FOCUSED) != 0) {
mPrivateFlags2 &= ~PFLAG2_ACCESSIBILITY_FOCUSED;
invalidate();
- if (AccessibilityManager.getInstance(mContext).isObservedEventType(
- AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED)) {
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
event.setAction(action);
@@ -11794,8 +11838,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
private void sendViewTextTraversedAtGranularityEvent(int action, int granularity,
int fromIndex, int toIndex) {
- if (mParent == null || !AccessibilityManager.getInstance(mContext).isObservedEventType(
- AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY)) {
+ if (mParent == null) {
return;
}
AccessibilityEvent event = AccessibilityEvent.obtain(
@@ -14275,7 +14318,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
public void setScaleX(float scaleX) {
if (scaleX != getScaleX()) {
- requireIsFinite(scaleX, "scaleX");
+ scaleX = sanitizeFloatPropertyValue(scaleX, "scaleX");
invalidateViewProperty(true, false);
mRenderNode.setScaleX(scaleX);
invalidateViewProperty(false, true);
@@ -14312,7 +14355,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
public void setScaleY(float scaleY) {
if (scaleY != getScaleY()) {
- requireIsFinite(scaleY, "scaleY");
+ scaleY = sanitizeFloatPropertyValue(scaleY, "scaleY");
invalidateViewProperty(true, false);
mRenderNode.setScaleY(scaleY);
invalidateViewProperty(false, true);
@@ -14862,13 +14905,41 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
}
- private static void requireIsFinite(float transform, String propertyName) {
- if (Float.isNaN(transform)) {
- throw new IllegalArgumentException("Cannot set '" + propertyName + "' to Float.NaN");
+ private static float sanitizeFloatPropertyValue(float value, String propertyName) {
+ return sanitizeFloatPropertyValue(value, propertyName, -Float.MAX_VALUE, Float.MAX_VALUE);
+ }
+
+ private static float sanitizeFloatPropertyValue(float value, String propertyName,
+ float min, float max) {
+ // The expected "nothing bad happened" path
+ if (value >= min && value <= max) return value;
+
+ if (value < min || value == Float.NEGATIVE_INFINITY) {
+ if (sThrowOnInvalidFloatProperties) {
+ throw new IllegalArgumentException("Cannot set '" + propertyName + "' to "
+ + value + ", the value must be >= " + min);
+ }
+ return min;
+ }
+
+ if (value > max || value == Float.POSITIVE_INFINITY) {
+ if (sThrowOnInvalidFloatProperties) {
+ throw new IllegalArgumentException("Cannot set '" + propertyName + "' to "
+ + value + ", the value must be <= " + max);
+ }
+ return max;
}
- if (Float.isInfinite(transform)) {
- throw new IllegalArgumentException("Cannot set '" + propertyName + "' to infinity");
+
+ if (Float.isNaN(value)) {
+ if (sThrowOnInvalidFloatProperties) {
+ throw new IllegalArgumentException(
+ "Cannot set '" + propertyName + "' to Float.NaN");
+ }
+ return 0; // Unclear which direction this NaN went so... 0?
}
+
+ // Shouldn't be possible to reach this.
+ throw new IllegalStateException("How do you get here?? " + value);
}
/**
@@ -14957,7 +15028,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
public void setElevation(float elevation) {
if (elevation != getElevation()) {
- requireIsFinite(elevation, "elevation");
+ elevation = sanitizeFloatPropertyValue(elevation, "elevation");
invalidateViewProperty(true, false);
mRenderNode.setElevation(elevation);
invalidateViewProperty(false, true);
@@ -15050,7 +15121,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
public void setTranslationZ(float translationZ) {
if (translationZ != getTranslationZ()) {
- requireIsFinite(translationZ, "translationZ");
+ translationZ = sanitizeFloatPropertyValue(translationZ, "translationZ");
invalidateViewProperty(true, false);
mRenderNode.setTranslationZ(translationZ);
invalidateViewProperty(false, true);
@@ -25721,6 +25792,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
final Rect mStableInsets = new Rect();
+ final DisplayCutout.ParcelableWrapper mDisplayCutout =
+ new DisplayCutout.ParcelableWrapper(DisplayCutout.NO_CUTOUT);
+
/**
* For windows that include areas that are not covered by real surface these are the outsets
* for real surface.
@@ -26185,8 +26259,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
@Override
public void run() {
- if (AccessibilityManager.getInstance(mContext).isObservedEventType(
- AccessibilityEvent.TYPE_VIEW_SCROLLED)) {
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_VIEW_SCROLLED);
event.setScrollDeltaX(mDeltaX);
@@ -26775,6 +26848,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
mTooltipInfo = new TooltipInfo();
mTooltipInfo.mShowTooltipRunnable = this::showHoverTooltip;
mTooltipInfo.mHideTooltipRunnable = this::hideTooltip;
+ mTooltipInfo.mHoverSlop = ViewConfiguration.get(mContext).getScaledHoverSlop();
+ mTooltipInfo.clearAnchorPos();
}
mTooltipInfo.mTooltipText = tooltipText;
}
@@ -26815,7 +26890,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
if (mAttachInfo == null || mTooltipInfo == null) {
return false;
}
- if ((mViewFlags & ENABLED_MASK) != ENABLED) {
+ if (fromLongClick && (mViewFlags & ENABLED_MASK) != ENABLED) {
return false;
}
if (TextUtils.isEmpty(mTooltipInfo.mTooltipText)) {
@@ -26841,6 +26916,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
mTooltipInfo.mTooltipPopup.hide();
mTooltipInfo.mTooltipPopup = null;
mTooltipInfo.mTooltipFromLongClick = false;
+ mTooltipInfo.clearAnchorPos();
if (mAttachInfo != null) {
mAttachInfo.mTooltipHost = null;
}
@@ -26862,14 +26938,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
switch(event.getAction()) {
case MotionEvent.ACTION_HOVER_MOVE:
- if ((mViewFlags & TOOLTIP) != TOOLTIP || (mViewFlags & ENABLED_MASK) != ENABLED) {
+ if ((mViewFlags & TOOLTIP) != TOOLTIP) {
break;
}
- if (!mTooltipInfo.mTooltipFromLongClick) {
+ if (!mTooltipInfo.mTooltipFromLongClick && mTooltipInfo.updateAnchorPos(event)) {
if (mTooltipInfo.mTooltipPopup == null) {
// Schedule showing the tooltip after a timeout.
- mTooltipInfo.mAnchorX = (int) event.getX();
- mTooltipInfo.mAnchorY = (int) event.getY();
removeCallbacks(mTooltipInfo.mShowTooltipRunnable);
postDelayed(mTooltipInfo.mShowTooltipRunnable,
ViewConfiguration.getHoverTooltipShowTimeout());
@@ -26891,6 +26965,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
return true;
case MotionEvent.ACTION_HOVER_EXIT:
+ mTooltipInfo.clearAnchorPos();
if (!mTooltipInfo.mTooltipFromLongClick) {
hideTooltip();
}
diff --git a/android/view/ViewConfiguration.java b/android/view/ViewConfiguration.java
index c44c8dda..c5a94daa 100644
--- a/android/view/ViewConfiguration.java
+++ b/android/view/ViewConfiguration.java
@@ -290,6 +290,7 @@ public class ViewConfiguration {
private final int mMaximumFlingVelocity;
private final int mScrollbarSize;
private final int mTouchSlop;
+ private final int mHoverSlop;
private final int mMinScrollbarTouchTarget;
private final int mDoubleTapTouchSlop;
private final int mPagingTouchSlop;
@@ -320,6 +321,7 @@ public class ViewConfiguration {
mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY;
mScrollbarSize = SCROLL_BAR_SIZE;
mTouchSlop = TOUCH_SLOP;
+ mHoverSlop = TOUCH_SLOP / 2;
mMinScrollbarTouchTarget = MIN_SCROLLBAR_TOUCH_TARGET;
mDoubleTapTouchSlop = DOUBLE_TAP_TOUCH_SLOP;
mPagingTouchSlop = PAGING_TOUCH_SLOP;
@@ -407,6 +409,8 @@ public class ViewConfiguration {
com.android.internal.R.bool.config_ui_enableFadingMarquee);
mTouchSlop = res.getDimensionPixelSize(
com.android.internal.R.dimen.config_viewConfigurationTouchSlop);
+ mHoverSlop = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_viewConfigurationHoverSlop);
mMinScrollbarTouchTarget = res.getDimensionPixelSize(
com.android.internal.R.dimen.config_minScrollbarTouchTarget);
mPagingTouchSlop = mTouchSlop * 2;
@@ -640,6 +644,14 @@ public class ViewConfiguration {
}
/**
+ * @return Distance in pixels a hover can wander while it is still considered "stationary".
+ *
+ */
+ public int getScaledHoverSlop() {
+ return mHoverSlop;
+ }
+
+ /**
* @return Distance in pixels the first touch can wander before we do not consider this a
* potential double tap event
* @hide
diff --git a/android/view/ViewRootImpl.java b/android/view/ViewRootImpl.java
index 1c9d8639..6c5091c2 100644
--- a/android/view/ViewRootImpl.java
+++ b/android/view/ViewRootImpl.java
@@ -20,6 +20,7 @@ import static android.view.Display.INVALID_DISPLAY;
import static android.view.View.PFLAG_DRAW_ANIMATION;
import static android.view.WindowCallbacks.RESIZE_MODE_DOCKED_DIVIDER;
import static android.view.WindowCallbacks.RESIZE_MODE_FREEFORM;
+import static android.view.WindowManager.LayoutParams.FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL;
@@ -142,10 +143,11 @@ public final class ViewRootImpl implements ViewParent,
private static final boolean DEBUG_KEEP_SCREEN_ON = false || LOCAL_LOGV;
/**
- * Set to false if we do not want to use the multi threaded renderer. Note that by disabling
+ * Set to false if we do not want to use the multi threaded renderer even though
+ * threaded renderer (aka hardware renderering) is used. Note that by disabling
* this, WindowCallbacks will not fire.
*/
- private static final boolean USE_MT_RENDERER = true;
+ private static final boolean MT_RENDERER_AVAILABLE = true;
/**
* Set this system property to true to force the view hierarchy to render
@@ -302,6 +304,7 @@ public final class ViewRootImpl implements ViewParent,
Rect mDirty;
public boolean mIsAnimating;
+ private boolean mUseMTRenderer;
private boolean mDragResizing;
private boolean mInvalidateRootRequested;
private int mResizeMode;
@@ -321,6 +324,15 @@ public final class ViewRootImpl implements ViewParent,
final Rect mTempRect; // used in the transaction to not thrash the heap.
final Rect mVisRect; // used to retrieve visible rect of focused view.
+ // This is used to reduce the race between window focus changes being dispatched from
+ // the window manager and input events coming through the input system.
+ @GuardedBy("this")
+ boolean mWindowFocusChanged;
+ @GuardedBy("this")
+ boolean mUpcomingWindowFocus;
+ @GuardedBy("this")
+ boolean mUpcomingInTouchMode;
+
public boolean mTraversalScheduled;
int mTraversalBarrier;
boolean mWillDrawSoon;
@@ -384,12 +396,15 @@ public final class ViewRootImpl implements ViewParent,
final Rect mPendingContentInsets = new Rect();
final Rect mPendingOutsets = new Rect();
final Rect mPendingBackDropFrame = new Rect();
+ final DisplayCutout.ParcelableWrapper mPendingDisplayCutout =
+ new DisplayCutout.ParcelableWrapper(DisplayCutout.NO_CUTOUT);
boolean mPendingAlwaysConsumeNavBar;
final ViewTreeObserver.InternalInsetsInfo mLastGivenInsets
= new ViewTreeObserver.InternalInsetsInfo();
final Rect mDispatchContentInsets = new Rect();
final Rect mDispatchStableInsets = new Rect();
+ DisplayCutout mDispatchDisplayCutout = DisplayCutout.NO_CUTOUT;
private WindowInsets mLastWindowInsets;
@@ -545,18 +560,14 @@ public final class ViewRootImpl implements ViewParent,
}
public void addWindowCallbacks(WindowCallbacks callback) {
- if (USE_MT_RENDERER) {
- synchronized (mWindowCallbacks) {
- mWindowCallbacks.add(callback);
- }
+ synchronized (mWindowCallbacks) {
+ mWindowCallbacks.add(callback);
}
}
public void removeWindowCallbacks(WindowCallbacks callback) {
- if (USE_MT_RENDERER) {
- synchronized (mWindowCallbacks) {
- mWindowCallbacks.remove(callback);
- }
+ synchronized (mWindowCallbacks) {
+ mWindowCallbacks.remove(callback);
}
}
@@ -682,7 +693,17 @@ public final class ViewRootImpl implements ViewParent,
// If the application owns the surface, don't enable hardware acceleration
if (mSurfaceHolder == null) {
+ // While this is supposed to enable only, it can effectively disable
+ // the acceleration too.
enableHardwareAcceleration(attrs);
+ final boolean useMTRenderer = MT_RENDERER_AVAILABLE
+ && mAttachInfo.mThreadedRenderer != null;
+ if (mUseMTRenderer != useMTRenderer) {
+ // Shouldn't be resizing, as it's done only in window setup,
+ // but end just in case.
+ endDragResizing();
+ mUseMTRenderer = useMTRenderer;
+ }
}
boolean restore = false;
@@ -730,7 +751,7 @@ public final class ViewRootImpl implements ViewParent,
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
- mAttachInfo.mOutsets, mInputChannel);
+ mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
} catch (RemoteException e) {
mAdded = false;
mView = null;
@@ -752,6 +773,7 @@ public final class ViewRootImpl implements ViewParent,
mPendingOverscanInsets.set(0, 0, 0, 0);
mPendingContentInsets.set(mAttachInfo.mContentInsets);
mPendingStableInsets.set(mAttachInfo.mStableInsets);
+ mPendingDisplayCutout.set(mAttachInfo.mDisplayCutout);
mPendingVisibleInsets.set(0, 0, 0, 0);
mAttachInfo.mAlwaysConsumeNavBar =
(res & WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_NAV_BAR) != 0;
@@ -1544,15 +1566,20 @@ public final class ViewRootImpl implements ViewParent,
if (mLastWindowInsets == null || forceConstruct) {
mDispatchContentInsets.set(mAttachInfo.mContentInsets);
mDispatchStableInsets.set(mAttachInfo.mStableInsets);
+ mDispatchDisplayCutout = mAttachInfo.mDisplayCutout.get();
+
Rect contentInsets = mDispatchContentInsets;
Rect stableInsets = mDispatchStableInsets;
+ DisplayCutout displayCutout = mDispatchDisplayCutout;
// For dispatch we preserve old logic, but for direct requests from Views we allow to
// immediately use pending insets.
if (!forceConstruct
&& (!mPendingContentInsets.equals(contentInsets) ||
- !mPendingStableInsets.equals(stableInsets))) {
+ !mPendingStableInsets.equals(stableInsets) ||
+ !mPendingDisplayCutout.get().equals(displayCutout))) {
contentInsets = mPendingContentInsets;
stableInsets = mPendingStableInsets;
+ displayCutout = mPendingDisplayCutout.get();
}
Rect outsets = mAttachInfo.mOutsets;
if (outsets.left > 0 || outsets.top > 0 || outsets.right > 0 || outsets.bottom > 0) {
@@ -1563,13 +1590,21 @@ public final class ViewRootImpl implements ViewParent,
mLastWindowInsets = new WindowInsets(contentInsets,
null /* windowDecorInsets */, stableInsets,
mContext.getResources().getConfiguration().isScreenRound(),
- mAttachInfo.mAlwaysConsumeNavBar, null /* displayCutout */);
+ mAttachInfo.mAlwaysConsumeNavBar, displayCutout);
}
return mLastWindowInsets;
}
void dispatchApplyInsets(View host) {
- host.dispatchApplyWindowInsets(getWindowInsets(true /* forceConstruct */));
+ WindowInsets insets = getWindowInsets(true /* forceConstruct */);
+ final boolean layoutInCutout =
+ (mWindowAttributes.flags2 & FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA) != 0;
+ if (!layoutInCutout) {
+ // Window is either not laid out in cutout or the status bar inset takes care of
+ // clearing the cutout, so we don't need to dispatch the cutout to the hierarchy.
+ insets = insets.consumeDisplayCutout();
+ }
+ host.dispatchApplyWindowInsets(insets);
}
private static boolean shouldUseDisplaySize(final WindowManager.LayoutParams lp) {
@@ -1730,6 +1765,9 @@ public final class ViewRootImpl implements ViewParent,
if (!mPendingStableInsets.equals(mAttachInfo.mStableInsets)) {
insetsChanged = true;
}
+ if (!mPendingDisplayCutout.equals(mAttachInfo.mDisplayCutout)) {
+ insetsChanged = true;
+ }
if (!mPendingVisibleInsets.equals(mAttachInfo.mVisibleInsets)) {
mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
if (DEBUG_LAYOUT) Log.v(mTag, "Visible insets changing to: "
@@ -1906,7 +1944,8 @@ public final class ViewRootImpl implements ViewParent,
+ " overscan=" + mPendingOverscanInsets.toShortString()
+ " content=" + mPendingContentInsets.toShortString()
+ " visible=" + mPendingVisibleInsets.toShortString()
- + " visible=" + mPendingStableInsets.toShortString()
+ + " stable=" + mPendingStableInsets.toShortString()
+ + " cutout=" + mPendingDisplayCutout.get().toString()
+ " outsets=" + mPendingOutsets.toShortString()
+ " surface=" + mSurface);
@@ -1931,6 +1970,8 @@ public final class ViewRootImpl implements ViewParent,
mAttachInfo.mVisibleInsets);
final boolean stableInsetsChanged = !mPendingStableInsets.equals(
mAttachInfo.mStableInsets);
+ final boolean cutoutChanged = !mPendingDisplayCutout.equals(
+ mAttachInfo.mDisplayCutout);
final boolean outsetsChanged = !mPendingOutsets.equals(mAttachInfo.mOutsets);
final boolean surfaceSizeChanged = (relayoutResult
& WindowManagerGlobal.RELAYOUT_RES_SURFACE_RESIZED) != 0;
@@ -1955,6 +1996,14 @@ public final class ViewRootImpl implements ViewParent,
// Need to relayout with content insets.
contentInsetsChanged = true;
}
+ if (cutoutChanged) {
+ mAttachInfo.mDisplayCutout.set(mPendingDisplayCutout);
+ if (DEBUG_LAYOUT) {
+ Log.v(mTag, "DisplayCutout changing to: " + mAttachInfo.mDisplayCutout);
+ }
+ // Need to relayout with content insets.
+ contentInsetsChanged = true;
+ }
if (alwaysConsumeNavBarChanged) {
mAttachInfo.mAlwaysConsumeNavBar = mPendingAlwaysConsumeNavBar;
contentInsetsChanged = true;
@@ -2056,6 +2105,7 @@ public final class ViewRootImpl implements ViewParent,
mResizeMode = freeformResizing
? RESIZE_MODE_FREEFORM
: RESIZE_MODE_DOCKED_DIVIDER;
+ // TODO: Need cutout?
startDragResizing(mPendingBackDropFrame,
mWinFrame.equals(mPendingBackDropFrame), mPendingVisibleInsets,
mPendingStableInsets, mResizeMode);
@@ -2064,7 +2114,7 @@ public final class ViewRootImpl implements ViewParent,
endDragResizing();
}
}
- if (!USE_MT_RENDERER) {
+ if (!mUseMTRenderer) {
if (dragResizing) {
mCanvasOffsetX = mWinFrame.left;
mCanvasOffsetY = mWinFrame.top;
@@ -2420,6 +2470,93 @@ public final class ViewRootImpl implements ViewParent,
}
}
+ private void handleWindowFocusChanged() {
+ final boolean hasWindowFocus;
+ final boolean inTouchMode;
+ synchronized (this) {
+ if (!mWindowFocusChanged) {
+ return;
+ }
+ mWindowFocusChanged = false;
+ hasWindowFocus = mUpcomingWindowFocus;
+ inTouchMode = mUpcomingInTouchMode;
+ }
+
+ if (mAdded) {
+ profileRendering(hasWindowFocus);
+
+ if (hasWindowFocus) {
+ ensureTouchModeLocally(inTouchMode);
+ if (mAttachInfo.mThreadedRenderer != null && mSurface.isValid()) {
+ mFullRedrawNeeded = true;
+ try {
+ final WindowManager.LayoutParams lp = mWindowAttributes;
+ final Rect surfaceInsets = lp != null ? lp.surfaceInsets : null;
+ mAttachInfo.mThreadedRenderer.initializeIfNeeded(
+ mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);
+ } catch (OutOfResourcesException e) {
+ Log.e(mTag, "OutOfResourcesException locking surface", e);
+ try {
+ if (!mWindowSession.outOfMemory(mWindow)) {
+ Slog.w(mTag, "No processes killed for memory;"
+ + " killing self");
+ Process.killProcess(Process.myPid());
+ }
+ } catch (RemoteException ex) {
+ }
+ // Retry in a bit.
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(
+ MSG_WINDOW_FOCUS_CHANGED), 500);
+ return;
+ }
+ }
+ }
+
+ mAttachInfo.mHasWindowFocus = hasWindowFocus;
+
+ mLastWasImTarget = WindowManager.LayoutParams
+ .mayUseInputMethod(mWindowAttributes.flags);
+
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) {
+ imm.onPreWindowFocus(mView, hasWindowFocus);
+ }
+ if (mView != null) {
+ mAttachInfo.mKeyDispatchState.reset();
+ mView.dispatchWindowFocusChanged(hasWindowFocus);
+ mAttachInfo.mTreeObserver.dispatchOnWindowFocusChange(hasWindowFocus);
+
+ if (mAttachInfo.mTooltipHost != null) {
+ mAttachInfo.mTooltipHost.hideTooltip();
+ }
+ }
+
+ // Note: must be done after the focus change callbacks,
+ // so all of the view state is set up correctly.
+ if (hasWindowFocus) {
+ if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) {
+ imm.onPostWindowFocus(mView, mView.findFocus(),
+ mWindowAttributes.softInputMode,
+ !mHasHadWindowFocus, mWindowAttributes.flags);
+ }
+ // Clear the forward bit. We can just do this directly, since
+ // the window manager doesn't care about it.
+ mWindowAttributes.softInputMode &=
+ ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
+ ((WindowManager.LayoutParams) mView.getLayoutParams())
+ .softInputMode &=
+ ~WindowManager.LayoutParams
+ .SOFT_INPUT_IS_FORWARD_NAVIGATION;
+ mHasHadWindowFocus = true;
+ } else {
+ if (mPointerCapture) {
+ handlePointerCaptureChanged(false);
+ }
+ }
+ }
+ mFirstInputStage.onWindowFocusChanged(hasWindowFocus);
+ }
+
private void handleOutOfResourcesException(Surface.OutOfResourcesException e) {
Log.e(mTag, "OutOfResourcesException initializing HW surface", e);
try {
@@ -2702,8 +2839,10 @@ public final class ViewRootImpl implements ViewParent,
@Override
public void onPostDraw(DisplayListCanvas canvas) {
drawAccessibilityFocusedDrawableIfNeeded(canvas);
- for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
- mWindowCallbacks.get(i).onPostDraw(canvas);
+ if (mUseMTRenderer) {
+ for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
+ mWindowCallbacks.get(i).onPostDraw(canvas);
+ }
}
}
@@ -3034,7 +3173,8 @@ public final class ViewRootImpl implements ViewParent,
return;
}
- if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
+ if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
+ scalingRequired, dirty, surfaceInsets)) {
return;
}
}
@@ -3050,11 +3190,22 @@ public final class ViewRootImpl implements ViewParent,
* @return true if drawing was successful, false if an error occurred
*/
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
- boolean scalingRequired, Rect dirty) {
+ boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
// Draw with software renderer.
final Canvas canvas;
+
+ // We already have the offset of surfaceInsets in xoff, yoff and dirty region,
+ // therefore we need to add it back when moving the dirty region.
+ int dirtyXOffset = xoff;
+ int dirtyYOffset = yoff;
+ if (surfaceInsets != null) {
+ dirtyXOffset += surfaceInsets.left;
+ dirtyYOffset += surfaceInsets.top;
+ }
+
try {
+ dirty.offset(-dirtyXOffset, -dirtyYOffset);
final int left = dirty.left;
final int top = dirty.top;
final int right = dirty.right;
@@ -3081,6 +3232,8 @@ public final class ViewRootImpl implements ViewParent,
// kill stuff (or ourself) for no reason.
mLayoutRequested = true; // ask wm for a new surface next time.
return false;
+ } finally {
+ dirty.offset(dirtyXOffset, dirtyYOffset); // Reset to the original value.
}
try {
@@ -3776,6 +3929,7 @@ public final class ViewRootImpl implements ViewParent,
&& mPendingOverscanInsets.equals(args.arg5)
&& mPendingContentInsets.equals(args.arg2)
&& mPendingStableInsets.equals(args.arg6)
+ && mPendingDisplayCutout.get().equals(args.arg9)
&& mPendingVisibleInsets.equals(args.arg3)
&& mPendingOutsets.equals(args.arg7)
&& mPendingBackDropFrame.equals(args.arg8)
@@ -3808,6 +3962,7 @@ public final class ViewRootImpl implements ViewParent,
|| !mPendingOverscanInsets.equals(args.arg5)
|| !mPendingContentInsets.equals(args.arg2)
|| !mPendingStableInsets.equals(args.arg6)
+ || !mPendingDisplayCutout.get().equals(args.arg9)
|| !mPendingVisibleInsets.equals(args.arg3)
|| !mPendingOutsets.equals(args.arg7);
@@ -3815,6 +3970,7 @@ public final class ViewRootImpl implements ViewParent,
mPendingOverscanInsets.set((Rect) args.arg5);
mPendingContentInsets.set((Rect) args.arg2);
mPendingStableInsets.set((Rect) args.arg6);
+ mPendingDisplayCutout.set((DisplayCutout) args.arg9);
mPendingVisibleInsets.set((Rect) args.arg3);
mPendingOutsets.set((Rect) args.arg7);
mPendingBackDropFrame.set((Rect) args.arg8);
@@ -3849,81 +4005,7 @@ public final class ViewRootImpl implements ViewParent,
}
break;
case MSG_WINDOW_FOCUS_CHANGED: {
- final boolean hasWindowFocus = msg.arg1 != 0;
- if (mAdded) {
- mAttachInfo.mHasWindowFocus = hasWindowFocus;
-
- profileRendering(hasWindowFocus);
-
- if (hasWindowFocus) {
- boolean inTouchMode = msg.arg2 != 0;
- ensureTouchModeLocally(inTouchMode);
- if (mAttachInfo.mThreadedRenderer != null && mSurface.isValid()) {
- mFullRedrawNeeded = true;
- try {
- final WindowManager.LayoutParams lp = mWindowAttributes;
- final Rect surfaceInsets = lp != null ? lp.surfaceInsets : null;
- mAttachInfo.mThreadedRenderer.initializeIfNeeded(
- mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);
- } catch (OutOfResourcesException e) {
- Log.e(mTag, "OutOfResourcesException locking surface", e);
- try {
- if (!mWindowSession.outOfMemory(mWindow)) {
- Slog.w(mTag, "No processes killed for memory;"
- + " killing self");
- Process.killProcess(Process.myPid());
- }
- } catch (RemoteException ex) {
- }
- // Retry in a bit.
- sendMessageDelayed(obtainMessage(msg.what, msg.arg1, msg.arg2),
- 500);
- return;
- }
- }
- }
-
- mLastWasImTarget = WindowManager.LayoutParams
- .mayUseInputMethod(mWindowAttributes.flags);
-
- InputMethodManager imm = InputMethodManager.peekInstance();
- if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) {
- imm.onPreWindowFocus(mView, hasWindowFocus);
- }
- if (mView != null) {
- mAttachInfo.mKeyDispatchState.reset();
- mView.dispatchWindowFocusChanged(hasWindowFocus);
- mAttachInfo.mTreeObserver.dispatchOnWindowFocusChange(hasWindowFocus);
-
- if (mAttachInfo.mTooltipHost != null) {
- mAttachInfo.mTooltipHost.hideTooltip();
- }
- }
-
- // Note: must be done after the focus change callbacks,
- // so all of the view state is set up correctly.
- if (hasWindowFocus) {
- if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) {
- imm.onPostWindowFocus(mView, mView.findFocus(),
- mWindowAttributes.softInputMode,
- !mHasHadWindowFocus, mWindowAttributes.flags);
- }
- // Clear the forward bit. We can just do this directly, since
- // the window manager doesn't care about it.
- mWindowAttributes.softInputMode &=
- ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
- ((WindowManager.LayoutParams) mView.getLayoutParams())
- .softInputMode &=
- ~WindowManager.LayoutParams
- .SOFT_INPUT_IS_FORWARD_NAVIGATION;
- mHasHadWindowFocus = true;
- } else {
- if (mPointerCapture) {
- handlePointerCaptureChanged(false);
- }
- }
- }
- mFirstInputStage.onWindowFocusChanged(hasWindowFocus);
+ handleWindowFocusChanged();
} break;
case MSG_DIE:
doDie();
@@ -6258,7 +6340,7 @@ public final class ViewRootImpl implements ViewParent,
(int) (mView.getMeasuredHeight() * appScale + 0.5f),
viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0,
mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
- mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame,
+ mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
mPendingMergedConfiguration, mSurface);
mPendingAlwaysConsumeNavBar =
@@ -6541,7 +6623,8 @@ public final class ViewRootImpl implements ViewParent,
private void dispatchResized(Rect frame, Rect overscanInsets, Rect contentInsets,
Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw,
MergedConfiguration mergedConfiguration, Rect backDropFrame, boolean forceLayout,
- boolean alwaysConsumeNavBar, int displayId) {
+ boolean alwaysConsumeNavBar, int displayId,
+ DisplayCutout.ParcelableWrapper displayCutout) {
if (DEBUG_LAYOUT) Log.v(mTag, "Resizing " + this + ": frame=" + frame.toShortString()
+ " contentInsets=" + contentInsets.toShortString()
+ " visibleInsets=" + visibleInsets.toShortString()
@@ -6550,7 +6633,7 @@ public final class ViewRootImpl implements ViewParent,
// Tell all listeners that we are resizing the window so that the chrome can get
// updated as fast as possible on a separate thread,
- if (mDragResizing) {
+ if (mDragResizing && mUseMTRenderer) {
boolean fullscreen = frame.equals(backDropFrame);
synchronized (mWindowCallbacks) {
for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
@@ -6578,6 +6661,7 @@ public final class ViewRootImpl implements ViewParent,
args.arg6 = sameProcessCall ? new Rect(stableInsets) : stableInsets;
args.arg7 = sameProcessCall ? new Rect(outsets) : outsets;
args.arg8 = sameProcessCall ? new Rect(backDropFrame) : backDropFrame;
+ args.arg9 = displayCutout.get(); // DisplayCutout is immutable.
args.argi1 = forceLayout ? 1 : 0;
args.argi2 = alwaysConsumeNavBar ? 1 : 0;
args.argi3 = displayId;
@@ -6792,6 +6876,7 @@ public final class ViewRootImpl implements ViewParent,
}
if (stage != null) {
+ handleWindowFocusChanged();
stage.deliver(q);
} else {
finishInputEvent(q);
@@ -7097,10 +7182,13 @@ public final class ViewRootImpl implements ViewParent,
}
public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {
+ synchronized (this) {
+ mWindowFocusChanged = true;
+ mUpcomingWindowFocus = hasFocus;
+ mUpcomingInTouchMode = inTouchMode;
+ }
Message msg = Message.obtain();
msg.what = MSG_WINDOW_FOCUS_CHANGED;
- msg.arg1 = hasFocus ? 1 : 0;
- msg.arg2 = inTouchMode ? 1 : 0;
mHandler.sendMessage(msg);
}
@@ -7610,12 +7698,13 @@ public final class ViewRootImpl implements ViewParent,
public void resized(Rect frame, Rect overscanInsets, Rect contentInsets,
Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw,
MergedConfiguration mergedConfiguration, Rect backDropFrame, boolean forceLayout,
- boolean alwaysConsumeNavBar, int displayId) {
+ boolean alwaysConsumeNavBar, int displayId,
+ DisplayCutout.ParcelableWrapper displayCutout) {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (viewAncestor != null) {
viewAncestor.dispatchResized(frame, overscanInsets, contentInsets,
visibleInsets, stableInsets, outsets, reportDraw, mergedConfiguration,
- backDropFrame, forceLayout, alwaysConsumeNavBar, displayId);
+ backDropFrame, forceLayout, alwaysConsumeNavBar, displayId, displayCutout);
}
}
@@ -7798,9 +7887,11 @@ public final class ViewRootImpl implements ViewParent,
Rect stableInsets, int resizeMode) {
if (!mDragResizing) {
mDragResizing = true;
- for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
- mWindowCallbacks.get(i).onWindowDragResizeStart(initialBounds, fullscreen,
- systemInsets, stableInsets, resizeMode);
+ if (mUseMTRenderer) {
+ for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
+ mWindowCallbacks.get(i).onWindowDragResizeStart(
+ initialBounds, fullscreen, systemInsets, stableInsets, resizeMode);
+ }
}
mFullRedrawNeeded = true;
}
@@ -7812,8 +7903,10 @@ public final class ViewRootImpl implements ViewParent,
private void endDragResizing() {
if (mDragResizing) {
mDragResizing = false;
- for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
- mWindowCallbacks.get(i).onWindowDragResizeEnd();
+ if (mUseMTRenderer) {
+ for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
+ mWindowCallbacks.get(i).onWindowDragResizeEnd();
+ }
}
mFullRedrawNeeded = true;
}
@@ -7821,19 +7914,21 @@ public final class ViewRootImpl implements ViewParent,
private boolean updateContentDrawBounds() {
boolean updated = false;
- for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
- updated |= mWindowCallbacks.get(i).onContentDrawn(
- mWindowAttributes.surfaceInsets.left,
- mWindowAttributes.surfaceInsets.top,
- mWidth, mHeight);
+ if (mUseMTRenderer) {
+ for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
+ updated |=
+ mWindowCallbacks.get(i).onContentDrawn(mWindowAttributes.surfaceInsets.left,
+ mWindowAttributes.surfaceInsets.top, mWidth, mHeight);
+ }
}
return updated | (mDragResizing && mReportNextDraw);
}
private void requestDrawWindow() {
- if (mReportNextDraw) {
- mWindowDrawCountDown = new CountDownLatch(mWindowCallbacks.size());
+ if (!mUseMTRenderer) {
+ return;
}
+ mWindowDrawCountDown = new CountDownLatch(mWindowCallbacks.size());
for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
mWindowCallbacks.get(i).onRequestDraw(mReportNextDraw);
}
@@ -7877,6 +7972,7 @@ public final class ViewRootImpl implements ViewParent,
if (!registered) {
mAttachInfo.mAccessibilityWindowId =
mAccessibilityManager.addAccessibilityInteractionConnection(mWindow,
+ mContext.getPackageName(),
new AccessibilityInteractionConnection(ViewRootImpl.this));
}
}
diff --git a/android/view/View_Delegate.java b/android/view/View_Delegate.java
index 408ec549..5d39e4c9 100644
--- a/android/view/View_Delegate.java
+++ b/android/view/View_Delegate.java
@@ -16,10 +16,13 @@
package android.view;
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
import com.android.layoutlib.bridge.android.BridgeContext;
import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
import android.content.Context;
+import android.graphics.Canvas;
import android.os.IBinder;
/**
@@ -44,4 +47,50 @@ public class View_Delegate {
}
return null;
}
+
+ @LayoutlibDelegate
+ /*package*/ static void draw(View thisView, Canvas canvas) {
+ try {
+ // This code is run within a catch to prevent misbehaving components from breaking
+ // all the layout.
+ thisView.draw_Original(canvas);
+ } catch (Throwable t) {
+ Bridge.getLog().error(LayoutLog.TAG_BROKEN, "View draw failed", t, null);
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean draw(
+ View thisView, Canvas canvas, ViewGroup parent, long drawingTime) {
+ try {
+ // This code is run within a catch to prevent misbehaving components from breaking
+ // all the layout.
+ return thisView.draw_Original(canvas, parent, drawingTime);
+ } catch (Throwable t) {
+ Bridge.getLog().error(LayoutLog.TAG_BROKEN, "View draw failed", t, null);
+ }
+ return false;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void measure(View thisView, int widthMeasureSpec, int heightMeasureSpec) {
+ try {
+ // This code is run within a catch to prevent misbehaving components from breaking
+ // all the layout.
+ thisView.measure_Original(widthMeasureSpec, heightMeasureSpec);
+ } catch (Throwable t) {
+ Bridge.getLog().error(LayoutLog.TAG_BROKEN, "View measure failed", t, null);
+ }
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void layout(View thisView, int l, int t, int r, int b) {
+ try {
+ // This code is run within a catch to prevent misbehaving components from breaking
+ // all the layout.
+ thisView.layout_Original(l, t, r, b);
+ } catch (Throwable th) {
+ Bridge.getLog().error(LayoutLog.TAG_BROKEN, "View layout failed", th, null);
+ }
+ }
}
diff --git a/android/view/WindowInsets.java b/android/view/WindowInsets.java
index df124ac5..e5cbe96b 100644
--- a/android/view/WindowInsets.java
+++ b/android/view/WindowInsets.java
@@ -17,7 +17,7 @@
package android.view;
-import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.graphics.Rect;
/**
@@ -49,7 +49,7 @@ public final class WindowInsets {
private boolean mSystemWindowInsetsConsumed = false;
private boolean mWindowDecorInsetsConsumed = false;
private boolean mStableInsetsConsumed = false;
- private boolean mCutoutConsumed = false;
+ private boolean mDisplayCutoutConsumed = false;
private static final Rect EMPTY_RECT = new Rect(0, 0, 0, 0);
@@ -80,8 +80,9 @@ public final class WindowInsets {
mIsRound = isRound;
mAlwaysConsumeNavBar = alwaysConsumeNavBar;
- mCutoutConsumed = displayCutout == null;
- mDisplayCutout = mCutoutConsumed ? DisplayCutout.NO_CUTOUT : displayCutout;
+ mDisplayCutoutConsumed = displayCutout == null;
+ mDisplayCutout = (mDisplayCutoutConsumed || displayCutout.isEmpty())
+ ? null : displayCutout;
}
/**
@@ -99,7 +100,7 @@ public final class WindowInsets {
mIsRound = src.mIsRound;
mAlwaysConsumeNavBar = src.mAlwaysConsumeNavBar;
mDisplayCutout = src.mDisplayCutout;
- mCutoutConsumed = src.mCutoutConsumed;
+ mDisplayCutoutConsumed = src.mDisplayCutoutConsumed;
}
/** @hide */
@@ -269,15 +270,16 @@ public final class WindowInsets {
*/
public boolean hasInsets() {
return hasSystemWindowInsets() || hasWindowDecorInsets() || hasStableInsets()
- || mDisplayCutout.hasCutout();
+ || mDisplayCutout != null;
}
/**
- * @return the display cutout
+ * Returns the display cutout if there is one.
+ *
+ * @return the display cutout or null if there is none
* @see DisplayCutout
- * @hide pending API
*/
- @NonNull
+ @Nullable
public DisplayCutout getDisplayCutout() {
return mDisplayCutout;
}
@@ -286,12 +288,11 @@ public final class WindowInsets {
* Returns a copy of this WindowInsets with the cutout fully consumed.
*
* @return A modified copy of this WindowInsets
- * @hide pending API
*/
- public WindowInsets consumeCutout() {
+ public WindowInsets consumeDisplayCutout() {
final WindowInsets result = new WindowInsets(this);
- result.mDisplayCutout = DisplayCutout.NO_CUTOUT;
- result.mCutoutConsumed = true;
+ result.mDisplayCutout = null;
+ result.mDisplayCutoutConsumed = true;
return result;
}
@@ -311,7 +312,7 @@ public final class WindowInsets {
*/
public boolean isConsumed() {
return mSystemWindowInsetsConsumed && mWindowDecorInsetsConsumed && mStableInsetsConsumed
- && mCutoutConsumed;
+ && mDisplayCutoutConsumed;
}
/**
@@ -530,7 +531,7 @@ public final class WindowInsets {
return "WindowInsets{systemWindowInsets=" + mSystemWindowInsets
+ " windowDecorInsets=" + mWindowDecorInsets
+ " stableInsets=" + mStableInsets
- + (mDisplayCutout.hasCutout() ? " cutout=" + mDisplayCutout : "")
+ + (mDisplayCutout != null ? " cutout=" + mDisplayCutout : "")
+ (isRound() ? " round" : "")
+ "}";
}
diff --git a/android/view/WindowManager.java b/android/view/WindowManager.java
index 905c0715..cbe012af 100644
--- a/android/view/WindowManager.java
+++ b/android/view/WindowManager.java
@@ -20,6 +20,7 @@ import static android.content.pm.ActivityInfo.COLOR_MODE_DEFAULT;
import android.Manifest.permission;
import android.annotation.IntDef;
+import android.annotation.LongDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
@@ -1268,6 +1269,33 @@ public interface WindowManager extends ViewManager {
}, formatToHexString = true)
public int flags;
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @LongDef(
+ flag = true,
+ value = {
+ LayoutParams.FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA,
+ })
+ @interface Flags2 {}
+
+ /**
+ * Window flag: allow placing the window within the area that overlaps with the
+ * display cutout.
+ *
+ * <p>
+ * The window must correctly position its contents to take the display cutout into account.
+ *
+ * @see DisplayCutout
+ */
+ public static final long FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA = 0x00000001;
+
+ /**
+ * Various behavioral options/flags. Default is none.
+ *
+ * @see #FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA
+ */
+ @Flags2 public long flags2;
+
/**
* If the window has requested hardware acceleration, but this is not
* allowed in the process it is in, then still render it as if it is
@@ -1642,12 +1670,20 @@ public interface WindowManager extends ViewManager {
* Visibility state for {@link #softInputMode}: please show the soft
* input area when normally appropriate (when the user is navigating
* forward to your window).
+ *
+ * <p>Applications that target {@link android.os.Build.VERSION_CODES#P} and later, this flag
+ * is ignored unless there is a focused view that returns {@code true} from
+ * {@link View#isInEditMode()} when the window is focused.</p>
*/
public static final int SOFT_INPUT_STATE_VISIBLE = 4;
/**
* Visibility state for {@link #softInputMode}: please always make the
* soft input area visible when this window receives input focus.
+ *
+ * <p>Applications that target {@link android.os.Build.VERSION_CODES#P} and later, this flag
+ * is ignored unless there is a focused view that returns {@code true} from
+ * {@link View#isInEditMode()} when the window is focused.</p>
*/
public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5;
@@ -1708,7 +1744,7 @@ public interface WindowManager extends ViewManager {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag = true, value = {
+ @IntDef(flag = true, prefix = { "SOFT_INPUT_" }, value = {
SOFT_INPUT_STATE_UNSPECIFIED,
SOFT_INPUT_STATE_UNCHANGED,
SOFT_INPUT_STATE_HIDDEN,
@@ -2211,6 +2247,7 @@ public interface WindowManager extends ViewManager {
out.writeInt(y);
out.writeInt(type);
out.writeInt(flags);
+ out.writeLong(flags2);
out.writeInt(privateFlags);
out.writeInt(softInputMode);
out.writeInt(gravity);
@@ -2266,6 +2303,7 @@ public interface WindowManager extends ViewManager {
y = in.readInt();
type = in.readInt();
flags = in.readInt();
+ flags2 = in.readLong();
privateFlags = in.readInt();
softInputMode = in.readInt();
gravity = in.readInt();
@@ -2398,6 +2436,10 @@ public interface WindowManager extends ViewManager {
flags = o.flags;
changes |= FLAGS_CHANGED;
}
+ if (flags2 != o.flags2) {
+ flags2 = o.flags2;
+ changes |= FLAGS_CHANGED;
+ }
if (privateFlags != o.privateFlags) {
privateFlags = o.privateFlags;
changes |= PRIVATE_FLAGS_CHANGED;
@@ -2651,6 +2693,11 @@ public interface WindowManager extends ViewManager {
sb.append(System.lineSeparator());
sb.append(prefix).append(" fl=").append(
ViewDebug.flagsToString(LayoutParams.class, "flags", flags));
+ if (flags2 != 0) {
+ sb.append(System.lineSeparator());
+ // TODO(roosa): add a long overload for ViewDebug.flagsToString.
+ sb.append(prefix).append(" fl2=0x").append(Long.toHexString(flags2));
+ }
if (privateFlags != 0) {
sb.append(System.lineSeparator());
sb.append(prefix).append(" pfl=").append(ViewDebug.flagsToString(
diff --git a/android/view/accessibility/AccessibilityInteractionClient.java b/android/view/accessibility/AccessibilityInteractionClient.java
index d890f329..72af203e 100644
--- a/android/view/accessibility/AccessibilityInteractionClient.java
+++ b/android/view/accessibility/AccessibilityInteractionClient.java
@@ -29,6 +29,7 @@ import android.util.LongSparseArray;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
import java.util.ArrayList;
import java.util.Collections;
@@ -213,7 +214,7 @@ public final class AccessibilityInteractionClient
*
* @param connectionId The id of a connection for interacting with the system.
* @param accessibilityWindowId A unique window id. Use
- * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
+ * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
* to query the currently active window.
* @return The {@link AccessibilityWindowInfo}.
*/
@@ -299,7 +300,7 @@ public final class AccessibilityInteractionClient
*
* @param connectionId The id of a connection for interacting with the system.
* @param accessibilityWindowId A unique window id. Use
- * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
+ * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
* to query the currently active window.
* @param accessibilityNodeId A unique view id or virtual descendant id from
* where to start the search. Use
@@ -335,18 +336,19 @@ public final class AccessibilityInteractionClient
}
final int interactionId = mInteractionIdCounter.getAndIncrement();
final long identityToken = Binder.clearCallingIdentity();
- final boolean success;
+ final String[] packageNames;
try {
- success = connection.findAccessibilityNodeInfoByAccessibilityId(
+ packageNames = connection.findAccessibilityNodeInfoByAccessibilityId(
accessibilityWindowId, accessibilityNodeId, interactionId, this,
prefetchFlags, Thread.currentThread().getId(), arguments);
} finally {
Binder.restoreCallingIdentity(identityToken);
}
- if (success) {
+ if (packageNames != null) {
List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
interactionId);
- finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, bypassCache);
+ finalizeAndCacheAccessibilityNodeInfos(infos, connectionId,
+ bypassCache, packageNames);
if (infos != null && !infos.isEmpty()) {
for (int i = 1; i < infos.size(); i++) {
infos.get(i).recycle();
@@ -373,7 +375,7 @@ public final class AccessibilityInteractionClient
*
* @param connectionId The id of a connection for interacting with the system.
* @param accessibilityWindowId A unique window id. Use
- * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
+ * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
* to query the currently active window.
* @param accessibilityNodeId A unique view id or virtual descendant id from
* where to start the search. Use
@@ -389,20 +391,21 @@ public final class AccessibilityInteractionClient
if (connection != null) {
final int interactionId = mInteractionIdCounter.getAndIncrement();
final long identityToken = Binder.clearCallingIdentity();
- final boolean success;
+ final String[] packageNames;
try {
- success = connection.findAccessibilityNodeInfosByViewId(
+ packageNames = connection.findAccessibilityNodeInfosByViewId(
accessibilityWindowId, accessibilityNodeId, viewId, interactionId, this,
Thread.currentThread().getId());
} finally {
Binder.restoreCallingIdentity(identityToken);
}
- if (success) {
+ if (packageNames != null) {
List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
interactionId);
if (infos != null) {
- finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, false);
+ finalizeAndCacheAccessibilityNodeInfos(infos, connectionId,
+ false, packageNames);
return infos;
}
}
@@ -426,7 +429,7 @@ public final class AccessibilityInteractionClient
*
* @param connectionId The id of a connection for interacting with the system.
* @param accessibilityWindowId A unique window id. Use
- * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
+ * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
* to query the currently active window.
* @param accessibilityNodeId A unique view id or virtual descendant id from
* where to start the search. Use
@@ -442,20 +445,21 @@ public final class AccessibilityInteractionClient
if (connection != null) {
final int interactionId = mInteractionIdCounter.getAndIncrement();
final long identityToken = Binder.clearCallingIdentity();
- final boolean success;
+ final String[] packageNames;
try {
- success = connection.findAccessibilityNodeInfosByText(
+ packageNames = connection.findAccessibilityNodeInfosByText(
accessibilityWindowId, accessibilityNodeId, text, interactionId, this,
Thread.currentThread().getId());
} finally {
Binder.restoreCallingIdentity(identityToken);
}
- if (success) {
+ if (packageNames != null) {
List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
interactionId);
if (infos != null) {
- finalizeAndCacheAccessibilityNodeInfos(infos, connectionId, false);
+ finalizeAndCacheAccessibilityNodeInfos(infos, connectionId,
+ false, packageNames);
return infos;
}
}
@@ -478,7 +482,7 @@ public final class AccessibilityInteractionClient
*
* @param connectionId The id of a connection for interacting with the system.
* @param accessibilityWindowId A unique window id. Use
- * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
+ * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
* to query the currently active window.
* @param accessibilityNodeId A unique view id or virtual descendant id from
* where to start the search. Use
@@ -494,19 +498,19 @@ public final class AccessibilityInteractionClient
if (connection != null) {
final int interactionId = mInteractionIdCounter.getAndIncrement();
final long identityToken = Binder.clearCallingIdentity();
- final boolean success;
+ final String[] packageNames;
try {
- success = connection.findFocus(accessibilityWindowId,
+ packageNames = connection.findFocus(accessibilityWindowId,
accessibilityNodeId, focusType, interactionId, this,
Thread.currentThread().getId());
} finally {
Binder.restoreCallingIdentity(identityToken);
}
- if (success) {
+ if (packageNames != null) {
AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
interactionId);
- finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false);
+ finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames);
return info;
}
} else {
@@ -527,7 +531,7 @@ public final class AccessibilityInteractionClient
*
* @param connectionId The id of a connection for interacting with the system.
* @param accessibilityWindowId A unique window id. Use
- * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
+ * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
* to query the currently active window.
* @param accessibilityNodeId A unique view id or virtual descendant id from
* where to start the search. Use
@@ -543,19 +547,19 @@ public final class AccessibilityInteractionClient
if (connection != null) {
final int interactionId = mInteractionIdCounter.getAndIncrement();
final long identityToken = Binder.clearCallingIdentity();
- final boolean success;
+ final String[] packageNames;
try {
- success = connection.focusSearch(accessibilityWindowId,
+ packageNames = connection.focusSearch(accessibilityWindowId,
accessibilityNodeId, direction, interactionId, this,
Thread.currentThread().getId());
} finally {
Binder.restoreCallingIdentity(identityToken);
}
- if (success) {
+ if (packageNames != null) {
AccessibilityNodeInfo info = getFindAccessibilityNodeInfoResultAndClear(
interactionId);
- finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false);
+ finalizeAndCacheAccessibilityNodeInfo(info, connectionId, false, packageNames);
return info;
}
} else {
@@ -574,7 +578,7 @@ public final class AccessibilityInteractionClient
*
* @param connectionId The id of a connection for interacting with the system.
* @param accessibilityWindowId A unique window id. Use
- * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
+ * {@link android.view.accessibility.AccessibilityWindowInfo#ACTIVE_WINDOW_ID}
* to query the currently active window.
* @param accessibilityNodeId A unique view id or virtual descendant id from
* where to start the search. Use
@@ -661,7 +665,7 @@ public final class AccessibilityInteractionClient
int interactionId) {
synchronized (mInstanceLock) {
final boolean success = waitForResultTimedLocked(interactionId);
- List<AccessibilityNodeInfo> result = null;
+ final List<AccessibilityNodeInfo> result;
if (success) {
result = mFindAccessibilityNodeInfosResult;
} else {
@@ -779,11 +783,22 @@ public final class AccessibilityInteractionClient
* @param connectionId The id of the connection to the system.
* @param bypassCache Whether or not to bypass the cache. The node is added to the cache if
* this value is {@code false}
+ * @param packageNames The valid package names a node can come from.
*/
private void finalizeAndCacheAccessibilityNodeInfo(AccessibilityNodeInfo info,
- int connectionId, boolean bypassCache) {
+ int connectionId, boolean bypassCache, String[] packageNames) {
if (info != null) {
info.setConnectionId(connectionId);
+ // Empty array means any package name is Okay
+ if (!ArrayUtils.isEmpty(packageNames)) {
+ CharSequence packageName = info.getPackageName();
+ if (packageName == null
+ || !ArrayUtils.contains(packageNames, packageName.toString())) {
+ // If the node package not one of the valid ones, pick the top one - this
+ // is one of the packages running in the introspected UID.
+ info.setPackageName(packageNames[0]);
+ }
+ }
info.setSealed(true);
if (!bypassCache) {
sAccessibilityCache.add(info);
@@ -798,14 +813,16 @@ public final class AccessibilityInteractionClient
* @param connectionId The id of the connection to the system.
* @param bypassCache Whether or not to bypass the cache. The nodes are added to the cache if
* this value is {@code false}
+ * @param packageNames The valid package names a node can come from.
*/
private void finalizeAndCacheAccessibilityNodeInfos(List<AccessibilityNodeInfo> infos,
- int connectionId, boolean bypassCache) {
+ int connectionId, boolean bypassCache, String[] packageNames) {
if (infos != null) {
final int infosCount = infos.size();
for (int i = 0; i < infosCount; i++) {
AccessibilityNodeInfo info = infos.get(i);
- finalizeAndCacheAccessibilityNodeInfo(info, connectionId, bypassCache);
+ finalizeAndCacheAccessibilityNodeInfo(info, connectionId,
+ bypassCache, packageNames);
}
}
}
diff --git a/android/view/accessibility/AccessibilityManager.java b/android/view/accessibility/AccessibilityManager.java
index 0375635f..dd8ba556 100644
--- a/android/view/accessibility/AccessibilityManager.java
+++ b/android/view/accessibility/AccessibilityManager.java
@@ -16,153 +16,46 @@
package android.view.accessibility;
-import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_ENABLE_ACCESSIBILITY_VOLUME;
-
-import android.Manifest;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.SdkConstant;
-import android.annotation.SystemService;
-import android.annotation.TestApi;
-import android.content.ComponentName;
import android.content.Context;
-import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
-import android.content.res.Resources;
-import android.os.Binder;
import android.os.Handler;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Process;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.os.SystemClock;
-import android.os.UserHandle;
-import android.util.ArrayMap;
-import android.util.Log;
-import android.util.SparseArray;
import android.view.IWindow;
import android.view.View;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.IntPair;
-
-import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
- * System level service that serves as an event dispatch for {@link AccessibilityEvent}s,
- * and provides facilities for querying the accessibility state of the system.
- * Accessibility events are generated when something notable happens in the user interface,
+ * System level service that serves as an event dispatch for {@link AccessibilityEvent}s.
+ * Such events are generated when something notable happens in the user interface,
* for example an {@link android.app.Activity} starts, the focus or selection of a
* {@link android.view.View} changes etc. Parties interested in handling accessibility
* events implement and register an accessibility service which extends
- * {@link android.accessibilityservice.AccessibilityService}.
+ * {@code android.accessibilityservice.AccessibilityService}.
*
* @see AccessibilityEvent
- * @see AccessibilityNodeInfo
- * @see android.accessibilityservice.AccessibilityService
- * @see Context#getSystemService
- * @see Context#ACCESSIBILITY_SERVICE
+ * @see android.content.Context#getSystemService
*/
-@SystemService(Context.ACCESSIBILITY_SERVICE)
+@SuppressWarnings("UnusedDeclaration")
public final class AccessibilityManager {
- private static final boolean DEBUG = false;
-
- private static final String LOG_TAG = "AccessibilityManager";
-
- /** @hide */
- public static final int STATE_FLAG_ACCESSIBILITY_ENABLED = 0x00000001;
-
- /** @hide */
- public static final int STATE_FLAG_TOUCH_EXPLORATION_ENABLED = 0x00000002;
-
- /** @hide */
- public static final int STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED = 0x00000004;
-
- /** @hide */
- public static final int DALTONIZER_DISABLED = -1;
- /** @hide */
- public static final int DALTONIZER_SIMULATE_MONOCHROMACY = 0;
+ private static AccessibilityManager sInstance = new AccessibilityManager(null, null, 0);
- /** @hide */
- public static final int DALTONIZER_CORRECT_DEUTERANOMALY = 12;
-
- /** @hide */
- public static final int AUTOCLICK_DELAY_DEFAULT = 600;
/**
- * Activity action: Launch UI to manage which accessibility service or feature is assigned
- * to the navigation bar Accessibility button.
- * <p>
- * Input: Nothing.
- * </p>
- * <p>
- * Output: Nothing.
- * </p>
- *
- * @hide
- */
- @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
- public static final String ACTION_CHOOSE_ACCESSIBILITY_BUTTON =
- "com.android.internal.intent.action.CHOOSE_ACCESSIBILITY_BUTTON";
-
- static final Object sInstanceSync = new Object();
-
- private static AccessibilityManager sInstance;
-
- private final Object mLock = new Object();
-
- private IAccessibilityManager mService;
-
- final int mUserId;
-
- final Handler mHandler;
-
- final Handler.Callback mCallback;
-
- boolean mIsEnabled;
-
- int mRelevantEventTypes = AccessibilityEvent.TYPES_ALL_MASK;
-
- boolean mIsTouchExplorationEnabled;
-
- boolean mIsHighTextContrastEnabled;
-
- private final ArrayMap<AccessibilityStateChangeListener, Handler>
- mAccessibilityStateChangeListeners = new ArrayMap<>();
-
- private final ArrayMap<TouchExplorationStateChangeListener, Handler>
- mTouchExplorationStateChangeListeners = new ArrayMap<>();
-
- private final ArrayMap<HighTextContrastChangeListener, Handler>
- mHighTextContrastStateChangeListeners = new ArrayMap<>();
-
- private final ArrayMap<AccessibilityServicesStateChangeListener, Handler>
- mServicesStateChangeListeners = new ArrayMap<>();
-
- /**
- * Map from a view's accessibility id to the list of request preparers set for that view
- */
- private SparseArray<List<AccessibilityRequestPreparer>> mRequestPreparerLists;
-
- /**
- * Listener for the system accessibility state. To listen for changes to the
- * accessibility state on the device, implement this interface and register
- * it with the system by calling {@link #addAccessibilityStateChangeListener}.
+ * Listener for the accessibility state.
*/
public interface AccessibilityStateChangeListener {
/**
- * Called when the accessibility enabled state changes.
+ * Called back on change in the accessibility state.
*
* @param enabled Whether accessibility is enabled.
*/
- void onAccessibilityStateChanged(boolean enabled);
+ public void onAccessibilityStateChanged(boolean enabled);
}
/**
@@ -178,25 +71,7 @@ public final class AccessibilityManager {
*
* @param enabled Whether touch exploration is enabled.
*/
- void onTouchExplorationStateChanged(boolean enabled);
- }
-
- /**
- * Listener for changes to the state of accessibility services. Changes include services being
- * enabled or disabled, or changes to the {@link AccessibilityServiceInfo} of a running service.
- * {@see #addAccessibilityServicesStateChangeListener}.
- *
- * @hide
- */
- @TestApi
- public interface AccessibilityServicesStateChangeListener {
-
- /**
- * Called when the state of accessibility services changes.
- *
- * @param manager The manager that is calling back
- */
- void onAccessibilityServicesStateChanged(AccessibilityManager manager);
+ public void onTouchExplorationStateChanged(boolean enabled);
}
/**
@@ -204,8 +79,6 @@ public final class AccessibilityManager {
* the high text contrast state on the device, implement this interface and
* register it with the system by calling
* {@link #addHighTextContrastStateChangeListener}.
- *
- * @hide
*/
public interface HighTextContrastChangeListener {
@@ -214,72 +87,26 @@ public final class AccessibilityManager {
*
* @param enabled Whether high text contrast is enabled.
*/
- void onHighTextContrastStateChanged(boolean enabled);
+ public void onHighTextContrastStateChanged(boolean enabled);
}
private final IAccessibilityManagerClient.Stub mClient =
new IAccessibilityManagerClient.Stub() {
- @Override
- public void setState(int state) {
- // We do not want to change this immediately as the application may
- // have already checked that accessibility is on and fired an event,
- // that is now propagating up the view tree, Hence, if accessibility
- // is now off an exception will be thrown. We want to have the exception
- // enforcement to guard against apps that fire unnecessary accessibility
- // events when accessibility is off.
- mHandler.obtainMessage(MyCallback.MSG_SET_STATE, state, 0).sendToTarget();
- }
-
- @Override
- public void notifyServicesStateChanged() {
- final ArrayMap<AccessibilityServicesStateChangeListener, Handler> listeners;
- synchronized (mLock) {
- if (mServicesStateChangeListeners.isEmpty()) {
- return;
+ public void setState(int state) {
}
- listeners = new ArrayMap<>(mServicesStateChangeListeners);
- }
- int numListeners = listeners.size();
- for (int i = 0; i < numListeners; i++) {
- final AccessibilityServicesStateChangeListener listener =
- mServicesStateChangeListeners.keyAt(i);
- mServicesStateChangeListeners.valueAt(i).post(() -> listener
- .onAccessibilityServicesStateChanged(AccessibilityManager.this));
- }
- }
+ public void notifyServicesStateChanged() {
+ }
- @Override
- public void setRelevantEventTypes(int eventTypes) {
- mRelevantEventTypes = eventTypes;
- }
- };
+ public void setRelevantEventTypes(int eventTypes) {
+ }
+ };
/**
* Get an AccessibilityManager instance (create one if necessary).
*
- * @param context Context in which this manager operates.
- *
- * @hide
*/
public static AccessibilityManager getInstance(Context context) {
- synchronized (sInstanceSync) {
- if (sInstance == null) {
- final int userId;
- if (Binder.getCallingUid() == Process.SYSTEM_UID
- || context.checkCallingOrSelfPermission(
- Manifest.permission.INTERACT_ACROSS_USERS)
- == PackageManager.PERMISSION_GRANTED
- || context.checkCallingOrSelfPermission(
- Manifest.permission.INTERACT_ACROSS_USERS_FULL)
- == PackageManager.PERMISSION_GRANTED) {
- userId = UserHandle.USER_CURRENT;
- } else {
- userId = UserHandle.myUserId();
- }
- sInstance = new AccessibilityManager(context, null, userId);
- }
- }
return sInstance;
}
@@ -287,68 +114,21 @@ public final class AccessibilityManager {
* Create an instance.
*
* @param context A {@link Context}.
- * @param service An interface to the backing service.
- * @param userId User id under which to run.
- *
- * @hide
*/
public AccessibilityManager(Context context, IAccessibilityManager service, int userId) {
- // Constructor can't be chained because we can't create an instance of an inner class
- // before calling another constructor.
- mCallback = new MyCallback();
- mHandler = new Handler(context.getMainLooper(), mCallback);
- mUserId = userId;
- synchronized (mLock) {
- tryConnectToServiceLocked(service);
- }
}
- /**
- * Create an instance.
- *
- * @param handler The handler to use
- * @param service An interface to the backing service.
- * @param userId User id under which to run.
- *
- * @hide
- */
- public AccessibilityManager(Handler handler, IAccessibilityManager service, int userId) {
- mCallback = new MyCallback();
- mHandler = handler;
- mUserId = userId;
- synchronized (mLock) {
- tryConnectToServiceLocked(service);
- }
- }
-
- /**
- * @hide
- */
public IAccessibilityManagerClient getClient() {
return mClient;
}
/**
- * @hide
- */
- @VisibleForTesting
- public Handler.Callback getCallback() {
- return mCallback;
- }
-
- /**
- * Returns if the accessibility in the system is enabled.
+ * Returns if the {@link AccessibilityManager} is enabled.
*
- * @return True if accessibility is enabled, false otherwise.
+ * @return True if this {@link AccessibilityManager} is enabled, false otherwise.
*/
public boolean isEnabled() {
- synchronized (mLock) {
- IAccessibilityManager service = getServiceLocked();
- if (service == null) {
- return false;
- }
- return mIsEnabled;
- }
+ return false;
}
/**
@@ -357,13 +137,7 @@ public final class AccessibilityManager {
* @return True if touch exploration is enabled, false otherwise.
*/
public boolean isTouchExplorationEnabled() {
- synchronized (mLock) {
- IAccessibilityManager service = getServiceLocked();
- if (service == null) {
- return false;
- }
- return mIsTouchExplorationEnabled;
- }
+ return true;
}
/**
@@ -373,84 +147,15 @@ public final class AccessibilityManager {
* doing its own rendering and does not rely on the platform rendering pipeline.
* </p>
*
- * @return True if high text contrast is enabled, false otherwise.
- *
- * @hide
*/
public boolean isHighTextContrastEnabled() {
- synchronized (mLock) {
- IAccessibilityManager service = getServiceLocked();
- if (service == null) {
- return false;
- }
- return mIsHighTextContrastEnabled;
- }
+ return false;
}
/**
* Sends an {@link AccessibilityEvent}.
- *
- * @param event The event to send.
- *
- * @throws IllegalStateException if accessibility is not enabled.
- *
- * <strong>Note:</strong> The preferred mechanism for sending custom accessibility
- * events is through calling
- * {@link android.view.ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)}
- * instead of this method to allow predecessors to augment/filter events sent by
- * their descendants.
*/
public void sendAccessibilityEvent(AccessibilityEvent event) {
- final IAccessibilityManager service;
- final int userId;
- synchronized (mLock) {
- service = getServiceLocked();
- if (service == null) {
- return;
- }
- if (!mIsEnabled) {
- Looper myLooper = Looper.myLooper();
- if (myLooper == Looper.getMainLooper()) {
- throw new IllegalStateException(
- "Accessibility off. Did you forget to check that?");
- } else {
- // If we're not running on the thread with the main looper, it's possible for
- // the state of accessibility to change between checking isEnabled and
- // calling this method. So just log the error rather than throwing the
- // exception.
- Log.e(LOG_TAG, "AccessibilityEvent sent with accessibility disabled");
- return;
- }
- }
- if ((event.getEventType() & mRelevantEventTypes) == 0) {
- if (DEBUG) {
- Log.i(LOG_TAG, "Not dispatching irrelevant event: " + event
- + " that is not among "
- + AccessibilityEvent.eventTypeToString(mRelevantEventTypes));
- }
- return;
- }
- userId = mUserId;
- }
- try {
- event.setEventTime(SystemClock.uptimeMillis());
- // it is possible that this manager is in the same process as the service but
- // client using it is called through Binder from another process. Example: MMS
- // app adds a SMS notification and the NotificationManagerService calls this method
- long identityToken = Binder.clearCallingIdentity();
- try {
- service.sendAccessibilityEvent(event, userId);
- } finally {
- Binder.restoreCallingIdentity(identityToken);
- }
- if (DEBUG) {
- Log.i(LOG_TAG, event + " sent");
- }
- } catch (RemoteException re) {
- Log.e(LOG_TAG, "Error during sending " + event + " ", re);
- } finally {
- event.recycle();
- }
}
/**
@@ -462,95 +167,27 @@ public final class AccessibilityManager {
* @return Whether the event is being observed.
*/
public boolean isObservedEventType(@AccessibilityEvent.EventType int type) {
- return mIsEnabled && (mRelevantEventTypes & type) != 0;
+ return false;
}
/**
- * Requests feedback interruption from all accessibility services.
+ * Requests interruption of the accessibility feedback from all accessibility services.
*/
public void interrupt() {
- final IAccessibilityManager service;
- final int userId;
- synchronized (mLock) {
- service = getServiceLocked();
- if (service == null) {
- return;
- }
- if (!mIsEnabled) {
- Looper myLooper = Looper.myLooper();
- if (myLooper == Looper.getMainLooper()) {
- throw new IllegalStateException(
- "Accessibility off. Did you forget to check that?");
- } else {
- // If we're not running on the thread with the main looper, it's possible for
- // the state of accessibility to change between checking isEnabled and
- // calling this method. So just log the error rather than throwing the
- // exception.
- Log.e(LOG_TAG, "Interrupt called with accessibility disabled");
- return;
- }
- }
- userId = mUserId;
- }
- try {
- service.interrupt(userId);
- if (DEBUG) {
- Log.i(LOG_TAG, "Requested interrupt from all services");
- }
- } catch (RemoteException re) {
- Log.e(LOG_TAG, "Error while requesting interrupt from all services. ", re);
- }
}
/**
* Returns the {@link ServiceInfo}s of the installed accessibility services.
*
* @return An unmodifiable list with {@link ServiceInfo}s.
- *
- * @deprecated Use {@link #getInstalledAccessibilityServiceList()}
*/
@Deprecated
public List<ServiceInfo> getAccessibilityServiceList() {
- List<AccessibilityServiceInfo> infos = getInstalledAccessibilityServiceList();
- List<ServiceInfo> services = new ArrayList<>();
- final int infoCount = infos.size();
- for (int i = 0; i < infoCount; i++) {
- AccessibilityServiceInfo info = infos.get(i);
- services.add(info.getResolveInfo().serviceInfo);
- }
- return Collections.unmodifiableList(services);
+ return Collections.emptyList();
}
- /**
- * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services.
- *
- * @return An unmodifiable list with {@link AccessibilityServiceInfo}s.
- */
public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() {
- final IAccessibilityManager service;
- final int userId;
- synchronized (mLock) {
- service = getServiceLocked();
- if (service == null) {
- return Collections.emptyList();
- }
- userId = mUserId;
- }
-
- List<AccessibilityServiceInfo> services = null;
- try {
- services = service.getInstalledAccessibilityServiceList(userId);
- if (DEBUG) {
- Log.i(LOG_TAG, "Installed AccessibilityServices " + services);
- }
- } catch (RemoteException re) {
- Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re);
- }
- if (services != null) {
- return Collections.unmodifiableList(services);
- } else {
- return Collections.emptyList();
- }
+ return Collections.emptyList();
}
/**
@@ -565,48 +202,21 @@ public final class AccessibilityManager {
* @see AccessibilityServiceInfo#FEEDBACK_HAPTIC
* @see AccessibilityServiceInfo#FEEDBACK_SPOKEN
* @see AccessibilityServiceInfo#FEEDBACK_VISUAL
- * @see AccessibilityServiceInfo#FEEDBACK_BRAILLE
*/
public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
int feedbackTypeFlags) {
- final IAccessibilityManager service;
- final int userId;
- synchronized (mLock) {
- service = getServiceLocked();
- if (service == null) {
- return Collections.emptyList();
- }
- userId = mUserId;
- }
-
- List<AccessibilityServiceInfo> services = null;
- try {
- services = service.getEnabledAccessibilityServiceList(feedbackTypeFlags, userId);
- if (DEBUG) {
- Log.i(LOG_TAG, "Installed AccessibilityServices " + services);
- }
- } catch (RemoteException re) {
- Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re);
- }
- if (services != null) {
- return Collections.unmodifiableList(services);
- } else {
- return Collections.emptyList();
- }
+ return Collections.emptyList();
}
/**
* Registers an {@link AccessibilityStateChangeListener} for changes in
- * the global accessibility state of the system. Equivalent to calling
- * {@link #addAccessibilityStateChangeListener(AccessibilityStateChangeListener, Handler)}
- * with a null handler.
+ * the global accessibility state of the system.
*
* @param listener The listener.
- * @return Always returns {@code true}.
+ * @return True if successfully registered.
*/
public boolean addAccessibilityStateChangeListener(
- @NonNull AccessibilityStateChangeListener listener) {
- addAccessibilityStateChangeListener(listener, null);
+ AccessibilityStateChangeListener listener) {
return true;
}
@@ -620,40 +230,22 @@ public final class AccessibilityManager {
* for a callback on the process's main handler.
*/
public void addAccessibilityStateChangeListener(
- @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) {
- synchronized (mLock) {
- mAccessibilityStateChangeListeners
- .put(listener, (handler == null) ? mHandler : handler);
- }
- }
+ @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) {}
- /**
- * Unregisters an {@link AccessibilityStateChangeListener}.
- *
- * @param listener The listener.
- * @return True if the listener was previously registered.
- */
public boolean removeAccessibilityStateChangeListener(
- @NonNull AccessibilityStateChangeListener listener) {
- synchronized (mLock) {
- int index = mAccessibilityStateChangeListeners.indexOfKey(listener);
- mAccessibilityStateChangeListeners.remove(listener);
- return (index >= 0);
- }
+ AccessibilityStateChangeListener listener) {
+ return true;
}
/**
* Registers a {@link TouchExplorationStateChangeListener} for changes in
- * the global touch exploration state of the system. Equivalent to calling
- * {@link #addTouchExplorationStateChangeListener(TouchExplorationStateChangeListener, Handler)}
- * with a null handler.
+ * the global touch exploration state of the system.
*
* @param listener The listener.
- * @return Always returns {@code true}.
+ * @return True if successfully registered.
*/
public boolean addTouchExplorationStateChangeListener(
@NonNull TouchExplorationStateChangeListener listener) {
- addTouchExplorationStateChangeListener(listener, null);
return true;
}
@@ -667,105 +259,17 @@ public final class AccessibilityManager {
* for a callback on the process's main handler.
*/
public void addTouchExplorationStateChangeListener(
- @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) {
- synchronized (mLock) {
- mTouchExplorationStateChangeListeners
- .put(listener, (handler == null) ? mHandler : handler);
- }
- }
+ @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) {}
/**
* Unregisters a {@link TouchExplorationStateChangeListener}.
*
* @param listener The listener.
- * @return True if listener was previously registered.
+ * @return True if successfully unregistered.
*/
public boolean removeTouchExplorationStateChangeListener(
@NonNull TouchExplorationStateChangeListener listener) {
- synchronized (mLock) {
- int index = mTouchExplorationStateChangeListeners.indexOfKey(listener);
- mTouchExplorationStateChangeListeners.remove(listener);
- return (index >= 0);
- }
- }
-
- /**
- * Registers a {@link AccessibilityServicesStateChangeListener}.
- *
- * @param listener The listener.
- * @param handler The handler on which the listener should be called back, or {@code null}
- * for a callback on the process's main handler.
- * @hide
- */
- @TestApi
- public void addAccessibilityServicesStateChangeListener(
- @NonNull AccessibilityServicesStateChangeListener listener, @Nullable Handler handler) {
- synchronized (mLock) {
- mServicesStateChangeListeners
- .put(listener, (handler == null) ? mHandler : handler);
- }
- }
-
- /**
- * Unregisters a {@link AccessibilityServicesStateChangeListener}.
- *
- * @param listener The listener.
- *
- * @hide
- */
- @TestApi
- public void removeAccessibilityServicesStateChangeListener(
- @NonNull AccessibilityServicesStateChangeListener listener) {
- // Final CopyOnWriteArrayList - no lock needed.
- mServicesStateChangeListeners.remove(listener);
- }
-
- /**
- * Registers a {@link AccessibilityRequestPreparer}.
- */
- public void addAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) {
- if (mRequestPreparerLists == null) {
- mRequestPreparerLists = new SparseArray<>(1);
- }
- int id = preparer.getView().getAccessibilityViewId();
- List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(id);
- if (requestPreparerList == null) {
- requestPreparerList = new ArrayList<>(1);
- mRequestPreparerLists.put(id, requestPreparerList);
- }
- requestPreparerList.add(preparer);
- }
-
- /**
- * Unregisters a {@link AccessibilityRequestPreparer}.
- */
- public void removeAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) {
- if (mRequestPreparerLists == null) {
- return;
- }
- int viewId = preparer.getView().getAccessibilityViewId();
- List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(viewId);
- if (requestPreparerList != null) {
- requestPreparerList.remove(preparer);
- if (requestPreparerList.isEmpty()) {
- mRequestPreparerLists.remove(viewId);
- }
- }
- }
-
- /**
- * Get the preparers that are registered for an accessibility ID
- *
- * @param id The ID of interest
- * @return The list of preparers, or {@code null} if there are none.
- *
- * @hide
- */
- public List<AccessibilityRequestPreparer> getRequestPreparersForAccessibilityId(int id) {
- if (mRequestPreparerLists == null) {
- return null;
- }
- return mRequestPreparerLists.get(id);
+ return true;
}
/**
@@ -777,12 +281,7 @@ public final class AccessibilityManager {
* @hide
*/
public void addHighTextContrastStateChangeListener(
- @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {
- synchronized (mLock) {
- mHighTextContrastStateChangeListeners
- .put(listener, (handler == null) ? mHandler : handler);
- }
- }
+ @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {}
/**
* Unregisters a {@link HighTextContrastChangeListener}.
@@ -792,51 +291,7 @@ public final class AccessibilityManager {
* @hide
*/
public void removeHighTextContrastStateChangeListener(
- @NonNull HighTextContrastChangeListener listener) {
- synchronized (mLock) {
- mHighTextContrastStateChangeListeners.remove(listener);
- }
- }
-
- /**
- * Check if the accessibility volume stream is active.
- *
- * @return True if accessibility volume is active (i.e. some service has requested it). False
- * otherwise.
- * @hide
- */
- public boolean isAccessibilityVolumeStreamActive() {
- List<AccessibilityServiceInfo> serviceInfos =
- getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
- for (int i = 0; i < serviceInfos.size(); i++) {
- if ((serviceInfos.get(i).flags & FLAG_ENABLE_ACCESSIBILITY_VOLUME) != 0) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Report a fingerprint gesture to accessibility. Only available for the system process.
- *
- * @param keyCode The key code of the gesture
- * @return {@code true} if accessibility consumes the event. {@code false} if not.
- * @hide
- */
- public boolean sendFingerprintGesture(int keyCode) {
- final IAccessibilityManager service;
- synchronized (mLock) {
- service = getServiceLocked();
- if (service == null) {
- return false;
- }
- }
- try {
- return service.sendFingerprintGesture(keyCode);
- } catch (RemoteException e) {
- return false;
- }
- }
+ @NonNull HighTextContrastChangeListener listener) {}
/**
* Sets the current state and notifies listeners, if necessary.
@@ -844,314 +299,14 @@ public final class AccessibilityManager {
* @param stateFlags The state flags.
*/
private void setStateLocked(int stateFlags) {
- final boolean enabled = (stateFlags & STATE_FLAG_ACCESSIBILITY_ENABLED) != 0;
- final boolean touchExplorationEnabled =
- (stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0;
- final boolean highTextContrastEnabled =
- (stateFlags & STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED) != 0;
-
- final boolean wasEnabled = mIsEnabled;
- final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled;
- final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled;
-
- // Ensure listeners get current state from isZzzEnabled() calls.
- mIsEnabled = enabled;
- mIsTouchExplorationEnabled = touchExplorationEnabled;
- mIsHighTextContrastEnabled = highTextContrastEnabled;
-
- if (wasEnabled != enabled) {
- notifyAccessibilityStateChanged();
- }
-
- if (wasTouchExplorationEnabled != touchExplorationEnabled) {
- notifyTouchExplorationStateChanged();
- }
-
- if (wasHighTextContrastEnabled != highTextContrastEnabled) {
- notifyHighTextContrastStateChanged();
- }
}
- /**
- * Find an installed service with the specified {@link ComponentName}.
- *
- * @param componentName The name to match to the service.
- *
- * @return The info corresponding to the installed service, or {@code null} if no such service
- * is installed.
- * @hide
- */
- public AccessibilityServiceInfo getInstalledServiceInfoWithComponentName(
- ComponentName componentName) {
- final List<AccessibilityServiceInfo> installedServiceInfos =
- getInstalledAccessibilityServiceList();
- if ((installedServiceInfos == null) || (componentName == null)) {
- return null;
- }
- for (int i = 0; i < installedServiceInfos.size(); i++) {
- if (componentName.equals(installedServiceInfos.get(i).getComponentName())) {
- return installedServiceInfos.get(i);
- }
- }
- return null;
- }
-
- /**
- * Adds an accessibility interaction connection interface for a given window.
- * @param windowToken The window token to which a connection is added.
- * @param connection The connection.
- *
- * @hide
- */
public int addAccessibilityInteractionConnection(IWindow windowToken,
IAccessibilityInteractionConnection connection) {
- final IAccessibilityManager service;
- final int userId;
- synchronized (mLock) {
- service = getServiceLocked();
- if (service == null) {
- return View.NO_ID;
- }
- userId = mUserId;
- }
- try {
- return service.addAccessibilityInteractionConnection(windowToken, connection, userId);
- } catch (RemoteException re) {
- Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re);
- }
return View.NO_ID;
}
- /**
- * Removed an accessibility interaction connection interface for a given window.
- * @param windowToken The window token to which a connection is removed.
- *
- * @hide
- */
public void removeAccessibilityInteractionConnection(IWindow windowToken) {
- final IAccessibilityManager service;
- synchronized (mLock) {
- service = getServiceLocked();
- if (service == null) {
- return;
- }
- }
- try {
- service.removeAccessibilityInteractionConnection(windowToken);
- } catch (RemoteException re) {
- Log.e(LOG_TAG, "Error while removing an accessibility interaction connection. ", re);
- }
- }
-
- /**
- * Perform the accessibility shortcut if the caller has permission.
- *
- * @hide
- */
- public void performAccessibilityShortcut() {
- final IAccessibilityManager service;
- synchronized (mLock) {
- service = getServiceLocked();
- if (service == null) {
- return;
- }
- }
- try {
- service.performAccessibilityShortcut();
- } catch (RemoteException re) {
- Log.e(LOG_TAG, "Error performing accessibility shortcut. ", re);
- }
- }
-
- /**
- * Notifies that the accessibility button in the system's navigation area has been clicked
- *
- * @hide
- */
- public void notifyAccessibilityButtonClicked() {
- final IAccessibilityManager service;
- synchronized (mLock) {
- service = getServiceLocked();
- if (service == null) {
- return;
- }
- }
- try {
- service.notifyAccessibilityButtonClicked();
- } catch (RemoteException re) {
- Log.e(LOG_TAG, "Error while dispatching accessibility button click", re);
- }
- }
-
- /**
- * Notifies that the visibility of the accessibility button in the system's navigation area
- * has changed.
- *
- * @param shown {@code true} if the accessibility button is visible within the system
- * navigation area, {@code false} otherwise
- * @hide
- */
- public void notifyAccessibilityButtonVisibilityChanged(boolean shown) {
- final IAccessibilityManager service;
- synchronized (mLock) {
- service = getServiceLocked();
- if (service == null) {
- return;
- }
- }
- try {
- service.notifyAccessibilityButtonVisibilityChanged(shown);
- } catch (RemoteException re) {
- Log.e(LOG_TAG, "Error while dispatching accessibility button visibility change", re);
- }
- }
-
- /**
- * Set an IAccessibilityInteractionConnection to replace the actions of a picture-in-picture
- * window. Intended for use by the System UI only.
- *
- * @param connection The connection to handle the actions. Set to {@code null} to avoid
- * affecting the actions.
- *
- * @hide
- */
- public void setPictureInPictureActionReplacingConnection(
- @Nullable IAccessibilityInteractionConnection connection) {
- final IAccessibilityManager service;
- synchronized (mLock) {
- service = getServiceLocked();
- if (service == null) {
- return;
- }
- }
- try {
- service.setPictureInPictureActionReplacingConnection(connection);
- } catch (RemoteException re) {
- Log.e(LOG_TAG, "Error setting picture in picture action replacement", re);
- }
}
- private IAccessibilityManager getServiceLocked() {
- if (mService == null) {
- tryConnectToServiceLocked(null);
- }
- return mService;
- }
-
- private void tryConnectToServiceLocked(IAccessibilityManager service) {
- if (service == null) {
- IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE);
- if (iBinder == null) {
- return;
- }
- service = IAccessibilityManager.Stub.asInterface(iBinder);
- }
-
- try {
- final long userStateAndRelevantEvents = service.addClient(mClient, mUserId);
- setStateLocked(IntPair.first(userStateAndRelevantEvents));
- mRelevantEventTypes = IntPair.second(userStateAndRelevantEvents);
- mService = service;
- } catch (RemoteException re) {
- Log.e(LOG_TAG, "AccessibilityManagerService is dead", re);
- }
- }
-
- /**
- * Notifies the registered {@link AccessibilityStateChangeListener}s.
- */
- private void notifyAccessibilityStateChanged() {
- final boolean isEnabled;
- final ArrayMap<AccessibilityStateChangeListener, Handler> listeners;
- synchronized (mLock) {
- if (mAccessibilityStateChangeListeners.isEmpty()) {
- return;
- }
- isEnabled = mIsEnabled;
- listeners = new ArrayMap<>(mAccessibilityStateChangeListeners);
- }
-
- int numListeners = listeners.size();
- for (int i = 0; i < numListeners; i++) {
- final AccessibilityStateChangeListener listener =
- mAccessibilityStateChangeListeners.keyAt(i);
- mAccessibilityStateChangeListeners.valueAt(i)
- .post(() -> listener.onAccessibilityStateChanged(isEnabled));
- }
- }
-
- /**
- * Notifies the registered {@link TouchExplorationStateChangeListener}s.
- */
- private void notifyTouchExplorationStateChanged() {
- final boolean isTouchExplorationEnabled;
- final ArrayMap<TouchExplorationStateChangeListener, Handler> listeners;
- synchronized (mLock) {
- if (mTouchExplorationStateChangeListeners.isEmpty()) {
- return;
- }
- isTouchExplorationEnabled = mIsTouchExplorationEnabled;
- listeners = new ArrayMap<>(mTouchExplorationStateChangeListeners);
- }
-
- int numListeners = listeners.size();
- for (int i = 0; i < numListeners; i++) {
- final TouchExplorationStateChangeListener listener =
- mTouchExplorationStateChangeListeners.keyAt(i);
- mTouchExplorationStateChangeListeners.valueAt(i)
- .post(() -> listener.onTouchExplorationStateChanged(isTouchExplorationEnabled));
- }
- }
-
- /**
- * Notifies the registered {@link HighTextContrastChangeListener}s.
- */
- private void notifyHighTextContrastStateChanged() {
- final boolean isHighTextContrastEnabled;
- final ArrayMap<HighTextContrastChangeListener, Handler> listeners;
- synchronized (mLock) {
- if (mHighTextContrastStateChangeListeners.isEmpty()) {
- return;
- }
- isHighTextContrastEnabled = mIsHighTextContrastEnabled;
- listeners = new ArrayMap<>(mHighTextContrastStateChangeListeners);
- }
-
- int numListeners = listeners.size();
- for (int i = 0; i < numListeners; i++) {
- final HighTextContrastChangeListener listener =
- mHighTextContrastStateChangeListeners.keyAt(i);
- mHighTextContrastStateChangeListeners.valueAt(i)
- .post(() -> listener.onHighTextContrastStateChanged(isHighTextContrastEnabled));
- }
- }
-
- /**
- * Determines if the accessibility button within the system navigation area is supported.
- *
- * @return {@code true} if the accessibility button is supported on this device,
- * {@code false} otherwise
- */
- public static boolean isAccessibilityButtonSupported() {
- final Resources res = Resources.getSystem();
- return res.getBoolean(com.android.internal.R.bool.config_showNavigationBar);
- }
-
- private final class MyCallback implements Handler.Callback {
- public static final int MSG_SET_STATE = 1;
-
- @Override
- public boolean handleMessage(Message message) {
- switch (message.what) {
- case MSG_SET_STATE: {
- // See comment at mClient
- final int state = message.arg1;
- synchronized (mLock) {
- setStateLocked(state);
- }
- } break;
- }
- return true;
- }
- }
}
diff --git a/android/view/accessibility/AccessibilityNodeInfo.java b/android/view/accessibility/AccessibilityNodeInfo.java
index faea9200..28ef6978 100644
--- a/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/android/view/accessibility/AccessibilityNodeInfo.java
@@ -2325,7 +2325,7 @@ public class AccessibilityNodeInfo implements Parcelable {
/**
* Returns whether the node is explicitly marked as a focusable unit by a screen reader. Note
* that {@code false} indicates that it is not explicitly marked, not that the node is not
- * a focusable unit. Screen readers should generally used other signals, such as
+ * a focusable unit. Screen readers should generally use other signals, such as
* {@link #isFocusable()}, or the presence of text in a node, to determine what should receive
* focus.
*
@@ -3695,8 +3695,9 @@ public class AccessibilityNodeInfo implements Parcelable {
if (DEBUG) {
builder.append("; sourceNodeId: " + mSourceNodeId);
- builder.append("; accessibilityViewId: " + getAccessibilityViewId(mSourceNodeId));
- builder.append("; virtualDescendantId: " + getVirtualDescendantId(mSourceNodeId));
+ builder.append("; windowId: " + mWindowId);
+ builder.append("; accessibilityViewId: ").append(getAccessibilityViewId(mSourceNodeId));
+ builder.append("; virtualDescendantId: ").append(getVirtualDescendantId(mSourceNodeId));
builder.append("; mParentNodeId: " + mParentNodeId);
builder.append("; traversalBefore: ").append(mTraversalBefore);
builder.append("; traversalAfter: ").append(mTraversalAfter);
@@ -3726,8 +3727,8 @@ public class AccessibilityNodeInfo implements Parcelable {
builder.append("]");
}
- builder.append("; boundsInParent: " + mBoundsInParent);
- builder.append("; boundsInScreen: " + mBoundsInScreen);
+ builder.append("; boundsInParent: ").append(mBoundsInParent);
+ builder.append("; boundsInScreen: ").append(mBoundsInScreen);
builder.append("; packageName: ").append(mPackageName);
builder.append("; className: ").append(mClassName);
diff --git a/android/view/accessibility/AccessibilityRequestPreparer.java b/android/view/accessibility/AccessibilityRequestPreparer.java
index 889feb98..25f830a5 100644
--- a/android/view/accessibility/AccessibilityRequestPreparer.java
+++ b/android/view/accessibility/AccessibilityRequestPreparer.java
@@ -44,10 +44,9 @@ public abstract class AccessibilityRequestPreparer {
public static final int REQUEST_TYPE_EXTRA_DATA = 0x00000001;
/** @hide */
- @IntDef(flag = true,
- value = {
- REQUEST_TYPE_EXTRA_DATA
- })
+ @IntDef(flag = true, prefix = { "REQUEST_TYPE_" }, value = {
+ REQUEST_TYPE_EXTRA_DATA
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface RequestTypes {}
diff --git a/android/view/accessibility/AccessibilityWindowInfo.java b/android/view/accessibility/AccessibilityWindowInfo.java
index f11767de..ef1a3f3b 100644
--- a/android/view/accessibility/AccessibilityWindowInfo.java
+++ b/android/view/accessibility/AccessibilityWindowInfo.java
@@ -87,6 +87,7 @@ public final class AccessibilityWindowInfo implements Parcelable {
private static final int BOOLEAN_PROPERTY_ACTIVE = 1 << 0;
private static final int BOOLEAN_PROPERTY_FOCUSED = 1 << 1;
private static final int BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED = 1 << 2;
+ private static final int BOOLEAN_PROPERTY_PICTURE_IN_PICTURE = 1 << 3;
// Housekeeping.
private static final int MAX_POOL_SIZE = 10;
@@ -103,8 +104,7 @@ public final class AccessibilityWindowInfo implements Parcelable {
private final Rect mBoundsInScreen = new Rect();
private LongArray mChildIds;
private CharSequence mTitle;
- private int mAnchorId = UNDEFINED_WINDOW_ID;
- private boolean mInPictureInPicture;
+ private long mAnchorId = AccessibilityNodeInfo.UNDEFINED_NODE_ID;
private int mConnectionId = UNDEFINED_WINDOW_ID;
@@ -202,7 +202,7 @@ public final class AccessibilityWindowInfo implements Parcelable {
*
* @hide
*/
- public void setAnchorId(int anchorId) {
+ public void setAnchorId(long anchorId) {
mAnchorId = anchorId;
}
@@ -212,7 +212,8 @@ public final class AccessibilityWindowInfo implements Parcelable {
* @return The anchor node, or {@code null} if none exists.
*/
public AccessibilityNodeInfo getAnchor() {
- if ((mConnectionId == UNDEFINED_WINDOW_ID) || (mAnchorId == UNDEFINED_WINDOW_ID)
+ if ((mConnectionId == UNDEFINED_WINDOW_ID)
+ || (mAnchorId == AccessibilityNodeInfo.UNDEFINED_NODE_ID)
|| (mParentId == UNDEFINED_WINDOW_ID)) {
return null;
}
@@ -224,17 +225,7 @@ public final class AccessibilityWindowInfo implements Parcelable {
/** @hide */
public void setPictureInPicture(boolean pictureInPicture) {
- mInPictureInPicture = pictureInPicture;
- }
-
- /**
- * Check if the window is in picture-in-picture mode.
- *
- * @return {@code true} if the window is in picture-in-picture mode, {@code false} otherwise.
- * @removed
- */
- public boolean inPictureInPicture() {
- return isInPictureInPictureMode();
+ setBooleanProperty(BOOLEAN_PROPERTY_PICTURE_IN_PICTURE, pictureInPicture);
}
/**
@@ -243,7 +234,7 @@ public final class AccessibilityWindowInfo implements Parcelable {
* @return {@code true} if the window is in picture-in-picture mode, {@code false} otherwise.
*/
public boolean isInPictureInPictureMode() {
- return mInPictureInPicture;
+ return getBooleanProperty(BOOLEAN_PROPERTY_PICTURE_IN_PICTURE);
}
/**
@@ -463,7 +454,6 @@ public final class AccessibilityWindowInfo implements Parcelable {
infoClone.mBoundsInScreen.set(info.mBoundsInScreen);
infoClone.mTitle = info.mTitle;
infoClone.mAnchorId = info.mAnchorId;
- infoClone.mInPictureInPicture = info.mInPictureInPicture;
if (info.mChildIds != null && info.mChildIds.size() > 0) {
if (infoClone.mChildIds == null) {
@@ -520,8 +510,7 @@ public final class AccessibilityWindowInfo implements Parcelable {
parcel.writeInt(mParentId);
mBoundsInScreen.writeToParcel(parcel, flags);
parcel.writeCharSequence(mTitle);
- parcel.writeInt(mAnchorId);
- parcel.writeInt(mInPictureInPicture ? 1 : 0);
+ parcel.writeLong(mAnchorId);
final LongArray childIds = mChildIds;
if (childIds == null) {
@@ -545,8 +534,7 @@ public final class AccessibilityWindowInfo implements Parcelable {
mParentId = parcel.readInt();
mBoundsInScreen.readFromParcel(parcel);
mTitle = parcel.readCharSequence();
- mAnchorId = parcel.readInt();
- mInPictureInPicture = parcel.readInt() == 1;
+ mAnchorId = parcel.readLong();
final int childCount = parcel.readInt();
if (childCount > 0) {
@@ -593,7 +581,7 @@ public final class AccessibilityWindowInfo implements Parcelable {
builder.append(", bounds=").append(mBoundsInScreen);
builder.append(", focused=").append(isFocused());
builder.append(", active=").append(isActive());
- builder.append(", pictureInPicture=").append(inPictureInPicture());
+ builder.append(", pictureInPicture=").append(isInPictureInPictureMode());
if (DEBUG) {
builder.append(", parent=").append(mParentId);
builder.append(", children=[");
@@ -611,7 +599,8 @@ public final class AccessibilityWindowInfo implements Parcelable {
builder.append(']');
} else {
builder.append(", hasParent=").append(mParentId != UNDEFINED_WINDOW_ID);
- builder.append(", isAnchored=").append(mAnchorId != UNDEFINED_WINDOW_ID);
+ builder.append(", isAnchored=")
+ .append(mAnchorId != AccessibilityNodeInfo.UNDEFINED_NODE_ID);
builder.append(", hasChildren=").append(mChildIds != null
&& mChildIds.size() > 0);
}
@@ -633,8 +622,7 @@ public final class AccessibilityWindowInfo implements Parcelable {
mChildIds.clear();
}
mConnectionId = UNDEFINED_WINDOW_ID;
- mAnchorId = UNDEFINED_WINDOW_ID;
- mInPictureInPicture = false;
+ mAnchorId = AccessibilityNodeInfo.UNDEFINED_NODE_ID;
mTitle = null;
}
diff --git a/android/view/autofill/AutofillManager.java b/android/view/autofill/AutofillManager.java
index 547e0db9..26974545 100644
--- a/android/view/autofill/AutofillManager.java
+++ b/android/view/autofill/AutofillManager.java
@@ -36,6 +36,7 @@ import android.os.Parcelable;
import android.os.RemoteException;
import android.service.autofill.AutofillService;
import android.service.autofill.FillEventHistory;
+import android.service.autofill.UserData;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -45,6 +46,7 @@ import android.view.View;
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.Preconditions;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -426,7 +428,7 @@ public final class AutofillManager {
* @hide
*/
public AutofillManager(Context context, IAutoFillManager service) {
- mContext = context;
+ mContext = Preconditions.checkNotNull(context, "context cannot be null");
mService = service;
}
@@ -455,7 +457,7 @@ public final class AutofillManager {
if (mSessionId != NO_SESSION) {
ensureServiceClientAddedIfNeededLocked();
- final AutofillClient client = getClientLocked();
+ final AutofillClient client = getClient();
if (client != null) {
try {
final boolean sessionWasRestored = mService.restoreSession(mSessionId,
@@ -528,10 +530,13 @@ public final class AutofillManager {
* @return whether autofill is enabled for the current user.
*/
public boolean isEnabled() {
- if (!hasAutofillFeature() || isDisabledByService()) {
+ if (!hasAutofillFeature()) {
return false;
}
synchronized (mLock) {
+ if (isDisabledByServiceLocked()) {
+ return false;
+ }
ensureServiceClientAddedIfNeededLocked();
return mEnabled;
}
@@ -603,19 +608,16 @@ public final class AutofillManager {
}
private boolean shouldIgnoreViewEnteredLocked(@NonNull View view, int flags) {
- if (isDisabledByService()) {
+ if (isDisabledByServiceLocked()) {
if (sVerbose) {
Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + view
+ ") on state " + getStateAsStringLocked());
}
return true;
}
- if (mState == STATE_FINISHED && (flags & FLAG_MANUAL_REQUEST) == 0) {
- if (sVerbose) {
- Log.v(TAG, "ignoring notifyViewEntered(flags=" + flags + ", view=" + view
- + ") on state " + getStateAsStringLocked());
- }
- return true;
+ if (sVerbose && isFinishedLocked()) {
+ Log.v(TAG, "not ignoring notifyViewEntered(flags=" + flags + ", view=" + view
+ + ") on state " + getStateAsStringLocked());
}
return false;
}
@@ -1007,6 +1009,76 @@ public final class AutofillManager {
}
/**
+ * Returns the component name of the {@link AutofillService} that is enabled for the current
+ * user.
+ */
+ @Nullable
+ public ComponentName getAutofillServiceComponentName() {
+ if (mService == null) return null;
+
+ try {
+ return mService.getAutofillServiceComponentName();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the user data used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>.
+ *
+ * <p><b>Note:</b> This method should only be called by an app providing an autofill service.
+ *
+ * @return value previously set by {@link #setUserData(UserData)} or {@code null} if it was
+ * reset or if the caller currently does not have an enabled autofill service for the user.
+ */
+ @Nullable public UserData getUserData() {
+ try {
+ return mService.getUserData();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ return null;
+ }
+ }
+
+ /**
+ * Sets the user data used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>
+ *
+ * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
+ * and it's ignored if the caller currently doesn't have an enabled autofill service for
+ * the user.
+ */
+ public void setUserData(@Nullable UserData userData) {
+ try {
+ mService.setUserData(userData);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Checks if <a href="AutofillService.html#FieldClassification">field classification</a> is
+ * enabled.
+ *
+ * <p>As field classification is an expensive operation, it could be disabled, either
+ * temporarily (for example, because the service exceeded a rate-limit threshold) or
+ * permanently (for example, because the device is a low-level device).
+ *
+ * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
+ * and it's ignored if the caller currently doesn't have an enabled autofill service for
+ * the user.
+ */
+ public boolean isFieldClassificationEnabled() {
+ try {
+ return mService.isFieldClassificationEnabled();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ return false;
+ }
+ }
+
+ /**
* Returns {@code true} if autofill is supported by the current device and
* is supported for this user.
*
@@ -1026,7 +1098,8 @@ public final class AutofillManager {
}
}
- private AutofillClient getClientLocked() {
+ // Note: don't need to use locked suffix because mContext is final.
+ private AutofillClient getClient() {
final AutofillClient client = mContext.getAutofillClient();
if (client == null && sDebug) {
Log.d(TAG, "No AutofillClient for " + mContext.getPackageName() + " on context "
@@ -1081,24 +1154,24 @@ public final class AutofillManager {
Log.v(TAG, "startSessionLocked(): id=" + id + ", bounds=" + bounds + ", value=" + value
+ ", flags=" + flags + ", state=" + getStateAsStringLocked());
}
- if (mState != STATE_UNKNOWN && (flags & FLAG_MANUAL_REQUEST) == 0) {
+ if (mState != STATE_UNKNOWN && !isFinishedLocked() && (flags & FLAG_MANUAL_REQUEST) == 0) {
if (sVerbose) {
Log.v(TAG, "not automatically starting session for " + id
- + " on state " + getStateAsStringLocked());
+ + " on state " + getStateAsStringLocked() + " and flags " + flags);
}
return;
}
try {
- final AutofillClient client = getClientLocked();
+ final AutofillClient client = getClient();
+ if (client == null) return; // NOTE: getClient() already logd it..
+
mSessionId = mService.startSession(mContext.getActivityToken(),
mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(),
mCallback != null, flags, client.getComponentName());
if (mSessionId != NO_SESSION) {
mState = STATE_ACTIVE;
}
- if (client != null) {
- client.autofillCallbackResetableStateAvailable();
- }
+ client.autofillCallbackResetableStateAvailable();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1150,7 +1223,9 @@ public final class AutofillManager {
try {
if (restartIfNecessary) {
- final AutofillClient client = getClientLocked();
+ final AutofillClient client = getClient();
+ if (client == null) return; // NOTE: getClient() already logd it..
+
final int newId = mService.updateOrRestartSession(mContext.getActivityToken(),
mServiceClient.asBinder(), id, bounds, value, mContext.getUserId(),
mCallback != null, flags, client.getComponentName(), mSessionId, action);
@@ -1158,9 +1233,7 @@ public final class AutofillManager {
if (sDebug) Log.d(TAG, "Session restarted: " + mSessionId + "=>" + newId);
mSessionId = newId;
mState = (mSessionId == NO_SESSION) ? STATE_UNKNOWN : STATE_ACTIVE;
- if (client != null) {
- client.autofillCallbackResetableStateAvailable();
- }
+ client.autofillCallbackResetableStateAvailable();
}
} else {
mService.updateSession(mSessionId, id, bounds, value, action, flags,
@@ -1173,7 +1246,7 @@ public final class AutofillManager {
}
private void ensureServiceClientAddedIfNeededLocked() {
- if (getClientLocked() == null) {
+ if (getClient() == null) {
return;
}
@@ -1256,7 +1329,7 @@ public final class AutofillManager {
AutofillCallback callback = null;
synchronized (mLock) {
if (mSessionId == sessionId) {
- AutofillClient client = getClientLocked();
+ AutofillClient client = getClient();
if (client != null) {
if (client.autofillCallbackRequestShowFillUi(anchor, width, height,
@@ -1281,7 +1354,7 @@ public final class AutofillManager {
Intent fillInIntent) {
synchronized (mLock) {
if (sessionId == mSessionId) {
- AutofillClient client = getClientLocked();
+ final AutofillClient client = getClient();
if (client != null) {
client.autofillCallbackAuthenticate(authenticationId, intent, fillInIntent);
}
@@ -1346,7 +1419,7 @@ public final class AutofillManager {
return;
}
- final AutofillClient client = getClientLocked();
+ final AutofillClient client = getClient();
if (client == null) {
return;
}
@@ -1523,7 +1596,7 @@ public final class AutofillManager {
// 1. If local and remote session id are off sync the UI would be stuck shown
// 2. There is a race between the user state being destroyed due the fill
// service being uninstalled and the UI being dismissed.
- AutofillClient client = getClientLocked();
+ AutofillClient client = getClient();
if (client != null) {
if (client.autofillCallbackRequestHideFillUi() && mCallback != null) {
callback = mCallback;
@@ -1553,7 +1626,7 @@ public final class AutofillManager {
AutofillCallback callback = null;
synchronized (mLock) {
- if (mSessionId == sessionId && getClientLocked() != null) {
+ if (mSessionId == sessionId && getClient() != null) {
callback = mCallback;
}
}
@@ -1610,7 +1683,7 @@ public final class AutofillManager {
* @return The view or {@code null} if view was not found
*/
private View findView(@NonNull AutofillId autofillId) {
- final AutofillClient client = getClientLocked();
+ final AutofillClient client = getClient();
if (client == null) {
return null;
@@ -1644,7 +1717,7 @@ public final class AutofillManager {
pw.print(pfx); pw.print("sessionId: "); pw.println(mSessionId);
pw.print(pfx); pw.print("state: "); pw.println(getStateAsStringLocked());
pw.print(pfx); pw.print("context: "); pw.println(mContext);
- pw.print(pfx); pw.print("client: "); pw.println(getClientLocked());
+ pw.print(pfx); pw.print("client: "); pw.println(getClient());
pw.print(pfx); pw.print("enabled: "); pw.println(mEnabled);
pw.print(pfx); pw.print("hasService: "); pw.println(mService != null);
pw.print(pfx); pw.print("hasCallback: "); pw.println(mCallback != null);
@@ -1686,12 +1759,16 @@ public final class AutofillManager {
return mState == STATE_ACTIVE;
}
- private boolean isDisabledByService() {
+ private boolean isDisabledByServiceLocked() {
return mState == STATE_DISABLED_BY_SERVICE;
}
+ private boolean isFinishedLocked() {
+ return mState == STATE_FINISHED;
+ }
+
private void post(Runnable runnable) {
- final AutofillClient client = getClientLocked();
+ final AutofillClient client = getClient();
if (client == null) {
if (sVerbose) Log.v(TAG, "ignoring post() because client is null");
return;
@@ -1774,7 +1851,7 @@ public final class AutofillManager {
* @param trackedIds The views to be tracked
*/
TrackedViews(@Nullable AutofillId[] trackedIds) {
- final AutofillClient client = getClientLocked();
+ final AutofillClient client = getClient();
if (trackedIds != null && client != null) {
final boolean[] isVisible;
@@ -1815,7 +1892,7 @@ public final class AutofillManager {
* @param isVisible visible if the view is visible in the view hierarchy.
*/
void notifyViewVisibilityChanged(@NonNull AutofillId id, boolean isVisible) {
- AutofillClient client = getClientLocked();
+ AutofillClient client = getClient();
if (sDebug) {
Log.d(TAG, "notifyViewVisibilityChanged(): id=" + id + " isVisible="
@@ -1852,7 +1929,7 @@ public final class AutofillManager {
void onVisibleForAutofillLocked() {
// The visibility of the views might have changed while the client was not be visible,
// hence update the visibility state for all views.
- AutofillClient client = getClientLocked();
+ AutofillClient client = getClient();
ArraySet<AutofillId> updatedVisibleTrackedIds = null;
ArraySet<AutofillId> updatedInvisibleTrackedIds = null;
if (client != null) {
@@ -1918,7 +1995,11 @@ public final class AutofillManager {
public abstract static class AutofillCallback {
/** @hide */
- @IntDef({EVENT_INPUT_SHOWN, EVENT_INPUT_HIDDEN, EVENT_INPUT_UNAVAILABLE})
+ @IntDef(prefix = { "EVENT_INPUT_" }, value = {
+ EVENT_INPUT_SHOWN,
+ EVENT_INPUT_HIDDEN,
+ EVENT_INPUT_UNAVAILABLE
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface AutofillEventType {}
diff --git a/android/view/autofill/AutofillPopupWindow.java b/android/view/autofill/AutofillPopupWindow.java
index b4688bb1..5cba21e3 100644
--- a/android/view/autofill/AutofillPopupWindow.java
+++ b/android/view/autofill/AutofillPopupWindow.java
@@ -108,11 +108,12 @@ public class AutofillPopupWindow extends PopupWindow {
// symmetrically when the dropdown is below and above the anchor.
final View actualAnchor;
if (virtualBounds != null) {
+ final int[] mLocationOnScreen = new int[] {virtualBounds.left, virtualBounds.top};
actualAnchor = new View(anchor.getContext()) {
@Override
public void getLocationOnScreen(int[] location) {
- location[0] = virtualBounds.left;
- location[1] = virtualBounds.top;
+ location[0] = mLocationOnScreen[0];
+ location[1] = mLocationOnScreen[1];
}
@Override
@@ -178,6 +179,12 @@ public class AutofillPopupWindow extends PopupWindow {
virtualBounds.right, virtualBounds.bottom);
actualAnchor.setScrollX(anchor.getScrollX());
actualAnchor.setScrollY(anchor.getScrollY());
+
+ anchor.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
+ mLocationOnScreen[0] = mLocationOnScreen[0] - (scrollX - oldScrollX);
+ mLocationOnScreen[1] = mLocationOnScreen[1] - (scrollY - oldScrollY);
+ });
+ actualAnchor.setWillNotDraw(true);
} else {
actualAnchor = anchor;
}
diff --git a/android/view/autofill/AutofillValue.java b/android/view/autofill/AutofillValue.java
index 3beae11c..8e649de5 100644
--- a/android/view/autofill/AutofillValue.java
+++ b/android/view/autofill/AutofillValue.java
@@ -177,7 +177,7 @@ public final class AutofillValue implements Parcelable {
.append("[type=").append(mType)
.append(", value=");
if (isText()) {
- string.append(((CharSequence) mValue).length()).append("_chars");
+ Helper.appendRedacted(string, (CharSequence) mValue);
} else {
string.append(mValue);
}
diff --git a/android/view/autofill/Helper.java b/android/view/autofill/Helper.java
index 829e7f3a..4b2c53c7 100644
--- a/android/view/autofill/Helper.java
+++ b/android/view/autofill/Helper.java
@@ -16,11 +16,8 @@
package android.view.autofill;
-import android.os.Bundle;
-
-import java.util.Arrays;
-import java.util.Objects;
-import java.util.Set;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
/** @hide */
public final class Helper {
@@ -29,25 +26,37 @@ public final class Helper {
public static boolean sDebug = false;
public static boolean sVerbose = false;
- public static final String REDACTED = "[REDACTED]";
+ /**
+ * Appends {@code value} to the {@code builder} redacting its contents.
+ */
+ public static void appendRedacted(@NonNull StringBuilder builder,
+ @Nullable CharSequence value) {
+ builder.append(getRedacted(value));
+ }
- static StringBuilder append(StringBuilder builder, Bundle bundle) {
- if (bundle == null || !sDebug) {
+ /**
+ * Gets the redacted version of a value.
+ */
+ @NonNull
+ public static String getRedacted(@Nullable CharSequence value) {
+ return (value == null) ? "null" : value.length() + "_chars";
+ }
+
+ /**
+ * Appends {@code values} to the {@code builder} redacting its contents.
+ */
+ public static void appendRedacted(@NonNull StringBuilder builder, @Nullable String[] values) {
+ if (values == null) {
builder.append("N/A");
- } else if (!sVerbose) {
- builder.append(REDACTED);
- } else {
- final Set<String> keySet = bundle.keySet();
- builder.append("[Bundle with ").append(keySet.size()).append(" extras:");
- for (String key : keySet) {
- final Object value = bundle.get(key);
- builder.append(' ').append(key).append('=');
- builder.append((value instanceof Object[])
- ? Arrays.toString((Objects[]) value) : value);
- }
- builder.append(']');
+ return;
+ }
+ builder.append("[");
+ for (String value : values) {
+ builder.append(" '");
+ appendRedacted(builder, value);
+ builder.append("'");
}
- return builder;
+ builder.append(" ]");
}
private Helper() {
diff --git a/android/view/inputmethod/InputMethodInfo.java b/android/view/inputmethod/InputMethodInfo.java
index f0645b89..c69543f6 100644
--- a/android/view/inputmethod/InputMethodInfo.java
+++ b/android/view/inputmethod/InputMethodInfo.java
@@ -67,6 +67,11 @@ public final class InputMethodInfo implements Parcelable {
final ResolveInfo mService;
/**
+ * IME only supports VR mode.
+ */
+ final boolean mIsVrOnly;
+
+ /**
* The unique string Id to identify the input method. This is generated
* from the input method component.
*/
@@ -149,6 +154,7 @@ public final class InputMethodInfo implements Parcelable {
PackageManager pm = context.getPackageManager();
String settingsActivityComponent = null;
+ boolean isVrOnly;
int isDefaultResId = 0;
XmlResourceParser parser = null;
@@ -179,6 +185,7 @@ public final class InputMethodInfo implements Parcelable {
com.android.internal.R.styleable.InputMethod);
settingsActivityComponent = sa.getString(
com.android.internal.R.styleable.InputMethod_settingsActivity);
+ isVrOnly = sa.getBoolean(com.android.internal.R.styleable.InputMethod_isVrOnly, false);
isDefaultResId = sa.getResourceId(
com.android.internal.R.styleable.InputMethod_isDefault, 0);
supportsSwitchingToNextInputMethod = sa.getBoolean(
@@ -254,6 +261,8 @@ public final class InputMethodInfo implements Parcelable {
mIsDefaultResId = isDefaultResId;
mIsAuxIme = isAuxIme;
mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod;
+ // TODO(b/68948291): remove this meta-data before release.
+ mIsVrOnly = isVrOnly || service.serviceInfo.metaData.getBoolean("isVrOnly", false);
}
InputMethodInfo(Parcel source) {
@@ -262,6 +271,7 @@ public final class InputMethodInfo implements Parcelable {
mIsDefaultResId = source.readInt();
mIsAuxIme = source.readInt() == 1;
mSupportsSwitchingToNextInputMethod = source.readInt() == 1;
+ mIsVrOnly = source.readBoolean();
mService = ResolveInfo.CREATOR.createFromParcel(source);
mSubtypes = new InputMethodSubtypeArray(source);
mForceDefault = false;
@@ -274,7 +284,8 @@ public final class InputMethodInfo implements Parcelable {
CharSequence label, String settingsActivity) {
this(buildDummyResolveInfo(packageName, className, label), false /* isAuxIme */,
settingsActivity, null /* subtypes */, 0 /* isDefaultResId */,
- false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */);
+ false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */,
+ false /* isVrOnly */);
}
/**
@@ -285,7 +296,7 @@ public final class InputMethodInfo implements Parcelable {
String settingsActivity, List<InputMethodSubtype> subtypes, int isDefaultResId,
boolean forceDefault) {
this(ri, isAuxIme, settingsActivity, subtypes, isDefaultResId, forceDefault,
- true /* supportsSwitchingToNextInputMethod */);
+ true /* supportsSwitchingToNextInputMethod */, false /* isVrOnly */);
}
/**
@@ -294,7 +305,7 @@ public final class InputMethodInfo implements Parcelable {
*/
public InputMethodInfo(ResolveInfo ri, boolean isAuxIme, String settingsActivity,
List<InputMethodSubtype> subtypes, int isDefaultResId, boolean forceDefault,
- boolean supportsSwitchingToNextInputMethod) {
+ boolean supportsSwitchingToNextInputMethod, boolean isVrOnly) {
final ServiceInfo si = ri.serviceInfo;
mService = ri;
mId = new ComponentName(si.packageName, si.name).flattenToShortString();
@@ -304,6 +315,7 @@ public final class InputMethodInfo implements Parcelable {
mSubtypes = new InputMethodSubtypeArray(subtypes);
mForceDefault = forceDefault;
mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod;
+ mIsVrOnly = isVrOnly;
}
private static ResolveInfo buildDummyResolveInfo(String packageName, String className,
@@ -398,6 +410,14 @@ public final class InputMethodInfo implements Parcelable {
}
/**
+ * Returns true if IME supports VR mode only.
+ * @hide
+ */
+ public boolean isVrOnly() {
+ return mIsVrOnly;
+ }
+
+ /**
* Return the count of the subtypes of Input Method.
*/
public int getSubtypeCount() {
@@ -444,6 +464,7 @@ public final class InputMethodInfo implements Parcelable {
public void dump(Printer pw, String prefix) {
pw.println(prefix + "mId=" + mId
+ " mSettingsActivityName=" + mSettingsActivityName
+ + " mIsVrOnly=" + mIsVrOnly
+ " mSupportsSwitchingToNextInputMethod=" + mSupportsSwitchingToNextInputMethod);
pw.println(prefix + "mIsDefaultResId=0x"
+ Integer.toHexString(mIsDefaultResId));
@@ -509,6 +530,7 @@ public final class InputMethodInfo implements Parcelable {
dest.writeInt(mIsDefaultResId);
dest.writeInt(mIsAuxIme ? 1 : 0);
dest.writeInt(mSupportsSwitchingToNextInputMethod ? 1 : 0);
+ dest.writeBoolean(mIsVrOnly);
mService.writeToParcel(dest, flags);
mSubtypes.writeToParcel(dest);
}
diff --git a/android/view/inputmethod/InputMethodManager.java b/android/view/inputmethod/InputMethodManager.java
index 92d1de8e..80d7b6b7 100644
--- a/android/view/inputmethod/InputMethodManager.java
+++ b/android/view/inputmethod/InputMethodManager.java
@@ -24,6 +24,7 @@ import android.annotation.RequiresPermission;
import android.annotation.SystemService;
import android.content.Context;
import android.graphics.Rect;
+import android.inputmethodservice.InputMethodService;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -280,10 +281,10 @@ public final class InputMethodManager {
boolean mActive = false;
/**
- * Set whenever this client becomes inactive, to know we need to reset
- * state with the IME the next time we receive focus.
+ * {@code true} if next {@link #onPostWindowFocus(View, View, int, boolean, int)} needs to
+ * restart input.
*/
- boolean mHasBeenInactive = true;
+ boolean mRestartOnNextWindowFocus = true;
/**
* As reported by IME through InputConnection.
@@ -488,7 +489,7 @@ public final class InputMethodManager {
// Some other client has starting using the IME, so note
// that this happened and make sure our own editor's
// state is reset.
- mHasBeenInactive = true;
+ mRestartOnNextWindowFocus = true;
try {
// Note that finishComposingText() is allowed to run
// even when we are not active.
@@ -499,7 +500,7 @@ public final class InputMethodManager {
// Check focus again in case that "onWindowFocus" is called before
// handling this message.
if (mServedView != null && mServedView.hasWindowFocus()) {
- if (checkFocusNoStartInput(mHasBeenInactive)) {
+ if (checkFocusNoStartInput(mRestartOnNextWindowFocus)) {
final int reason = active ?
InputMethodClient.START_INPUT_REASON_ACTIVATED_BY_IMMS :
InputMethodClient.START_INPUT_REASON_DEACTIVATED_BY_IMMS;
@@ -697,6 +698,19 @@ public final class InputMethodManager {
}
}
+ /**
+ * Returns a list of VR InputMethod currently installed.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.RESTRICTED_VR_ACCESS)
+ public List<InputMethodInfo> getVrInputMethodList() {
+ try {
+ return mService.getVrInputMethodList();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
public List<InputMethodInfo> getEnabledInputMethodList() {
try {
return mService.getEnabledInputMethodList();
@@ -722,7 +736,20 @@ public final class InputMethodManager {
}
}
+ /**
+ * @deprecated Use {@link InputMethodService#showStatusIcon(int)} instead. This method was
+ * intended for IME developers who should be accessing APIs through the service. APIs in this
+ * class are intended for app developers interacting with the IME.
+ */
+ @Deprecated
public void showStatusIcon(IBinder imeToken, String packageName, int iconId) {
+ showStatusIconInternal(imeToken, packageName, iconId);
+ }
+
+ /**
+ * @hide
+ */
+ public void showStatusIconInternal(IBinder imeToken, String packageName, int iconId) {
try {
mService.updateStatusIcon(imeToken, packageName, iconId);
} catch (RemoteException e) {
@@ -730,7 +757,20 @@ public final class InputMethodManager {
}
}
+ /**
+ * @deprecated Use {@link InputMethodService#hideStatusIcon()} instead. This method was
+ * intended for IME developers who should be accessing APIs through the service. APIs in
+ * this class are intended for app developers interacting with the IME.
+ */
+ @Deprecated
public void hideStatusIcon(IBinder imeToken) {
+ hideStatusIconInternal(imeToken);
+ }
+
+ /**
+ * @hide
+ */
+ public void hideStatusIconInternal(IBinder imeToken) {
try {
mService.updateStatusIcon(imeToken, null, 0);
} catch (RemoteException e) {
@@ -1108,7 +1148,6 @@ public final class InputMethodManager {
}
}
-
/**
* This method toggles the input method window display.
* If the input window is already displayed, it gets hidden.
@@ -1294,48 +1333,31 @@ public final class InputMethodManager {
+ Integer.toHexString(controlFlags));
final InputBindResult res = mService.startInputOrWindowGainedFocus(
startInputReason, mClient, windowGainingFocus, controlFlags, softInputMode,
- windowFlags, tba, servedContext, missingMethodFlags);
+ windowFlags, tba, servedContext, missingMethodFlags,
+ view.getContext().getApplicationInfo().targetSdkVersion);
if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res);
- if (res != null) {
- if (res.id != null) {
- setInputChannelLocked(res.channel);
- mBindSequence = res.sequence;
- mCurMethod = res.method;
- mCurId = res.id;
- mNextUserActionNotificationSequenceNumber =
- res.userActionNotificationSequenceNumber;
- } else {
- if (res.channel != null && res.channel != mCurChannel) {
- res.channel.dispose();
- }
- if (mCurMethod == null) {
- // This means there is no input method available.
- if (DEBUG) Log.v(TAG, "ABORT input: no input method!");
- return true;
- }
- }
- } else {
- if (startInputReason
- == InputMethodClient.START_INPUT_REASON_WINDOW_FOCUS_GAIN) {
- // We are here probably because of an obsolete window-focus-in message sent
- // to windowGainingFocus. Since IMMS determines whether a Window can have
- // IME focus or not by using the latest window focus state maintained in the
- // WMS, this kind of race condition cannot be avoided. One obvious example
- // would be that we have already received a window-focus-out message but the
- // UI thread is still handling previous window-focus-in message here.
- // TODO: InputBindResult should have the error code.
- if (DEBUG) Log.w(TAG, "startInputOrWindowGainedFocus failed. "
- + "Window focus may have already been lost. "
- + "win=" + windowGainingFocus + " view=" + dumpViewInfo(view));
- if (!mActive) {
- // mHasBeenInactive is a latch switch to forcefully refresh IME focus
- // state when an inactive (mActive == false) client is gaining window
- // focus. In case we have unnecessary disable the latch due to this
- // spurious wakeup, we re-enable the latch here.
- // TODO: Come up with more robust solution.
- mHasBeenInactive = true;
- }
- }
+ if (res == null) {
+ Log.wtf(TAG, "startInputOrWindowGainedFocus must not return"
+ + " null. startInputReason="
+ + InputMethodClient.getStartInputReason(startInputReason)
+ + " editorInfo=" + tba
+ + " controlFlags=#" + Integer.toHexString(controlFlags));
+ return false;
+ }
+ if (res.id != null) {
+ setInputChannelLocked(res.channel);
+ mBindSequence = res.sequence;
+ mCurMethod = res.method;
+ mCurId = res.id;
+ mNextUserActionNotificationSequenceNumber =
+ res.userActionNotificationSequenceNumber;
+ } else if (res.channel != null && res.channel != mCurChannel) {
+ res.channel.dispose();
+ }
+ switch (res.result) {
+ case InputBindResult.ResultCode.ERROR_NOT_IME_TARGET_WINDOW:
+ mRestartOnNextWindowFocus = true;
+ break;
}
if (mCurMethod != null && mCompletions != null) {
try {
@@ -1511,9 +1533,9 @@ public final class InputMethodManager {
+ " softInputMode=" + InputMethodClient.softInputModeToString(softInputMode)
+ " first=" + first + " flags=#"
+ Integer.toHexString(windowFlags));
- if (mHasBeenInactive) {
- if (DEBUG) Log.v(TAG, "Has been inactive! Starting fresh");
- mHasBeenInactive = false;
+ if (mRestartOnNextWindowFocus) {
+ if (DEBUG) Log.v(TAG, "Restarting due to mRestartOnNextWindowFocus");
+ mRestartOnNextWindowFocus = false;
forceNewFocus = true;
}
focusInLocked(focusedView != null ? focusedView : rootView);
@@ -1549,7 +1571,8 @@ public final class InputMethodManager {
mService.startInputOrWindowGainedFocus(
InputMethodClient.START_INPUT_REASON_WINDOW_FOCUS_GAIN_REPORT_ONLY, mClient,
rootView.getWindowToken(), controlFlags, softInputMode, windowFlags, null,
- null, 0 /* missingMethodFlags */);
+ null, 0 /* missingMethodFlags */,
+ rootView.getContext().getApplicationInfo().targetSdkVersion);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1787,8 +1810,19 @@ public final class InputMethodManager {
* when it was started, which allows it to perform this operation on
* itself.
* @param id The unique identifier for the new input method to be switched to.
+ * @deprecated Use {@link InputMethodService#setInputMethod(String)} instead. This method
+ * was intended for IME developers who should be accessing APIs through the service. APIs in
+ * this class are intended for app developers interacting with the IME.
*/
+ @Deprecated
public void setInputMethod(IBinder token, String id) {
+ setInputMethodInternal(token, id);
+ }
+
+ /**
+ * @hide
+ */
+ public void setInputMethodInternal(IBinder token, String id) {
try {
mService.setInputMethod(token, id);
} catch (RemoteException e) {
@@ -1804,8 +1838,21 @@ public final class InputMethodManager {
* itself.
* @param id The unique identifier for the new input method to be switched to.
* @param subtype The new subtype of the new input method to be switched to.
+ * @deprecated Use
+ * {@link InputMethodService#setInputMethodAndSubtype(String, InputMethodSubtype)}
+ * instead. This method was intended for IME developers who should be accessing APIs through
+ * the service. APIs in this class are intended for app developers interacting with the IME.
*/
+ @Deprecated
public void setInputMethodAndSubtype(IBinder token, String id, InputMethodSubtype subtype) {
+ setInputMethodAndSubtypeInternal(token, id, subtype);
+ }
+
+ /**
+ * @hide
+ */
+ public void setInputMethodAndSubtypeInternal(
+ IBinder token, String id, InputMethodSubtype subtype) {
try {
mService.setInputMethodAndSubtype(token, id, subtype);
} catch (RemoteException e) {
@@ -1824,8 +1871,19 @@ public final class InputMethodManager {
* @param flags Provides additional operating flags. Currently may be
* 0 or have the {@link #HIDE_IMPLICIT_ONLY},
* {@link #HIDE_NOT_ALWAYS} bit set.
+ * @deprecated Use {@link InputMethodService#hideSoftInputFromInputMethod(int)}
+ * instead. This method was intended for IME developers who should be accessing APIs through
+ * the service. APIs in this class are intended for app developers interacting with the IME.
*/
+ @Deprecated
public void hideSoftInputFromInputMethod(IBinder token, int flags) {
+ hideSoftInputFromInputMethodInternal(token, flags);
+ }
+
+ /**
+ * @hide
+ */
+ public void hideSoftInputFromInputMethodInternal(IBinder token, int flags) {
try {
mService.hideMySoftInput(token, flags);
} catch (RemoteException e) {
@@ -1845,8 +1903,19 @@ public final class InputMethodManager {
* @param flags Provides additional operating flags. Currently may be
* 0 or have the {@link #SHOW_IMPLICIT} or
* {@link #SHOW_FORCED} bit set.
+ * @deprecated Use {@link InputMethodService#showSoftInputFromInputMethod(int)}
+ * instead. This method was intended for IME developers who should be accessing APIs through
+ * the service. APIs in this class are intended for app developers interacting with the IME.
*/
+ @Deprecated
public void showSoftInputFromInputMethod(IBinder token, int flags) {
+ showSoftInputFromInputMethodInternal(token, flags);
+ }
+
+ /**
+ * @hide
+ */
+ public void showSoftInputFromInputMethodInternal(IBinder token, int flags) {
try {
mService.showMySoftInput(token, flags);
} catch (RemoteException e) {
@@ -2226,8 +2295,19 @@ public final class InputMethodManager {
* which allows it to perform this operation on itself.
* @return true if the current input method and subtype was successfully switched to the last
* used input method and subtype.
+ * @deprecated Use {@link InputMethodService#switchToLastInputMethod()} instead. This method
+ * was intended for IME developers who should be accessing APIs through the service. APIs in
+ * this class are intended for app developers interacting with the IME.
*/
+ @Deprecated
public boolean switchToLastInputMethod(IBinder imeToken) {
+ return switchToLastInputMethodInternal(imeToken);
+ }
+
+ /**
+ * @hide
+ */
+ public boolean switchToLastInputMethodInternal(IBinder imeToken) {
synchronized (mH) {
try {
return mService.switchToLastInputMethod(imeToken);
@@ -2246,8 +2326,19 @@ public final class InputMethodManager {
* belongs to the current IME
* @return true if the current input method and subtype was successfully switched to the next
* input method and subtype.
+ * @deprecated Use {@link InputMethodService#switchToNextInputMethod(boolean)} instead. This
+ * method was intended for IME developers who should be accessing APIs through the service.
+ * APIs in this class are intended for app developers interacting with the IME.
*/
+ @Deprecated
public boolean switchToNextInputMethod(IBinder imeToken, boolean onlyCurrentIme) {
+ return switchToNextInputMethodInternal(imeToken, onlyCurrentIme);
+ }
+
+ /**
+ * @hide
+ */
+ public boolean switchToNextInputMethodInternal(IBinder imeToken, boolean onlyCurrentIme) {
synchronized (mH) {
try {
return mService.switchToNextInputMethod(imeToken, onlyCurrentIme);
@@ -2267,8 +2358,19 @@ public final class InputMethodManager {
* between IMEs and subtypes.
* @param imeToken Supplies the identifying token given to an input method when it was started,
* which allows it to perform this operation on itself.
+ * @deprecated Use {@link InputMethodService#shouldOfferSwitchingToNextInputMethod()}
+ * instead. This method was intended for IME developers who should be accessing APIs through
+ * the service. APIs in this class are intended for app developers interacting with the IME.
*/
+ @Deprecated
public boolean shouldOfferSwitchingToNextInputMethod(IBinder imeToken) {
+ return shouldOfferSwitchingToNextInputMethodInternal(imeToken);
+ }
+
+ /**
+ * @hide
+ */
+ public boolean shouldOfferSwitchingToNextInputMethodInternal(IBinder imeToken) {
synchronized (mH) {
try {
return mService.shouldOfferSwitchingToNextInputMethod(imeToken);
@@ -2365,7 +2467,7 @@ public final class InputMethodManager {
p.println(" mMainLooper=" + mMainLooper);
p.println(" mIInputContext=" + mIInputContext);
p.println(" mActive=" + mActive
- + " mHasBeenInactive=" + mHasBeenInactive
+ + " mRestartOnNextWindowFocus=" + mRestartOnNextWindowFocus
+ " mBindSequence=" + mBindSequence
+ " mCurId=" + mCurId);
p.println(" mFullscreenMode=" + mFullscreenMode);
diff --git a/android/view/inputmethod/InputMethodManagerInternal.java b/android/view/inputmethod/InputMethodManagerInternal.java
index 77df4e38..e13813e5 100644
--- a/android/view/inputmethod/InputMethodManagerInternal.java
+++ b/android/view/inputmethod/InputMethodManagerInternal.java
@@ -16,6 +16,8 @@
package android.view.inputmethod;
+import android.content.ComponentName;
+
/**
* Input method manager local system service interface.
*
@@ -37,4 +39,9 @@ public interface InputMethodManagerInternal {
* Hides the current input method, if visible.
*/
void hideCurrentInputMethod();
+
+ /**
+ * Switches to VR InputMethod defined in the packageName of {@param componentName}.
+ */
+ void startVrInputMethodNoCheck(ComponentName componentName);
}
diff --git a/android/view/textclassifier/EntityConfidence.java b/android/view/textclassifier/EntityConfidence.java
index 0589d204..19660d95 100644
--- a/android/view/textclassifier/EntityConfidence.java
+++ b/android/view/textclassifier/EntityConfidence.java
@@ -18,13 +18,12 @@ package android.view.textclassifier;
import android.annotation.FloatRange;
import android.annotation.NonNull;
+import android.util.ArrayMap;
import com.android.internal.util.Preconditions;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -36,42 +35,43 @@ import java.util.Map;
*/
final class EntityConfidence<T> {
- private final Map<T, Float> mEntityConfidence = new HashMap<>();
-
- private final Comparator<T> mEntityComparator = (e1, e2) -> {
- float score1 = mEntityConfidence.get(e1);
- float score2 = mEntityConfidence.get(e2);
- if (score1 > score2) {
- return -1;
- }
- if (score1 < score2) {
- return 1;
- }
- return 0;
- };
+ private final ArrayMap<T, Float> mEntityConfidence = new ArrayMap<>();
+ private final ArrayList<T> mSortedEntities = new ArrayList<>();
EntityConfidence() {}
EntityConfidence(@NonNull EntityConfidence<T> source) {
Preconditions.checkNotNull(source);
mEntityConfidence.putAll(source.mEntityConfidence);
+ mSortedEntities.addAll(source.mSortedEntities);
}
/**
- * Sets an entity type for the classified text and assigns a confidence score.
+ * Constructs an EntityConfidence from a map of entity to confidence.
*
- * @param confidenceScore a value from 0 (low confidence) to 1 (high confidence).
- * 0 implies the entity does not exist for the classified text.
- * Values greater than 1 are clamped to 1.
+ * Map entries that have 0 confidence are removed, and values greater than 1 are clamped to 1.
+ *
+ * @param source a map from entity to a confidence value in the range 0 (low confidence) to
+ * 1 (high confidence).
*/
- public void setEntityType(
- @NonNull T type, @FloatRange(from = 0.0, to = 1.0) float confidenceScore) {
- Preconditions.checkNotNull(type);
- if (confidenceScore > 0) {
- mEntityConfidence.put(type, Math.min(1, confidenceScore));
- } else {
- mEntityConfidence.remove(type);
+ EntityConfidence(@NonNull Map<T, Float> source) {
+ Preconditions.checkNotNull(source);
+
+ // Prune non-existent entities and clamp to 1.
+ mEntityConfidence.ensureCapacity(source.size());
+ for (Map.Entry<T, Float> it : source.entrySet()) {
+ if (it.getValue() <= 0) continue;
+ mEntityConfidence.put(it.getKey(), Math.min(1, it.getValue()));
}
+
+ // Create a list of entities sorted by decreasing confidence for getEntities().
+ mSortedEntities.ensureCapacity(mEntityConfidence.size());
+ mSortedEntities.addAll(mEntityConfidence.keySet());
+ mSortedEntities.sort((e1, e2) -> {
+ float score1 = mEntityConfidence.get(e1);
+ float score2 = mEntityConfidence.get(e2);
+ return Float.compare(score2, score1);
+ });
}
/**
@@ -80,10 +80,7 @@ final class EntityConfidence<T> {
*/
@NonNull
public List<T> getEntities() {
- List<T> entities = new ArrayList<>(mEntityConfidence.size());
- entities.addAll(mEntityConfidence.keySet());
- entities.sort(mEntityComparator);
- return Collections.unmodifiableList(entities);
+ return Collections.unmodifiableList(mSortedEntities);
}
/**
diff --git a/android/view/textclassifier/TextClassification.java b/android/view/textclassifier/TextClassification.java
index f675c355..7ffbf635 100644
--- a/android/view/textclassifier/TextClassification.java
+++ b/android/view/textclassifier/TextClassification.java
@@ -24,6 +24,7 @@ import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.LocaleList;
+import android.util.ArrayMap;
import android.view.View.OnClickListener;
import android.view.textclassifier.TextClassifier.EntityType;
@@ -32,12 +33,14 @@ import com.android.internal.util.Preconditions;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
/**
* Information for generating a widget to handle classified text.
*
* <p>A TextClassification object contains icons, labels, onClickListeners and intents that may
- * be used to build a widget that can be used to act on classified text.
+ * be used to build a widget that can be used to act on classified text. There is the concept of a
+ * <i>primary action</i> and other <i>secondary actions</i>.
*
* <p>e.g. building a view that, when clicked, shares the classified text with the preferred app:
*
@@ -62,11 +65,18 @@ import java.util.Locale;
* view.startActionMode(new ActionMode.Callback() {
*
* public boolean onCreateActionMode(ActionMode mode, Menu menu) {
- * for (int i = 0; i < classification.getActionCount(); i++) {
- * if (thisAppHasPermissionToInvokeIntent(classification.getIntent(i))) {
- * menu.add(Menu.NONE, i, 20, classification.getLabel(i))
- * .setIcon(classification.getIcon(i))
- * .setIntent(classification.getIntent(i));
+ * // Add the "primary" action.
+ * if (thisAppHasPermissionToInvokeIntent(classification.getIntent())) {
+ * menu.add(Menu.NONE, 0, 20, classification.getLabel())
+ * .setIcon(classification.getIcon())
+ * .setIntent(classification.getIntent());
+ * }
+ * // Add the "secondary" actions.
+ * for (int i = 0; i < classification.getSecondaryActionsCount(); i++) {
+ * if (thisAppHasPermissionToInvokeIntent(classification.getSecondaryIntent(i))) {
+ * menu.add(Menu.NONE, i + 1, 20, classification.getSecondaryLabel(i))
+ * .setIcon(classification.getSecondaryIcon(i))
+ * .setIntent(classification.getSecondaryIntent(i));
* }
* }
* return true;
@@ -90,36 +100,43 @@ public final class TextClassification {
static final TextClassification EMPTY = new TextClassification.Builder().build();
@NonNull private final String mText;
- @NonNull private final List<Drawable> mIcons;
- @NonNull private final List<String> mLabels;
- @NonNull private final List<Intent> mIntents;
- @NonNull private final List<OnClickListener> mOnClickListeners;
+ @Nullable private final Drawable mPrimaryIcon;
+ @Nullable private final String mPrimaryLabel;
+ @Nullable private final Intent mPrimaryIntent;
+ @Nullable private final OnClickListener mPrimaryOnClickListener;
+ @NonNull private final List<Drawable> mSecondaryIcons;
+ @NonNull private final List<String> mSecondaryLabels;
+ @NonNull private final List<Intent> mSecondaryIntents;
+ @NonNull private final List<OnClickListener> mSecondaryOnClickListeners;
@NonNull private final EntityConfidence<String> mEntityConfidence;
- @NonNull private final List<String> mEntities;
- private int mLogType;
- @NonNull private final String mVersionInfo;
+ @NonNull private final String mSignature;
private TextClassification(
@Nullable String text,
- @NonNull List<Drawable> icons,
- @NonNull List<String> labels,
- @NonNull List<Intent> intents,
- @NonNull List<OnClickListener> onClickListeners,
- @NonNull EntityConfidence<String> entityConfidence,
- int logType,
- @NonNull String versionInfo) {
- Preconditions.checkArgument(labels.size() == intents.size());
- Preconditions.checkArgument(icons.size() == intents.size());
- Preconditions.checkArgument(onClickListeners.size() == intents.size());
+ @Nullable Drawable primaryIcon,
+ @Nullable String primaryLabel,
+ @Nullable Intent primaryIntent,
+ @Nullable OnClickListener primaryOnClickListener,
+ @NonNull List<Drawable> secondaryIcons,
+ @NonNull List<String> secondaryLabels,
+ @NonNull List<Intent> secondaryIntents,
+ @NonNull List<OnClickListener> secondaryOnClickListeners,
+ @NonNull Map<String, Float> entityConfidence,
+ @NonNull String signature) {
+ Preconditions.checkArgument(secondaryLabels.size() == secondaryIntents.size());
+ Preconditions.checkArgument(secondaryIcons.size() == secondaryIntents.size());
+ Preconditions.checkArgument(secondaryOnClickListeners.size() == secondaryIntents.size());
mText = text;
- mIcons = icons;
- mLabels = labels;
- mIntents = intents;
- mOnClickListeners = onClickListeners;
+ mPrimaryIcon = primaryIcon;
+ mPrimaryLabel = primaryLabel;
+ mPrimaryIntent = primaryIntent;
+ mPrimaryOnClickListener = primaryOnClickListener;
+ mSecondaryIcons = secondaryIcons;
+ mSecondaryLabels = secondaryLabels;
+ mSecondaryIntents = secondaryIntents;
+ mSecondaryOnClickListeners = secondaryOnClickListeners;
mEntityConfidence = new EntityConfidence<>(entityConfidence);
- mEntities = mEntityConfidence.getEntities();
- mLogType = logType;
- mVersionInfo = versionInfo;
+ mSignature = signature;
}
/**
@@ -135,7 +152,7 @@ public final class TextClassification {
*/
@IntRange(from = 0)
public int getEntityCount() {
- return mEntities.size();
+ return mEntityConfidence.getEntities().size();
}
/**
@@ -147,7 +164,7 @@ public final class TextClassification {
*/
@NonNull
public @EntityType String getEntity(int index) {
- return mEntities.get(index);
+ return mEntityConfidence.getEntities().get(index);
}
/**
@@ -161,130 +178,160 @@ public final class TextClassification {
}
/**
- * Returns the number of actions that are available to act on the classified text.
- * @see #getIntent(int)
- * @see #getLabel(int)
- * @see #getIcon(int)
- * @see #getOnClickListener(int)
+ * Returns the number of <i>secondary</i> actions that are available to act on the classified
+ * text.
+ *
+ * <p><strong>Note: </strong> that there may or may not be a <i>primary</i> action.
+ *
+ * @see #getSecondaryIntent(int)
+ * @see #getSecondaryLabel(int)
+ * @see #getSecondaryIcon(int)
+ * @see #getSecondaryOnClickListener(int)
*/
@IntRange(from = 0)
- public int getActionCount() {
- return mIntents.size();
+ public int getSecondaryActionsCount() {
+ return mSecondaryIntents.size();
}
/**
- * Returns one of the icons that maybe rendered on a widget used to act on the classified text.
+ * Returns one of the <i>secondary</i> icons that maybe rendered on a widget used to act on the
+ * classified text.
+ *
* @param index Index of the action to get the icon for.
+ *
* @throws IndexOutOfBoundsException if the specified index is out of range.
- * @see #getActionCount() for the number of entities available.
- * @see #getIntent(int)
- * @see #getLabel(int)
- * @see #getOnClickListener(int)
+ *
+ * @see #getSecondaryActionsCount() for the number of actions available.
+ * @see #getSecondaryIntent(int)
+ * @see #getSecondaryLabel(int)
+ * @see #getSecondaryOnClickListener(int)
+ * @see #getIcon()
*/
@Nullable
- public Drawable getIcon(int index) {
- return mIcons.get(index);
+ public Drawable getSecondaryIcon(int index) {
+ return mSecondaryIcons.get(index);
}
/**
- * Returns an icon for the default intent that may be rendered on a widget used to act on the
- * classified text.
+ * Returns an icon for the <i>primary</i> intent that may be rendered on a widget used to act
+ * on the classified text.
+ *
+ * @see #getSecondaryIcon(int)
*/
@Nullable
public Drawable getIcon() {
- return mIcons.isEmpty() ? null : mIcons.get(0);
+ return mPrimaryIcon;
}
/**
- * Returns one of the labels that may be rendered on a widget used to act on the classified
- * text.
+ * Returns one of the <i>secondary</i> labels that may be rendered on a widget used to act on
+ * the classified text.
+ *
* @param index Index of the action to get the label for.
+ *
* @throws IndexOutOfBoundsException if the specified index is out of range.
- * @see #getActionCount()
- * @see #getIntent(int)
- * @see #getIcon(int)
- * @see #getOnClickListener(int)
+ *
+ * @see #getSecondaryActionsCount()
+ * @see #getSecondaryIntent(int)
+ * @see #getSecondaryIcon(int)
+ * @see #getSecondaryOnClickListener(int)
+ * @see #getLabel()
*/
@Nullable
- public CharSequence getLabel(int index) {
- return mLabels.get(index);
+ public CharSequence getSecondaryLabel(int index) {
+ return mSecondaryLabels.get(index);
}
/**
- * Returns a label for the default intent that may be rendered on a widget used to act on the
- * classified text.
+ * Returns a label for the <i>primary</i> intent that may be rendered on a widget used to act
+ * on the classified text.
+ *
+ * @see #getSecondaryLabel(int)
*/
@Nullable
public CharSequence getLabel() {
- return mLabels.isEmpty() ? null : mLabels.get(0);
+ return mPrimaryLabel;
}
/**
- * Returns one of the intents that may be fired to act on the classified text.
+ * Returns one of the <i>secondary</i> intents that may be fired to act on the classified text.
+ *
* @param index Index of the action to get the intent for.
+ *
* @throws IndexOutOfBoundsException if the specified index is out of range.
- * @see #getActionCount()
- * @see #getLabel(int)
- * @see #getIcon(int)
- * @see #getOnClickListener(int)
+ *
+ * @see #getSecondaryActionsCount()
+ * @see #getSecondaryLabel(int)
+ * @see #getSecondaryIcon(int)
+ * @see #getSecondaryOnClickListener(int)
+ * @see #getIntent()
*/
@Nullable
- public Intent getIntent(int index) {
- return mIntents.get(index);
+ public Intent getSecondaryIntent(int index) {
+ return mSecondaryIntents.get(index);
}
/**
- * Returns the default intent that may be fired to act on the classified text.
+ * Returns the <i>primary</i> intent that may be fired to act on the classified text.
+ *
+ * @see #getSecondaryIntent(int)
*/
@Nullable
public Intent getIntent() {
- return mIntents.isEmpty() ? null : mIntents.get(0);
+ return mPrimaryIntent;
}
/**
- * Returns one of the OnClickListeners that may be triggered to act on the classified text.
+ * Returns one of the <i>secondary</i> OnClickListeners that may be triggered to act on the
+ * classified text.
+ *
* @param index Index of the action to get the click listener for.
+ *
* @throws IndexOutOfBoundsException if the specified index is out of range.
- * @see #getActionCount()
- * @see #getIntent(int)
- * @see #getLabel(int)
- * @see #getIcon(int)
+ *
+ * @see #getSecondaryActionsCount()
+ * @see #getSecondaryIntent(int)
+ * @see #getSecondaryLabel(int)
+ * @see #getSecondaryIcon(int)
+ * @see #getOnClickListener()
*/
@Nullable
- public OnClickListener getOnClickListener(int index) {
- return mOnClickListeners.get(index);
+ public OnClickListener getSecondaryOnClickListener(int index) {
+ return mSecondaryOnClickListeners.get(index);
}
/**
- * Returns the default OnClickListener that may be triggered to act on the classified text.
+ * Returns the <i>primary</i> OnClickListener that may be triggered to act on the classified
+ * text.
+ *
+ * @see #getSecondaryOnClickListener(int)
*/
@Nullable
public OnClickListener getOnClickListener() {
- return mOnClickListeners.isEmpty() ? null : mOnClickListeners.get(0);
+ return mPrimaryOnClickListener;
}
/**
- * Returns the MetricsLogger subtype for the action that is performed for this result.
- * @hide
- */
- public int getLogType() {
- return mLogType;
- }
-
- /**
- * Returns information about the classifier model used to generate this TextClassification.
- * @hide
+ * Returns the signature for this object.
+ * The TextClassifier that generates this object may use it as a way to internally identify
+ * this object.
*/
@NonNull
- public String getVersionInfo() {
- return mVersionInfo;
+ public String getSignature() {
+ return mSignature;
}
@Override
public String toString() {
- return String.format(Locale.US,
- "TextClassification {text=%s, entities=%s, labels=%s, intents=%s}",
- mText, mEntityConfidence, mLabels, mIntents);
+ return String.format(Locale.US, "TextClassification {"
+ + "text=%s, entities=%s, "
+ + "primaryLabel=%s, secondaryLabels=%s, "
+ + "primaryIntent=%s, secondaryIntents=%s, "
+ + "signature=%s}",
+ mText, mEntityConfidence,
+ mPrimaryLabel, mSecondaryLabels,
+ mPrimaryIntent, mSecondaryIntents,
+ mSignature);
}
/**
@@ -303,18 +350,33 @@ public final class TextClassification {
/**
* Builder for building {@link TextClassification} objects.
+ *
+ * <p>e.g.
+ *
+ * <pre>{@code
+ * TextClassification classification = new TextClassification.Builder()
+ * .setText(classifiedText)
+ * .setEntityType(TextClassifier.TYPE_EMAIL, 0.9)
+ * .setEntityType(TextClassifier.TYPE_OTHER, 0.1)
+ * .setPrimaryAction(intent, label, icon, onClickListener)
+ * .addSecondaryAction(intent1, label1, icon1, onClickListener1)
+ * .addSecondaryAction(intent2, label2, icon2, onClickListener2)
+ * .build();
+ * }</pre>
*/
public static final class Builder {
@NonNull private String mText;
- @NonNull private final List<Drawable> mIcons = new ArrayList<>();
- @NonNull private final List<String> mLabels = new ArrayList<>();
- @NonNull private final List<Intent> mIntents = new ArrayList<>();
- @NonNull private final List<OnClickListener> mOnClickListeners = new ArrayList<>();
- @NonNull private final EntityConfidence<String> mEntityConfidence =
- new EntityConfidence<>();
- private int mLogType;
- @NonNull private String mVersionInfo = "";
+ @NonNull private final List<Drawable> mSecondaryIcons = new ArrayList<>();
+ @NonNull private final List<String> mSecondaryLabels = new ArrayList<>();
+ @NonNull private final List<Intent> mSecondaryIntents = new ArrayList<>();
+ @NonNull private final List<OnClickListener> mSecondaryOnClickListeners = new ArrayList<>();
+ @NonNull private final Map<String, Float> mEntityConfidence = new ArrayMap<>();
+ @Nullable Drawable mPrimaryIcon;
+ @Nullable String mPrimaryLabel;
+ @Nullable Intent mPrimaryIntent;
+ @Nullable OnClickListener mPrimaryOnClickListener;
+ @NonNull private String mSignature = "";
/**
* Sets the classified text.
@@ -326,6 +388,8 @@ public final class TextClassification {
/**
* Sets an entity type for the classification result and assigns a confidence score.
+ * If a confidence score had already been set for the specified entity type, this will
+ * override that score.
*
* @param confidenceScore a value from 0 (low confidence) to 1 (high confidence).
* 0 implies the entity does not exist for the classified text.
@@ -334,110 +398,128 @@ public final class TextClassification {
public Builder setEntityType(
@NonNull @EntityType String type,
@FloatRange(from = 0.0, to = 1.0) float confidenceScore) {
- mEntityConfidence.setEntityType(type, confidenceScore);
+ mEntityConfidence.put(type, confidenceScore);
return this;
}
/**
- * Adds an action that may be performed on the classified text. The label and icon are used
- * for rendering of widgets that offer the intent. Actions should be added in order of
- * priority and the first one will be treated as the default.
+ * Adds an <i>secondary</i> action that may be performed on the classified text.
+ * Secondary actions are in addition to the <i>primary</i> action which may or may not
+ * exist.
+ *
+ * <p>The label and icon are used for rendering of widgets that offer the intent.
+ * Actions should be added in order of priority.
+ *
+ * <p><stong>Note: </stong> If all input parameters are set to null, this method will be a
+ * no-op.
+ *
+ * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener)
*/
- public Builder addAction(
- Intent intent, @Nullable String label, @Nullable Drawable icon,
+ public Builder addSecondaryAction(
+ @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon,
@Nullable OnClickListener onClickListener) {
- mIntents.add(intent);
- mLabels.add(label);
- mIcons.add(icon);
- mOnClickListeners.add(onClickListener);
+ if (intent != null || label != null || icon != null || onClickListener != null) {
+ mSecondaryIntents.add(intent);
+ mSecondaryLabels.add(label);
+ mSecondaryIcons.add(icon);
+ mSecondaryOnClickListeners.add(onClickListener);
+ }
return this;
}
/**
- * Removes all actions.
+ * Removes all the <i>secondary</i> actions.
*/
- public Builder clearActions() {
- mIntents.clear();
- mOnClickListeners.clear();
- mLabels.clear();
- mIcons.clear();
+ public Builder clearSecondaryActions() {
+ mSecondaryIntents.clear();
+ mSecondaryOnClickListeners.clear();
+ mSecondaryLabels.clear();
+ mSecondaryIcons.clear();
return this;
}
/**
- * Sets the icon for the default action that may be rendered on a widget used to act on the
- * classified text.
+ * Sets the <i>primary</i> action that may be performed on the classified text. This is
+ * equivalent to calling {@code
+ * setIntent(intent).setLabel(label).setIcon(icon).setOnClickListener(onClickListener)}.
+ *
+ * <p><strong>Note: </strong>If all input parameters are null, there will be no
+ * <i>primary</i> action but there may still be <i>secondary</i> actions.
+ *
+ * @see #addSecondaryAction(Intent, String, Drawable, OnClickListener)
*/
- public Builder setIcon(@Nullable Drawable icon) {
- ensureDefaultActionAvailable();
- mIcons.set(0, icon);
- return this;
+ public Builder setPrimaryAction(
+ @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon,
+ @Nullable OnClickListener onClickListener) {
+ return setIntent(intent).setLabel(label).setIcon(icon)
+ .setOnClickListener(onClickListener);
}
/**
- * Sets the label for the default action that may be rendered on a widget used to act on the
- * classified text.
+ * Sets the icon for the <i>primary</i> action that may be rendered on a widget used to act
+ * on the classified text.
+ *
+ * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener)
*/
- public Builder setLabel(@Nullable String label) {
- ensureDefaultActionAvailable();
- mLabels.set(0, label);
+ public Builder setIcon(@Nullable Drawable icon) {
+ mPrimaryIcon = icon;
return this;
}
/**
- * Sets the intent for the default action that may be fired to act on the classified text.
+ * Sets the label for the <i>primary</i> action that may be rendered on a widget used to
+ * act on the classified text.
+ *
+ * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener)
*/
- public Builder setIntent(@Nullable Intent intent) {
- ensureDefaultActionAvailable();
- mIntents.set(0, intent);
+ public Builder setLabel(@Nullable String label) {
+ mPrimaryLabel = label;
return this;
}
/**
- * Sets the MetricsLogger subtype for the action that is performed for this result.
- * @hide
+ * Sets the intent for the <i>primary</i> action that may be fired to act on the classified
+ * text.
+ *
+ * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener)
*/
- public Builder setLogType(int type) {
- mLogType = type;
+ public Builder setIntent(@Nullable Intent intent) {
+ mPrimaryIntent = intent;
return this;
}
/**
- * Sets the OnClickListener for the default action that may be triggered to act on the
- * classified text.
+ * Sets the OnClickListener for the <i>primary</i> action that may be triggered to act on
+ * the classified text.
+ *
+ * @see #setPrimaryAction(Intent, String, Drawable, OnClickListener)
*/
public Builder setOnClickListener(@Nullable OnClickListener onClickListener) {
- ensureDefaultActionAvailable();
- mOnClickListeners.set(0, onClickListener);
+ mPrimaryOnClickListener = onClickListener;
return this;
}
/**
- * Sets information about the classifier model used to generate this TextClassification.
- * @hide
+ * Sets a signature for the TextClassification object.
+ * The TextClassifier that generates the TextClassification object may use it as a way to
+ * internally identify the TextClassification object.
*/
- Builder setVersionInfo(@NonNull String versionInfo) {
- mVersionInfo = Preconditions.checkNotNull(versionInfo);
+ public Builder setSignature(@NonNull String signature) {
+ mSignature = Preconditions.checkNotNull(signature);
return this;
}
/**
- * Ensures that we have storage for the default action.
- */
- private void ensureDefaultActionAvailable() {
- if (mIntents.isEmpty()) mIntents.add(null);
- if (mLabels.isEmpty()) mLabels.add(null);
- if (mIcons.isEmpty()) mIcons.add(null);
- if (mOnClickListeners.isEmpty()) mOnClickListeners.add(null);
- }
-
- /**
* Builds and returns a {@link TextClassification} object.
*/
public TextClassification build() {
return new TextClassification(
- mText, mIcons, mLabels, mIntents, mOnClickListeners, mEntityConfidence,
- mLogType, mVersionInfo);
+ mText,
+ mPrimaryIcon, mPrimaryLabel,
+ mPrimaryIntent, mPrimaryOnClickListener,
+ mSecondaryIcons, mSecondaryLabels,
+ mSecondaryIntents, mSecondaryOnClickListeners,
+ mEntityConfidence, mSignature);
}
}
diff --git a/android/view/textclassifier/TextClassifier.java b/android/view/textclassifier/TextClassifier.java
index 5aaa5ad1..ed604303 100644
--- a/android/view/textclassifier/TextClassifier.java
+++ b/android/view/textclassifier/TextClassifier.java
@@ -16,17 +16,23 @@
package android.view.textclassifier;
+import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringDef;
import android.annotation.WorkerThread;
import android.os.LocaleList;
+import android.util.ArraySet;
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
/**
* Interface for providing text classification related features.
@@ -37,7 +43,7 @@ import java.lang.annotation.RetentionPolicy;
public interface TextClassifier {
/** @hide */
- String DEFAULT_LOG_TAG = "TextClassifierImpl";
+ String DEFAULT_LOG_TAG = "androidtc";
String TYPE_UNKNOWN = "";
String TYPE_OTHER = "other";
@@ -48,11 +54,30 @@ public interface TextClassifier {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @StringDef({
- TYPE_UNKNOWN, TYPE_OTHER, TYPE_EMAIL, TYPE_PHONE, TYPE_ADDRESS, TYPE_URL
+ @StringDef(prefix = { "TYPE_" }, value = {
+ TYPE_UNKNOWN,
+ TYPE_OTHER,
+ TYPE_EMAIL,
+ TYPE_PHONE,
+ TYPE_ADDRESS,
+ TYPE_URL,
})
@interface EntityType {}
+ /** Designates that the TextClassifier should identify all entity types it can. **/
+ int ENTITY_PRESET_ALL = 0;
+ /** Designates that the TextClassifier should identify no entities. **/
+ int ENTITY_PRESET_NONE = 1;
+ /** Designates that the TextClassifier should identify a base set of entities determined by the
+ * TextClassifier. **/
+ int ENTITY_PRESET_BASE = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "ENTITY_CONFIG_" },
+ value = {ENTITY_PRESET_ALL, ENTITY_PRESET_NONE, ENTITY_PRESET_BASE})
+ @interface EntityPreset {}
+
/**
* No-op TextClassifier.
* This may be used to turn off TextClassifier features.
@@ -212,6 +237,8 @@ public interface TextClassifier {
* Returns a {@link TextLinks} that may be applied to the text to annotate it with links
* information.
*
+ * If no options are supplied, default values will be used, determined by the TextClassifier.
+ *
* @param text the text to generate annotations for
* @param options configuration for link generation
*
@@ -246,6 +273,16 @@ public interface TextClassifier {
}
/**
+ * Returns a {@link Collection} of the entity types in the specified preset.
+ *
+ * @see #ENTITIES_ALL
+ * @see #ENTITIES_NONE
+ */
+ default Collection<String> getEntitiesForPreset(@EntityPreset int entityPreset) {
+ return Collections.EMPTY_LIST;
+ }
+
+ /**
* Logs a TextClassifier event.
*
* @param source the text classifier used to generate this event
@@ -263,6 +300,62 @@ public interface TextClassifier {
return TextClassifierConstants.DEFAULT;
}
+ /**
+ * Configuration object for specifying what entities to identify.
+ *
+ * Configs are initially based on a predefined preset, and can be modified from there.
+ */
+ final class EntityConfig {
+ private final @TextClassifier.EntityPreset int mEntityPreset;
+ private final Collection<String> mExcludedEntityTypes;
+ private final Collection<String> mIncludedEntityTypes;
+
+ public EntityConfig(@TextClassifier.EntityPreset int mEntityPreset) {
+ this.mEntityPreset = mEntityPreset;
+ mExcludedEntityTypes = new ArraySet<>();
+ mIncludedEntityTypes = new ArraySet<>();
+ }
+
+ /**
+ * Specifies an entity to include in addition to any specified by the enity preset.
+ *
+ * Note that if an entity has been excluded, the exclusion will take precedence.
+ */
+ public EntityConfig includeEntities(String... entities) {
+ for (String entity : entities) {
+ mIncludedEntityTypes.add(entity);
+ }
+ return this;
+ }
+
+ /**
+ * Specifies an entity to be excluded.
+ */
+ public EntityConfig excludeEntities(String... entities) {
+ for (String entity : entities) {
+ mExcludedEntityTypes.add(entity);
+ }
+ return this;
+ }
+
+ /**
+ * Returns an unmodifiable list of the final set of entities to find.
+ */
+ public List<String> getEntities(TextClassifier textClassifier) {
+ ArrayList<String> entities = new ArrayList<>();
+ for (String entity : textClassifier.getEntitiesForPreset(mEntityPreset)) {
+ if (!mExcludedEntityTypes.contains(entity)) {
+ entities.add(entity);
+ }
+ }
+ for (String entity : mIncludedEntityTypes) {
+ if (!mExcludedEntityTypes.contains(entity) && !entities.contains(entity)) {
+ entities.add(entity);
+ }
+ }
+ return Collections.unmodifiableList(entities);
+ }
+ }
/**
* Utility functions for TextClassifier methods.
diff --git a/android/view/textclassifier/TextClassifierImpl.java b/android/view/textclassifier/TextClassifierImpl.java
index df5e35f0..aea3cb06 100644
--- a/android/view/textclassifier/TextClassifierImpl.java
+++ b/android/view/textclassifier/TextClassifierImpl.java
@@ -32,7 +32,7 @@ import android.provider.ContactsContract;
import android.provider.Settings;
import android.text.util.Linkify;
import android.util.Patterns;
-import android.widget.TextViewMetrics;
+import android.view.View.OnClickListener;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.logging.MetricsLogger;
@@ -42,6 +42,9 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -66,6 +69,18 @@ final class TextClassifierImpl implements TextClassifier {
private static final String MODEL_FILE_REGEX = "textclassifier\\.smartselection\\.(.*)\\.model";
private static final String UPDATED_MODEL_FILE_PATH =
"/data/misc/textclassifier/textclassifier.smartselection.model";
+ private static final List<String> ENTITY_TYPES_ALL =
+ Collections.unmodifiableList(Arrays.asList(
+ TextClassifier.TYPE_ADDRESS,
+ TextClassifier.TYPE_EMAIL,
+ TextClassifier.TYPE_PHONE,
+ TextClassifier.TYPE_URL));
+ private static final List<String> ENTITY_TYPES_BASE =
+ Collections.unmodifiableList(Arrays.asList(
+ TextClassifier.TYPE_ADDRESS,
+ TextClassifier.TYPE_EMAIL,
+ TextClassifier.TYPE_PHONE,
+ TextClassifier.TYPE_URL));
private final Context mContext;
@@ -121,8 +136,8 @@ final class TextClassifierImpl implements TextClassifier {
tsBuilder.setEntityType(results[i].mCollection, results[i].mScore);
}
return tsBuilder
- .setLogSource(LOG_TAG)
- .setVersionInfo(getVersionInfo())
+ .setSignature(
+ getSignature(string, selectionStartIndex, selectionEndIndex))
.build();
} else {
// We can not trust the result. Log the issue and ignore the result.
@@ -154,8 +169,7 @@ final class TextClassifierImpl implements TextClassifier {
getHintFlags(string, startIndex, endIndex));
if (results.length > 0) {
final TextClassification classificationResult =
- createClassificationResult(
- results, string.subSequence(startIndex, endIndex));
+ createClassificationResult(results, string, startIndex, endIndex);
return classificationResult;
}
}
@@ -169,17 +183,23 @@ final class TextClassifierImpl implements TextClassifier {
@Override
public TextLinks generateLinks(
- @NonNull CharSequence text, @NonNull TextLinks.Options options) {
+ @NonNull CharSequence text, @Nullable TextLinks.Options options) {
Utils.validateInput(text);
final String textString = text.toString();
final TextLinks.Builder builder = new TextLinks.Builder(textString);
try {
- LocaleList defaultLocales = options != null ? options.getDefaultLocales() : null;
+ final LocaleList defaultLocales = options != null ? options.getDefaultLocales() : null;
+ final Collection<String> entitiesToIdentify =
+ options != null && options.getEntityConfig() != null
+ ? options.getEntityConfig().getEntities(this) : ENTITY_TYPES_ALL;
final SmartSelection smartSelection = getSmartSelection(defaultLocales);
final SmartSelection.AnnotatedSpan[] annotations = smartSelection.annotate(textString);
for (SmartSelection.AnnotatedSpan span : annotations) {
- final Map<String, Float> entityScores = new HashMap<>();
final SmartSelection.ClassificationResult[] results = span.getClassification();
+ if (results.length == 0 || !entitiesToIdentify.contains(results[0].mCollection)) {
+ continue;
+ }
+ final Map<String, Float> entityScores = new HashMap<>();
for (int i = 0; i < results.length; i++) {
entityScores.put(results[i].mCollection, results[i].mScore);
}
@@ -194,6 +214,20 @@ final class TextClassifierImpl implements TextClassifier {
}
@Override
+ public Collection<String> getEntitiesForPreset(@TextClassifier.EntityPreset int entityPreset) {
+ switch (entityPreset) {
+ case TextClassifier.ENTITY_PRESET_NONE:
+ return Collections.emptyList();
+ case TextClassifier.ENTITY_PRESET_BASE:
+ return ENTITY_TYPES_BASE;
+ case TextClassifier.ENTITY_PRESET_ALL:
+ // fall through
+ default:
+ return ENTITY_TYPES_ALL;
+ }
+ }
+
+ @Override
public void logEvent(String source, String event) {
if (LOG_TAG.equals(source)) {
mMetricsLogger.count(event, 1);
@@ -229,13 +263,13 @@ final class TextClassifierImpl implements TextClassifier {
}
}
- @NonNull
- private String getVersionInfo() {
+ private String getSignature(String text, int start, int end) {
synchronized (mSmartSelectionLock) {
- if (mLocale != null) {
- return String.format("%s_v%d", mLocale.toLanguageTag(), mVersion);
- }
- return "";
+ final String versionInfo = (mLocale != null)
+ ? String.format(Locale.US, "%s_v%d", mLocale.toLanguageTag(), mVersion)
+ : "";
+ final int hash = Objects.hash(text, start, end, mContext.getPackageName());
+ return String.format(Locale.US, "%s|%s|%d", LOG_TAG, versionInfo, hash);
}
}
@@ -371,9 +405,11 @@ final class TextClassifierImpl implements TextClassifier {
}
private TextClassification createClassificationResult(
- SmartSelection.ClassificationResult[] classifications, CharSequence text) {
+ SmartSelection.ClassificationResult[] classifications,
+ String text, int start, int end) {
+ final String classifiedText = text.substring(start, end);
final TextClassification.Builder builder = new TextClassification.Builder()
- .setText(text.toString());
+ .setText(classifiedText);
final int size = classifications.length;
for (int i = 0; i < size; i++) {
@@ -381,50 +417,54 @@ final class TextClassifierImpl implements TextClassifier {
}
final String type = getHighestScoringType(classifications);
- builder.setLogType(IntentFactory.getLogType(type));
-
- final List<Intent> intents = IntentFactory.create(mContext, type, text.toString());
- for (Intent intent : intents) {
- extendClassificationWithIntent(intent, builder);
- }
+ addActions(builder, IntentFactory.create(mContext, type, classifiedText));
- return builder.setVersionInfo(getVersionInfo()).build();
+ return builder.setSignature(getSignature(text, start, end)).build();
}
- /** Extends the classification with the intent if it can be resolved. */
- private void extendClassificationWithIntent(Intent intent, TextClassification.Builder builder) {
- final PackageManager pm;
- final ResolveInfo resolveInfo;
- if (intent != null) {
- pm = mContext.getPackageManager();
- resolveInfo = pm.resolveActivity(intent, 0);
- } else {
- pm = null;
- resolveInfo = null;
- }
- if (resolveInfo != null && resolveInfo.activityInfo != null) {
- final String packageName = resolveInfo.activityInfo.packageName;
- CharSequence label;
- Drawable icon;
- if ("android".equals(packageName)) {
- // Requires the chooser to find an activity to handle the intent.
- label = IntentFactory.getLabel(mContext, intent);
- icon = null;
+ /** Extends the classification with the intents that can be resolved. */
+ private void addActions(
+ TextClassification.Builder builder, List<Intent> intents) {
+ final PackageManager pm = mContext.getPackageManager();
+ final int size = intents.size();
+ for (int i = 0; i < size; i++) {
+ final Intent intent = intents.get(i);
+ final ResolveInfo resolveInfo;
+ if (intent != null) {
+ resolveInfo = pm.resolveActivity(intent, 0);
} else {
- // A default activity will handle the intent.
- intent.setComponent(new ComponentName(packageName, resolveInfo.activityInfo.name));
- icon = resolveInfo.activityInfo.loadIcon(pm);
- if (icon == null) {
- icon = resolveInfo.loadIcon(pm);
+ resolveInfo = null;
+ }
+ if (resolveInfo != null && resolveInfo.activityInfo != null) {
+ final String packageName = resolveInfo.activityInfo.packageName;
+ CharSequence label;
+ Drawable icon;
+ if ("android".equals(packageName)) {
+ // Requires the chooser to find an activity to handle the intent.
+ label = IntentFactory.getLabel(mContext, intent);
+ icon = null;
+ } else {
+ // A default activity will handle the intent.
+ intent.setComponent(
+ new ComponentName(packageName, resolveInfo.activityInfo.name));
+ icon = resolveInfo.activityInfo.loadIcon(pm);
+ if (icon == null) {
+ icon = resolveInfo.loadIcon(pm);
+ }
+ label = resolveInfo.activityInfo.loadLabel(pm);
+ if (label == null) {
+ label = resolveInfo.loadLabel(pm);
+ }
}
- label = resolveInfo.activityInfo.loadLabel(pm);
- if (label == null) {
- label = resolveInfo.loadLabel(pm);
+ final String labelString = (label != null) ? label.toString() : null;
+ final OnClickListener onClickListener =
+ TextClassification.createStartActivityOnClickListener(mContext, intent);
+ if (i == 0) {
+ builder.setPrimaryAction(intent, labelString, icon, onClickListener);
+ } else {
+ builder.addSecondaryAction(intent, labelString, icon, onClickListener);
}
}
- builder.addAction(
- intent, label != null ? label.toString() : null, icon,
- TextClassification.createStartActivityOnClickListener(mContext, intent));
}
}
@@ -557,22 +597,5 @@ final class TextClassifierImpl implements TextClassifier {
return null;
}
}
-
- @Nullable
- public static int getLogType(String type) {
- type = type.trim().toLowerCase(Locale.ENGLISH);
- switch (type) {
- case TextClassifier.TYPE_EMAIL:
- return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_EMAIL;
- case TextClassifier.TYPE_PHONE:
- return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_PHONE;
- case TextClassifier.TYPE_ADDRESS:
- return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_ADDRESS;
- case TextClassifier.TYPE_URL:
- return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_URL;
- default:
- return TextViewMetrics.SUBTYPE_ASSIST_MENU_ITEM_OTHER;
- }
- }
}
}
diff --git a/android/view/textclassifier/TextLinks.java b/android/view/textclassifier/TextLinks.java
index 76748d2b..6c587cf9 100644
--- a/android/view/textclassifier/TextLinks.java
+++ b/android/view/textclassifier/TextLinks.java
@@ -22,6 +22,8 @@ import android.annotation.Nullable;
import android.os.LocaleList;
import android.text.SpannableString;
import android.text.style.ClickableSpan;
+import android.view.View;
+import android.widget.TextView;
import com.android.internal.util.Preconditions;
@@ -103,11 +105,7 @@ public final class TextLinks {
mOriginalText = originalText;
mStart = start;
mEnd = end;
- mEntityScores = new EntityConfidence<>();
-
- for (Map.Entry<String, Float> entry : entityScores.entrySet()) {
- mEntityScores.setEntityType(entry.getKey(), entry.getValue());
- }
+ mEntityScores = new EntityConfidence<>(entityScores);
}
/**
@@ -163,11 +161,12 @@ public final class TextLinks {
public static final class Options {
private LocaleList mDefaultLocales;
+ private TextClassifier.EntityConfig mEntityConfig;
/**
- * @param defaultLocales ordered list of locale preferences that may be used to disambiguate
- * the provided text. If no locale preferences exist, set this to null or an empty
- * locale list.
+ * @param defaultLocales ordered list of locale preferences that may be used to
+ * disambiguate the provided text. If no locale preferences exist,
+ * set this to null or an empty locale list.
*/
public Options setDefaultLocales(@Nullable LocaleList defaultLocales) {
mDefaultLocales = defaultLocales;
@@ -175,6 +174,17 @@ public final class TextLinks {
}
/**
+ * Sets the entity configuration to use. This determines what types of entities the
+ * TextClassifier will look for.
+ *
+ * @param entityConfig EntityConfig to use
+ */
+ public Options setEntityConfig(@Nullable TextClassifier.EntityConfig entityConfig) {
+ mEntityConfig = entityConfig;
+ return this;
+ }
+
+ /**
* @return ordered list of locale preferences that can be used to disambiguate
* the provided text.
*/
@@ -182,6 +192,15 @@ public final class TextLinks {
public LocaleList getDefaultLocales() {
return mDefaultLocales;
}
+
+ /**
+ * @return The config representing the set of entities to look for.
+ * @see #setEntityConfig(TextClassifier.EntityConfig)
+ */
+ @Nullable
+ public TextClassifier.EntityConfig getEntityConfig() {
+ return mEntityConfig;
+ }
}
/**
@@ -193,9 +212,14 @@ public final class TextLinks {
* @hide
*/
public static final Function<TextLink, ClickableSpan> DEFAULT_SPAN_FACTORY =
- textLink -> {
- // TODO: Implement.
- throw new UnsupportedOperationException("Not yet implemented");
+ textLink -> new ClickableSpan() {
+ @Override
+ public void onClick(View widget) {
+ if (widget instanceof TextView) {
+ final TextView textView = (TextView) widget;
+ textView.requestActionMode(textLink);
+ }
+ }
};
/**
diff --git a/android/view/textclassifier/TextSelection.java b/android/view/textclassifier/TextSelection.java
index 480b27a7..25e9e7ec 100644
--- a/android/view/textclassifier/TextSelection.java
+++ b/android/view/textclassifier/TextSelection.java
@@ -21,12 +21,13 @@ import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.LocaleList;
+import android.util.ArrayMap;
import android.view.textclassifier.TextClassifier.EntityType;
import com.android.internal.util.Preconditions;
-import java.util.List;
import java.util.Locale;
+import java.util.Map;
/**
* Information about where text selection should be.
@@ -36,19 +37,15 @@ public final class TextSelection {
private final int mStartIndex;
private final int mEndIndex;
@NonNull private final EntityConfidence<String> mEntityConfidence;
- @NonNull private final List<String> mEntities;
- @NonNull private final String mLogSource;
- @NonNull private final String mVersionInfo;
+ @NonNull private final String mSignature;
private TextSelection(
- int startIndex, int endIndex, @NonNull EntityConfidence<String> entityConfidence,
- @NonNull String logSource, @NonNull String versionInfo) {
+ int startIndex, int endIndex, @NonNull Map<String, Float> entityConfidence,
+ @NonNull String signature) {
mStartIndex = startIndex;
mEndIndex = endIndex;
mEntityConfidence = new EntityConfidence<>(entityConfidence);
- mEntities = mEntityConfidence.getEntities();
- mLogSource = logSource;
- mVersionInfo = versionInfo;
+ mSignature = signature;
}
/**
@@ -70,7 +67,7 @@ public final class TextSelection {
*/
@IntRange(from = 0)
public int getEntityCount() {
- return mEntities.size();
+ return mEntityConfidence.getEntities().size();
}
/**
@@ -82,7 +79,7 @@ public final class TextSelection {
*/
@NonNull
public @EntityType String getEntity(int index) {
- return mEntities.get(index);
+ return mEntityConfidence.getEntities().get(index);
}
/**
@@ -96,27 +93,21 @@ public final class TextSelection {
}
/**
- * Returns a tag for the source classifier used to generate this result.
- * @hide
+ * Returns the signature for this object.
+ * The TextClassifier that generates this object may use it as a way to internally identify
+ * this object.
*/
@NonNull
- public String getSourceClassifier() {
- return mLogSource;
- }
-
- /**
- * Returns information about the classifier model used to generate this TextSelection.
- * @hide
- */
- @NonNull
- public String getVersionInfo() {
- return mVersionInfo;
+ public String getSignature() {
+ return mSignature;
}
@Override
public String toString() {
- return String.format(Locale.US,
- "TextSelection {%d, %d, %s}", mStartIndex, mEndIndex, mEntityConfidence);
+ return String.format(
+ Locale.US,
+ "TextSelection {startIndex=%d, endIndex=%d, entities=%s, signature=%s}",
+ mStartIndex, mEndIndex, mEntityConfidence, mSignature);
}
/**
@@ -126,10 +117,8 @@ public final class TextSelection {
private final int mStartIndex;
private final int mEndIndex;
- @NonNull private final EntityConfidence<String> mEntityConfidence =
- new EntityConfidence<>();
- @NonNull private String mLogSource = "";
- @NonNull private String mVersionInfo = "";
+ @NonNull private final Map<String, Float> mEntityConfidence = new ArrayMap<>();
+ @NonNull private String mSignature = "";
/**
* Creates a builder used to build {@link TextSelection} objects.
@@ -154,25 +143,18 @@ public final class TextSelection {
public Builder setEntityType(
@NonNull @EntityType String type,
@FloatRange(from = 0.0, to = 1.0) float confidenceScore) {
- mEntityConfidence.setEntityType(type, confidenceScore);
+ mEntityConfidence.put(type, confidenceScore);
return this;
}
/**
- * Sets a tag for the source classifier used to generate this result.
- * @hide
- */
- Builder setLogSource(@NonNull String logSource) {
- mLogSource = Preconditions.checkNotNull(logSource);
- return this;
- }
-
- /**
- * Sets information about the classifier model used to generate this TextSelection.
- * @hide
+ * Sets a signature for the TextSelection object.
+ *
+ * The TextClassifier that generates the TextSelection object may use it as a way to
+ * internally identify the TextSelection object.
*/
- Builder setVersionInfo(@NonNull String versionInfo) {
- mVersionInfo = Preconditions.checkNotNull(versionInfo);
+ public Builder setSignature(@NonNull String signature) {
+ mSignature = Preconditions.checkNotNull(signature);
return this;
}
@@ -181,7 +163,7 @@ public final class TextSelection {
*/
public TextSelection build() {
return new TextSelection(
- mStartIndex, mEndIndex, mEntityConfidence, mLogSource, mVersionInfo);
+ mStartIndex, mEndIndex, mEntityConfidence, mSignature);
}
}
diff --git a/android/view/textclassifier/logging/SmartSelectionEventTracker.java b/android/view/textclassifier/logging/SmartSelectionEventTracker.java
index 2833564f..157b3d82 100644
--- a/android/view/textclassifier/logging/SmartSelectionEventTracker.java
+++ b/android/view/textclassifier/logging/SmartSelectionEventTracker.java
@@ -473,7 +473,7 @@ public final class SmartSelectionEventTracker {
final String entityType = classification.getEntityCount() > 0
? classification.getEntity(0)
: TextClassifier.TYPE_UNKNOWN;
- final String versionTag = classification.getVersionInfo();
+ final String versionTag = getVersionInfo(classification.getSignature());
return new SelectionEvent(
start, end, EventType.SELECTION_MODIFIED, entityType, versionTag);
}
@@ -489,7 +489,7 @@ public final class SmartSelectionEventTracker {
*/
public static SelectionEvent selectionModified(
int start, int end, @NonNull TextSelection selection) {
- final boolean smartSelection = selection.getSourceClassifier()
+ final boolean smartSelection = getSourceClassifier(selection.getSignature())
.equals(TextClassifier.DEFAULT_LOG_TAG);
final int eventType;
if (smartSelection) {
@@ -503,7 +503,7 @@ public final class SmartSelectionEventTracker {
final String entityType = selection.getEntityCount() > 0
? selection.getEntity(0)
: TextClassifier.TYPE_UNKNOWN;
- final String versionTag = selection.getVersionInfo();
+ final String versionTag = getVersionInfo(selection.getSignature());
return new SelectionEvent(start, end, eventType, entityType, versionTag);
}
@@ -538,26 +538,25 @@ public final class SmartSelectionEventTracker {
final String entityType = classification.getEntityCount() > 0
? classification.getEntity(0)
: TextClassifier.TYPE_UNKNOWN;
- final String versionTag = classification.getVersionInfo();
+ final String versionTag = getVersionInfo(classification.getSignature());
return new SelectionEvent(start, end, actionType, entityType, versionTag);
}
- private boolean isActionType() {
- switch (mEventType) {
- case ActionType.OVERTYPE: // fall through
- case ActionType.COPY: // fall through
- case ActionType.PASTE: // fall through
- case ActionType.CUT: // fall through
- case ActionType.SHARE: // fall through
- case ActionType.SMART_SHARE: // fall through
- case ActionType.DRAG: // fall through
- case ActionType.ABANDON: // fall through
- case ActionType.SELECT_ALL: // fall through
- case ActionType.RESET: // fall through
- return true;
- default:
- return false;
+ private static String getVersionInfo(String signature) {
+ final int start = signature.indexOf("|");
+ final int end = signature.indexOf("|", start);
+ if (start >= 0 && end >= start) {
+ return signature.substring(start, end);
+ }
+ return "";
+ }
+
+ private static String getSourceClassifier(String signature) {
+ final int end = signature.indexOf("|");
+ if (end >= 0) {
+ return signature.substring(0, end);
}
+ return "";
}
private boolean isTerminal() {
diff --git a/android/view/textservice/TextServicesManager.java b/android/view/textservice/TextServicesManager.java
index f368c74a..8e1f2183 100644
--- a/android/view/textservice/TextServicesManager.java
+++ b/android/view/textservice/TextServicesManager.java
@@ -1,213 +1,58 @@
/*
- * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
package android.view.textservice;
-import android.annotation.SystemService;
-import android.content.Context;
import android.os.Bundle;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.os.ServiceManager.ServiceNotFoundException;
-import android.util.Log;
import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
-import com.android.internal.textservice.ITextServicesManager;
-
import java.util.Locale;
/**
- * System API to the overall text services, which arbitrates interaction between applications
- * and text services.
- *
- * The user can change the current text services in Settings. And also applications can specify
- * the target text services.
- *
- * <h3>Architecture Overview</h3>
- *
- * <p>There are three primary parties involved in the text services
- * framework (TSF) architecture:</p>
- *
- * <ul>
- * <li> The <strong>text services manager</strong> as expressed by this class
- * is the central point of the system that manages interaction between all
- * other parts. It is expressed as the client-side API here which exists
- * in each application context and communicates with a global system service
- * that manages the interaction across all processes.
- * <li> A <strong>text service</strong> implements a particular
- * interaction model allowing the client application to retrieve information of text.
- * The system binds to the current text service that is in use, causing it to be created and run.
- * <li> Multiple <strong>client applications</strong> arbitrate with the text service
- * manager for connections to text services.
- * </ul>
- *
- * <h3>Text services sessions</h3>
- * <ul>
- * <li>The <strong>spell checker session</strong> is one of the text services.
- * {@link android.view.textservice.SpellCheckerSession}</li>
- * </ul>
- *
+ * A stub class of TextServicesManager for Layout-Lib.
*/
-@SystemService(Context.TEXT_SERVICES_MANAGER_SERVICE)
public final class TextServicesManager {
- private static final String TAG = TextServicesManager.class.getSimpleName();
- private static final boolean DBG = false;
-
- private static TextServicesManager sInstance;
-
- private final ITextServicesManager mService;
-
- private TextServicesManager() throws ServiceNotFoundException {
- mService = ITextServicesManager.Stub.asInterface(
- ServiceManager.getServiceOrThrow(Context.TEXT_SERVICES_MANAGER_SERVICE));
- }
+ private static final TextServicesManager sInstance = new TextServicesManager();
+ private static final SpellCheckerInfo[] EMPTY_SPELL_CHECKER_INFO = new SpellCheckerInfo[0];
/**
* Retrieve the global TextServicesManager instance, creating it if it doesn't already exist.
* @hide
*/
public static TextServicesManager getInstance() {
- synchronized (TextServicesManager.class) {
- if (sInstance == null) {
- try {
- sInstance = new TextServicesManager();
- } catch (ServiceNotFoundException e) {
- throw new IllegalStateException(e);
- }
- }
- return sInstance;
- }
- }
-
- /**
- * Returns the language component of a given locale string.
- */
- private static String parseLanguageFromLocaleString(String locale) {
- final int idx = locale.indexOf('_');
- if (idx < 0) {
- return locale;
- } else {
- return locale.substring(0, idx);
- }
+ return sInstance;
}
- /**
- * Get a spell checker session for the specified spell checker
- * @param locale the locale for the spell checker. If {@code locale} is null and
- * referToSpellCheckerLanguageSettings is true, the locale specified in Settings will be
- * returned. If {@code locale} is not null and referToSpellCheckerLanguageSettings is true,
- * the locale specified in Settings will be returned only when it is same as {@code locale}.
- * Exceptionally, when referToSpellCheckerLanguageSettings is true and {@code locale} is
- * only language (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be
- * selected.
- * @param listener a spell checker session lister for getting results from a spell checker.
- * @param referToSpellCheckerLanguageSettings if true, the session for one of enabled
- * languages in settings will be returned.
- * @return the spell checker session of the spell checker
- */
public SpellCheckerSession newSpellCheckerSession(Bundle bundle, Locale locale,
SpellCheckerSessionListener listener, boolean referToSpellCheckerLanguageSettings) {
- if (listener == null) {
- throw new NullPointerException();
- }
- if (!referToSpellCheckerLanguageSettings && locale == null) {
- throw new IllegalArgumentException("Locale should not be null if you don't refer"
- + " settings.");
- }
-
- if (referToSpellCheckerLanguageSettings && !isSpellCheckerEnabled()) {
- return null;
- }
-
- final SpellCheckerInfo sci;
- try {
- sci = mService.getCurrentSpellChecker(null);
- } catch (RemoteException e) {
- return null;
- }
- if (sci == null) {
- return null;
- }
- SpellCheckerSubtype subtypeInUse = null;
- if (referToSpellCheckerLanguageSettings) {
- subtypeInUse = getCurrentSpellCheckerSubtype(true);
- if (subtypeInUse == null) {
- return null;
- }
- if (locale != null) {
- final String subtypeLocale = subtypeInUse.getLocale();
- final String subtypeLanguage = parseLanguageFromLocaleString(subtypeLocale);
- if (subtypeLanguage.length() < 2 || !locale.getLanguage().equals(subtypeLanguage)) {
- return null;
- }
- }
- } else {
- final String localeStr = locale.toString();
- for (int i = 0; i < sci.getSubtypeCount(); ++i) {
- final SpellCheckerSubtype subtype = sci.getSubtypeAt(i);
- final String tempSubtypeLocale = subtype.getLocale();
- final String tempSubtypeLanguage = parseLanguageFromLocaleString(tempSubtypeLocale);
- if (tempSubtypeLocale.equals(localeStr)) {
- subtypeInUse = subtype;
- break;
- } else if (tempSubtypeLanguage.length() >= 2 &&
- locale.getLanguage().equals(tempSubtypeLanguage)) {
- subtypeInUse = subtype;
- }
- }
- }
- if (subtypeInUse == null) {
- return null;
- }
- final SpellCheckerSession session = new SpellCheckerSession(sci, mService, listener);
- try {
- mService.getSpellCheckerService(sci.getId(), subtypeInUse.getLocale(),
- session.getTextServicesSessionListener(),
- session.getSpellCheckerSessionListener(), bundle);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- return session;
+ return null;
}
/**
* @hide
*/
public SpellCheckerInfo[] getEnabledSpellCheckers() {
- try {
- final SpellCheckerInfo[] retval = mService.getEnabledSpellCheckers();
- if (DBG) {
- Log.d(TAG, "getEnabledSpellCheckers: " + (retval != null ? retval.length : "null"));
- }
- return retval;
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return EMPTY_SPELL_CHECKER_INFO;
}
/**
* @hide
*/
public SpellCheckerInfo getCurrentSpellChecker() {
- try {
- // Passing null as a locale for ICS
- return mService.getCurrentSpellChecker(null);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return null;
}
/**
@@ -215,22 +60,13 @@ public final class TextServicesManager {
*/
public SpellCheckerSubtype getCurrentSpellCheckerSubtype(
boolean allowImplicitlySelectedSubtype) {
- try {
- // Passing null as a locale until we support multiple enabled spell checker subtypes.
- return mService.getCurrentSpellCheckerSubtype(null, allowImplicitlySelectedSubtype);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return null;
}
/**
* @hide
*/
public boolean isSpellCheckerEnabled() {
- try {
- return mService.isSpellCheckerEnabled();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
+ return false;
}
}
diff --git a/android/webkit/FilterMethods.java b/android/webkit/FilterMethods.java
new file mode 100644
index 00000000..76552b7f
--- /dev/null
+++ b/android/webkit/FilterMethods.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.webkit;
+
+public class FilterMethods {
+
+ public void method1(boolean param) {
+ }
+
+ public void method2(int x) {
+ }
+
+ public void method3(WebViewClient client) {
+ }
+}
diff --git a/android/webkit/SafeBrowsingResponse.java b/android/webkit/SafeBrowsingResponse.java
index 1d3a617a..960b56bd 100644
--- a/android/webkit/SafeBrowsingResponse.java
+++ b/android/webkit/SafeBrowsingResponse.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,36 +16,5 @@
package android.webkit;
-/**
- * Used to indicate an action to take when hitting a malicious URL. Instances of this class are
- * created by the WebView and passed to {@link android.webkit.WebViewClient#onSafeBrowsingHit}. The
- * host application must call {@link #showInterstitial(boolean)}, {@link #proceed(boolean)}, or
- * {@link #backToSafety(boolean)} to set the WebView's response to the Safe Browsing hit.
- *
- * <p>
- * If reporting is enabled, all reports will be sent according to the privacy policy referenced by
- * {@link android.webkit.WebView#getSafeBrowsingPrivacyPolicyUrl()}.
- */
-public abstract class SafeBrowsingResponse {
-
- /**
- * Display the default interstitial.
- *
- * @param allowReporting {@code true} if the interstitial should show a reporting checkbox.
- */
- public abstract void showInterstitial(boolean allowReporting);
-
- /**
- * Act as if the user clicked "visit this unsafe site."
- *
- * @param report {@code true} to enable Safe Browsing reporting.
- */
- public abstract void proceed(boolean report);
-
- /**
- * Act as if the user clicked "back to safety."
- *
- * @param report {@code true} to enable Safe Browsing reporting.
- */
- public abstract void backToSafety(boolean report);
+public class SafeBrowsingResponse {
}
diff --git a/android/webkit/SingleClassAndMethod.java b/android/webkit/SingleClassAndMethod.java
new file mode 100644
index 00000000..3100ca80
--- /dev/null
+++ b/android/webkit/SingleClassAndMethod.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.webkit;
+
+public class SingleClassAndMethod {
+
+ public void method(boolean param) {
+ File x = new File("");
+ }
+}
diff --git a/android/webkit/TracingConfig.java b/android/webkit/TracingConfig.java
new file mode 100644
index 00000000..75e2bf70
--- /dev/null
+++ b/android/webkit/TracingConfig.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Holds tracing configuration information and predefined settings.
+ */
+public class TracingConfig {
+
+ private final String mCustomCategoryPattern;
+ private final @PresetCategories int mPresetCategories;
+ private @TracingMode int mTracingMode;
+
+ /** @hide */
+ @IntDef({CATEGORIES_NONE, CATEGORIES_WEB_DEVELOPER, CATEGORIES_INPUT_LATENCY,
+ CATEGORIES_RENDERING, CATEGORIES_JAVASCRIPT_AND_RENDERING, CATEGORIES_FRAME_VIEWER})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PresetCategories {}
+
+ /**
+ * Indicates that there are no preset categories.
+ */
+ public static final int CATEGORIES_NONE = -1;
+
+ /**
+ * Predefined categories typically useful for web developers.
+ * Typically includes blink, compositor, renderer.scheduler and v8 categories.
+ */
+ public static final int CATEGORIES_WEB_DEVELOPER = 0;
+
+ /**
+ * Predefined categories for analyzing input latency issues.
+ * Typically includes input, renderer.scheduler categories.
+ */
+ public static final int CATEGORIES_INPUT_LATENCY = 1;
+
+ /**
+ * Predefined categories for analyzing rendering issues.
+ * Typically includes blink, compositor and gpu categories.
+ */
+ public static final int CATEGORIES_RENDERING = 2;
+
+ /**
+ * Predefined categories for analyzing javascript and rendering issues.
+ * Typically includes blink, compositor, gpu, renderer.schduler and v8 categories.
+ */
+ public static final int CATEGORIES_JAVASCRIPT_AND_RENDERING = 3;
+
+ /**
+ * Predefined categories for studying difficult rendering performance problems.
+ * Typically includes blink, compositor, gpu, renderer.scheduler, v8 and
+ * some other compositor categories which are disabled by default.
+ */
+ public static final int CATEGORIES_FRAME_VIEWER = 4;
+
+ /** @hide */
+ @IntDef({RECORD_UNTIL_FULL, RECORD_CONTINUOUSLY, RECORD_UNTIL_FULL_LARGE_BUFFER,
+ RECORD_TO_CONSOLE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TracingMode {}
+
+ /**
+ * Record trace events until the internal tracing buffer is full. Default tracing mode.
+ * Typically the buffer memory usage is between {@link #RECORD_CONTINUOUSLY} and the
+ * {@link #RECORD_UNTIL_FULL_LARGE_BUFFER}. Depending on the implementation typically allows
+ * up to 256k events to be stored.
+ */
+ public static final int RECORD_UNTIL_FULL = 0;
+
+ /**
+ * Record trace events continuously using an internal ring buffer. Overwrites
+ * old events if they exceed buffer capacity. Uses less memory than both
+ * {@link #RECORD_UNTIL_FULL} and {@link #RECORD_UNTIL_FULL_LARGE_BUFFER} modes.
+ * Depending on the implementation typically allows up to 64k events to be stored.
+ */
+ public static final int RECORD_CONTINUOUSLY = 1;
+
+ /**
+ * Record trace events using a larger internal tracing buffer until it is full.
+ * Uses more memory than the other modes and may not be suitable on devices
+ * with smaller RAM. Depending on the implementation typically allows up to
+ * 512 million events to be stored.
+ */
+ public static final int RECORD_UNTIL_FULL_LARGE_BUFFER = 2;
+
+ /**
+ * Record trace events to console (logcat). The events are discarded and nothing
+ * is sent back to the caller. Uses the least memory as compared to the other modes.
+ */
+ public static final int RECORD_TO_CONSOLE = 3;
+
+ /**
+ * Create config with the preset categories.
+ * <p>
+ * Example:
+ * TracingConfig(CATEGORIES_WEB_DEVELOPER) -- records trace events from the "web developer"
+ * preset categories.
+ *
+ * @param presetCategories preset categories to use, one of {@link #CATEGORIES_WEB_DEVELOPER},
+ * {@link #CATEGORIES_INPUT_LATENCY}, {@link #CATEGORIES_RENDERING},
+ * {@link #CATEGORIES_JAVASCRIPT_AND_RENDERING} or
+ * {@link #CATEGORIES_FRAME_VIEWER}.
+ *
+ * Note: for specifying custom categories without presets use
+ * {@link #TracingConfig(int, String, int)}.
+ *
+ */
+ public TracingConfig(@PresetCategories int presetCategories) {
+ this(presetCategories, "", RECORD_UNTIL_FULL);
+ }
+
+ /**
+ * Create a configuration with both preset categories and custom categories.
+ * Also allows to specify the tracing mode.
+ *
+ * Note that the categories are defined by the currently-in-use version of WebView. They live
+ * in chromium code and are not part of the Android API. See
+ * See <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool">
+ * chromium documentation on tracing</a> for more details.
+ *
+ * <p>
+ * Examples:
+ *
+ * Preset category with a specified trace mode:
+ * TracingConfig(CATEGORIES_WEB_DEVELOPER, "", RECORD_UNTIL_FULL_LARGE_BUFFER);
+ * Custom categories:
+ * TracingConfig(CATEGORIES_NONE, "browser", RECORD_UNTIL_FULL)
+ * -- records only the trace events from the "browser" category.
+ * TraceConfig(CATEGORIES_NONE, "-input,-gpu", RECORD_UNTIL_FULL)
+ * -- records all trace events excluding the events from the "input" and 'gpu' categories.
+ * TracingConfig(CATEGORIES_NONE, "blink*,devtools*", RECORD_UNTIL_FULL)
+ * -- records only the trace events matching the "blink*" and "devtools*" patterns
+ * (e.g. "blink_gc" and "devtools.timeline" categories).
+ *
+ * Combination of preset and additional custom categories:
+ * TracingConfig(CATEGORIES_WEB_DEVELOPER, "memory-infra", RECORD_CONTINUOUSLY)
+ * -- records events from the "web developer" categories and events from the "memory-infra"
+ * category to understand where memory is being used.
+ *
+ * @param presetCategories preset categories to use, one of {@link #CATEGORIES_WEB_DEVELOPER},
+ * {@link #CATEGORIES_INPUT_LATENCY}, {@link #CATEGORIES_RENDERING},
+ * {@link #CATEGORIES_JAVASCRIPT_AND_RENDERING} or
+ * {@link #CATEGORIES_FRAME_VIEWER}.
+ * @param customCategories a comma-delimited list of category wildcards. A category can
+ * have an optional '-' prefix to make it an excluded category.
+ * @param tracingMode tracing mode to use, one of {@link #RECORD_UNTIL_FULL},
+ * {@link #RECORD_CONTINUOUSLY}, {@link #RECORD_UNTIL_FULL_LARGE_BUFFER}
+ * or {@link #RECORD_TO_CONSOLE}.
+ */
+ public TracingConfig(@PresetCategories int presetCategories,
+ @NonNull String customCategories, @TracingMode int tracingMode) {
+ mPresetCategories = presetCategories;
+ mCustomCategoryPattern = customCategories;
+ mTracingMode = RECORD_UNTIL_FULL;
+ }
+
+ /**
+ * Returns the custom category pattern for this configuration.
+ *
+ * @return empty string if no custom category pattern is specified.
+ */
+ @NonNull
+ public String getCustomCategoryPattern() {
+ return mCustomCategoryPattern;
+ }
+
+ /**
+ * Returns the preset categories value of this configuration.
+ */
+ @PresetCategories
+ public int getPresetCategories() {
+ return mPresetCategories;
+ }
+
+ /**
+ * Returns the tracing mode of this configuration.
+ */
+ @TracingMode
+ public int getTracingMode() {
+ return mTracingMode;
+ }
+
+}
diff --git a/android/webkit/TracingController.java b/android/webkit/TracingController.java
new file mode 100644
index 00000000..cadb8a18
--- /dev/null
+++ b/android/webkit/TracingController.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+
+/**
+ * Manages tracing of WebViews. In particular provides functionality for the app
+ * to enable/disable tracing of parts of code and to collect tracing data.
+ * This is useful for profiling performance issues, debugging and memory usage
+ * analysis in production and real life scenarios.
+ * <p>
+ * The resulting trace data is sent back as a byte sequence in json format. This
+ * file can be loaded in "chrome://tracing" for further analysis.
+ * <p>
+ * Note: All methods in this class must be called on the UI thread. All callbacks
+ * are also called on the UI thread.
+ * <p>
+ * Example usage:
+ * <pre class="prettyprint">
+ * TracingController tracingController = TracingController.getInstance();
+ * tracingController.start(new TraceConfig(CATEGORIES_WEB_DEVELOPER));
+ * [..]
+ * tracingController.stopAndFlush(new TraceFileOutput("trace.json"), null);
+ * </pre></p>
+ */
+public abstract class TracingController {
+
+ /**
+ * Interface for capturing tracing data.
+ */
+ public interface TracingOutputStream {
+ /**
+ * Will be called to return tracing data in chunks.
+ * Tracing data is returned in json format an array of bytes.
+ */
+ void write(byte[] chunk);
+
+ /**
+ * Called when tracing is finished and the data collection is over.
+ * There will be no calls to #write after #complete is called.
+ */
+ void complete();
+ }
+
+ /**
+ * Returns the default TracingController instance. At present there is
+ * only one TracingController instance for all WebView instances,
+ * however this restriction may be relaxed in the future.
+ *
+ * @return the default TracingController instance
+ */
+ @NonNull
+ public static TracingController getInstance() {
+ return WebViewFactory.getProvider().getTracingController();
+ }
+
+ /**
+ * Starts tracing all webviews. Depeding on the trace mode in traceConfig
+ * specifies how the trace events are recorded.
+ *
+ * For tracing modes {@link TracingConfig#RECORD_UNTIL_FULL},
+ * {@link TracingConfig#RECORD_CONTINUOUSLY} and
+ * {@link TracingConfig#RECORD_UNTIL_FULL_LARGE_BUFFER} the events are recorded
+ * using an internal buffer and flushed to the outputStream when
+ * {@link #stopAndFlush(TracingOutputStream, Handler)} is called.
+ *
+ * @param tracingConfig configuration options to use for tracing
+ * @return false if the system is already tracing, true otherwise.
+ */
+ public abstract boolean start(TracingConfig tracingConfig);
+
+ /**
+ * Stops tracing and discards all tracing data.
+ *
+ * This method is particularly useful in conjunction with the
+ * {@link TracingConfig#RECORD_TO_CONSOLE} tracing mode because tracing data is logged to
+ * console and not sent to an outputStream as with
+ * {@link #stopAndFlush(TracingOutputStream, Handler)}.
+ *
+ * @return false if the system was not tracing at the time of the call, true
+ * otherwise.
+ */
+ public abstract boolean stop();
+
+ /**
+ * Stops tracing and flushes tracing data to the specifid outputStream.
+ *
+ * Note that if the {@link TracingConfig#RECORD_TO_CONSOLE} tracing mode is used
+ * nothing will be sent to the outputStream and no TracingOuputStream methods will be
+ * called. In that case it is more convenient to just use {@link #stop()} instead.
+ *
+ * @param outputStream the output steam the tracing data will be sent to.
+ * @param handler the {@link android.os.Handler} on which the outputStream callbacks
+ * will be invoked. If the handler is null the current thread's Looper
+ * will be used.
+ * @return false if the system was not tracing at the time of the call, true
+ * otherwise.
+ */
+ public abstract boolean stopAndFlush(TracingOutputStream outputStream,
+ @Nullable Handler handler);
+
+ /** True if the system is tracing */
+ public abstract boolean isTracing();
+
+ // TODO: consider adding getTraceBufferUsage, percentage and approx event count.
+ // TODO: consider adding String getCategories(), for obtaining the actual list
+ // of categories used (given that presets are ints).
+
+}
diff --git a/android/webkit/TracingFileOutputStream.java b/android/webkit/TracingFileOutputStream.java
new file mode 100644
index 00000000..8a5fa36c
--- /dev/null
+++ b/android/webkit/TracingFileOutputStream.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.annotation.NonNull;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Simple TracingOutputStream implementation which writes the trace data from
+ * {@link TracingController} to a new file.
+ *
+ */
+public class TracingFileOutputStream implements TracingController.TracingOutputStream {
+
+ private FileOutputStream mFileOutput;
+
+ public TracingFileOutputStream(@NonNull String filename) throws FileNotFoundException {
+ mFileOutput = new FileOutputStream(filename);
+ }
+
+ /**
+ * Writes bytes chunk to the file.
+ */
+ public void write(byte[] chunk) {
+ try {
+ mFileOutput.write(chunk);
+ } catch (IOException e) {
+ onIOException(e);
+ }
+ }
+
+ /**
+ * Closes the file.
+ */
+ public void complete() {
+ try {
+ mFileOutput.close();
+ } catch (IOException e) {
+ onIOException(e);
+ }
+ }
+
+ private void onIOException(IOException e) {
+ throw new RuntimeException(e);
+ }
+}
diff --git a/android/webkit/UserPackage.java b/android/webkit/UserPackage.java
index 63519a65..03ff0ca6 100644
--- a/android/webkit/UserPackage.java
+++ b/android/webkit/UserPackage.java
@@ -34,7 +34,7 @@ public class UserPackage {
private final UserInfo mUserInfo;
private final PackageInfo mPackageInfo;
- public static final int MINIMUM_SUPPORTED_SDK = Build.VERSION_CODES.O_MR1;
+ public static final int MINIMUM_SUPPORTED_SDK = Build.VERSION_CODES.P;
public UserPackage(UserInfo user, PackageInfo packageInfo) {
this.mUserInfo = user;
diff --git a/android/webkit/WebKitTypeAsMethodParameter.java b/android/webkit/WebKitTypeAsMethodParameter.java
new file mode 100644
index 00000000..0b229944
--- /dev/null
+++ b/android/webkit/WebKitTypeAsMethodParameter.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+public abstract class WebKitTypeAsMethodParameter {
+ public void method(WebViewClient webViewClient);
+ public void methodX(SafeBrowsingResponse response) {
+ return null;
+ }
+ public void urlCall(String url) {
+ return null;
+ }
+}
diff --git a/android/webkit/WebKitTypeAsMethodReturn.java b/android/webkit/WebKitTypeAsMethodReturn.java
new file mode 100644
index 00000000..d8d8bd03
--- /dev/null
+++ b/android/webkit/WebKitTypeAsMethodReturn.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+public class WebKitTypeAsMethodReturn {
+ public WebViewClient method() {
+ return null;
+ }
+ public SafeBrowsingResponse method2() {
+ return null;
+ }
+}
diff --git a/android/webkit/WebSettings.java b/android/webkit/WebSettings.java
index e4937392..90cc4814 100644
--- a/android/webkit/WebSettings.java
+++ b/android/webkit/WebSettings.java
@@ -122,7 +122,13 @@ public abstract class WebSettings {
}
/** @hide */
- @IntDef({LOAD_DEFAULT, LOAD_NORMAL, LOAD_CACHE_ELSE_NETWORK, LOAD_NO_CACHE, LOAD_CACHE_ONLY})
+ @IntDef(prefix = { "LOAD_" }, value = {
+ LOAD_DEFAULT,
+ LOAD_NORMAL,
+ LOAD_CACHE_ELSE_NETWORK,
+ LOAD_NO_CACHE,
+ LOAD_CACHE_ONLY
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface CacheMode {}
@@ -1415,13 +1421,12 @@ public abstract class WebSettings {
/**
* @hide
*/
- @IntDef(flag = true,
- value = {
- MENU_ITEM_NONE,
- MENU_ITEM_SHARE,
- MENU_ITEM_WEB_SEARCH,
- MENU_ITEM_PROCESS_TEXT
- })
+ @IntDef(flag = true, prefix = { "MENU_ITEM_" }, value = {
+ MENU_ITEM_NONE,
+ MENU_ITEM_SHARE,
+ MENU_ITEM_WEB_SEARCH,
+ MENU_ITEM_PROCESS_TEXT
+ })
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.PARAMETER, ElementType.METHOD})
private @interface MenuItemFlags {}
diff --git a/android/webkit/WebView.java b/android/webkit/WebView.java
index 6f992548..202f2046 100644
--- a/android/webkit/WebView.java
+++ b/android/webkit/WebView.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2006 The Android Open Source Project
+ * Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,3020 +16,223 @@
package android.webkit;
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.SystemApi;
-import android.annotation.Widget;
+import com.android.layoutlib.bridge.MockView;
+
import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageInfo;
-import android.content.res.Configuration;
import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Paint;
import android.graphics.Picture;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.net.http.SslCertificate;
-import android.os.Build;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
import android.os.Message;
-import android.os.RemoteException;
-import android.os.StrictMode;
-import android.print.PrintDocumentAdapter;
-import android.security.KeyChain;
import android.util.AttributeSet;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.DragEvent;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewDebug;
-import android.view.ViewGroup;
-import android.view.ViewHierarchyEncoder;
-import android.view.ViewStructure;
-import android.view.ViewTreeObserver;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeProvider;
-import android.view.autofill.AutofillValue;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-import android.view.textclassifier.TextClassifier;
-import android.widget.AbsoluteLayout;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.List;
-import java.util.Map;
/**
- * <p>A View that displays web pages. This class is the basis upon which you
- * can roll your own web browser or simply display some online content within your Activity.
- * It uses the WebKit rendering engine to display
- * web pages and includes methods to navigate forward and backward
- * through a history, zoom in and out, perform text searches and more.
- *
- * <p>Note that, in order for your Activity to access the Internet and load web pages
- * in a WebView, you must add the {@code INTERNET} permissions to your
- * Android Manifest file:
- *
- * <pre>
- * {@code <uses-permission android:name="android.permission.INTERNET" />}
- * </pre>
- *
- * <p>This must be a child of the <a
- * href="{@docRoot}guide/topics/manifest/manifest-element.html">{@code <manifest>}</a>
- * element.
- *
- * <p>For more information, read
- * <a href="{@docRoot}guide/webapps/webview.html">Building Web Apps in WebView</a>.
- *
- * <h3>Basic usage</h3>
- *
- * <p>By default, a WebView provides no browser-like widgets, does not
- * enable JavaScript and web page errors are ignored. If your goal is only
- * to display some HTML as a part of your UI, this is probably fine;
- * the user won't need to interact with the web page beyond reading
- * it, and the web page won't need to interact with the user. If you
- * actually want a full-blown web browser, then you probably want to
- * invoke the Browser application with a URL Intent rather than show it
- * with a WebView. For example:
- * <pre>
- * Uri uri = Uri.parse("https://www.example.com");
- * Intent intent = new Intent(Intent.ACTION_VIEW, uri);
- * startActivity(intent);
- * </pre>
- * <p>See {@link android.content.Intent} for more information.
- *
- * <p>To provide a WebView in your own Activity, include a {@code <WebView>} in your layout,
- * or set the entire Activity window as a WebView during {@link
- * android.app.Activity#onCreate(Bundle) onCreate()}:
- *
- * <pre class="prettyprint">
- * WebView webview = new WebView(this);
- * setContentView(webview);
- * </pre>
- *
- * <p>Then load the desired web page:
- *
- * <pre>
- * // Simplest usage: note that an exception will NOT be thrown
- * // if there is an error loading this page (see below).
- * webview.loadUrl("https://example.com/");
- *
- * // OR, you can also load from an HTML string:
- * String summary = "&lt;html>&lt;body>You scored &lt;b>192&lt;/b> points.&lt;/body>&lt;/html>";
- * webview.loadData(summary, "text/html", null);
- * // ... although note that there are restrictions on what this HTML can do.
- * // See the JavaDocs for {@link #loadData(String,String,String) loadData()} and {@link
- * #loadDataWithBaseURL(String,String,String,String,String) loadDataWithBaseURL()} for more info.
- * </pre>
- *
- * <p>A WebView has several customization points where you can add your
- * own behavior. These are:
- *
- * <ul>
- * <li>Creating and setting a {@link android.webkit.WebChromeClient} subclass.
- * This class is called when something that might impact a
- * browser UI happens, for instance, progress updates and
- * JavaScript alerts are sent here (see <a
- * href="{@docRoot}guide/developing/debug-tasks.html#DebuggingWebPages">Debugging Tasks</a>).
- * </li>
- * <li>Creating and setting a {@link android.webkit.WebViewClient} subclass.
- * It will be called when things happen that impact the
- * rendering of the content, eg, errors or form submissions. You
- * can also intercept URL loading here (via {@link
- * android.webkit.WebViewClient#shouldOverrideUrlLoading(WebView,String)
- * shouldOverrideUrlLoading()}).</li>
- * <li>Modifying the {@link android.webkit.WebSettings}, such as
- * enabling JavaScript with {@link android.webkit.WebSettings#setJavaScriptEnabled(boolean)
- * setJavaScriptEnabled()}. </li>
- * <li>Injecting Java objects into the WebView using the
- * {@link android.webkit.WebView#addJavascriptInterface} method. This
- * method allows you to inject Java objects into a page's JavaScript
- * context, so that they can be accessed by JavaScript in the page.</li>
- * </ul>
- *
- * <p>Here's a more complicated example, showing error handling,
- * settings, and progress notification:
- *
- * <pre class="prettyprint">
- * // Let's display the progress in the activity title bar, like the
- * // browser app does.
- * getWindow().requestFeature(Window.FEATURE_PROGRESS);
- *
- * webview.getSettings().setJavaScriptEnabled(true);
- *
- * final Activity activity = this;
- * webview.setWebChromeClient(new WebChromeClient() {
- * public void onProgressChanged(WebView view, int progress) {
- * // Activities and WebViews measure progress with different scales.
- * // The progress meter will automatically disappear when we reach 100%
- * activity.setProgress(progress * 1000);
- * }
- * });
- * webview.setWebViewClient(new WebViewClient() {
- * public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
- * Toast.makeText(activity, "Oh no! " + description, Toast.LENGTH_SHORT).show();
- * }
- * });
- *
- * webview.loadUrl("https://developer.android.com/");
- * </pre>
- *
- * <h3>Zoom</h3>
- *
- * <p>To enable the built-in zoom, set
- * {@link #getSettings() WebSettings}.{@link WebSettings#setBuiltInZoomControls(boolean)}
- * (introduced in API level {@link android.os.Build.VERSION_CODES#CUPCAKE}).
- *
- * <p class="note"><b>Note:</b> Using zoom if either the height or width is set to
- * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} may lead to undefined behavior
- * and should be avoided.
- *
- * <h3>Cookie and window management</h3>
- *
- * <p>For obvious security reasons, your application has its own
- * cache, cookie store etc.&mdash;it does not share the Browser
- * application's data.
- *
- * <p>By default, requests by the HTML to open new windows are
- * ignored. This is {@code true} whether they be opened by JavaScript or by
- * the target attribute on a link. You can customize your
- * {@link WebChromeClient} to provide your own behavior for opening multiple windows,
- * and render them in whatever manner you want.
- *
- * <p>The standard behavior for an Activity is to be destroyed and
- * recreated when the device orientation or any other configuration changes. This will cause
- * the WebView to reload the current page. If you don't want that, you
- * can set your Activity to handle the {@code orientation} and {@code keyboardHidden}
- * changes, and then just leave the WebView alone. It'll automatically
- * re-orient itself as appropriate. Read <a
- * href="{@docRoot}guide/topics/resources/runtime-changes.html">Handling Runtime Changes</a> for
- * more information about how to handle configuration changes during runtime.
- *
- *
- * <h3>Building web pages to support different screen densities</h3>
- *
- * <p>The screen density of a device is based on the screen resolution. A screen with low density
- * has fewer available pixels per inch, where a screen with high density
- * has more &mdash; sometimes significantly more &mdash; pixels per inch. The density of a
- * screen is important because, other things being equal, a UI element (such as a button) whose
- * height and width are defined in terms of screen pixels will appear larger on the lower density
- * screen and smaller on the higher density screen.
- * For simplicity, Android collapses all actual screen densities into three generalized densities:
- * high, medium, and low.
- * <p>By default, WebView scales a web page so that it is drawn at a size that matches the default
- * appearance on a medium density screen. So, it applies 1.5x scaling on a high density screen
- * (because its pixels are smaller) and 0.75x scaling on a low density screen (because its pixels
- * are bigger).
- * Starting with API level {@link android.os.Build.VERSION_CODES#ECLAIR}, WebView supports DOM, CSS,
- * and meta tag features to help you (as a web developer) target screens with different screen
- * densities.
- * <p>Here's a summary of the features you can use to handle different screen densities:
- * <ul>
- * <li>The {@code window.devicePixelRatio} DOM property. The value of this property specifies the
- * default scaling factor used for the current device. For example, if the value of {@code
- * window.devicePixelRatio} is "1.0", then the device is considered a medium density (mdpi) device
- * and default scaling is not applied to the web page; if the value is "1.5", then the device is
- * considered a high density device (hdpi) and the page content is scaled 1.5x; if the
- * value is "0.75", then the device is considered a low density device (ldpi) and the content is
- * scaled 0.75x.</li>
- * <li>The {@code -webkit-device-pixel-ratio} CSS media query. Use this to specify the screen
- * densities for which this style sheet is to be used. The corresponding value should be either
- * "0.75", "1", or "1.5", to indicate that the styles are for devices with low density, medium
- * density, or high density screens, respectively. For example:
- * <pre>
- * &lt;link rel="stylesheet" media="screen and (-webkit-device-pixel-ratio:1.5)" href="hdpi.css" /&gt;</pre>
- * <p>The {@code hdpi.css} stylesheet is only used for devices with a screen pixel ration of 1.5,
- * which is the high density pixel ratio.
- * </li>
- * </ul>
- *
- * <h3>HTML5 Video support</h3>
- *
- * <p>In order to support inline HTML5 video in your application you need to have hardware
- * acceleration turned on.
- *
- * <h3>Full screen support</h3>
- *
- * <p>In order to support full screen &mdash; for video or other HTML content &mdash; you need to set a
- * {@link android.webkit.WebChromeClient} and implement both
- * {@link WebChromeClient#onShowCustomView(View, WebChromeClient.CustomViewCallback)}
- * and {@link WebChromeClient#onHideCustomView()}. If the implementation of either of these two methods is
- * missing then the web contents will not be allowed to enter full screen. Optionally you can implement
- * {@link WebChromeClient#getVideoLoadingProgressView()} to customize the View displayed whilst a video
- * is loading.
- *
- * <h3>HTML5 Geolocation API support</h3>
- *
- * <p>For applications targeting Android N and later releases
- * (API level > {@link android.os.Build.VERSION_CODES#M}) the geolocation api is only supported on
- * secure origins such as https. For such applications requests to geolocation api on non-secure
- * origins are automatically denied without invoking the corresponding
- * {@link WebChromeClient#onGeolocationPermissionsShowPrompt(String, GeolocationPermissions.Callback)}
- * method.
- *
- * <h3>Layout size</h3>
- * <p>
- * It is recommended to set the WebView layout height to a fixed value or to
- * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} instead of using
- * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.
- * When using {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
- * for the height none of the WebView's parents should use a
- * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} layout height since that could result in
- * incorrect sizing of the views.
- *
- * <p>Setting the WebView's height to {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
- * enables the following behaviors:
- * <ul>
- * <li>The HTML body layout height is set to a fixed value. This means that elements with a height
- * relative to the HTML body may not be sized correctly. </li>
- * <li>For applications targeting {@link android.os.Build.VERSION_CODES#KITKAT} and earlier SDKs the
- * HTML viewport meta tag will be ignored in order to preserve backwards compatibility. </li>
- * </ul>
- *
- * <p>
- * Using a layout width of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} is not
- * supported. If such a width is used the WebView will attempt to use the width of the parent
- * instead.
- *
- * <h3>Metrics</h3>
- *
- * <p>
- * WebView may upload anonymous diagnostic data to Google when the user has consented. This data
- * helps Google improve WebView. Data is collected on a per-app basis for each app which has
- * instantiated a WebView. An individual app can opt out of this feature by putting the following
- * tag in its manifest's {@code <application>} element:
- * <pre>
- * &lt;manifest&gt;
- * &lt;application&gt;
- * ...
- * &lt;meta-data android:name=&quot;android.webkit.WebView.MetricsOptOut&quot;
- * android:value=&quot;true&quot; /&gt;
- * &lt;/application&gt;
- * &lt;/manifest&gt;
- * </pre>
- * <p>
- * Data will only be uploaded for a given app if the user has consented AND the app has not opted
- * out.
- *
- * <h3>Safe Browsing</h3>
- *
- * <p>
- * If Safe Browsing is enabled, WebView will block malicious URLs and present a warning UI to the
- * user to allow them to navigate back safely or proceed to the malicious page.
- * <p>
- * The recommended way for apps to enable the feature is putting the following tag in the manifest's
- * {@code <application>} element:
- * <p>
- * <pre>
- * &lt;manifest&gt;
- * &lt;application&gt;
- * ...
- * &lt;meta-data android:name=&quot;android.webkit.WebView.EnableSafeBrowsing&quot;
- * android:value=&quot;true&quot; /&gt;
- * &lt;/application&gt;
- * &lt;/manifest&gt;
- * </pre>
+ * Mock version of the WebView.
+ * Only non override public methods from the real WebView have been added in there.
+ * Methods that take an unknown class as parameter or as return object, have been removed for now.
+ *
+ * TODO: generate automatically.
*
*/
-// Implementation notes.
-// The WebView is a thin API class that delegates its public API to a backend WebViewProvider
-// class instance. WebView extends {@link AbsoluteLayout} for backward compatibility reasons.
-// Methods are delegated to the provider implementation: all public API methods introduced in this
-// file are fully delegated, whereas public and protected methods from the View base classes are
-// only delegated where a specific need exists for them to do so.
-@Widget
-public class WebView extends AbsoluteLayout
- implements ViewTreeObserver.OnGlobalFocusChangeListener,
- ViewGroup.OnHierarchyChangeListener, ViewDebug.HierarchyHandler {
-
- private static final String LOGTAG = "WebView";
-
- // Throwing an exception for incorrect thread usage if the
- // build target is JB MR2 or newer. Defaults to false, and is
- // set in the WebView constructor.
- private static volatile boolean sEnforceThreadChecking = false;
-
- /**
- * Transportation object for returning WebView across thread boundaries.
- */
- public class WebViewTransport {
- private WebView mWebview;
+public class WebView extends MockView {
- /**
- * Sets the WebView to the transportation object.
- *
- * @param webview the WebView to transport
- */
- public synchronized void setWebView(WebView webview) {
- mWebview = webview;
- }
-
- /**
- * Gets the WebView object.
- *
- * @return the transported WebView object
- */
- public synchronized WebView getWebView() {
- return mWebview;
- }
- }
-
- /**
- * URI scheme for telephone number.
- */
- public static final String SCHEME_TEL = "tel:";
/**
- * URI scheme for email address.
- */
- public static final String SCHEME_MAILTO = "mailto:";
- /**
- * URI scheme for map address.
- */
- public static final String SCHEME_GEO = "geo:0,0?q=";
-
- /**
- * Interface to listen for find results.
- */
- public interface FindListener {
- /**
- * Notifies the listener about progress made by a find operation.
- *
- * @param activeMatchOrdinal the zero-based ordinal of the currently selected match
- * @param numberOfMatches how many matches have been found
- * @param isDoneCounting whether the find operation has actually completed. The listener
- * may be notified multiple times while the
- * operation is underway, and the numberOfMatches
- * value should not be considered final unless
- * isDoneCounting is {@code true}.
- */
- public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches,
- boolean isDoneCounting);
- }
-
- /**
- * Callback interface supplied to {@link #postVisualStateCallback} for receiving
- * notifications about the visual state.
- */
- public static abstract class VisualStateCallback {
- /**
- * Invoked when the visual state is ready to be drawn in the next {@link #onDraw}.
- *
- * @param requestId The identifier passed to {@link #postVisualStateCallback} when this
- * callback was posted.
- */
- public abstract void onComplete(long requestId);
- }
-
- /**
- * Interface to listen for new pictures as they change.
- *
- * @deprecated This interface is now obsolete.
- */
- @Deprecated
- public interface PictureListener {
- /**
- * Used to provide notification that the WebView's picture has changed.
- * See {@link WebView#capturePicture} for details of the picture.
- *
- * @param view the WebView that owns the picture
- * @param picture the new picture. Applications targeting
- * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} or above
- * will always receive a {@code null} Picture.
- * @deprecated Deprecated due to internal changes.
- */
- @Deprecated
- void onNewPicture(WebView view, @Nullable Picture picture);
- }
-
- public static class HitTestResult {
- /**
- * Default HitTestResult, where the target is unknown.
- */
- public static final int UNKNOWN_TYPE = 0;
- /**
- * @deprecated This type is no longer used.
- */
- @Deprecated
- public static final int ANCHOR_TYPE = 1;
- /**
- * HitTestResult for hitting a phone number.
- */
- public static final int PHONE_TYPE = 2;
- /**
- * HitTestResult for hitting a map address.
- */
- public static final int GEO_TYPE = 3;
- /**
- * HitTestResult for hitting an email address.
- */
- public static final int EMAIL_TYPE = 4;
- /**
- * HitTestResult for hitting an HTML::img tag.
- */
- public static final int IMAGE_TYPE = 5;
- /**
- * @deprecated This type is no longer used.
- */
- @Deprecated
- public static final int IMAGE_ANCHOR_TYPE = 6;
- /**
- * HitTestResult for hitting a HTML::a tag with src=http.
- */
- public static final int SRC_ANCHOR_TYPE = 7;
- /**
- * HitTestResult for hitting a HTML::a tag with src=http + HTML::img.
- */
- public static final int SRC_IMAGE_ANCHOR_TYPE = 8;
- /**
- * HitTestResult for hitting an edit text area.
- */
- public static final int EDIT_TEXT_TYPE = 9;
-
- private int mType;
- private String mExtra;
-
- /**
- * @hide Only for use by WebViewProvider implementations
- */
- @SystemApi
- public HitTestResult() {
- mType = UNKNOWN_TYPE;
- }
-
- /**
- * @hide Only for use by WebViewProvider implementations
- */
- @SystemApi
- public void setType(int type) {
- mType = type;
- }
-
- /**
- * @hide Only for use by WebViewProvider implementations
- */
- @SystemApi
- public void setExtra(String extra) {
- mExtra = extra;
- }
-
- /**
- * Gets the type of the hit test result. See the XXX_TYPE constants
- * defined in this class.
- *
- * @return the type of the hit test result
- */
- public int getType() {
- return mType;
- }
-
- /**
- * Gets additional type-dependant information about the result. See
- * {@link WebView#getHitTestResult()} for details. May either be {@code null}
- * or contain extra information about this result.
- *
- * @return additional type-dependant information about the result
- */
- @Nullable
- public String getExtra() {
- return mExtra;
- }
- }
-
- /**
- * Constructs a new WebView with an Activity Context object.
- *
- * <p class="note"><b>Note:</b> WebView should always be instantiated with an Activity Context.
- * If instantiated with an Application Context, WebView will be unable to provide several
- * features, such as JavaScript dialogs and autofill.
- *
- * @param context an Activity Context to access application assets
+ * Construct a new WebView with a Context object.
+ * @param context A Context object used to access application assets.
*/
public WebView(Context context) {
this(context, null);
}
/**
- * Constructs a new WebView with layout parameters.
- *
- * @param context an Activity Context to access application assets
- * @param attrs an AttributeSet passed to our parent
+ * Construct a new WebView with layout parameters.
+ * @param context A Context object used to access application assets.
+ * @param attrs An AttributeSet passed to our parent.
*/
public WebView(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.webViewStyle);
}
/**
- * Constructs a new WebView with layout parameters and a default style.
- *
- * @param context an Activity Context to access application assets
- * @param attrs an AttributeSet passed to our parent
- * @param defStyleAttr an attribute in the current theme that contains a
- * reference to a style resource that supplies default values for
- * the view. Can be 0 to not look for defaults.
+ * Construct a new WebView with layout parameters and a default style.
+ * @param context A Context object used to access application assets.
+ * @param attrs An AttributeSet passed to our parent.
+ * @param defStyle The default style resource ID.
*/
- public WebView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
+ public WebView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
}
-
- /**
- * Constructs a new WebView with layout parameters and a default style.
- *
- * @param context an Activity Context to access application assets
- * @param attrs an AttributeSet passed to our parent
- * @param defStyleAttr an attribute in the current theme that contains a
- * reference to a style resource that supplies default values for
- * the view. Can be 0 to not look for defaults.
- * @param defStyleRes a resource identifier of a style resource that
- * supplies default values for the view, used only if
- * defStyleAttr is 0 or can not be found in the theme. Can be 0
- * to not look for defaults.
- */
- public WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- this(context, attrs, defStyleAttr, defStyleRes, null, false);
- }
-
- /**
- * Constructs a new WebView with layout parameters and a default style.
- *
- * @param context an Activity Context to access application assets
- * @param attrs an AttributeSet passed to our parent
- * @param defStyleAttr an attribute in the current theme that contains a
- * reference to a style resource that supplies default values for
- * the view. Can be 0 to not look for defaults.
- * @param privateBrowsing whether this WebView will be initialized in
- * private mode
- *
- * @deprecated Private browsing is no longer supported directly via
- * WebView and will be removed in a future release. Prefer using
- * {@link WebSettings}, {@link WebViewDatabase}, {@link CookieManager}
- * and {@link WebStorage} for fine-grained control of privacy data.
- */
- @Deprecated
- public WebView(Context context, AttributeSet attrs, int defStyleAttr,
- boolean privateBrowsing) {
- this(context, attrs, defStyleAttr, 0, null, privateBrowsing);
- }
-
- /**
- * Constructs a new WebView with layout parameters, a default style and a set
- * of custom JavaScript interfaces to be added to this WebView at initialization
- * time. This guarantees that these interfaces will be available when the JS
- * context is initialized.
- *
- * @param context an Activity Context to access application assets
- * @param attrs an AttributeSet passed to our parent
- * @param defStyleAttr an attribute in the current theme that contains a
- * reference to a style resource that supplies default values for
- * the view. Can be 0 to not look for defaults.
- * @param javaScriptInterfaces a Map of interface names, as keys, and
- * object implementing those interfaces, as
- * values
- * @param privateBrowsing whether this WebView will be initialized in
- * private mode
- * @hide This is used internally by dumprendertree, as it requires the JavaScript interfaces to
- * be added synchronously, before a subsequent loadUrl call takes effect.
- */
- protected WebView(Context context, AttributeSet attrs, int defStyleAttr,
- Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) {
- this(context, attrs, defStyleAttr, 0, javaScriptInterfaces, privateBrowsing);
- }
-
- /**
- * @hide
- */
- @SuppressWarnings("deprecation") // for super() call into deprecated base class constructor.
- protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes,
- Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) {
- super(context, attrs, defStyleAttr, defStyleRes);
-
- // WebView is important by default, unless app developer overrode attribute.
- if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
- setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
- }
-
- if (context == null) {
- throw new IllegalArgumentException("Invalid context argument");
- }
- sEnforceThreadChecking = context.getApplicationInfo().targetSdkVersion >=
- Build.VERSION_CODES.JELLY_BEAN_MR2;
- checkThread();
-
- ensureProviderCreated();
- mProvider.init(javaScriptInterfaces, privateBrowsing);
- // Post condition of creating a webview is the CookieSyncManager.getInstance() is allowed.
- CookieSyncManager.setGetInstanceIsAllowed();
- }
-
- /**
- * Specifies whether the horizontal scrollbar has overlay style.
- *
- * @deprecated This method has no effect.
- * @param overlay {@code true} if horizontal scrollbar should have overlay style
- */
- @Deprecated
+
+ // START FAKE PUBLIC METHODS
+
public void setHorizontalScrollbarOverlay(boolean overlay) {
}
- /**
- * Specifies whether the vertical scrollbar has overlay style.
- *
- * @deprecated This method has no effect.
- * @param overlay {@code true} if vertical scrollbar should have overlay style
- */
- @Deprecated
public void setVerticalScrollbarOverlay(boolean overlay) {
}
- /**
- * Gets whether horizontal scrollbar has overlay style.
- *
- * @deprecated This method is now obsolete.
- * @return {@code true}
- */
- @Deprecated
public boolean overlayHorizontalScrollbar() {
- // The old implementation defaulted to true, so return true for consistency
- return true;
+ return false;
}
- /**
- * Gets whether vertical scrollbar has overlay style.
- *
- * @deprecated This method is now obsolete.
- * @return {@code false}
- */
- @Deprecated
public boolean overlayVerticalScrollbar() {
- // The old implementation defaulted to false, so return false for consistency
return false;
}
- /**
- * Gets the visible height (in pixels) of the embedded title bar (if any).
- *
- * @deprecated This method is now obsolete.
- * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
- */
- @Deprecated
- public int getVisibleTitleHeight() {
- checkThread();
- return mProvider.getVisibleTitleHeight();
- }
-
- /**
- * Gets the SSL certificate for the main top-level page or {@code null} if there is
- * no certificate (the site is not secure).
- *
- * @return the SSL certificate for the main top-level page
- */
- @Nullable
- public SslCertificate getCertificate() {
- checkThread();
- return mProvider.getCertificate();
- }
-
- /**
- * Sets the SSL certificate for the main top-level page.
- *
- * @deprecated Calling this function has no useful effect, and will be
- * ignored in future releases.
- */
- @Deprecated
- public void setCertificate(SslCertificate certificate) {
- checkThread();
- mProvider.setCertificate(certificate);
- }
-
- //-------------------------------------------------------------------------
- // Methods called by activity
- //-------------------------------------------------------------------------
-
- /**
- * Sets a username and password pair for the specified host. This data is
- * used by the WebView to autocomplete username and password fields in web
- * forms. Note that this is unrelated to the credentials used for HTTP
- * authentication.
- *
- * @param host the host that required the credentials
- * @param username the username for the given host
- * @param password the password for the given host
- * @see WebViewDatabase#clearUsernamePassword
- * @see WebViewDatabase#hasUsernamePassword
- * @deprecated Saving passwords in WebView will not be supported in future versions.
- */
- @Deprecated
public void savePassword(String host, String username, String password) {
- checkThread();
- mProvider.savePassword(host, username, password);
}
- /**
- * Stores HTTP authentication credentials for a given host and realm to the {@link WebViewDatabase}
- * instance.
- *
- * @param host the host to which the credentials apply
- * @param realm the realm to which the credentials apply
- * @param username the username
- * @param password the password
- * @deprecated Use {@link WebViewDatabase#setHttpAuthUsernamePassword} instead
- */
- @Deprecated
public void setHttpAuthUsernamePassword(String host, String realm,
String username, String password) {
- checkThread();
- mProvider.setHttpAuthUsernamePassword(host, realm, username, password);
}
- /**
- * Retrieves HTTP authentication credentials for a given host and realm from the {@link
- * WebViewDatabase} instance.
- * @param host the host to which the credentials apply
- * @param realm the realm to which the credentials apply
- * @return the credentials as a String array, if found. The first element
- * is the username and the second element is the password. {@code null} if
- * no credentials are found.
- * @deprecated Use {@link WebViewDatabase#getHttpAuthUsernamePassword} instead
- */
- @Deprecated
- @Nullable
public String[] getHttpAuthUsernamePassword(String host, String realm) {
- checkThread();
- return mProvider.getHttpAuthUsernamePassword(host, realm);
+ return null;
}
- /**
- * Destroys the internal state of this WebView. This method should be called
- * after this WebView has been removed from the view system. No other
- * methods may be called on this WebView after destroy.
- */
public void destroy() {
- checkThread();
- mProvider.destroy();
}
- /**
- * Enables platform notifications of data state and proxy changes.
- * Notifications are enabled by default.
- *
- * @deprecated This method is now obsolete.
- * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
- */
- @Deprecated
public static void enablePlatformNotifications() {
- // noop
}
- /**
- * Disables platform notifications of data state and proxy changes.
- * Notifications are enabled by default.
- *
- * @deprecated This method is now obsolete.
- * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
- */
- @Deprecated
public static void disablePlatformNotifications() {
- // noop
}
- /**
- * Used only by internal tests to free up memory.
- *
- * @hide
- */
- public static void freeMemoryForTests() {
- getFactory().getStatics().freeMemoryForTests();
- }
-
- /**
- * Informs WebView of the network state. This is used to set
- * the JavaScript property window.navigator.isOnline and
- * generates the online/offline event as specified in HTML5, sec. 5.7.7
- *
- * @param networkUp a boolean indicating if network is available
- */
- public void setNetworkAvailable(boolean networkUp) {
- checkThread();
- mProvider.setNetworkAvailable(networkUp);
- }
-
- /**
- * Saves the state of this WebView used in
- * {@link android.app.Activity#onSaveInstanceState}. Please note that this
- * method no longer stores the display data for this WebView. The previous
- * behavior could potentially leak files if {@link #restoreState} was never
- * called.
- *
- * @param outState the Bundle to store this WebView's state
- * @return the same copy of the back/forward list used to save the state, {@code null} if the
- * method fails.
- */
- @Nullable
- public WebBackForwardList saveState(Bundle outState) {
- checkThread();
- return mProvider.saveState(outState);
- }
-
- /**
- * Saves the current display data to the Bundle given. Used in conjunction
- * with {@link #saveState}.
- * @param b a Bundle to store the display data
- * @param dest the file to store the serialized picture data. Will be
- * overwritten with this WebView's picture data.
- * @return {@code true} if the picture was successfully saved
- * @deprecated This method is now obsolete.
- * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
- */
- @Deprecated
- public boolean savePicture(Bundle b, final File dest) {
- checkThread();
- return mProvider.savePicture(b, dest);
- }
-
- /**
- * Restores the display data that was saved in {@link #savePicture}. Used in
- * conjunction with {@link #restoreState}. Note that this will not work if
- * this WebView is hardware accelerated.
- *
- * @param b a Bundle containing the saved display data
- * @param src the file where the picture data was stored
- * @return {@code true} if the picture was successfully restored
- * @deprecated This method is now obsolete.
- * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
- */
- @Deprecated
- public boolean restorePicture(Bundle b, File src) {
- checkThread();
- return mProvider.restorePicture(b, src);
- }
-
- /**
- * Restores the state of this WebView from the given Bundle. This method is
- * intended for use in {@link android.app.Activity#onRestoreInstanceState}
- * and should be called to restore the state of this WebView. If
- * it is called after this WebView has had a chance to build state (load
- * pages, create a back/forward list, etc.) there may be undesirable
- * side-effects. Please note that this method no longer restores the
- * display data for this WebView.
- *
- * @param inState the incoming Bundle of state
- * @return the restored back/forward list or {@code null} if restoreState failed
- */
- @Nullable
- public WebBackForwardList restoreState(Bundle inState) {
- checkThread();
- return mProvider.restoreState(inState);
- }
-
- /**
- * Loads the given URL with the specified additional HTTP headers.
- * <p>
- * Also see compatibility note on {@link #evaluateJavascript}.
- *
- * @param url the URL of the resource to load
- * @param additionalHttpHeaders the additional headers to be used in the
- * HTTP request for this URL, specified as a map from name to
- * value. Note that if this map contains any of the headers
- * that are set by default by this WebView, such as those
- * controlling caching, accept types or the User-Agent, their
- * values may be overridden by this WebView's defaults.
- */
- public void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
- checkThread();
- mProvider.loadUrl(url, additionalHttpHeaders);
- }
-
- /**
- * Loads the given URL.
- * <p>
- * Also see compatibility note on {@link #evaluateJavascript}.
- *
- * @param url the URL of the resource to load
- */
public void loadUrl(String url) {
- checkThread();
- mProvider.loadUrl(url);
- }
-
- /**
- * Loads the URL with postData using "POST" method into this WebView. If url
- * is not a network URL, it will be loaded with {@link #loadUrl(String)}
- * instead, ignoring the postData param.
- *
- * @param url the URL of the resource to load
- * @param postData the data will be passed to "POST" request, which must be
- * be "application/x-www-form-urlencoded" encoded.
- */
- public void postUrl(String url, byte[] postData) {
- checkThread();
- if (URLUtil.isNetworkUrl(url)) {
- mProvider.postUrl(url, postData);
- } else {
- mProvider.loadUrl(url);
- }
}
- /**
- * Loads the given data into this WebView using a 'data' scheme URL.
- * <p>
- * Note that JavaScript's same origin policy means that script running in a
- * page loaded using this method will be unable to access content loaded
- * using any scheme other than 'data', including 'http(s)'. To avoid this
- * restriction, use {@link
- * #loadDataWithBaseURL(String,String,String,String,String)
- * loadDataWithBaseURL()} with an appropriate base URL.
- * <p>
- * The encoding parameter specifies whether the data is base64 or URL
- * encoded. If the data is base64 encoded, the value of the encoding
- * parameter must be 'base64'. For all other values of the parameter,
- * including {@code null}, it is assumed that the data uses ASCII encoding for
- * octets inside the range of safe URL characters and use the standard %xx
- * hex encoding of URLs for octets outside that range. For example, '#',
- * '%', '\', '?' should be replaced by %23, %25, %27, %3f respectively.
- * <p>
- * The 'data' scheme URL formed by this method uses the default US-ASCII
- * charset. If you need need to set a different charset, you should form a
- * 'data' scheme URL which explicitly specifies a charset parameter in the
- * mediatype portion of the URL and call {@link #loadUrl(String)} instead.
- * Note that the charset obtained from the mediatype portion of a data URL
- * always overrides that specified in the HTML or XML document itself.
- *
- * @param data a String of data in the given encoding
- * @param mimeType the MIMEType of the data, e.g. 'text/html'. If {@code null},
- * defaults to 'text/html'.
- * @param encoding the encoding of the data
- */
- public void loadData(String data, @Nullable String mimeType, @Nullable String encoding) {
- checkThread();
- mProvider.loadData(data, mimeType, encoding);
+ public void loadData(String data, String mimeType, String encoding) {
}
- /**
- * Loads the given data into this WebView, using baseUrl as the base URL for
- * the content. The base URL is used both to resolve relative URLs and when
- * applying JavaScript's same origin policy. The historyUrl is used for the
- * history entry.
- * <p>
- * Note that content specified in this way can access local device files
- * (via 'file' scheme URLs) only if baseUrl specifies a scheme other than
- * 'http', 'https', 'ftp', 'ftps', 'about' or 'javascript'.
- * <p>
- * If the base URL uses the data scheme, this method is equivalent to
- * calling {@link #loadData(String,String,String) loadData()} and the
- * historyUrl is ignored, and the data will be treated as part of a data: URL.
- * If the base URL uses any other scheme, then the data will be loaded into
- * the WebView as a plain string (i.e. not part of a data URL) and any URL-encoded
- * entities in the string will not be decoded.
- * <p>
- * Note that the baseUrl is sent in the 'Referer' HTTP header when
- * requesting subresources (images, etc.) of the page loaded using this method.
- *
- * @param baseUrl the URL to use as the page's base URL. If {@code null} defaults to
- * 'about:blank'.
- * @param data a String of data in the given encoding
- * @param mimeType the MIMEType of the data, e.g. 'text/html'. If {@code null},
- * defaults to 'text/html'.
- * @param encoding the encoding of the data
- * @param historyUrl the URL to use as the history entry. If {@code null} defaults
- * to 'about:blank'. If non-null, this must be a valid URL.
- */
- public void loadDataWithBaseURL(@Nullable String baseUrl, String data,
- @Nullable String mimeType, @Nullable String encoding, @Nullable String historyUrl) {
- checkThread();
- mProvider.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
+ public void loadDataWithBaseURL(String baseUrl, String data,
+ String mimeType, String encoding, String failUrl) {
}
- /**
- * Asynchronously evaluates JavaScript in the context of the currently displayed page.
- * If non-null, |resultCallback| will be invoked with any result returned from that
- * execution. This method must be called on the UI thread and the callback will
- * be made on the UI thread.
- * <p>
- * Compatibility note. Applications targeting {@link android.os.Build.VERSION_CODES#N} or
- * later, JavaScript state from an empty WebView is no longer persisted across navigations like
- * {@link #loadUrl(String)}. For example, global variables and functions defined before calling
- * {@link #loadUrl(String)} will not exist in the loaded page. Applications should use
- * {@link #addJavascriptInterface} instead to persist JavaScript objects across navigations.
- *
- * @param script the JavaScript to execute.
- * @param resultCallback A callback to be invoked when the script execution
- * completes with the result of the execution (if any).
- * May be {@code null} if no notification of the result is required.
- */
- public void evaluateJavascript(String script, @Nullable ValueCallback<String> resultCallback) {
- checkThread();
- mProvider.evaluateJavaScript(script, resultCallback);
- }
-
- /**
- * Saves the current view as a web archive.
- *
- * @param filename the filename where the archive should be placed
- */
- public void saveWebArchive(String filename) {
- checkThread();
- mProvider.saveWebArchive(filename);
- }
-
- /**
- * Saves the current view as a web archive.
- *
- * @param basename the filename where the archive should be placed
- * @param autoname if {@code false}, takes basename to be a file. If {@code true}, basename
- * is assumed to be a directory in which a filename will be
- * chosen according to the URL of the current page.
- * @param callback called after the web archive has been saved. The
- * parameter for onReceiveValue will either be the filename
- * under which the file was saved, or {@code null} if saving the
- * file failed.
- */
- public void saveWebArchive(String basename, boolean autoname, @Nullable ValueCallback<String>
- callback) {
- checkThread();
- mProvider.saveWebArchive(basename, autoname, callback);
- }
-
- /**
- * Stops the current load.
- */
public void stopLoading() {
- checkThread();
- mProvider.stopLoading();
}
- /**
- * Reloads the current URL.
- */
public void reload() {
- checkThread();
- mProvider.reload();
}
- /**
- * Gets whether this WebView has a back history item.
- *
- * @return {@code true} if this WebView has a back history item
- */
public boolean canGoBack() {
- checkThread();
- return mProvider.canGoBack();
+ return false;
}
- /**
- * Goes back in the history of this WebView.
- */
public void goBack() {
- checkThread();
- mProvider.goBack();
}
- /**
- * Gets whether this WebView has a forward history item.
- *
- * @return {@code true} if this WebView has a forward history item
- */
public boolean canGoForward() {
- checkThread();
- return mProvider.canGoForward();
+ return false;
}
- /**
- * Goes forward in the history of this WebView.
- */
public void goForward() {
- checkThread();
- mProvider.goForward();
}
- /**
- * Gets whether the page can go back or forward the given
- * number of steps.
- *
- * @param steps the negative or positive number of steps to move the
- * history
- */
public boolean canGoBackOrForward(int steps) {
- checkThread();
- return mProvider.canGoBackOrForward(steps);
+ return false;
}
- /**
- * Goes to the history item that is the number of steps away from
- * the current item. Steps is negative if backward and positive
- * if forward.
- *
- * @param steps the number of steps to take back or forward in the back
- * forward list
- */
public void goBackOrForward(int steps) {
- checkThread();
- mProvider.goBackOrForward(steps);
- }
-
- /**
- * Gets whether private browsing is enabled in this WebView.
- */
- public boolean isPrivateBrowsingEnabled() {
- checkThread();
- return mProvider.isPrivateBrowsingEnabled();
}
- /**
- * Scrolls the contents of this WebView up by half the view size.
- *
- * @param top {@code true} to jump to the top of the page
- * @return {@code true} if the page was scrolled
- */
public boolean pageUp(boolean top) {
- checkThread();
- return mProvider.pageUp(top);
+ return false;
}
-
- /**
- * Scrolls the contents of this WebView down by half the page size.
- *
- * @param bottom {@code true} to jump to bottom of page
- * @return {@code true} if the page was scrolled
- */
+
public boolean pageDown(boolean bottom) {
- checkThread();
- return mProvider.pageDown(bottom);
- }
-
- /**
- * Posts a {@link VisualStateCallback}, which will be called when
- * the current state of the WebView is ready to be drawn.
- *
- * <p>Because updates to the DOM are processed asynchronously, updates to the DOM may not
- * immediately be reflected visually by subsequent {@link WebView#onDraw} invocations. The
- * {@link VisualStateCallback} provides a mechanism to notify the caller when the contents of
- * the DOM at the current time are ready to be drawn the next time the {@link WebView}
- * draws.
- *
- * <p>The next draw after the callback completes is guaranteed to reflect all the updates to the
- * DOM up to the point at which the {@link VisualStateCallback} was posted, but it may also
- * contain updates applied after the callback was posted.
- *
- * <p>The state of the DOM covered by this API includes the following:
- * <ul>
- * <li>primitive HTML elements (div, img, span, etc..)</li>
- * <li>images</li>
- * <li>CSS animations</li>
- * <li>WebGL</li>
- * <li>canvas</li>
- * </ul>
- * It does not include the state of:
- * <ul>
- * <li>the video tag</li>
- * </ul>
- *
- * <p>To guarantee that the {@link WebView} will successfully render the first frame
- * after the {@link VisualStateCallback#onComplete} method has been called a set of conditions
- * must be met:
- * <ul>
- * <li>If the {@link WebView}'s visibility is set to {@link View#VISIBLE VISIBLE} then
- * the {@link WebView} must be attached to the view hierarchy.</li>
- * <li>If the {@link WebView}'s visibility is set to {@link View#INVISIBLE INVISIBLE}
- * then the {@link WebView} must be attached to the view hierarchy and must be made
- * {@link View#VISIBLE VISIBLE} from the {@link VisualStateCallback#onComplete} method.</li>
- * <li>If the {@link WebView}'s visibility is set to {@link View#GONE GONE} then the
- * {@link WebView} must be attached to the view hierarchy and its
- * {@link AbsoluteLayout.LayoutParams LayoutParams}'s width and height need to be set to fixed
- * values and must be made {@link View#VISIBLE VISIBLE} from the
- * {@link VisualStateCallback#onComplete} method.</li>
- * </ul>
- *
- * <p>When using this API it is also recommended to enable pre-rasterization if the {@link
- * WebView} is off screen to avoid flickering. See {@link WebSettings#setOffscreenPreRaster} for
- * more details and do consider its caveats.
- *
- * @param requestId An id that will be returned in the callback to allow callers to match
- * requests with callbacks.
- * @param callback The callback to be invoked.
- */
- public void postVisualStateCallback(long requestId, VisualStateCallback callback) {
- checkThread();
- mProvider.insertVisualStateCallback(requestId, callback);
+ return false;
}
- /**
- * Clears this WebView so that onDraw() will draw nothing but white background,
- * and onMeasure() will return 0 if MeasureSpec is not MeasureSpec.EXACTLY.
- * @deprecated Use WebView.loadUrl("about:blank") to reliably reset the view state
- * and release page resources (including any running JavaScript).
- */
- @Deprecated
public void clearView() {
- checkThread();
- mProvider.clearView();
}
-
- /**
- * Gets a new picture that captures the current contents of this WebView.
- * The picture is of the entire document being displayed, and is not
- * limited to the area currently displayed by this WebView. Also, the
- * picture is a static copy and is unaffected by later changes to the
- * content being displayed.
- * <p>
- * Note that due to internal changes, for API levels between
- * {@link android.os.Build.VERSION_CODES#HONEYCOMB} and
- * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH} inclusive, the
- * picture does not include fixed position elements or scrollable divs.
- * <p>
- * Note that from {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} the returned picture
- * should only be drawn into bitmap-backed Canvas - using any other type of Canvas will involve
- * additional conversion at a cost in memory and performance. Also the
- * {@link android.graphics.Picture#createFromStream} and
- * {@link android.graphics.Picture#writeToStream} methods are not supported on the
- * returned object.
- *
- * @deprecated Use {@link #onDraw} to obtain a bitmap snapshot of the WebView, or
- * {@link #saveWebArchive} to save the content to a file.
- *
- * @return a picture that captures the current contents of this WebView
- */
- @Deprecated
+
public Picture capturePicture() {
- checkThread();
- return mProvider.capturePicture();
- }
-
- /**
- * @deprecated Use {@link #createPrintDocumentAdapter(String)} which requires user
- * to provide a print document name.
- */
- @Deprecated
- public PrintDocumentAdapter createPrintDocumentAdapter() {
- checkThread();
- return mProvider.createPrintDocumentAdapter("default");
+ return null;
}
- /**
- * Creates a PrintDocumentAdapter that provides the content of this WebView for printing.
- *
- * The adapter works by converting the WebView contents to a PDF stream. The WebView cannot
- * be drawn during the conversion process - any such draws are undefined. It is recommended
- * to use a dedicated off screen WebView for the printing. If necessary, an application may
- * temporarily hide a visible WebView by using a custom PrintDocumentAdapter instance
- * wrapped around the object returned and observing the onStart and onFinish methods. See
- * {@link android.print.PrintDocumentAdapter} for more information.
- *
- * @param documentName The user-facing name of the printed document. See
- * {@link android.print.PrintDocumentInfo}
- */
- public PrintDocumentAdapter createPrintDocumentAdapter(String documentName) {
- checkThread();
- return mProvider.createPrintDocumentAdapter(documentName);
- }
-
- /**
- * Gets the current scale of this WebView.
- *
- * @return the current scale
- *
- * @deprecated This method is prone to inaccuracy due to race conditions
- * between the web rendering and UI threads; prefer
- * {@link WebViewClient#onScaleChanged}.
- */
- @Deprecated
- @ViewDebug.ExportedProperty(category = "webview")
public float getScale() {
- checkThread();
- return mProvider.getScale();
+ return 0;
}
- /**
- * Sets the initial scale for this WebView. 0 means default.
- * The behavior for the default scale depends on the state of
- * {@link WebSettings#getUseWideViewPort()} and
- * {@link WebSettings#getLoadWithOverviewMode()}.
- * If the content fits into the WebView control by width, then
- * the zoom is set to 100%. For wide content, the behavior
- * depends on the state of {@link WebSettings#getLoadWithOverviewMode()}.
- * If its value is {@code true}, the content will be zoomed out to be fit
- * by width into the WebView control, otherwise not.
- *
- * If initial scale is greater than 0, WebView starts with this value
- * as initial scale.
- * Please note that unlike the scale properties in the viewport meta tag,
- * this method doesn't take the screen density into account.
- *
- * @param scaleInPercent the initial scale in percent
- */
public void setInitialScale(int scaleInPercent) {
- checkThread();
- mProvider.setInitialScale(scaleInPercent);
}
- /**
- * Invokes the graphical zoom picker widget for this WebView. This will
- * result in the zoom widget appearing on the screen to control the zoom
- * level of this WebView.
- */
public void invokeZoomPicker() {
- checkThread();
- mProvider.invokeZoomPicker();
- }
-
- /**
- * Gets a HitTestResult based on the current cursor node. If a HTML::a
- * tag is found and the anchor has a non-JavaScript URL, the HitTestResult
- * type is set to SRC_ANCHOR_TYPE and the URL is set in the "extra" field.
- * If the anchor does not have a URL or if it is a JavaScript URL, the type
- * will be UNKNOWN_TYPE and the URL has to be retrieved through
- * {@link #requestFocusNodeHref} asynchronously. If a HTML::img tag is
- * found, the HitTestResult type is set to IMAGE_TYPE and the URL is set in
- * the "extra" field. A type of
- * SRC_IMAGE_ANCHOR_TYPE indicates an anchor with a URL that has an image as
- * a child node. If a phone number is found, the HitTestResult type is set
- * to PHONE_TYPE and the phone number is set in the "extra" field of
- * HitTestResult. If a map address is found, the HitTestResult type is set
- * to GEO_TYPE and the address is set in the "extra" field of HitTestResult.
- * If an email address is found, the HitTestResult type is set to EMAIL_TYPE
- * and the email is set in the "extra" field of HitTestResult. Otherwise,
- * HitTestResult type is set to UNKNOWN_TYPE.
- */
- public HitTestResult getHitTestResult() {
- checkThread();
- return mProvider.getHitTestResult();
}
- /**
- * Requests the anchor or image element URL at the last tapped point.
- * If hrefMsg is {@code null}, this method returns immediately and does not
- * dispatch hrefMsg to its target. If the tapped point hits an image,
- * an anchor, or an image in an anchor, the message associates
- * strings in named keys in its data. The value paired with the key
- * may be an empty string.
- *
- * @param hrefMsg the message to be dispatched with the result of the
- * request. The message data contains three keys. "url"
- * returns the anchor's href attribute. "title" returns the
- * anchor's text. "src" returns the image's src attribute.
- */
- public void requestFocusNodeHref(@Nullable Message hrefMsg) {
- checkThread();
- mProvider.requestFocusNodeHref(hrefMsg);
+ public void requestFocusNodeHref(Message hrefMsg) {
}
- /**
- * Requests the URL of the image last touched by the user. msg will be sent
- * to its target with a String representing the URL as its object.
- *
- * @param msg the message to be dispatched with the result of the request
- * as the data member with "url" as key. The result can be {@code null}.
- */
public void requestImageRef(Message msg) {
- checkThread();
- mProvider.requestImageRef(msg);
}
- /**
- * Gets the URL for the current page. This is not always the same as the URL
- * passed to WebViewClient.onPageStarted because although the load for
- * that URL has begun, the current page may not have changed.
- *
- * @return the URL for the current page
- */
- @ViewDebug.ExportedProperty(category = "webview")
public String getUrl() {
- checkThread();
- return mProvider.getUrl();
+ return null;
}
- /**
- * Gets the original URL for the current page. This is not always the same
- * as the URL passed to WebViewClient.onPageStarted because although the
- * load for that URL has begun, the current page may not have changed.
- * Also, there may have been redirects resulting in a different URL to that
- * originally requested.
- *
- * @return the URL that was originally requested for the current page
- */
- @ViewDebug.ExportedProperty(category = "webview")
- public String getOriginalUrl() {
- checkThread();
- return mProvider.getOriginalUrl();
- }
-
- /**
- * Gets the title for the current page. This is the title of the current page
- * until WebViewClient.onReceivedTitle is called.
- *
- * @return the title for the current page
- */
- @ViewDebug.ExportedProperty(category = "webview")
public String getTitle() {
- checkThread();
- return mProvider.getTitle();
+ return null;
}
- /**
- * Gets the favicon for the current page. This is the favicon of the current
- * page until WebViewClient.onReceivedIcon is called.
- *
- * @return the favicon for the current page
- */
public Bitmap getFavicon() {
- checkThread();
- return mProvider.getFavicon();
+ return null;
}
- /**
- * Gets the touch icon URL for the apple-touch-icon <link> element, or
- * a URL on this site's server pointing to the standard location of a
- * touch icon.
- *
- * @hide
- */
- public String getTouchIconUrl() {
- return mProvider.getTouchIconUrl();
- }
-
- /**
- * Gets the progress for the current page.
- *
- * @return the progress for the current page between 0 and 100
- */
public int getProgress() {
- checkThread();
- return mProvider.getProgress();
+ return 0;
}
-
- /**
- * Gets the height of the HTML content.
- *
- * @return the height of the HTML content
- */
- @ViewDebug.ExportedProperty(category = "webview")
+
public int getContentHeight() {
- checkThread();
- return mProvider.getContentHeight();
- }
-
- /**
- * Gets the width of the HTML content.
- *
- * @return the width of the HTML content
- * @hide
- */
- @ViewDebug.ExportedProperty(category = "webview")
- public int getContentWidth() {
- return mProvider.getContentWidth();
+ return 0;
}
- /**
- * Pauses all layout, parsing, and JavaScript timers for all WebViews. This
- * is a global requests, not restricted to just this WebView. This can be
- * useful if the application has been paused.
- */
public void pauseTimers() {
- checkThread();
- mProvider.pauseTimers();
}
- /**
- * Resumes all layout, parsing, and JavaScript timers for all WebViews.
- * This will resume dispatching all timers.
- */
public void resumeTimers() {
- checkThread();
- mProvider.resumeTimers();
- }
-
- /**
- * Does a best-effort attempt to pause any processing that can be paused
- * safely, such as animations and geolocation. Note that this call
- * does not pause JavaScript. To pause JavaScript globally, use
- * {@link #pauseTimers}.
- *
- * To resume WebView, call {@link #onResume}.
- */
- public void onPause() {
- checkThread();
- mProvider.onPause();
- }
-
- /**
- * Resumes a WebView after a previous call to {@link #onPause}.
- */
- public void onResume() {
- checkThread();
- mProvider.onResume();
- }
-
- /**
- * Gets whether this WebView is paused, meaning onPause() was called.
- * Calling onResume() sets the paused state back to {@code false}.
- *
- * @hide
- */
- public boolean isPaused() {
- return mProvider.isPaused();
- }
-
- /**
- * Informs this WebView that memory is low so that it can free any available
- * memory.
- * @deprecated Memory caches are automatically dropped when no longer needed, and in response
- * to system memory pressure.
- */
- @Deprecated
- public void freeMemory() {
- checkThread();
- mProvider.freeMemory();
}
- /**
- * Clears the resource cache. Note that the cache is per-application, so
- * this will clear the cache for all WebViews used.
- *
- * @param includeDiskFiles if {@code false}, only the RAM cache is cleared
- */
- public void clearCache(boolean includeDiskFiles) {
- checkThread();
- mProvider.clearCache(includeDiskFiles);
+ public void clearCache() {
}
- /**
- * Removes the autocomplete popup from the currently focused form field, if
- * present. Note this only affects the display of the autocomplete popup,
- * it does not remove any saved form data from this WebView's store. To do
- * that, use {@link WebViewDatabase#clearFormData}.
- */
public void clearFormData() {
- checkThread();
- mProvider.clearFormData();
}
- /**
- * Tells this WebView to clear its internal back/forward list.
- */
public void clearHistory() {
- checkThread();
- mProvider.clearHistory();
}
- /**
- * Clears the SSL preferences table stored in response to proceeding with
- * SSL certificate errors.
- */
public void clearSslPreferences() {
- checkThread();
- mProvider.clearSslPreferences();
- }
-
- /**
- * Clears the client certificate preferences stored in response
- * to proceeding/cancelling client cert requests. Note that WebView
- * automatically clears these preferences when it receives a
- * {@link KeyChain#ACTION_STORAGE_CHANGED} intent. The preferences are
- * shared by all the WebViews that are created by the embedder application.
- *
- * @param onCleared A runnable to be invoked when client certs are cleared.
- * The runnable will be called in UI thread.
- */
- public static void clearClientCertPreferences(@Nullable Runnable onCleared) {
- getFactory().getStatics().clearClientCertPreferences(onCleared);
- }
-
- /**
- * Starts Safe Browsing initialization.
- * <p>
- * URL loads are not guaranteed to be protected by Safe Browsing until after {@code callback} is
- * invoked with {@code true}. Safe Browsing is not fully supported on all devices. For those
- * devices {@code callback} will receive {@code false}.
- * <p>
- * This does not enable the Safe Browsing feature itself, and should only be called if Safe
- * Browsing is enabled by the manifest tag or {@link WebSettings#setSafeBrowsingEnabled}. This
- * prepares resources used for Safe Browsing.
- * <p>
- * This should be called with the Application Context (and will always use the Application
- * context to do its work regardless).
- *
- * @param context Application Context.
- * @param callback will be called on the UI thread with {@code true} if initialization is
- * successful, {@code false} otherwise.
- */
- public static void startSafeBrowsing(Context context,
- @Nullable ValueCallback<Boolean> callback) {
- getFactory().getStatics().initSafeBrowsing(context, callback);
- }
-
- /**
- * Sets the list of domains that are exempt from SafeBrowsing checks. The list is
- * global for all the WebViews.
- * <p>
- * Each rule should take one of these:
- * <table>
- * <tr><th> Rule </th> <th> Example </th> <th> Matches Subdomain</th> </tr>
- * <tr><td> HOSTNAME </td> <td> example.com </td> <td> Yes </td> </tr>
- * <tr><td> .HOSTNAME </td> <td> .example.com </td> <td> No </td> </tr>
- * <tr><td> IPV4_LITERAL </td> <td> 192.168.1.1 </td> <td> No </td></tr>
- * <tr><td> IPV6_LITERAL_WITH_BRACKETS </td><td>[10:20:30:40:50:60:70:80]</td><td>No</td></tr>
- * </table>
- * <p>
- * All other rules, including wildcards, are invalid.
- *
- * @param urls the list of URLs
- * @param callback will be called with {@code true} if URLs are successfully added to the
- * whitelist. It will be called with {@code false} if any URLs are malformed. The callback will
- * be run on the UI thread
- */
- public static void setSafeBrowsingWhitelist(@NonNull List<String> urls,
- @Nullable ValueCallback<Boolean> callback) {
- getFactory().getStatics().setSafeBrowsingWhitelist(urls, callback);
- }
-
- /**
- * Returns a URL pointing to the privacy policy for Safe Browsing reporting.
- *
- * @return the url pointing to a privacy policy document which can be displayed to users.
- */
- @NonNull
- public static Uri getSafeBrowsingPrivacyPolicyUrl() {
- return getFactory().getStatics().getSafeBrowsingPrivacyPolicyUrl();
- }
-
- /**
- * Gets the WebBackForwardList for this WebView. This contains the
- * back/forward list for use in querying each item in the history stack.
- * This is a copy of the private WebBackForwardList so it contains only a
- * snapshot of the current state. Multiple calls to this method may return
- * different objects. The object returned from this method will not be
- * updated to reflect any new state.
- */
- public WebBackForwardList copyBackForwardList() {
- checkThread();
- return mProvider.copyBackForwardList();
-
- }
-
- /**
- * Registers the listener to be notified as find-on-page operations
- * progress. This will replace the current listener.
- *
- * @param listener an implementation of {@link FindListener}
- */
- public void setFindListener(FindListener listener) {
- checkThread();
- setupFindListenerIfNeeded();
- mFindListener.mUserFindListener = listener;
- }
-
- /**
- * Highlights and scrolls to the next match found by
- * {@link #findAllAsync}, wrapping around page boundaries as necessary.
- * Notifies any registered {@link FindListener}. If {@link #findAllAsync(String)}
- * has not been called yet, or if {@link #clearMatches} has been called since the
- * last find operation, this function does nothing.
- *
- * @param forward the direction to search
- * @see #setFindListener
- */
- public void findNext(boolean forward) {
- checkThread();
- mProvider.findNext(forward);
- }
-
- /**
- * Finds all instances of find on the page and highlights them.
- * Notifies any registered {@link FindListener}.
- *
- * @param find the string to find
- * @return the number of occurrences of the String "find" that were found
- * @deprecated {@link #findAllAsync} is preferred.
- * @see #setFindListener
- */
- @Deprecated
- public int findAll(String find) {
- checkThread();
- StrictMode.noteSlowCall("findAll blocks UI: prefer findAllAsync");
- return mProvider.findAll(find);
- }
-
- /**
- * Finds all instances of find on the page and highlights them,
- * asynchronously. Notifies any registered {@link FindListener}.
- * Successive calls to this will cancel any pending searches.
- *
- * @param find the string to find.
- * @see #setFindListener
- */
- public void findAllAsync(String find) {
- checkThread();
- mProvider.findAllAsync(find);
- }
-
- /**
- * Starts an ActionMode for finding text in this WebView. Only works if this
- * WebView is attached to the view system.
- *
- * @param text if non-null, will be the initial text to search for.
- * Otherwise, the last String searched for in this WebView will
- * be used to start.
- * @param showIme if {@code true}, show the IME, assuming the user will begin typing.
- * If {@code false} and text is non-null, perform a find all.
- * @return {@code true} if the find dialog is shown, {@code false} otherwise
- * @deprecated This method does not work reliably on all Android versions;
- * implementing a custom find dialog using WebView.findAllAsync()
- * provides a more robust solution.
- */
- @Deprecated
- public boolean showFindDialog(@Nullable String text, boolean showIme) {
- checkThread();
- return mProvider.showFindDialog(text, showIme);
}
- /**
- * Gets the first substring consisting of the address of a physical
- * location. Currently, only addresses in the United States are detected,
- * and consist of:
- * <ul>
- * <li>a house number</li>
- * <li>a street name</li>
- * <li>a street type (Road, Circle, etc), either spelled out or
- * abbreviated</li>
- * <li>a city name</li>
- * <li>a state or territory, either spelled out or two-letter abbr</li>
- * <li>an optional 5 digit or 9 digit zip code</li>
- * </ul>
- * All names must be correctly capitalized, and the zip code, if present,
- * must be valid for the state. The street type must be a standard USPS
- * spelling or abbreviation. The state or territory must also be spelled
- * or abbreviated using USPS standards. The house number may not exceed
- * five digits.
- *
- * @param addr the string to search for addresses
- * @return the address, or if no address is found, {@code null}
- */
- @Nullable
public static String findAddress(String addr) {
- // TODO: Rewrite this in Java so it is not needed to start up chromium
- // Could also be deprecated
- return getFactory().getStatics().findAddress(addr);
- }
-
- /**
- * For apps targeting the L release, WebView has a new default behavior that reduces
- * memory footprint and increases performance by intelligently choosing
- * the portion of the HTML document that needs to be drawn. These
- * optimizations are transparent to the developers. However, under certain
- * circumstances, an App developer may want to disable them:
- * <ol>
- * <li>When an app uses {@link #onDraw} to do own drawing and accesses portions
- * of the page that is way outside the visible portion of the page.</li>
- * <li>When an app uses {@link #capturePicture} to capture a very large HTML document.
- * Note that capturePicture is a deprecated API.</li>
- * </ol>
- * Enabling drawing the entire HTML document has a significant performance
- * cost. This method should be called before any WebViews are created.
- */
- public static void enableSlowWholeDocumentDraw() {
- getFactory().getStatics().enableSlowWholeDocumentDraw();
+ return null;
}
- /**
- * Clears the highlighting surrounding text matches created by
- * {@link #findAllAsync}.
- */
- public void clearMatches() {
- checkThread();
- mProvider.clearMatches();
- }
-
- /**
- * Queries the document to see if it contains any image references. The
- * message object will be dispatched with arg1 being set to 1 if images
- * were found and 0 if the document does not reference any images.
- *
- * @param response the message that will be dispatched with the result
- */
public void documentHasImages(Message response) {
- checkThread();
- mProvider.documentHasImages(response);
}
- /**
- * Sets the WebViewClient that will receive various notifications and
- * requests. This will replace the current handler.
- *
- * @param client an implementation of WebViewClient
- * @see #getWebViewClient
- */
public void setWebViewClient(WebViewClient client) {
- checkThread();
- mProvider.setWebViewClient(client);
- }
-
- /**
- * Gets the WebViewClient.
- *
- * @return the WebViewClient, or a default client if not yet set
- * @see #setWebViewClient
- */
- public WebViewClient getWebViewClient() {
- checkThread();
- return mProvider.getWebViewClient();
}
- /**
- * Registers the interface to be used when content can not be handled by
- * the rendering engine, and should be downloaded instead. This will replace
- * the current handler.
- *
- * @param listener an implementation of DownloadListener
- */
public void setDownloadListener(DownloadListener listener) {
- checkThread();
- mProvider.setDownloadListener(listener);
}
- /**
- * Sets the chrome handler. This is an implementation of WebChromeClient for
- * use in handling JavaScript dialogs, favicons, titles, and the progress.
- * This will replace the current handler.
- *
- * @param client an implementation of WebChromeClient
- * @see #getWebChromeClient
- */
public void setWebChromeClient(WebChromeClient client) {
- checkThread();
- mProvider.setWebChromeClient(client);
}
- /**
- * Gets the chrome handler.
- *
- * @return the WebChromeClient, or {@code null} if not yet set
- * @see #setWebChromeClient
- */
- @Nullable
- public WebChromeClient getWebChromeClient() {
- checkThread();
- return mProvider.getWebChromeClient();
- }
-
- /**
- * Sets the Picture listener. This is an interface used to receive
- * notifications of a new Picture.
- *
- * @param listener an implementation of WebView.PictureListener
- * @deprecated This method is now obsolete.
- */
- @Deprecated
- public void setPictureListener(PictureListener listener) {
- checkThread();
- mProvider.setPictureListener(listener);
- }
-
- /**
- * Injects the supplied Java object into this WebView. The object is
- * injected into the JavaScript context of the main frame, using the
- * supplied name. This allows the Java object's methods to be
- * accessed from JavaScript. For applications targeted to API
- * level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
- * and above, only public methods that are annotated with
- * {@link android.webkit.JavascriptInterface} can be accessed from JavaScript.
- * For applications targeted to API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} or below,
- * all public methods (including the inherited ones) can be accessed, see the
- * important security note below for implications.
- * <p> Note that injected objects will not appear in JavaScript until the page is next
- * (re)loaded. JavaScript should be enabled before injecting the object. For example:
- * <pre>
- * class JsObject {
- * {@literal @}JavascriptInterface
- * public String toString() { return "injectedObject"; }
- * }
- * webview.getSettings().setJavaScriptEnabled(true);
- * webView.addJavascriptInterface(new JsObject(), "injectedObject");
- * webView.loadData("<!DOCTYPE html><title></title>", "text/html", null);
- * webView.loadUrl("javascript:alert(injectedObject.toString())");</pre>
- * <p>
- * <strong>IMPORTANT:</strong>
- * <ul>
- * <li> This method can be used to allow JavaScript to control the host
- * application. This is a powerful feature, but also presents a security
- * risk for apps targeting {@link android.os.Build.VERSION_CODES#JELLY_BEAN} or earlier.
- * Apps that target a version later than {@link android.os.Build.VERSION_CODES#JELLY_BEAN}
- * are still vulnerable if the app runs on a device running Android earlier than 4.2.
- * The most secure way to use this method is to target {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
- * and to ensure the method is called only when running on Android 4.2 or later.
- * With these older versions, JavaScript could use reflection to access an
- * injected object's public fields. Use of this method in a WebView
- * containing untrusted content could allow an attacker to manipulate the
- * host application in unintended ways, executing Java code with the
- * permissions of the host application. Use extreme care when using this
- * method in a WebView which could contain untrusted content.</li>
- * <li> JavaScript interacts with Java object on a private, background
- * thread of this WebView. Care is therefore required to maintain thread
- * safety.
- * </li>
- * <li> The Java object's fields are not accessible.</li>
- * <li> For applications targeted to API level {@link android.os.Build.VERSION_CODES#LOLLIPOP}
- * and above, methods of injected Java objects are enumerable from
- * JavaScript.</li>
- * </ul>
- *
- * @param object the Java object to inject into this WebView's JavaScript
- * context. {@code null} values are ignored.
- * @param name the name used to expose the object in JavaScript
- */
- public void addJavascriptInterface(Object object, String name) {
- checkThread();
- mProvider.addJavascriptInterface(object, name);
- }
-
- /**
- * Removes a previously injected Java object from this WebView. Note that
- * the removal will not be reflected in JavaScript until the page is next
- * (re)loaded. See {@link #addJavascriptInterface}.
- *
- * @param name the name used to expose the object in JavaScript
- */
- public void removeJavascriptInterface(@NonNull String name) {
- checkThread();
- mProvider.removeJavascriptInterface(name);
- }
-
- /**
- * Creates a message channel to communicate with JS and returns the message
- * ports that represent the endpoints of this message channel. The HTML5 message
- * channel functionality is described
- * <a href="https://html.spec.whatwg.org/multipage/comms.html#messagechannel">here
- * </a>
- *
- * <p>The returned message channels are entangled and already in started state.
- *
- * @return the two message ports that form the message channel.
- */
- public WebMessagePort[] createWebMessageChannel() {
- checkThread();
- return mProvider.createWebMessageChannel();
- }
-
- /**
- * Post a message to main frame. The embedded application can restrict the
- * messages to a certain target origin. See
- * <a href="https://html.spec.whatwg.org/multipage/comms.html#posting-messages">
- * HTML5 spec</a> for how target origin can be used.
- * <p>
- * A target origin can be set as a wildcard ("*"). However this is not recommended.
- * See the page above for security issues.
- *
- * @param message the WebMessage
- * @param targetOrigin the target origin.
- */
- public void postWebMessage(WebMessage message, Uri targetOrigin) {
- checkThread();
- mProvider.postMessageToMainFrame(message, targetOrigin);
- }
-
- /**
- * Gets the WebSettings object used to control the settings for this
- * WebView.
- *
- * @return a WebSettings object that can be used to control this WebView's
- * settings
- */
- public WebSettings getSettings() {
- checkThread();
- return mProvider.getSettings();
- }
-
- /**
- * Enables debugging of web contents (HTML / CSS / JavaScript)
- * loaded into any WebViews of this application. This flag can be enabled
- * in order to facilitate debugging of web layouts and JavaScript
- * code running inside WebViews. Please refer to WebView documentation
- * for the debugging guide.
- *
- * The default is {@code false}.
- *
- * @param enabled whether to enable web contents debugging
- */
- public static void setWebContentsDebuggingEnabled(boolean enabled) {
- getFactory().getStatics().setWebContentsDebuggingEnabled(enabled);
- }
-
- /**
- * Gets the list of currently loaded plugins.
- *
- * @return the list of currently loaded plugins
- * @deprecated This was used for Gears, which has been deprecated.
- * @hide
- */
- @Deprecated
- public static synchronized PluginList getPluginList() {
- return new PluginList();
- }
-
- /**
- * @deprecated This was used for Gears, which has been deprecated.
- * @hide
- */
- @Deprecated
- public void refreshPlugins(boolean reloadOpenPages) {
- checkThread();
- }
-
- /**
- * Puts this WebView into text selection mode. Do not rely on this
- * functionality; it will be deprecated in the future.
- *
- * @deprecated This method is now obsolete.
- * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
- */
- @Deprecated
- public void emulateShiftHeld() {
- checkThread();
- }
-
- /**
- * @deprecated WebView no longer needs to implement
- * ViewGroup.OnHierarchyChangeListener. This method does nothing now.
- */
- @Override
- // Cannot add @hide as this can always be accessed via the interface.
- @Deprecated
- public void onChildViewAdded(View parent, View child) {}
-
- /**
- * @deprecated WebView no longer needs to implement
- * ViewGroup.OnHierarchyChangeListener. This method does nothing now.
- */
- @Override
- // Cannot add @hide as this can always be accessed via the interface.
- @Deprecated
- public void onChildViewRemoved(View p, View child) {}
-
- /**
- * @deprecated WebView should not have implemented
- * ViewTreeObserver.OnGlobalFocusChangeListener. This method does nothing now.
- */
- @Override
- // Cannot add @hide as this can always be accessed via the interface.
- @Deprecated
- public void onGlobalFocusChanged(View oldFocus, View newFocus) {
- }
-
- /**
- * @deprecated Only the default case, {@code true}, will be supported in a future version.
- */
- @Deprecated
- public void setMapTrackballToArrowKeys(boolean setMap) {
- checkThread();
- mProvider.setMapTrackballToArrowKeys(setMap);
+ public void addJavascriptInterface(Object obj, String interfaceName) {
}
-
- public void flingScroll(int vx, int vy) {
- checkThread();
- mProvider.flingScroll(vx, vy);
- }
-
- /**
- * Gets the zoom controls for this WebView, as a separate View. The caller
- * is responsible for inserting this View into the layout hierarchy.
- * <p/>
- * API level {@link android.os.Build.VERSION_CODES#CUPCAKE} introduced
- * built-in zoom mechanisms for the WebView, as opposed to these separate
- * zoom controls. The built-in mechanisms are preferred and can be enabled
- * using {@link WebSettings#setBuiltInZoomControls}.
- *
- * @deprecated the built-in zoom mechanisms are preferred
- * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN}
- */
- @Deprecated
public View getZoomControls() {
- checkThread();
- return mProvider.getZoomControls();
+ return null;
}
- /**
- * Gets whether this WebView can be zoomed in.
- *
- * @return {@code true} if this WebView can be zoomed in
- *
- * @deprecated This method is prone to inaccuracy due to race conditions
- * between the web rendering and UI threads; prefer
- * {@link WebViewClient#onScaleChanged}.
- */
- @Deprecated
- public boolean canZoomIn() {
- checkThread();
- return mProvider.canZoomIn();
- }
-
- /**
- * Gets whether this WebView can be zoomed out.
- *
- * @return {@code true} if this WebView can be zoomed out
- *
- * @deprecated This method is prone to inaccuracy due to race conditions
- * between the web rendering and UI threads; prefer
- * {@link WebViewClient#onScaleChanged}.
- */
- @Deprecated
- public boolean canZoomOut() {
- checkThread();
- return mProvider.canZoomOut();
- }
-
- /**
- * Performs a zoom operation in this WebView.
- *
- * @param zoomFactor the zoom factor to apply. The zoom factor will be clamped to the WebView's
- * zoom limits. This value must be in the range 0.01 to 100.0 inclusive.
- */
- public void zoomBy(float zoomFactor) {
- checkThread();
- if (zoomFactor < 0.01)
- throw new IllegalArgumentException("zoomFactor must be greater than 0.01.");
- if (zoomFactor > 100.0)
- throw new IllegalArgumentException("zoomFactor must be less than 100.");
- mProvider.zoomBy(zoomFactor);
- }
-
- /**
- * Performs zoom in in this WebView.
- *
- * @return {@code true} if zoom in succeeds, {@code false} if no zoom changes
- */
public boolean zoomIn() {
- checkThread();
- return mProvider.zoomIn();
+ return false;
}
- /**
- * Performs zoom out in this WebView.
- *
- * @return {@code true} if zoom out succeeds, {@code false} if no zoom changes
- */
public boolean zoomOut() {
- checkThread();
- return mProvider.zoomOut();
- }
-
- /**
- * @deprecated This method is now obsolete.
- * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
- */
- @Deprecated
- public void debugDump() {
- checkThread();
- }
-
- /**
- * See {@link ViewDebug.HierarchyHandler#dumpViewHierarchyWithProperties(BufferedWriter, int)}
- * @hide
- */
- @Override
- public void dumpViewHierarchyWithProperties(BufferedWriter out, int level) {
- mProvider.dumpViewHierarchyWithProperties(out, level);
- }
-
- /**
- * See {@link ViewDebug.HierarchyHandler#findHierarchyView(String, int)}
- * @hide
- */
- @Override
- public View findHierarchyView(String className, int hashCode) {
- return mProvider.findHierarchyView(className, hashCode);
- }
-
- /** @hide */
- @IntDef({
- RENDERER_PRIORITY_WAIVED,
- RENDERER_PRIORITY_BOUND,
- RENDERER_PRIORITY_IMPORTANT
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface RendererPriority {}
-
- /**
- * The renderer associated with this WebView is bound with
- * {@link Context#BIND_WAIVE_PRIORITY}. At this priority level
- * {@link WebView} renderers will be strong targets for out of memory
- * killing.
- *
- * Use with {@link #setRendererPriorityPolicy}.
- */
- public static final int RENDERER_PRIORITY_WAIVED = 0;
- /**
- * The renderer associated with this WebView is bound with
- * the default priority for services.
- *
- * Use with {@link #setRendererPriorityPolicy}.
- */
- public static final int RENDERER_PRIORITY_BOUND = 1;
- /**
- * The renderer associated with this WebView is bound with
- * {@link Context#BIND_IMPORTANT}.
- *
- * Use with {@link #setRendererPriorityPolicy}.
- */
- public static final int RENDERER_PRIORITY_IMPORTANT = 2;
-
- /**
- * Set the renderer priority policy for this {@link WebView}. The
- * priority policy will be used to determine whether an out of
- * process renderer should be considered to be a target for OOM
- * killing.
- *
- * Because a renderer can be associated with more than one
- * WebView, the final priority it is computed as the maximum of
- * any attached WebViews. When a WebView is destroyed it will
- * cease to be considerered when calculating the renderer
- * priority. Once no WebViews remain associated with the renderer,
- * the priority of the renderer will be reduced to
- * {@link #RENDERER_PRIORITY_WAIVED}.
- *
- * The default policy is to set the priority to
- * {@link #RENDERER_PRIORITY_IMPORTANT} regardless of visibility,
- * and this should not be changed unless the caller also handles
- * renderer crashes with
- * {@link WebViewClient#onRenderProcessGone}. Any other setting
- * will result in WebView renderers being killed by the system
- * more aggressively than the application.
- *
- * @param rendererRequestedPriority the minimum priority at which
- * this WebView desires the renderer process to be bound.
- * @param waivedWhenNotVisible if {@code true}, this flag specifies that
- * when this WebView is not visible, it will be treated as
- * if it had requested a priority of
- * {@link #RENDERER_PRIORITY_WAIVED}.
- */
- public void setRendererPriorityPolicy(
- @RendererPriority int rendererRequestedPriority,
- boolean waivedWhenNotVisible) {
- mProvider.setRendererPriorityPolicy(rendererRequestedPriority, waivedWhenNotVisible);
- }
-
- /**
- * Get the requested renderer priority for this WebView.
- *
- * @return the requested renderer priority policy.
- */
- @RendererPriority
- public int getRendererRequestedPriority() {
- return mProvider.getRendererRequestedPriority();
- }
-
- /**
- * Return whether this WebView requests a priority of
- * {@link #RENDERER_PRIORITY_WAIVED} when not visible.
- *
- * @return whether this WebView requests a priority of
- * {@link #RENDERER_PRIORITY_WAIVED} when not visible.
- */
- public boolean getRendererPriorityWaivedWhenNotVisible() {
- return mProvider.getRendererPriorityWaivedWhenNotVisible();
- }
-
- /**
- * Sets the {@link TextClassifier} for this WebView.
- */
- public void setTextClassifier(@Nullable TextClassifier textClassifier) {
- mProvider.setTextClassifier(textClassifier);
- }
-
- /**
- * Returns the {@link TextClassifier} used by this WebView.
- * If no TextClassifier has been set, this WebView uses the default set by the system.
- */
- @NonNull
- public TextClassifier getTextClassifier() {
- return mProvider.getTextClassifier();
- }
-
- //-------------------------------------------------------------------------
- // Interface for WebView providers
- //-------------------------------------------------------------------------
-
- /**
- * Gets the WebViewProvider. Used by providers to obtain the underlying
- * implementation, e.g. when the application responds to
- * WebViewClient.onCreateWindow() request.
- *
- * @hide WebViewProvider is not public API.
- */
- @SystemApi
- public WebViewProvider getWebViewProvider() {
- return mProvider;
- }
-
- /**
- * Callback interface, allows the provider implementation to access non-public methods
- * and fields, and make super-class calls in this WebView instance.
- * @hide Only for use by WebViewProvider implementations
- */
- @SystemApi
- public class PrivateAccess {
- // ---- Access to super-class methods ----
- public int super_getScrollBarStyle() {
- return WebView.super.getScrollBarStyle();
- }
-
- public void super_scrollTo(int scrollX, int scrollY) {
- WebView.super.scrollTo(scrollX, scrollY);
- }
-
- public void super_computeScroll() {
- WebView.super.computeScroll();
- }
-
- public boolean super_onHoverEvent(MotionEvent event) {
- return WebView.super.onHoverEvent(event);
- }
-
- public boolean super_performAccessibilityAction(int action, Bundle arguments) {
- return WebView.super.performAccessibilityActionInternal(action, arguments);
- }
-
- public boolean super_performLongClick() {
- return WebView.super.performLongClick();
- }
-
- public boolean super_setFrame(int left, int top, int right, int bottom) {
- return WebView.super.setFrame(left, top, right, bottom);
- }
-
- public boolean super_dispatchKeyEvent(KeyEvent event) {
- return WebView.super.dispatchKeyEvent(event);
- }
-
- public boolean super_onGenericMotionEvent(MotionEvent event) {
- return WebView.super.onGenericMotionEvent(event);
- }
-
- public boolean super_requestFocus(int direction, Rect previouslyFocusedRect) {
- return WebView.super.requestFocus(direction, previouslyFocusedRect);
- }
-
- public void super_setLayoutParams(ViewGroup.LayoutParams params) {
- WebView.super.setLayoutParams(params);
- }
-
- public void super_startActivityForResult(Intent intent, int requestCode) {
- WebView.super.startActivityForResult(intent, requestCode);
- }
-
- // ---- Access to non-public methods ----
- public void overScrollBy(int deltaX, int deltaY,
- int scrollX, int scrollY,
- int scrollRangeX, int scrollRangeY,
- int maxOverScrollX, int maxOverScrollY,
- boolean isTouchEvent) {
- WebView.this.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY,
- maxOverScrollX, maxOverScrollY, isTouchEvent);
- }
-
- public void awakenScrollBars(int duration) {
- WebView.this.awakenScrollBars(duration);
- }
-
- public void awakenScrollBars(int duration, boolean invalidate) {
- WebView.this.awakenScrollBars(duration, invalidate);
- }
-
- public float getVerticalScrollFactor() {
- return WebView.this.getVerticalScrollFactor();
- }
-
- public float getHorizontalScrollFactor() {
- return WebView.this.getHorizontalScrollFactor();
- }
-
- public void setMeasuredDimension(int measuredWidth, int measuredHeight) {
- WebView.this.setMeasuredDimension(measuredWidth, measuredHeight);
- }
-
- public void onScrollChanged(int l, int t, int oldl, int oldt) {
- WebView.this.onScrollChanged(l, t, oldl, oldt);
- }
-
- public int getHorizontalScrollbarHeight() {
- return WebView.this.getHorizontalScrollbarHeight();
- }
-
- public void super_onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar,
- int l, int t, int r, int b) {
- WebView.super.onDrawVerticalScrollBar(canvas, scrollBar, l, t, r, b);
- }
-
- // ---- Access to (non-public) fields ----
- /** Raw setter for the scroll X value, without invoking onScrollChanged handlers etc. */
- public void setScrollXRaw(int scrollX) {
- WebView.this.mScrollX = scrollX;
- }
-
- /** Raw setter for the scroll Y value, without invoking onScrollChanged handlers etc. */
- public void setScrollYRaw(int scrollY) {
- WebView.this.mScrollY = scrollY;
- }
-
- }
-
- //-------------------------------------------------------------------------
- // Package-private internal stuff
- //-------------------------------------------------------------------------
-
- // Only used by android.webkit.FindActionModeCallback.
- void setFindDialogFindListener(FindListener listener) {
- checkThread();
- setupFindListenerIfNeeded();
- mFindListener.mFindDialogFindListener = listener;
- }
-
- // Only used by android.webkit.FindActionModeCallback.
- void notifyFindDialogDismissed() {
- checkThread();
- mProvider.notifyFindDialogDismissed();
- }
-
- //-------------------------------------------------------------------------
- // Private internal stuff
- //-------------------------------------------------------------------------
-
- private WebViewProvider mProvider;
-
- /**
- * In addition to the FindListener that the user may set via the WebView.setFindListener
- * API, FindActionModeCallback will register it's own FindListener. We keep them separate
- * via this class so that the two FindListeners can potentially exist at once.
- */
- private class FindListenerDistributor implements FindListener {
- private FindListener mFindDialogFindListener;
- private FindListener mUserFindListener;
-
- @Override
- public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches,
- boolean isDoneCounting) {
- if (mFindDialogFindListener != null) {
- mFindDialogFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches,
- isDoneCounting);
- }
-
- if (mUserFindListener != null) {
- mUserFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches,
- isDoneCounting);
- }
- }
- }
- private FindListenerDistributor mFindListener;
-
- private void setupFindListenerIfNeeded() {
- if (mFindListener == null) {
- mFindListener = new FindListenerDistributor();
- mProvider.setFindListener(mFindListener);
- }
- }
-
- private void ensureProviderCreated() {
- checkThread();
- if (mProvider == null) {
- // As this can get called during the base class constructor chain, pass the minimum
- // number of dependencies here; the rest are deferred to init().
- mProvider = getFactory().createWebView(this, new PrivateAccess());
- }
- }
-
- private static WebViewFactoryProvider getFactory() {
- return WebViewFactory.getProvider();
- }
-
- private final Looper mWebViewThread = Looper.myLooper();
-
- private void checkThread() {
- // Ignore mWebViewThread == null because this can be called during in the super class
- // constructor, before this class's own constructor has even started.
- if (mWebViewThread != null && Looper.myLooper() != mWebViewThread) {
- Throwable throwable = new Throwable(
- "A WebView method was called on thread '" +
- Thread.currentThread().getName() + "'. " +
- "All WebView methods must be called on the same thread. " +
- "(Expected Looper " + mWebViewThread + " called on " + Looper.myLooper() +
- ", FYI main Looper is " + Looper.getMainLooper() + ")");
- Log.w(LOGTAG, Log.getStackTraceString(throwable));
- StrictMode.onWebViewMethodCalledOnWrongThread(throwable);
-
- if (sEnforceThreadChecking) {
- throw new RuntimeException(throwable);
- }
- }
- }
-
- //-------------------------------------------------------------------------
- // Override View methods
- //-------------------------------------------------------------------------
-
- // TODO: Add a test that enumerates all methods in ViewDelegte & ScrollDelegate, and ensures
- // there's a corresponding override (or better, caller) for each of them in here.
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- mProvider.getViewDelegate().onAttachedToWindow();
- }
-
- /** @hide */
- @Override
- protected void onDetachedFromWindowInternal() {
- mProvider.getViewDelegate().onDetachedFromWindow();
- super.onDetachedFromWindowInternal();
- }
-
- /** @hide */
- @Override
- public void onMovedToDisplay(int displayId, Configuration config) {
- mProvider.getViewDelegate().onMovedToDisplay(displayId, config);
- }
-
- @Override
- public void setLayoutParams(ViewGroup.LayoutParams params) {
- mProvider.getViewDelegate().setLayoutParams(params);
- }
-
- @Override
- public void setOverScrollMode(int mode) {
- super.setOverScrollMode(mode);
- // This method may be called in the constructor chain, before the WebView provider is
- // created.
- ensureProviderCreated();
- mProvider.getViewDelegate().setOverScrollMode(mode);
- }
-
- @Override
- public void setScrollBarStyle(int style) {
- mProvider.getViewDelegate().setScrollBarStyle(style);
- super.setScrollBarStyle(style);
- }
-
- @Override
- protected int computeHorizontalScrollRange() {
- return mProvider.getScrollDelegate().computeHorizontalScrollRange();
- }
-
- @Override
- protected int computeHorizontalScrollOffset() {
- return mProvider.getScrollDelegate().computeHorizontalScrollOffset();
- }
-
- @Override
- protected int computeVerticalScrollRange() {
- return mProvider.getScrollDelegate().computeVerticalScrollRange();
- }
-
- @Override
- protected int computeVerticalScrollOffset() {
- return mProvider.getScrollDelegate().computeVerticalScrollOffset();
- }
-
- @Override
- protected int computeVerticalScrollExtent() {
- return mProvider.getScrollDelegate().computeVerticalScrollExtent();
- }
-
- @Override
- public void computeScroll() {
- mProvider.getScrollDelegate().computeScroll();
- }
-
- @Override
- public boolean onHoverEvent(MotionEvent event) {
- return mProvider.getViewDelegate().onHoverEvent(event);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- return mProvider.getViewDelegate().onTouchEvent(event);
- }
-
- @Override
- public boolean onGenericMotionEvent(MotionEvent event) {
- return mProvider.getViewDelegate().onGenericMotionEvent(event);
- }
-
- @Override
- public boolean onTrackballEvent(MotionEvent event) {
- return mProvider.getViewDelegate().onTrackballEvent(event);
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- return mProvider.getViewDelegate().onKeyDown(keyCode, event);
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- return mProvider.getViewDelegate().onKeyUp(keyCode, event);
- }
-
- @Override
- public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
- return mProvider.getViewDelegate().onKeyMultiple(keyCode, repeatCount, event);
- }
-
- /*
- TODO: These are not currently implemented in WebViewClassic, but it seems inconsistent not
- to be delegating them too.
-
- @Override
- public boolean onKeyPreIme(int keyCode, KeyEvent event) {
- return mProvider.getViewDelegate().onKeyPreIme(keyCode, event);
- }
- @Override
- public boolean onKeyLongPress(int keyCode, KeyEvent event) {
- return mProvider.getViewDelegate().onKeyLongPress(keyCode, event);
- }
- @Override
- public boolean onKeyShortcut(int keyCode, KeyEvent event) {
- return mProvider.getViewDelegate().onKeyShortcut(keyCode, event);
- }
- */
-
- @Override
- public AccessibilityNodeProvider getAccessibilityNodeProvider() {
- AccessibilityNodeProvider provider =
- mProvider.getViewDelegate().getAccessibilityNodeProvider();
- return provider == null ? super.getAccessibilityNodeProvider() : provider;
- }
-
- @Deprecated
- @Override
- public boolean shouldDelayChildPressedState() {
- return mProvider.getViewDelegate().shouldDelayChildPressedState();
- }
-
- @Override
- public CharSequence getAccessibilityClassName() {
- return WebView.class.getName();
- }
-
- @Override
- public void onProvideVirtualStructure(ViewStructure structure) {
- mProvider.getViewDelegate().onProvideVirtualStructure(structure);
- }
-
- /**
- * {@inheritDoc}
- *
- * <p>The {@link ViewStructure} traditionally represents a {@link View}, while for web pages
- * it represent HTML nodes. Hence, it's necessary to "map" the HTML properties in a way that is
- * understood by the {@link android.service.autofill.AutofillService} implementations:
- *
- * <ol>
- * <li>Only the HTML nodes inside a {@code FORM} are generated.
- * <li>The source of the HTML is set using {@link ViewStructure#setWebDomain(String)} in the
- * node representing the WebView.
- * <li>If a web page has multiple {@code FORM}s, only the data for the current form is
- * represented&mdash;if the user taps a field from another form, then the current autofill
- * context is canceled (by calling {@link android.view.autofill.AutofillManager#cancel()} and
- * a new context is created for that {@code FORM}.
- * <li>Similarly, if the page has {@code IFRAME} nodes, they are not initially represented in
- * the view structure until the user taps a field from a {@code FORM} inside the
- * {@code IFRAME}, in which case it would be treated the same way as multiple forms described
- * above, except that the {@link ViewStructure#setWebDomain(String) web domain} of the
- * {@code FORM} contains the {@code src} attribute from the {@code IFRAME} node.
- * <li>The W3C autofill field ({@code autocomplete} tag attribute) maps to
- * {@link ViewStructure#setAutofillHints(String[])}.
- * <li>If the view is editable, the {@link ViewStructure#setAutofillType(int)} and
- * {@link ViewStructure#setAutofillValue(AutofillValue)} must be set.
- * <li>The {@code placeholder} attribute maps to {@link ViewStructure#setHint(CharSequence)}.
- * <li>Other HTML attributes can be represented through
- * {@link ViewStructure#setHtmlInfo(android.view.ViewStructure.HtmlInfo)}.
- * </ol>
- *
- * <p>If the WebView implementation can determine that the value of a field was set statically
- * (for example, not through Javascript), it should also call
- * {@code structure.setDataIsSensitive(false)}.
- *
- * <p>For example, an HTML form with 2 fields for username and password:
- *
- * <pre class="prettyprint">
- * &lt;label&gt;Username:&lt;/label&gt;
- * &lt;input type="text" name="username" id="user" value="Type your username" autocomplete="username" placeholder="Email or username"&gt;
- * &lt;label&gt;Password:&lt;/label&gt;
- * &lt;input type="password" name="password" id="pass" autocomplete="current-password" placeholder="Password"&gt;
- * </pre>
- *
- * <p>Would map to:
- *
- * <pre class="prettyprint">
- * int index = structure.addChildCount(2);
- * ViewStructure username = structure.newChild(index);
- * username.setAutofillId(structure.getAutofillId(), 1); // id 1 - first child
- * username.setAutofillHints("username");
- * username.setHtmlInfo(username.newHtmlInfoBuilder("input")
- * .addAttribute("type", "text")
- * .addAttribute("name", "username")
- * .addAttribute("label", "Username:")
- * .build());
- * username.setHint("Email or username");
- * username.setAutofillType(View.AUTOFILL_TYPE_TEXT);
- * username.setAutofillValue(AutofillValue.forText("Type your username"));
- * // Value of the field is not sensitive because it was created statically and not changed.
- * username.setDataIsSensitive(false);
- *
- * ViewStructure password = structure.newChild(index + 1);
- * username.setAutofillId(structure, 2); // id 2 - second child
- * password.setAutofillHints("current-password");
- * password.setHtmlInfo(password.newHtmlInfoBuilder("input")
- * .addAttribute("type", "password")
- * .addAttribute("name", "password")
- * .addAttribute("label", "Password:")
- * .build());
- * password.setHint("Password");
- * password.setAutofillType(View.AUTOFILL_TYPE_TEXT);
- * </pre>
- */
- @Override
- public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
- mProvider.getViewDelegate().onProvideAutofillVirtualStructure(structure, flags);
- }
-
- @Override
- public void autofill(SparseArray<AutofillValue>values) {
- mProvider.getViewDelegate().autofill(values);
- }
-
- /** @hide */
- @Override
- public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
- super.onInitializeAccessibilityNodeInfoInternal(info);
- mProvider.getViewDelegate().onInitializeAccessibilityNodeInfo(info);
- }
-
- /** @hide */
- @Override
- public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
- super.onInitializeAccessibilityEventInternal(event);
- mProvider.getViewDelegate().onInitializeAccessibilityEvent(event);
- }
-
- /** @hide */
- @Override
- public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
- return mProvider.getViewDelegate().performAccessibilityAction(action, arguments);
- }
-
- /** @hide */
- @Override
- protected void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar,
- int l, int t, int r, int b) {
- mProvider.getViewDelegate().onDrawVerticalScrollBar(canvas, scrollBar, l, t, r, b);
- }
-
- @Override
- protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
- mProvider.getViewDelegate().onOverScrolled(scrollX, scrollY, clampedX, clampedY);
- }
-
- @Override
- protected void onWindowVisibilityChanged(int visibility) {
- super.onWindowVisibilityChanged(visibility);
- mProvider.getViewDelegate().onWindowVisibilityChanged(visibility);
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- mProvider.getViewDelegate().onDraw(canvas);
- }
-
- @Override
- public boolean performLongClick() {
- return mProvider.getViewDelegate().performLongClick();
- }
-
- @Override
- protected void onConfigurationChanged(Configuration newConfig) {
- mProvider.getViewDelegate().onConfigurationChanged(newConfig);
- }
-
- /**
- * Creates a new InputConnection for an InputMethod to interact with the WebView.
- * This is similar to {@link View#onCreateInputConnection} but note that WebView
- * calls InputConnection methods on a thread other than the UI thread.
- * If these methods are overridden, then the overriding methods should respect
- * thread restrictions when calling View methods or accessing data.
- */
- @Override
- public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
- return mProvider.getViewDelegate().onCreateInputConnection(outAttrs);
- }
-
- @Override
- public boolean onDragEvent(DragEvent event) {
- return mProvider.getViewDelegate().onDragEvent(event);
- }
-
- @Override
- protected void onVisibilityChanged(View changedView, int visibility) {
- super.onVisibilityChanged(changedView, visibility);
- // This method may be called in the constructor chain, before the WebView provider is
- // created.
- ensureProviderCreated();
- mProvider.getViewDelegate().onVisibilityChanged(changedView, visibility);
- }
-
- @Override
- public void onWindowFocusChanged(boolean hasWindowFocus) {
- mProvider.getViewDelegate().onWindowFocusChanged(hasWindowFocus);
- super.onWindowFocusChanged(hasWindowFocus);
- }
-
- @Override
- protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
- mProvider.getViewDelegate().onFocusChanged(focused, direction, previouslyFocusedRect);
- super.onFocusChanged(focused, direction, previouslyFocusedRect);
- }
-
- /** @hide */
- @Override
- protected boolean setFrame(int left, int top, int right, int bottom) {
- return mProvider.getViewDelegate().setFrame(left, top, right, bottom);
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int ow, int oh) {
- super.onSizeChanged(w, h, ow, oh);
- mProvider.getViewDelegate().onSizeChanged(w, h, ow, oh);
- }
-
- @Override
- protected void onScrollChanged(int l, int t, int oldl, int oldt) {
- super.onScrollChanged(l, t, oldl, oldt);
- mProvider.getViewDelegate().onScrollChanged(l, t, oldl, oldt);
- }
-
- @Override
- public boolean dispatchKeyEvent(KeyEvent event) {
- return mProvider.getViewDelegate().dispatchKeyEvent(event);
- }
-
- @Override
- public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
- return mProvider.getViewDelegate().requestFocus(direction, previouslyFocusedRect);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- mProvider.getViewDelegate().onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- @Override
- public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
- return mProvider.getViewDelegate().requestChildRectangleOnScreen(child, rect, immediate);
- }
-
- @Override
- public void setBackgroundColor(int color) {
- mProvider.getViewDelegate().setBackgroundColor(color);
- }
-
- @Override
- public void setLayerType(int layerType, Paint paint) {
- super.setLayerType(layerType, paint);
- mProvider.getViewDelegate().setLayerType(layerType, paint);
- }
-
- @Override
- protected void dispatchDraw(Canvas canvas) {
- mProvider.getViewDelegate().preDispatchDraw(canvas);
- super.dispatchDraw(canvas);
- }
-
- @Override
- public void onStartTemporaryDetach() {
- super.onStartTemporaryDetach();
- mProvider.getViewDelegate().onStartTemporaryDetach();
- }
-
- @Override
- public void onFinishTemporaryDetach() {
- super.onFinishTemporaryDetach();
- mProvider.getViewDelegate().onFinishTemporaryDetach();
- }
-
- @Override
- public Handler getHandler() {
- return mProvider.getViewDelegate().getHandler(super.getHandler());
- }
-
- @Override
- public View findFocus() {
- return mProvider.getViewDelegate().findFocus(super.findFocus());
- }
-
- /**
- * If WebView has already been loaded into the current process this method will return the
- * package that was used to load it. Otherwise, the package that would be used if the WebView
- * was loaded right now will be returned; this does not cause WebView to be loaded, so this
- * information may become outdated at any time.
- * The WebView package changes either when the current WebView package is updated, disabled, or
- * uninstalled. It can also be changed through a Developer Setting.
- * If the WebView package changes, any app process that has loaded WebView will be killed. The
- * next time the app starts and loads WebView it will use the new WebView package instead.
- * @return the current WebView package, or {@code null} if there is none.
- */
- @Nullable
- public static PackageInfo getCurrentWebViewPackage() {
- PackageInfo webviewPackage = WebViewFactory.getLoadedPackageInfo();
- if (webviewPackage != null) {
- return webviewPackage;
- }
-
- IWebViewUpdateService service = WebViewFactory.getUpdateService();
- if (service == null) {
- return null;
- }
- try {
- return service.getCurrentWebViewPackage();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
- * Receive the result from a previous call to {@link #startActivityForResult(Intent, int)}.
- *
- * @param requestCode The integer request code originally supplied to
- * startActivityForResult(), allowing you to identify who this
- * result came from.
- * @param resultCode The integer result code returned by the child activity
- * through its setResult().
- * @param data An Intent, which can return result data to the caller
- * (various data can be attached to Intent "extras").
- * @hide
- */
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- mProvider.getViewDelegate().onActivityResult(requestCode, resultCode, data);
- }
-
- /** @hide */
- @Override
- protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
- super.encodeProperties(encoder);
-
- checkThread();
- encoder.addProperty("webview:contentHeight", mProvider.getContentHeight());
- encoder.addProperty("webview:contentWidth", mProvider.getContentWidth());
- encoder.addProperty("webview:scale", mProvider.getScale());
- encoder.addProperty("webview:title", mProvider.getTitle());
- encoder.addProperty("webview:url", mProvider.getUrl());
- encoder.addProperty("webview:originalUrl", mProvider.getOriginalUrl());
+ return false;
}
}
diff --git a/android/webkit/WebViewClient.java b/android/webkit/WebViewClient.java
index 46c39834..f5d220c0 100644
--- a/android/webkit/WebViewClient.java
+++ b/android/webkit/WebViewClient.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2008 The Android Open Source Project
+ * Copyright 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,545 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package android.webkit;
-import android.annotation.IntDef;
-import android.annotation.Nullable;
-import android.graphics.Bitmap;
-import android.net.http.SslError;
-import android.os.Message;
-import android.view.InputEvent;
-import android.view.KeyEvent;
-import android.view.ViewRootImpl;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
public class WebViewClient {
-
- /**
- * Give the host application a chance to take over the control when a new
- * url is about to be loaded in the current WebView. If WebViewClient is not
- * provided, by default WebView will ask Activity Manager to choose the
- * proper handler for the url. If WebViewClient is provided, return {@code true}
- * means the host application handles the url, while return {@code false} means the
- * current WebView handles the url.
- * This method is not called for requests using the POST "method".
- *
- * @param view The WebView that is initiating the callback.
- * @param url The url to be loaded.
- * @return {@code true} if the host application wants to leave the current WebView
- * and handle the url itself, otherwise return {@code false}.
- * @deprecated Use {@link #shouldOverrideUrlLoading(WebView, WebResourceRequest)
- * shouldOverrideUrlLoading(WebView, WebResourceRequest)} instead.
- */
- @Deprecated
- public boolean shouldOverrideUrlLoading(WebView view, String url) {
- return false;
- }
-
- /**
- * Give the host application a chance to take over the control when a new
- * url is about to be loaded in the current WebView. If WebViewClient is not
- * provided, by default WebView will ask Activity Manager to choose the
- * proper handler for the url. If WebViewClient is provided, return {@code true}
- * means the host application handles the url, while return {@code false} means the
- * current WebView handles the url.
- *
- * <p>Notes:
- * <ul>
- * <li>This method is not called for requests using the POST &quot;method&quot;.</li>
- * <li>This method is also called for subframes with non-http schemes, thus it is
- * strongly disadvised to unconditionally call {@link WebView#loadUrl(String)}
- * with the request's url from inside the method and then return {@code true},
- * as this will make WebView to attempt loading a non-http url, and thus fail.</li>
- * </ul>
- *
- * @param view The WebView that is initiating the callback.
- * @param request Object containing the details of the request.
- * @return {@code true} if the host application wants to leave the current WebView
- * and handle the url itself, otherwise return {@code false}.
- */
- public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
- return shouldOverrideUrlLoading(view, request.getUrl().toString());
- }
-
- /**
- * Notify the host application that a page has started loading. This method
- * is called once for each main frame load so a page with iframes or
- * framesets will call onPageStarted one time for the main frame. This also
- * means that onPageStarted will not be called when the contents of an
- * embedded frame changes, i.e. clicking a link whose target is an iframe,
- * it will also not be called for fragment navigations (navigations to
- * #fragment_id).
- *
- * @param view The WebView that is initiating the callback.
- * @param url The url to be loaded.
- * @param favicon The favicon for this page if it already exists in the
- * database.
- */
- public void onPageStarted(WebView view, String url, Bitmap favicon) {
- }
-
- /**
- * Notify the host application that a page has finished loading. This method
- * is called only for main frame. When onPageFinished() is called, the
- * rendering picture may not be updated yet. To get the notification for the
- * new Picture, use {@link WebView.PictureListener#onNewPicture}.
- *
- * @param view The WebView that is initiating the callback.
- * @param url The url of the page.
- */
- public void onPageFinished(WebView view, String url) {
- }
-
- /**
- * Notify the host application that the WebView will load the resource
- * specified by the given url.
- *
- * @param view The WebView that is initiating the callback.
- * @param url The url of the resource the WebView will load.
- */
- public void onLoadResource(WebView view, String url) {
- }
-
- /**
- * Notify the host application that {@link android.webkit.WebView} content left over from
- * previous page navigations will no longer be drawn.
- *
- * <p>This callback can be used to determine the point at which it is safe to make a recycled
- * {@link android.webkit.WebView} visible, ensuring that no stale content is shown. It is called
- * at the earliest point at which it can be guaranteed that {@link WebView#onDraw} will no
- * longer draw any content from previous navigations. The next draw will display either the
- * {@link WebView#setBackgroundColor background color} of the {@link WebView}, or some of the
- * contents of the newly loaded page.
- *
- * <p>This method is called when the body of the HTTP response has started loading, is reflected
- * in the DOM, and will be visible in subsequent draws. This callback occurs early in the
- * document loading process, and as such you should expect that linked resources (for example,
- * CSS and images) may not be available.
- *
- * <p>For more fine-grained notification of visual state updates, see {@link
- * WebView#postVisualStateCallback}.
- *
- * <p>Please note that all the conditions and recommendations applicable to
- * {@link WebView#postVisualStateCallback} also apply to this API.
- *
- * <p>This callback is only called for main frame navigations.
- *
- * @param view The {@link android.webkit.WebView} for which the navigation occurred.
- * @param url The URL corresponding to the page navigation that triggered this callback.
- */
- public void onPageCommitVisible(WebView view, String url) {
- }
-
- /**
- * Notify the host application of a resource request and allow the
- * application to return the data. If the return value is {@code null}, the WebView
- * will continue to load the resource as usual. Otherwise, the return
- * response and data will be used.
- *
- * <p class="note"><b>Note:</b> This method is called on a thread
- * other than the UI thread so clients should exercise caution
- * when accessing private data or the view system.
- *
- * <p class="note"><b>Note:</b> When Safe Browsing is enabled, these URLs still undergo Safe
- * Browsing checks. If this is undesired, whitelist the URL with {@link
- * WebView#setSafeBrowsingWhitelist} or ignore the warning with {@link #onSafeBrowsingHit}.
- *
- * @param view The {@link android.webkit.WebView} that is requesting the
- * resource.
- * @param url The raw url of the resource.
- * @return A {@link android.webkit.WebResourceResponse} containing the
- * response information or {@code null} if the WebView should load the
- * resource itself.
- * @deprecated Use {@link #shouldInterceptRequest(WebView, WebResourceRequest)
- * shouldInterceptRequest(WebView, WebResourceRequest)} instead.
- */
- @Deprecated
- @Nullable
- public WebResourceResponse shouldInterceptRequest(WebView view,
- String url) {
- return null;
- }
-
- /**
- * Notify the host application of a resource request and allow the
- * application to return the data. If the return value is {@code null}, the WebView
- * will continue to load the resource as usual. Otherwise, the return
- * response and data will be used.
- *
- * <p class="note"><b>Note:</b> This method is called on a thread
- * other than the UI thread so clients should exercise caution
- * when accessing private data or the view system.
- *
- * <p class="note"><b>Note:</b> When Safe Browsing is enabled, these URLs still undergo Safe
- * Browsing checks. If this is undesired, whitelist the URL with {@link
- * WebView#setSafeBrowsingWhitelist} or ignore the warning with {@link #onSafeBrowsingHit}.
- *
- * @param view The {@link android.webkit.WebView} that is requesting the
- * resource.
- * @param request Object containing the details of the request.
- * @return A {@link android.webkit.WebResourceResponse} containing the
- * response information or {@code null} if the WebView should load the
- * resource itself.
- */
- @Nullable
- public WebResourceResponse shouldInterceptRequest(WebView view,
- WebResourceRequest request) {
- return shouldInterceptRequest(view, request.getUrl().toString());
- }
-
- /**
- * Notify the host application that there have been an excessive number of
- * HTTP redirects. As the host application if it would like to continue
- * trying to load the resource. The default behavior is to send the cancel
- * message.
- *
- * @param view The WebView that is initiating the callback.
- * @param cancelMsg The message to send if the host wants to cancel
- * @param continueMsg The message to send if the host wants to continue
- * @deprecated This method is no longer called. When the WebView encounters
- * a redirect loop, it will cancel the load.
- */
- @Deprecated
- public void onTooManyRedirects(WebView view, Message cancelMsg,
- Message continueMsg) {
- cancelMsg.sendToTarget();
- }
-
- // These ints must match up to the hidden values in EventHandler.
- /** Generic error */
- public static final int ERROR_UNKNOWN = -1;
- /** Server or proxy hostname lookup failed */
- public static final int ERROR_HOST_LOOKUP = -2;
- /** Unsupported authentication scheme (not basic or digest) */
- public static final int ERROR_UNSUPPORTED_AUTH_SCHEME = -3;
- /** User authentication failed on server */
- public static final int ERROR_AUTHENTICATION = -4;
- /** User authentication failed on proxy */
- public static final int ERROR_PROXY_AUTHENTICATION = -5;
- /** Failed to connect to the server */
- public static final int ERROR_CONNECT = -6;
- /** Failed to read or write to the server */
- public static final int ERROR_IO = -7;
- /** Connection timed out */
- public static final int ERROR_TIMEOUT = -8;
- /** Too many redirects */
- public static final int ERROR_REDIRECT_LOOP = -9;
- /** Unsupported URI scheme */
- public static final int ERROR_UNSUPPORTED_SCHEME = -10;
- /** Failed to perform SSL handshake */
- public static final int ERROR_FAILED_SSL_HANDSHAKE = -11;
- /** Malformed URL */
- public static final int ERROR_BAD_URL = -12;
- /** Generic file error */
- public static final int ERROR_FILE = -13;
- /** File not found */
- public static final int ERROR_FILE_NOT_FOUND = -14;
- /** Too many requests during this load */
- public static final int ERROR_TOO_MANY_REQUESTS = -15;
- /** Resource load was canceled by Safe Browsing */
- public static final int ERROR_UNSAFE_RESOURCE = -16;
-
- /** @hide */
- @IntDef({
- SAFE_BROWSING_THREAT_UNKNOWN,
- SAFE_BROWSING_THREAT_MALWARE,
- SAFE_BROWSING_THREAT_PHISHING,
- SAFE_BROWSING_THREAT_UNWANTED_SOFTWARE
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface SafeBrowsingThreat {}
-
- /** The resource was blocked for an unknown reason */
- public static final int SAFE_BROWSING_THREAT_UNKNOWN = 0;
- /** The resource was blocked because it contains malware */
- public static final int SAFE_BROWSING_THREAT_MALWARE = 1;
- /** The resource was blocked because it contains deceptive content */
- public static final int SAFE_BROWSING_THREAT_PHISHING = 2;
- /** The resource was blocked because it contains unwanted software */
- public static final int SAFE_BROWSING_THREAT_UNWANTED_SOFTWARE = 3;
-
- /**
- * Report an error to the host application. These errors are unrecoverable
- * (i.e. the main resource is unavailable). The {@code errorCode} parameter
- * corresponds to one of the {@code ERROR_*} constants.
- * @param view The WebView that is initiating the callback.
- * @param errorCode The error code corresponding to an ERROR_* value.
- * @param description A String describing the error.
- * @param failingUrl The url that failed to load.
- * @deprecated Use {@link #onReceivedError(WebView, WebResourceRequest, WebResourceError)
- * onReceivedError(WebView, WebResourceRequest, WebResourceError)} instead.
- */
- @Deprecated
- public void onReceivedError(WebView view, int errorCode,
- String description, String failingUrl) {
- }
-
- /**
- * Report web resource loading error to the host application. These errors usually indicate
- * inability to connect to the server. Note that unlike the deprecated version of the callback,
- * the new version will be called for any resource (iframe, image, etc.), not just for the main
- * page. Thus, it is recommended to perform minimum required work in this callback.
- * @param view The WebView that is initiating the callback.
- * @param request The originating request.
- * @param error Information about the error occurred.
- */
- public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
- if (request.isForMainFrame()) {
- onReceivedError(view,
- error.getErrorCode(), error.getDescription().toString(),
- request.getUrl().toString());
- }
- }
-
- /**
- * Notify the host application that an HTTP error has been received from the server while
- * loading a resource. HTTP errors have status codes &gt;= 400. This callback will be called
- * for any resource (iframe, image, etc.), not just for the main page. Thus, it is recommended
- * to perform minimum required work in this callback. Note that the content of the server
- * response may not be provided within the {@code errorResponse} parameter.
- * @param view The WebView that is initiating the callback.
- * @param request The originating request.
- * @param errorResponse Information about the error occurred.
- */
- public void onReceivedHttpError(
- WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
- }
-
- /**
- * As the host application if the browser should resend data as the
- * requested page was a result of a POST. The default is to not resend the
- * data.
- *
- * @param view The WebView that is initiating the callback.
- * @param dontResend The message to send if the browser should not resend
- * @param resend The message to send if the browser should resend data
- */
- public void onFormResubmission(WebView view, Message dontResend,
- Message resend) {
- dontResend.sendToTarget();
- }
-
- /**
- * Notify the host application to update its visited links database.
- *
- * @param view The WebView that is initiating the callback.
- * @param url The url being visited.
- * @param isReload {@code true} if this url is being reloaded.
- */
- public void doUpdateVisitedHistory(WebView view, String url,
- boolean isReload) {
- }
-
- /**
- * Notify the host application that an SSL error occurred while loading a
- * resource. The host application must call either handler.cancel() or
- * handler.proceed(). Note that the decision may be retained for use in
- * response to future SSL errors. The default behavior is to cancel the
- * load.
- *
- * @param view The WebView that is initiating the callback.
- * @param handler An SslErrorHandler object that will handle the user's
- * response.
- * @param error The SSL error object.
- */
- public void onReceivedSslError(WebView view, SslErrorHandler handler,
- SslError error) {
- handler.cancel();
- }
-
- /**
- * Notify the host application to handle a SSL client certificate request. The host application
- * is responsible for showing the UI if desired and providing the keys. There are three ways to
- * respond: {@link ClientCertRequest#proceed}, {@link ClientCertRequest#cancel}, or {@link
- * ClientCertRequest#ignore}. Webview stores the response in memory (for the life of the
- * application) if {@link ClientCertRequest#proceed} or {@link ClientCertRequest#cancel} is
- * called and does not call {@code onReceivedClientCertRequest()} again for the same host and
- * port pair. Webview does not store the response if {@link ClientCertRequest#ignore}
- * is called. Note that, multiple layers in chromium network stack might be
- * caching the responses, so the behavior for ignore is only a best case
- * effort.
- *
- * This method is called on the UI thread. During the callback, the
- * connection is suspended.
- *
- * For most use cases, the application program should implement the
- * {@link android.security.KeyChainAliasCallback} interface and pass it to
- * {@link android.security.KeyChain#choosePrivateKeyAlias} to start an
- * activity for the user to choose the proper alias. The keychain activity will
- * provide the alias through the callback method in the implemented interface. Next
- * the application should create an async task to call
- * {@link android.security.KeyChain#getPrivateKey} to receive the key.
- *
- * An example implementation of client certificates can be seen at
- * <A href="https://android.googlesource.com/platform/packages/apps/Browser/+/android-5.1.1_r1/src/com/android/browser/Tab.java">
- * AOSP Browser</a>
- *
- * The default behavior is to cancel, returning no client certificate.
- *
- * @param view The WebView that is initiating the callback
- * @param request An instance of a {@link ClientCertRequest}
- *
- */
- public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
- request.cancel();
- }
-
- /**
- * Notifies the host application that the WebView received an HTTP
- * authentication request. The host application can use the supplied
- * {@link HttpAuthHandler} to set the WebView's response to the request.
- * The default behavior is to cancel the request.
- *
- * @param view the WebView that is initiating the callback
- * @param handler the HttpAuthHandler used to set the WebView's response
- * @param host the host requiring authentication
- * @param realm the realm for which authentication is required
- * @see WebView#getHttpAuthUsernamePassword
- */
- public void onReceivedHttpAuthRequest(WebView view,
- HttpAuthHandler handler, String host, String realm) {
- handler.cancel();
- }
-
- /**
- * Give the host application a chance to handle the key event synchronously.
- * e.g. menu shortcut key events need to be filtered this way. If return
- * true, WebView will not handle the key event. If return {@code false}, WebView
- * will always handle the key event, so none of the super in the view chain
- * will see the key event. The default behavior returns {@code false}.
- *
- * @param view The WebView that is initiating the callback.
- * @param event The key event.
- * @return {@code true} if the host application wants to handle the key event
- * itself, otherwise return {@code false}
- */
- public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
- return false;
- }
-
- /**
- * Notify the host application that a key was not handled by the WebView.
- * Except system keys, WebView always consumes the keys in the normal flow
- * or if {@link #shouldOverrideKeyEvent} returns {@code true}. This is called asynchronously
- * from where the key is dispatched. It gives the host application a chance
- * to handle the unhandled key events.
- *
- * @param view The WebView that is initiating the callback.
- * @param event The key event.
- */
- public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
- onUnhandledInputEventInternal(view, event);
- }
-
- /**
- * Notify the host application that a input event was not handled by the WebView.
- * Except system keys, WebView always consumes input events in the normal flow
- * or if {@link #shouldOverrideKeyEvent} returns {@code true}. This is called asynchronously
- * from where the event is dispatched. It gives the host application a chance
- * to handle the unhandled input events.
- *
- * Note that if the event is a {@link android.view.MotionEvent}, then it's lifetime is only
- * that of the function call. If the WebViewClient wishes to use the event beyond that, then it
- * <i>must</i> create a copy of the event.
- *
- * It is the responsibility of overriders of this method to call
- * {@link #onUnhandledKeyEvent(WebView, KeyEvent)}
- * when appropriate if they wish to continue receiving events through it.
- *
- * @param view The WebView that is initiating the callback.
- * @param event The input event.
- * @removed
- */
- public void onUnhandledInputEvent(WebView view, InputEvent event) {
- if (event instanceof KeyEvent) {
- onUnhandledKeyEvent(view, (KeyEvent) event);
- return;
- }
- onUnhandledInputEventInternal(view, event);
- }
-
- private void onUnhandledInputEventInternal(WebView view, InputEvent event) {
- ViewRootImpl root = view.getViewRootImpl();
- if (root != null) {
- root.dispatchUnhandledInputEvent(event);
- }
- }
-
- /**
- * Notify the host application that the scale applied to the WebView has
- * changed.
- *
- * @param view The WebView that is initiating the callback.
- * @param oldScale The old scale factor
- * @param newScale The new scale factor
- */
- public void onScaleChanged(WebView view, float oldScale, float newScale) {
- }
-
- /**
- * Notify the host application that a request to automatically log in the
- * user has been processed.
- * @param view The WebView requesting the login.
- * @param realm The account realm used to look up accounts.
- * @param account An optional account. If not {@code null}, the account should be
- * checked against accounts on the device. If it is a valid
- * account, it should be used to log in the user.
- * @param args Authenticator specific arguments used to log in the user.
- */
- public void onReceivedLoginRequest(WebView view, String realm,
- @Nullable String account, String args) {
- }
-
- /**
- * Notify host application that the given WebView's render process has exited.
- *
- * Multiple WebView instances may be associated with a single render process;
- * onRenderProcessGone will be called for each WebView that was affected.
- * The application's implementation of this callback should only attempt to
- * clean up the specific WebView given as a parameter, and should not assume
- * that other WebView instances are affected.
- *
- * The given WebView can't be used, and should be removed from the view hierarchy,
- * all references to it should be cleaned up, e.g any references in the Activity
- * or other classes saved using {@link android.view.View#findViewById} and similar calls, etc.
- *
- * To cause an render process crash for test purpose, the application can
- * call {@code loadUrl("chrome://crash")} on the WebView. Note that multiple WebView
- * instances may be affected if they share a render process, not just the
- * specific WebView which loaded chrome://crash.
- *
- * @param view The WebView which needs to be cleaned up.
- * @param detail the reason why it exited.
- * @return {@code true} if the host application handled the situation that process has
- * exited, otherwise, application will crash if render process crashed,
- * or be killed if render process was killed by the system.
- */
- public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) {
- return false;
- }
-
- /**
- * Notify the host application that a loading URL has been flagged by Safe Browsing.
- *
- * The application must invoke the callback to indicate the preferred response. The default
- * behavior is to show an interstitial to the user, with the reporting checkbox visible.
- *
- * If the application needs to show its own custom interstitial UI, the callback can be invoked
- * asynchronously with {@link SafeBrowsingResponse#backToSafety} or {@link
- * SafeBrowsingResponse#proceed}, depending on user response.
- *
- * @param view The WebView that hit the malicious resource.
- * @param request Object containing the details of the request.
- * @param threatType The reason the resource was caught by Safe Browsing, corresponding to a
- * {@code SAFE_BROWSING_THREAT_*} value.
- * @param callback Applications must invoke one of the callback methods.
- */
- public void onSafeBrowsingHit(WebView view, WebResourceRequest request,
- @SafeBrowsingThreat int threatType, SafeBrowsingResponse callback) {
- callback.showInterstitial(/* allowReporting */ true);
- }
}
diff --git a/android/webkit/WebViewDelegate.java b/android/webkit/WebViewDelegate.java
index 73399313..f0670914 100644
--- a/android/webkit/WebViewDelegate.java
+++ b/android/webkit/WebViewDelegate.java
@@ -218,4 +218,11 @@ public final class WebViewDelegate {
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Returns the data directory suffix to use, or null for none.
+ */
+ public String getDataDirectorySuffix() {
+ return WebViewFactory.getDataDirectorySuffix();
+ }
}
diff --git a/android/webkit/WebViewFactory.java b/android/webkit/WebViewFactory.java
index 797bdfb7..b3522ec9 100644
--- a/android/webkit/WebViewFactory.java
+++ b/android/webkit/WebViewFactory.java
@@ -33,6 +33,7 @@ import android.util.AndroidRuntimeException;
import android.util.ArraySet;
import android.util.Log;
+import java.io.File;
import java.lang.reflect.Method;
/**
@@ -46,7 +47,7 @@ public final class WebViewFactory {
// visible for WebViewZygoteInit to look up the class by reflection and call preloadInZygote.
/** @hide */
private static final String CHROMIUM_WEBVIEW_FACTORY =
- "com.android.webview.chromium.WebViewChromiumFactoryProviderForOMR1";
+ "com.android.webview.chromium.WebViewChromiumFactoryProviderForP";
private static final String CHROMIUM_WEBVIEW_FACTORY_METHOD = "create";
@@ -63,6 +64,8 @@ public final class WebViewFactory {
private static final Object sProviderLock = new Object();
private static PackageInfo sPackageInfo;
private static Boolean sWebViewSupported;
+ private static boolean sWebViewDisabled;
+ private static String sDataDirectorySuffix; // stored here so it can be set without loading WV
// Error codes for loadWebViewNativeLibraryFromPackage
public static final int LIBLOAD_SUCCESS = 0;
@@ -115,6 +118,45 @@ public final class WebViewFactory {
/**
* @hide
*/
+ static void disableWebView() {
+ synchronized (sProviderLock) {
+ if (sProviderInstance != null) {
+ throw new IllegalStateException(
+ "Can't disable WebView: WebView already initialized");
+ }
+ sWebViewDisabled = true;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ static void setDataDirectorySuffix(String suffix) {
+ synchronized (sProviderLock) {
+ if (sProviderInstance != null) {
+ throw new IllegalStateException(
+ "Can't set data directory suffix: WebView already initialized");
+ }
+ if (suffix.indexOf(File.separatorChar) >= 0) {
+ throw new IllegalArgumentException("Suffix " + suffix
+ + " contains a path separator");
+ }
+ sDataDirectorySuffix = suffix;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ static String getDataDirectorySuffix() {
+ synchronized (sProviderLock) {
+ return sDataDirectorySuffix;
+ }
+ }
+
+ /**
+ * @hide
+ */
public static String getWebViewLibrary(ApplicationInfo ai) {
if (ai.metaData != null)
return ai.metaData.getString("com.android.webview.WebViewLibrary");
@@ -204,6 +246,11 @@ public final class WebViewFactory {
throw new UnsupportedOperationException();
}
+ if (sWebViewDisabled) {
+ throw new IllegalStateException(
+ "WebView.disableWebView() was called: WebView is disabled");
+ }
+
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getProvider()");
try {
@@ -265,10 +312,10 @@ public final class WebViewFactory {
+ "packageName mismatch, expected: "
+ chosen.packageName + " actual: " + toUse.packageName);
}
- if (chosen.versionCode > toUse.versionCode) {
+ if (chosen.getLongVersionCode() > toUse.getLongVersionCode()) {
throw new MissingWebViewPackageException("Failed to verify WebView provider, "
- + "version code is lower than expected: " + chosen.versionCode
- + " actual: " + toUse.versionCode);
+ + "version code is lower than expected: " + chosen.getLongVersionCode()
+ + " actual: " + toUse.getLongVersionCode());
}
if (getWebViewLibrary(toUse.applicationInfo) == null) {
throw new MissingWebViewPackageException("Tried to load an invalid WebView provider: "
@@ -401,7 +448,7 @@ public final class WebViewFactory {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
}
Log.i(LOGTAG, "Loading " + sPackageInfo.packageName + " version " +
- sPackageInfo.versionName + " (code " + sPackageInfo.versionCode + ")");
+ sPackageInfo.versionName + " (code " + sPackageInfo.getLongVersionCode() + ")");
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getChromiumProviderClass()");
try {
diff --git a/android/webkit/WebViewFactoryProvider.java b/android/webkit/WebViewFactoryProvider.java
index 4c47abc6..3ced6a5f 100644
--- a/android/webkit/WebViewFactoryProvider.java
+++ b/android/webkit/WebViewFactoryProvider.java
@@ -134,6 +134,14 @@ public interface WebViewFactoryProvider {
TokenBindingService getTokenBindingService();
/**
+ * Gets the TracingController instance for this WebView implementation. The
+ * implementation must return the same instance on subsequent calls.
+ *
+ * @return the TracingController instance
+ */
+ TracingController getTracingController();
+
+ /**
* Gets the ServiceWorkerController instance for this WebView implementation. The
* implementation must return the same instance on subsequent calls.
*
diff --git a/android/webkit/WebViewLibraryLoader.java b/android/webkit/WebViewLibraryLoader.java
index de0b97d1..eb2b6bcc 100644
--- a/android/webkit/WebViewLibraryLoader.java
+++ b/android/webkit/WebViewLibraryLoader.java
@@ -123,10 +123,11 @@ public class WebViewLibraryLoader {
throw new IllegalArgumentException(
"Native library paths to the WebView RelRo process must not be null!");
}
- int pid = LocalServices.getService(ActivityManagerInternal.class).startIsolatedProcess(
- RelroFileCreator.class.getName(), new String[] { nativeLib.path },
- "WebViewLoader-" + abi, abi, Process.SHARED_RELRO_UID, crashHandler);
- if (pid <= 0) throw new Exception("Failed to start the relro file creator process");
+ boolean success = LocalServices.getService(ActivityManagerInternal.class)
+ .startIsolatedProcess(
+ RelroFileCreator.class.getName(), new String[] { nativeLib.path },
+ "WebViewLoader-" + abi, abi, Process.SHARED_RELRO_UID, crashHandler);
+ if (!success) throw new Exception("Failed to start the relro file creator process");
} catch (Throwable t) {
// Log and discard errors as we must not crash the system server.
Log.e(LOGTAG, "error starting relro file creator for abi " + abi, t);
diff --git a/android/widget/DatePicker.java b/android/widget/DatePicker.java
index dfb36423..b2b93faf 100644
--- a/android/widget/DatePicker.java
+++ b/android/widget/DatePicker.java
@@ -107,7 +107,10 @@ public class DatePicker extends FrameLayout {
public static final int MODE_CALENDAR = 2;
/** @hide */
- @IntDef({MODE_SPINNER, MODE_CALENDAR})
+ @IntDef(prefix = { "MODE_" }, value = {
+ MODE_SPINNER,
+ MODE_CALENDAR
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface DatePickerMode {}
diff --git a/android/widget/EditText.java b/android/widget/EditText.java
index 56c3e4a5..336c20cd 100644
--- a/android/widget/EditText.java
+++ b/android/widget/EditText.java
@@ -105,6 +105,11 @@ public class EditText extends TextView {
@Override
public Editable getText() {
+ CharSequence text = super.getText();
+ if (text instanceof Editable) {
+ return (Editable) super.getText();
+ }
+ super.setText(text, BufferType.EDITABLE);
return (Editable) super.getText();
}
diff --git a/android/widget/Editor.java b/android/widget/Editor.java
index 05cba1e5..05d18d18 100644
--- a/android/widget/Editor.java
+++ b/android/widget/Editor.java
@@ -41,7 +41,6 @@ import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
-import android.metrics.LogMaker;
import android.os.Bundle;
import android.os.LocaleList;
import android.os.Parcel;
@@ -108,6 +107,7 @@ import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextLinks;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.TextView.Drawables;
import android.widget.TextView.OnEditorActionListener;
@@ -175,6 +175,13 @@ public class Editor {
int SELECTION_END = 2;
}
+ @IntDef({TextActionMode.SELECTION, TextActionMode.INSERTION, TextActionMode.TEXT_LINK})
+ @interface TextActionMode {
+ int SELECTION = 0;
+ int INSERTION = 1;
+ int TEXT_LINK = 2;
+ }
+
// Each Editor manages its own undo stack.
private final UndoManager mUndoManager = new UndoManager();
private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
@@ -545,7 +552,8 @@ public class Editor {
chooseSize(mErrorPopup, mError, tv);
tv.setText(mError);
- mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY());
+ mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY(),
+ Gravity.TOP | Gravity.LEFT);
mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
}
@@ -2053,7 +2061,7 @@ public class Editor {
stopTextActionMode();
ActionMode.Callback actionModeCallback =
- new TextActionModeCallback(false /* hasSelection */);
+ new TextActionModeCallback(TextActionMode.INSERTION);
mTextActionMode = mTextView.startActionMode(
actionModeCallback, ActionMode.TYPE_FLOATING);
if (mTextActionMode != null && getInsertionController() != null) {
@@ -2079,7 +2087,23 @@ public class Editor {
* Asynchronously starts a selection action mode using the TextClassifier.
*/
void startSelectionActionModeAsync(boolean adjustSelection) {
- getSelectionActionModeHelper().startActionModeAsync(adjustSelection);
+ getSelectionActionModeHelper().startSelectionActionModeAsync(adjustSelection);
+ }
+
+ void startLinkActionModeAsync(TextLinks.TextLink link) {
+ Preconditions.checkNotNull(link);
+ if (!(mTextView.getText() instanceof Spannable)) {
+ return;
+ }
+ Spannable text = (Spannable) mTextView.getText();
+ stopTextActionMode();
+ if (mTextView.isTextSelectable()) {
+ Selection.setSelection((Spannable) text, link.getStart(), link.getEnd());
+ } else {
+ //TODO: Nonselectable text
+ }
+
+ getSelectionActionModeHelper().startLinkActionModeAsync(link);
}
/**
@@ -2145,7 +2169,7 @@ public class Editor {
return true;
}
- boolean startSelectionActionModeInternal() {
+ boolean startActionModeInternal(@TextActionMode int actionMode) {
if (extractedTextModeWillBeStarted()) {
return false;
}
@@ -2159,8 +2183,7 @@ public class Editor {
return false;
}
- ActionMode.Callback actionModeCallback =
- new TextActionModeCallback(true /* hasSelection */);
+ ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode);
mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
final boolean selectionStarted = mTextActionMode != null;
@@ -3828,8 +3851,9 @@ public class Editor {
private final int mHandleHeight;
private final Map<MenuItem, OnClickListener> mAssistClickHandlers = new HashMap<>();
- public TextActionModeCallback(boolean hasSelection) {
- mHasSelection = hasSelection;
+ TextActionModeCallback(@TextActionMode int mode) {
+ mHasSelection = mode == TextActionMode.SELECTION
+ || (mTextIsSelectable && mode == TextActionMode.TEXT_LINK);
if (mHasSelection) {
SelectionModifierCursorController selectionController = getSelectionController();
if (selectionController.mStartHandle == null) {
@@ -3982,31 +4006,39 @@ public class Editor {
}
final TextClassification textClassification =
getSelectionActionModeHelper().getTextClassification();
- final int count = textClassification != null ? textClassification.getActionCount() : 0;
+ if (textClassification == null) {
+ return;
+ }
+ if (isValidAssistMenuItem(
+ textClassification.getIcon(),
+ textClassification.getLabel(),
+ textClassification.getOnClickListener(),
+ textClassification.getIntent())) {
+ final MenuItem item = menu.add(
+ TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST,
+ textClassification.getLabel())
+ .setIcon(textClassification.getIcon())
+ .setIntent(textClassification.getIntent());
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ mAssistClickHandlers.put(item, textClassification.getOnClickListener());
+ }
+ final int count = textClassification.getSecondaryActionsCount();
for (int i = 0; i < count; i++) {
- if (!isValidAssistMenuItem(i)) {
+ if (!isValidAssistMenuItem(
+ textClassification.getSecondaryIcon(i),
+ textClassification.getSecondaryLabel(i),
+ textClassification.getSecondaryOnClickListener(i),
+ textClassification.getSecondaryIntent(i))) {
continue;
}
- final int groupId = TextView.ID_ASSIST;
- final int order = (i == 0)
- ? MENU_ITEM_ORDER_ASSIST
- : MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i;
- final int id = (i == 0) ? TextView.ID_ASSIST : Menu.NONE;
- final int showAsFlag = (i == 0)
- ? MenuItem.SHOW_AS_ACTION_ALWAYS
- : MenuItem.SHOW_AS_ACTION_NEVER;
+ final int order = MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i;
final MenuItem item = menu.add(
- groupId, id, order, textClassification.getLabel(i))
- .setIcon(textClassification.getIcon(i))
- .setIntent(textClassification.getIntent(i));
- item.setShowAsAction(showAsFlag);
- mAssistClickHandlers.put(item, textClassification.getOnClickListener(i));
- if (id == TextView.ID_ASSIST) {
- mMetricsLogger.write(
- new LogMaker(MetricsEvent.TEXT_SELECTION_MENU_ITEM_ASSIST)
- .setType(MetricsEvent.TYPE_OPEN)
- .setSubtype(textClassification.getLogType()));
- }
+ TextView.ID_ASSIST, Menu.NONE, order,
+ textClassification.getSecondaryLabel(i))
+ .setIcon(textClassification.getSecondaryIcon(i))
+ .setIntent(textClassification.getSecondaryIntent(i));
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+ mAssistClickHandlers.put(item, textClassification.getSecondaryOnClickListener(i));
}
}
@@ -4022,18 +4054,9 @@ public class Editor {
}
}
- private boolean isValidAssistMenuItem(int index) {
- final TextClassification textClassification =
- getSelectionActionModeHelper().getTextClassification();
- if (!mTextView.isDeviceProvisioned() || textClassification == null
- || index < 0 || index >= textClassification.getActionCount()) {
- return false;
- }
- final Drawable icon = textClassification.getIcon(index);
- final CharSequence label = textClassification.getLabel(index);
+ private boolean isValidAssistMenuItem(
+ Drawable icon, CharSequence label, OnClickListener onClick, Intent intent) {
final boolean hasUi = icon != null || !TextUtils.isEmpty(label);
- final OnClickListener onClick = textClassification.getOnClickListener(index);
- final Intent intent = textClassification.getIntent(index);
final boolean hasAction = onClick != null || isSupportedIntent(intent);
return hasUi && hasAction;
}
@@ -4079,11 +4102,6 @@ public class Editor {
if (onClickListener != null) {
onClickListener.onClick(mTextView);
stopTextActionMode();
- if (assistMenuItem.getItemId() == TextView.ID_ASSIST) {
- mMetricsLogger.action(
- MetricsEvent.ACTION_TEXT_SELECTION_MENU_ITEM_ASSIST,
- textClassification.getLogType());
- }
}
// We tried our best.
return true;
@@ -4930,7 +4948,10 @@ public class Editor {
}
@Retention(RetentionPolicy.SOURCE)
- @IntDef({HANDLE_TYPE_SELECTION_START, HANDLE_TYPE_SELECTION_END})
+ @IntDef(prefix = { "HANDLE_TYPE_" }, value = {
+ HANDLE_TYPE_SELECTION_START,
+ HANDLE_TYPE_SELECTION_END
+ })
public @interface HandleType {}
public static final int HANDLE_TYPE_SELECTION_START = 0;
public static final int HANDLE_TYPE_SELECTION_END = 1;
@@ -6135,7 +6156,11 @@ public class Editor {
}
@Retention(RetentionPolicy.SOURCE)
- @IntDef({MERGE_EDIT_MODE_FORCE_MERGE, MERGE_EDIT_MODE_NEVER_MERGE, MERGE_EDIT_MODE_NORMAL})
+ @IntDef(prefix = { "MERGE_EDIT_MODE_" }, value = {
+ MERGE_EDIT_MODE_FORCE_MERGE,
+ MERGE_EDIT_MODE_NEVER_MERGE,
+ MERGE_EDIT_MODE_NORMAL
+ })
private @interface MergeMode {}
private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0;
private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1;
@@ -6594,7 +6619,7 @@ public class Editor {
Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
getLabel(resolveInfo))
.setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
- .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
}
}
diff --git a/android/widget/GridLayout.java b/android/widget/GridLayout.java
index cbd1e0ad..012b918f 100644
--- a/android/widget/GridLayout.java
+++ b/android/widget/GridLayout.java
@@ -172,7 +172,10 @@ public class GridLayout extends ViewGroup {
// Public constants
/** @hide */
- @IntDef({HORIZONTAL, VERTICAL})
+ @IntDef(prefix = { "HORIZONTAL", "VERTICAL" }, value = {
+ HORIZONTAL,
+ VERTICAL
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface Orientation {}
@@ -198,7 +201,10 @@ public class GridLayout extends ViewGroup {
public static final int UNDEFINED = Integer.MIN_VALUE;
/** @hide */
- @IntDef({ALIGN_BOUNDS, ALIGN_MARGINS})
+ @IntDef(prefix = { "ALIGN_" }, value = {
+ ALIGN_BOUNDS,
+ ALIGN_MARGINS
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface AlignmentMode {}
diff --git a/android/widget/GridView.java b/android/widget/GridView.java
index fcb44af6..1ec9b2f0 100644
--- a/android/widget/GridView.java
+++ b/android/widget/GridView.java
@@ -65,7 +65,12 @@ import java.lang.annotation.RetentionPolicy;
@RemoteView
public class GridView extends AbsListView {
/** @hide */
- @IntDef({NO_STRETCH, STRETCH_SPACING, STRETCH_COLUMN_WIDTH, STRETCH_SPACING_UNIFORM})
+ @IntDef(prefix = { "NO_STRETCH", "STRETCH_" }, value = {
+ NO_STRETCH,
+ STRETCH_SPACING,
+ STRETCH_COLUMN_WIDTH,
+ STRETCH_SPACING_UNIFORM
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface StretchMode {}
diff --git a/android/widget/LinearLayout.java b/android/widget/LinearLayout.java
index 380bf7ad..7ea1f1ed 100644
--- a/android/widget/LinearLayout.java
+++ b/android/widget/LinearLayout.java
@@ -95,13 +95,12 @@ public class LinearLayout extends ViewGroup {
public static final int VERTICAL = 1;
/** @hide */
- @IntDef(flag = true,
- value = {
- SHOW_DIVIDER_NONE,
- SHOW_DIVIDER_BEGINNING,
- SHOW_DIVIDER_MIDDLE,
- SHOW_DIVIDER_END
- })
+ @IntDef(flag = true, prefix = { "SHOW_DIVIDER_" }, value = {
+ SHOW_DIVIDER_NONE,
+ SHOW_DIVIDER_BEGINNING,
+ SHOW_DIVIDER_MIDDLE,
+ SHOW_DIVIDER_END
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface DividerMode {}
diff --git a/android/widget/Magnifier.java b/android/widget/Magnifier.java
index bd48f455..26dfcc2d 100644
--- a/android/widget/Magnifier.java
+++ b/android/widget/Magnifier.java
@@ -125,7 +125,7 @@ public final class Magnifier {
mView.getWidth() - mBitmap.getWidth()));
final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2;
- if (startX != mPrevStartCoordsInSurface.x || startY != mPrevStartCoordsInSurface.y) {
+ if (xPosInView != mPrevPosInView.x || yPosInView != mPrevPosInView.y) {
performPixelCopy(startX, startY);
mPrevPosInView.x = xPosInView;
diff --git a/android/widget/NumberPicker.java b/android/widget/NumberPicker.java
index b3792806..d98b865d 100644
--- a/android/widget/NumberPicker.java
+++ b/android/widget/NumberPicker.java
@@ -510,7 +510,11 @@ public class NumberPicker extends LinearLayout {
*/
public interface OnScrollListener {
/** @hide */
- @IntDef({SCROLL_STATE_IDLE, SCROLL_STATE_TOUCH_SCROLL, SCROLL_STATE_FLING})
+ @IntDef(prefix = { "SCROLL_STATE_" }, value = {
+ SCROLL_STATE_IDLE,
+ SCROLL_STATE_TOUCH_SCROLL,
+ SCROLL_STATE_FLING
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface ScrollState {}
@@ -1952,8 +1956,7 @@ public class NumberPicker extends LinearLayout {
CharSequence beforeText = mInputText.getText();
if (!text.equals(beforeText.toString())) {
mInputText.setText(text);
- if (AccessibilityManager.getInstance(mContext).isObservedEventType(
- AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED)) {
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
mInputText.onInitializeAccessibilityEvent(event);
@@ -2613,7 +2616,7 @@ public class NumberPicker extends LinearLayout {
}
private void sendAccessibilityEventForVirtualText(int eventType) {
- if (AccessibilityManager.getInstance(mContext).isObservedEventType(eventType)) {
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
mInputText.onInitializeAccessibilityEvent(event);
mInputText.onPopulateAccessibilityEvent(event);
@@ -2624,7 +2627,7 @@ public class NumberPicker extends LinearLayout {
private void sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType,
String text) {
- if (AccessibilityManager.getInstance(mContext).isObservedEventType(eventType)) {
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
event.setClassName(Button.class.getName());
event.setPackageName(mContext.getPackageName());
diff --git a/android/widget/SelectionActionModeHelper.java b/android/widget/SelectionActionModeHelper.java
index d0ad27af..2c6466cd 100644
--- a/android/widget/SelectionActionModeHelper.java
+++ b/android/widget/SelectionActionModeHelper.java
@@ -35,6 +35,7 @@ import android.util.Log;
import android.view.ActionMode;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
import android.view.textclassifier.TextSelection;
import android.view.textclassifier.logging.SmartSelectionEventTracker;
import android.view.textclassifier.logging.SmartSelectionEventTracker.SelectionEvent;
@@ -97,7 +98,10 @@ public final class SelectionActionModeHelper {
}
}
- public void startActionModeAsync(boolean adjustSelection) {
+ /**
+ * Starts Selection ActionMode.
+ */
+ public void startSelectionActionModeAsync(boolean adjustSelection) {
// Check if the smart selection should run for editable text.
adjustSelection &= !mTextView.isTextEditable()
|| mTextView.getTextClassifier().getSettings()
@@ -109,7 +113,7 @@ public final class SelectionActionModeHelper {
mTextView.getSelectionEnd());
cancelAsyncTask();
if (skipTextClassification()) {
- startActionMode(null);
+ startSelectionActionMode(null);
} else {
resetTextClassificationHelper();
mTextClassificationAsyncTask = new TextClassificationAsyncTask(
@@ -119,8 +123,27 @@ public final class SelectionActionModeHelper {
? mTextClassificationHelper::suggestSelection
: mTextClassificationHelper::classifyText,
mSmartSelectSprite != null
- ? this::startActionModeWithSmartSelectAnimation
- : this::startActionMode)
+ ? this::startSelectionActionModeWithSmartSelectAnimation
+ : this::startSelectionActionMode)
+ .execute();
+ }
+ }
+
+ /**
+ * Starts Link ActionMode.
+ */
+ public void startLinkActionModeAsync(TextLinks.TextLink textLink) {
+ //TODO: tracking/logging
+ cancelAsyncTask();
+ if (skipTextClassification()) {
+ startLinkActionMode(null);
+ } else {
+ resetTextClassificationHelper(textLink.getStart(), textLink.getEnd());
+ mTextClassificationAsyncTask = new TextClassificationAsyncTask(
+ mTextView,
+ mTextClassificationHelper.getTimeoutDuration(),
+ mTextClassificationHelper::classifyText,
+ this::startLinkActionMode)
.execute();
}
}
@@ -200,9 +223,19 @@ public final class SelectionActionModeHelper {
return noOpTextClassifier || noSelection || password;
}
- private void startActionMode(@Nullable SelectionResult result) {
+ private void startLinkActionMode(@Nullable SelectionResult result) {
+ startActionMode(Editor.TextActionMode.TEXT_LINK, result);
+ }
+
+ private void startSelectionActionMode(@Nullable SelectionResult result) {
+ startActionMode(Editor.TextActionMode.SELECTION, result);
+ }
+
+ private void startActionMode(
+ @Editor.TextActionMode int actionMode, @Nullable SelectionResult result) {
final CharSequence text = getText(mTextView);
- if (result != null && text instanceof Spannable) {
+ if (result != null && text instanceof Spannable
+ && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
// Do not change the selection if TextClassifier should be dark launched.
if (!mTextView.getTextClassifier().getSettings().isDarkLaunch()) {
Selection.setSelection((Spannable) text, result.mStart, result.mEnd);
@@ -211,12 +244,13 @@ public final class SelectionActionModeHelper {
} else {
mTextClassification = null;
}
- if (mEditor.startSelectionActionModeInternal()) {
+ if (mEditor.startActionModeInternal(actionMode)) {
final SelectionModifierCursorController controller = mEditor.getSelectionController();
- if (controller != null) {
+ if (controller != null
+ && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
controller.show();
}
- if (result != null) {
+ if (result != null && actionMode == Editor.TextActionMode.SELECTION) {
mSelectionTracker.onSmartSelection(result);
}
}
@@ -224,10 +258,11 @@ public final class SelectionActionModeHelper {
mTextClassificationAsyncTask = null;
}
- private void startActionModeWithSmartSelectAnimation(@Nullable SelectionResult result) {
+ private void startSelectionActionModeWithSmartSelectAnimation(
+ @Nullable SelectionResult result) {
final Layout layout = mTextView.getLayout();
- final Runnable onAnimationEndCallback = () -> startActionMode(result);
+ final Runnable onAnimationEndCallback = () -> startSelectionActionMode(result);
// TODO do not trigger the animation if the change included only non-printable characters
final boolean didSelectionChange =
result != null && (mTextView.getSelectionStart() != result.mStart
@@ -386,15 +421,24 @@ public final class SelectionActionModeHelper {
mTextClassificationAsyncTask = null;
}
- private void resetTextClassificationHelper() {
+ private void resetTextClassificationHelper(int selectionStart, int selectionEnd) {
+ if (selectionStart < 0 || selectionEnd < 0) {
+ // Use selection indices
+ selectionStart = mTextView.getSelectionStart();
+ selectionEnd = mTextView.getSelectionEnd();
+ }
mTextClassificationHelper.init(
mTextView.getContext(),
mTextView.getTextClassifier(),
getText(mTextView),
- mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
+ selectionStart, selectionEnd,
mTextView.getTextLocales());
}
+ private void resetTextClassificationHelper() {
+ resetTextClassificationHelper(-1, -1);
+ }
+
private void cancelSmartSelectAnimation() {
if (mSmartSelectSprite != null) {
mSmartSelectSprite.cancelAnimation();
diff --git a/android/widget/TextClock.java b/android/widget/TextClock.java
index 12790403..53318c99 100644
--- a/android/widget/TextClock.java
+++ b/android/widget/TextClock.java
@@ -20,6 +20,7 @@ import static android.view.ViewDebug.ExportedProperty;
import static android.widget.RemoteViews.RemoteView;
import android.annotation.NonNull;
+import android.annotation.TestApi;
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
@@ -141,6 +142,9 @@ public class TextClock extends TextView {
private boolean mShowCurrentUserTime;
private ContentObserver mFormatChangeObserver;
+ // Used by tests to stop time change events from triggering the text update
+ private boolean mStopTicking;
+
private class FormatChangeObserver extends ContentObserver {
public FormatChangeObserver(Handler handler) {
@@ -163,6 +167,9 @@ public class TextClock extends TextView {
private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
+ if (mStopTicking) {
+ return; // Test disabled the clock ticks
+ }
if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
final String timeZone = intent.getStringExtra("time-zone");
createTime(timeZone);
@@ -173,6 +180,9 @@ public class TextClock extends TextView {
private final Runnable mTicker = new Runnable() {
public void run() {
+ if (mStopTicking) {
+ return; // Test disabled the clock ticks
+ }
onTimeChanged();
long now = SystemClock.uptimeMillis();
@@ -546,6 +556,15 @@ public class TextClock extends TextView {
}
}
+ /**
+ * Used by tests to stop the clock tick from updating the text.
+ * @hide
+ */
+ @TestApi
+ public void disableClockTick() {
+ mStopTicking = true;
+ }
+
private void registerReceiver() {
final IntentFilter filter = new IntentFilter();
@@ -570,11 +589,12 @@ public class TextClock extends TextView {
mFormatChangeObserver = new FormatChangeObserver(getHandler());
}
final ContentResolver resolver = getContext().getContentResolver();
+ Uri uri = Settings.System.getUriFor(Settings.System.TIME_12_24);
if (mShowCurrentUserTime) {
- resolver.registerContentObserver(Settings.System.CONTENT_URI, true,
+ resolver.registerContentObserver(uri, true,
mFormatChangeObserver, UserHandle.USER_ALL);
} else {
- resolver.registerContentObserver(Settings.System.CONTENT_URI, true,
+ resolver.registerContentObserver(uri, true,
mFormatChangeObserver);
}
}
diff --git a/android/widget/TextView.java b/android/widget/TextView.java
index 71532a72..1e17f34a 100644
--- a/android/widget/TextView.java
+++ b/android/widget/TextView.java
@@ -77,6 +77,7 @@ import android.text.InputFilter;
import android.text.InputType;
import android.text.Layout;
import android.text.ParcelableSpan;
+import android.text.PremeasuredText;
import android.text.Selection;
import android.text.SpanWatcher;
import android.text.Spannable;
@@ -159,6 +160,7 @@ import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
import android.view.textservice.SpellCheckerSubtype;
import android.view.textservice.TextServicesManager;
import android.widget.RemoteViews.RemoteView;
@@ -167,6 +169,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.FastMath;
+import com.android.internal.util.Preconditions;
import com.android.internal.widget.EditableInputConnection;
import libcore.util.EmptyArray;
@@ -750,7 +753,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = 1;
/** @hide */
- @IntDef({AUTO_SIZE_TEXT_TYPE_NONE, AUTO_SIZE_TEXT_TYPE_UNIFORM})
+ @IntDef(prefix = { "AUTO_SIZE_TEXT_TYPE_" }, value = {
+ AUTO_SIZE_TEXT_TYPE_NONE,
+ AUTO_SIZE_TEXT_TYPE_UNIFORM
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface AutoSizeTextType {}
// Default minimum size for auto-sizing text in scaled pixels.
@@ -4861,6 +4867,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
* Sets line spacing for this TextView. Each line other than the last line will have its height
* multiplied by {@code mult} and have {@code add} added to it.
*
+ * @param add The value in pixels that should be added to each line other than the last line.
+ * This will be applied after the multiplier
+ * @param mult The value by which each line height other than the last line will be multiplied
+ * by
*
* @attr ref android.R.styleable#TextView_lineSpacingExtra
* @attr ref android.R.styleable#TextView_lineSpacingMultiplier
@@ -5326,7 +5336,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
if (imm != null) imm.restartInput(this);
} else if (type == BufferType.SPANNABLE || mMovement != null) {
text = mSpannableFactory.newSpannable(text);
- } else if (!(text instanceof CharWrapper)) {
+ } else if (!(text instanceof PremeasuredText || text instanceof CharWrapper)) {
text = TextUtils.stringOrSpannedString(text);
}
@@ -5610,10 +5620,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
spannable = (Spannable) text;
} else {
spannable = mSpannableFactory.newSpannable(text);
- text = spannable;
}
SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class);
+ if (spans.length == 0) {
+ return text;
+ } else {
+ text = spannable;
+ }
+
for (int i = 0; i < spans.length; i++) {
spannable.removeSpan(spans[i]);
}
@@ -10836,10 +10851,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText,
int fromIndex, int removedCount, int addedCount) {
- if (!AccessibilityManager.getInstance(mContext).isObservedEventType(
- AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED)) {
- return;
- }
AccessibilityEvent event =
AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
event.setFromIndex(fromIndex);
@@ -11146,6 +11157,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
/**
+ * Starts an ActionMode for the specified TextLink.
+ *
+ * @return Whether or not we're attempting to start the action mode.
+ * @hide
+ */
+ public boolean requestActionMode(@NonNull TextLinks.TextLink link) {
+ Preconditions.checkNotNull(link);
+ if (mEditor != null) {
+ mEditor.startLinkActionModeAsync(link);
+ return true;
+ }
+ return false;
+ }
+ /**
* @hide
*/
protected void stopTextActionMode() {
diff --git a/android/widget/TextViewMetrics.java b/android/widget/TextViewMetrics.java
index 96d17943..738a5742 100644
--- a/android/widget/TextViewMetrics.java
+++ b/android/widget/TextViewMetrics.java
@@ -37,29 +37,4 @@ public final class TextViewMetrics {
* Long press on TextView - drag and drop started.
*/
public static final int SUBTYPE_LONG_PRESS_DRAG_AND_DROP = 2;
-
- /**
- * Assist menu item (shown or clicked) - classification: other.
- */
- public static final int SUBTYPE_ASSIST_MENU_ITEM_OTHER = 0;
-
- /**
- * Assist menu item (shown or clicked) - classification: email.
- */
- public static final int SUBTYPE_ASSIST_MENU_ITEM_EMAIL = 1;
-
- /**
- * Assist menu item (shown or clicked) - classification: phone.
- */
- public static final int SUBTYPE_ASSIST_MENU_ITEM_PHONE = 2;
-
- /**
- * Assist menu item (shown or clicked) - classification: address.
- */
- public static final int SUBTYPE_ASSIST_MENU_ITEM_ADDRESS = 3;
-
- /**
- * Assist menu item (shown or clicked) - classification: url.
- */
- public static final int SUBTYPE_ASSIST_MENU_ITEM_URL = 4;
}
diff --git a/android/widget/TimePicker.java b/android/widget/TimePicker.java
index ae6881e4..cfec3f2f 100644
--- a/android/widget/TimePicker.java
+++ b/android/widget/TimePicker.java
@@ -77,7 +77,10 @@ public class TimePicker extends FrameLayout {
public static final int MODE_CLOCK = 2;
/** @hide */
- @IntDef({MODE_SPINNER, MODE_CLOCK})
+ @IntDef(prefix = { "MODE_" }, value = {
+ MODE_SPINNER,
+ MODE_CLOCK
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface TimePickerMode {}
diff --git a/android/widget/Toast.java b/android/widget/Toast.java
index bfde6ac3..edcf209b 100644
--- a/android/widget/Toast.java
+++ b/android/widget/Toast.java
@@ -71,7 +71,10 @@ public class Toast {
static final boolean localLOGV = false;
/** @hide */
- @IntDef({LENGTH_SHORT, LENGTH_LONG})
+ @IntDef(prefix = { "LENGTH_" }, value = {
+ LENGTH_SHORT,
+ LENGTH_LONG
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface Duration {}
@@ -504,8 +507,7 @@ public class Toast {
private void trySendAccessibilityEvent() {
AccessibilityManager accessibilityManager =
AccessibilityManager.getInstance(mView.getContext());
- if (!accessibilityManager.isObservedEventType(
- AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED)) {
+ if (!accessibilityManager.isEnabled()) {
return;
}
// treat toasts as notifications since they are used to